@aravinthan_p/appnest-engine 1.0.21 → 1.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -62
- package/appnest-command-line/appnest-command.js +41 -28
- package/appnest-command-line/frameworkDocsUtils.js +73 -0
- package/appnest-command-line/precheckUtils.js +7 -20
- package/appnest-command-line/setupAppUtils.js +15 -0
- package/appnest-command-line/validateUtils/index.js +2 -8
- package/appnest-command-line/validateUtils/lint/lint-backend.js +4 -3
- package/appnest-command-line/validateUtils/lint/lint-frontend.js +4 -4
- package/appnest-command-line/validateUtils/lint/lint-utils.js +67 -56
- package/appnest-command-line/validateUtils/lint/lint.js +5 -5
- package/appnest-command-line/validateUtils/manifestUtils.js +359 -120
- package/appnest-command-line/zipUtils.js +1 -1
- package/appnest-engine-commands.md +46 -0
- package/appnest-install-frontend/package.json +1 -1
- package/appnest-install-frontend/vite.config.js +4 -4
- package/appnest-proxy/server.js +12 -12
- package/cli.js +6 -4
- package/package.json +3 -1
|
@@ -3,160 +3,399 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
|
-
const REQUIRED_KEYS = [
|
|
7
|
-
'platform-version',
|
|
8
|
-
'parentProduct',
|
|
9
|
-
'location',
|
|
10
|
-
'whitelistedDomains',
|
|
11
|
-
'productEvents',
|
|
12
|
-
'apiFunctions',
|
|
13
|
-
'customInstallationFrontend',
|
|
14
|
-
'installationParams',
|
|
15
|
-
'oauth',
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
const INSTALLATION_PARAM_TYPES = ['string', 'number', 'boolean', 'object', 'array'];
|
|
19
6
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
7
|
+
const validateInstallationParameters = (installation_parameters) => {
|
|
8
|
+
const INSTALLATION_PARAM_TYPES = ['string', 'api_key'];
|
|
9
|
+
if (!installation_parameters || typeof installation_parameters !== 'object' || installation_parameters === null || Array.isArray(installation_parameters)) {
|
|
10
|
+
return { valid: false, errors: ['installation_parameters must be a key-value map (object)'] };
|
|
11
|
+
}
|
|
12
|
+
for (const [name, value] of Object.entries(installation_parameters)) {
|
|
13
|
+
if (typeof value !== 'object' || value === null) {
|
|
14
|
+
return { valid: false, errors: [`installation_parameters["${name}"] must be an object`] };
|
|
15
|
+
}
|
|
16
|
+
if(value.display_name != null && typeof value.display_name !== 'string') {
|
|
17
|
+
return { valid: false, errors: [`installation_parameters["${name}"] must be an object with "display_name" string`] };
|
|
18
|
+
}
|
|
19
|
+
if(value.description != null && typeof value.description !== 'string') {
|
|
20
|
+
return { valid: false, errors: [`installation_parameters["${name}"] must be an object with "description" string`] };
|
|
21
|
+
}
|
|
22
|
+
if(value.required != null && typeof value.required !== 'boolean') {
|
|
23
|
+
return { valid: false, errors: [`installation_parameters["${name}"] must be an object with "required" boolean`] };
|
|
24
|
+
}
|
|
25
|
+
if(value.secure != null && typeof value.secure !== 'boolean') {
|
|
26
|
+
return { valid: false, errors: [`installation_parameters["${name}"] must be an object with "secure" boolean`] };
|
|
27
|
+
}
|
|
28
|
+
if(value.type != null && !INSTALLATION_PARAM_TYPES.includes(value.type)) {
|
|
29
|
+
return { valid: false, errors: [`installation_parameters["${name}"] must be an object with "type" one of: ${INSTALLATION_PARAM_TYPES.join(', ')}`] };
|
|
30
|
+
}
|
|
31
31
|
}
|
|
32
|
+
return { valid: true, installation_parameters };
|
|
33
|
+
}
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
} catch (e) {
|
|
38
|
-
return { valid: false, errors: [`manifest.json is invalid JSON: ${e.message}`] };
|
|
35
|
+
const validateAppOauthConfig = (app_oauth_config) => {
|
|
36
|
+
console.log('🔍 Validating app oauth config:', app_oauth_config);
|
|
37
|
+
if (typeof app_oauth_config !== 'object' || app_oauth_config === null || Array.isArray(app_oauth_config)) {
|
|
38
|
+
return { valid: false, errors: ['app_oauth_config must be a key-value map (object)'] };
|
|
39
39
|
}
|
|
40
|
+
//find the length of the object if greater then 1 then return error
|
|
41
|
+
if (Object.keys(app_oauth_config).length > 1) {
|
|
42
|
+
return { valid: false, errors: ['app_oauth_config must be a single key-value map (object)'] };
|
|
43
|
+
}
|
|
44
|
+
for (const [name, value] of Object.entries(app_oauth_config)) {
|
|
45
|
+
if (typeof value !== 'object' || value === null) {
|
|
46
|
+
return { valid: false, errors: [`app_oauth_config["${name}"] must be an object`] };
|
|
47
|
+
}
|
|
48
|
+
if(value.client_id != null && typeof value.client_id !== 'string') {
|
|
49
|
+
return { valid: false, errors: [`app_oauth_config["${name}"] must be an object with "client_id" string`] };
|
|
50
|
+
}
|
|
51
|
+
if(value.client_secret != null && typeof value.client_secret !== 'string') {
|
|
52
|
+
return { valid: false, errors: [`app_oauth_config["${name}"] must be an object with "client_secret" string`] };
|
|
53
|
+
}
|
|
54
|
+
if(value.authorize_url != null && typeof value.authorize_url !== 'string') {
|
|
55
|
+
return { valid: false, errors: [`app_oauth_config["${name}"] must be an object with "authorize_url" string`] };
|
|
56
|
+
}
|
|
57
|
+
if(value.token_url != null && typeof value.token_url !== 'string') {
|
|
58
|
+
return { valid: false, errors: [`app_oauth_config["${name}"] must be an object with "token_url" string`] };
|
|
59
|
+
}
|
|
60
|
+
if(value.options != null && typeof value.options !== 'object') {
|
|
61
|
+
return { valid: false, errors: [`app_oauth_config["${name}"] must be an object with "options" object`] };
|
|
62
|
+
}
|
|
63
|
+
if(value.options.response_type != null && typeof value.options.response_type !== 'string') {
|
|
64
|
+
return { valid: false, errors: [`app_oauth_config["${name}"] must be an object with "options" object with "response_type" string`] };
|
|
65
|
+
}
|
|
66
|
+
if(value.options.access_type != null && typeof value.options.access_type !== 'string') {
|
|
67
|
+
return { valid: false, errors: [`app_oauth_config["${name}"] must be an object with "options" object with "access_type" string`] };
|
|
68
|
+
}
|
|
69
|
+
if(value.options.prompt != null && typeof value.options.prompt !== 'string') {
|
|
70
|
+
return { valid: false, errors: [`app_oauth_config["${name}"] must be an object with "options" object with "prompt" string`] };
|
|
71
|
+
}
|
|
72
|
+
if(value.options.scopes != null && (!Array.isArray(value.options.scopes) || value.options.scopes.some((s) => typeof s !== 'string'))) {
|
|
73
|
+
return { valid: false, errors: [`app_oauth_config["${name}"] must be an object with "options" object with "scopes" array of strings`] };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { valid: true, app_oauth_config };
|
|
77
|
+
}
|
|
40
78
|
|
|
41
|
-
|
|
42
|
-
|
|
79
|
+
const validateWhitelistedDomains = (whitelisted_domains) => {
|
|
80
|
+
console.log('🔍 Validating whitelisted domains:', whitelisted_domains);
|
|
81
|
+
if (!Array.isArray(whitelisted_domains)) {
|
|
82
|
+
return { valid: false, errors: ['whitelisted_domains must be an array of strings'] };
|
|
43
83
|
}
|
|
84
|
+
for (const domain of whitelisted_domains) {
|
|
85
|
+
if (typeof domain !== 'string') {
|
|
86
|
+
return { valid: false, errors: ['whitelisted_domains must contain only strings'] };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { valid: true, whitelisted_domains };
|
|
90
|
+
}
|
|
44
91
|
|
|
45
|
-
|
|
46
|
-
|
|
92
|
+
const validateConfiguredFunctions = ({event_listener_functions, backend_api_functions}) => {
|
|
93
|
+
const exported_functions = getAppBackendServerExportFunctionNames();
|
|
94
|
+
console.log('🔍 Exported functions:', exported_functions);
|
|
95
|
+
const configuredFunctions = [...Object.keys(event_listener_functions), ...Object.keys(backend_api_functions)];
|
|
96
|
+
console.log('🔍 Configured functions:', configuredFunctions);
|
|
97
|
+
const missingFunctions = configuredFunctions.filter((func) => !exported_functions.includes(func));
|
|
98
|
+
if(missingFunctions.length > 0) {
|
|
99
|
+
console.error('❌ Missing exported functions in app-backend/server.js but configured in manifest.json:', missingFunctions);
|
|
100
|
+
return { valid: false, errors: [`Missing exported functions in app-backend/server.js but configured in manifest.json: ${missingFunctions.join(', ')}`] };
|
|
101
|
+
}
|
|
102
|
+
return { valid: true, configuredFunctions };
|
|
103
|
+
}
|
|
47
104
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
105
|
+
const validateEventListenerFunctions = (event_listener_functions) => {
|
|
106
|
+
console.log('🔍 Validating event listener functions:', event_listener_functions);
|
|
107
|
+
if (!event_listener_functions || typeof event_listener_functions !== 'object' || event_listener_functions === null || Array.isArray(event_listener_functions)) {
|
|
108
|
+
return { valid: false, errors: ['event_listener_functions must be a key-value map (object)'] };
|
|
109
|
+
}
|
|
110
|
+
for (const [name, value] of Object.entries(event_listener_functions)) {
|
|
111
|
+
if(name != null && typeof name !== 'string') {
|
|
112
|
+
return { valid: false, errors: [`event_listener_functions["${name}"] must be an object with "name" string`] };
|
|
113
|
+
}
|
|
114
|
+
if (typeof value !== 'object' || value === null) {
|
|
115
|
+
return { valid: false, errors: [`event_listener_functions["${name}"] must be an object`] };
|
|
116
|
+
}
|
|
117
|
+
if(value.handler != null && typeof value.handler !== 'string') {
|
|
118
|
+
return { valid: false, errors: [`event_listener_functions["${name}"] must be an object with "handler" string`] };
|
|
119
|
+
}
|
|
120
|
+
if(value.handler == name) {
|
|
121
|
+
return { valid: false, errors: [`event_listener_functions["${name}"] must be an object with "handler" string not equal to "name"`] };
|
|
51
122
|
}
|
|
52
123
|
}
|
|
124
|
+
return { valid: true, event_listener_functions };
|
|
125
|
+
}
|
|
53
126
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
127
|
+
const validateBackendApiFunctions = (backend_api_functions) => {
|
|
128
|
+
console.log('🔍 Validating backend api functions:', backend_api_functions);
|
|
129
|
+
if (!backend_api_functions || typeof backend_api_functions !== 'object' || backend_api_functions === null || Array.isArray(backend_api_functions)) {
|
|
130
|
+
return { valid: false, errors: ['backend_api_functions must be a key-value map (object)'] };
|
|
131
|
+
}
|
|
132
|
+
for (const [name, value] of Object.entries(backend_api_functions)) {
|
|
133
|
+
if (typeof value !== 'object' || value === null) {
|
|
134
|
+
return { valid: false, errors: [`backend_api_functions["${name}"] must be an object`] };
|
|
135
|
+
}
|
|
136
|
+
if(value.timeout != null && typeof value.timeout !== 'number') {
|
|
137
|
+
return { valid: false, errors: [`backend_api_functions["${name}"] must be an object with "timeout" number`] };
|
|
57
138
|
}
|
|
58
139
|
}
|
|
140
|
+
return { valid: true, backend_api_functions };
|
|
141
|
+
}
|
|
59
142
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
143
|
+
|
|
144
|
+
const validateFrontendLocations = (frontend_locations) => {
|
|
145
|
+
console.log('🔍 Validating frontend locations:', frontend_locations);
|
|
146
|
+
if (!frontend_locations || typeof frontend_locations !== 'object' || frontend_locations === null || Array.isArray(frontend_locations)) {
|
|
147
|
+
return { valid: false, errors: ['frontend_locations must be a key-value map (object)'] };
|
|
148
|
+
}
|
|
149
|
+
for (const [name, value] of Object.entries(frontend_locations)) {
|
|
150
|
+
if(name != null && typeof name !== 'string') {
|
|
151
|
+
return { valid: false, errors: [`frontend_locations["${name}"] must be an object with "name" string`] };
|
|
152
|
+
}
|
|
153
|
+
if (typeof value !== 'object' || value === null) {
|
|
154
|
+
return { valid: false, errors: [`frontend_locations["${name}"] must be an object`] };
|
|
155
|
+
}
|
|
156
|
+
if(value.url != null && typeof value.url !== 'string') {
|
|
157
|
+
return { valid: false, errors: [`frontend_locations["${name}"] must be an object with "url" string`] };
|
|
158
|
+
}
|
|
159
|
+
if(value.url == 'index.html') {
|
|
160
|
+
return { valid: false, errors: [`frontend_locations["${name}"] must be an object with "url" string not equal to "index.html"`] };
|
|
69
161
|
}
|
|
70
162
|
}
|
|
163
|
+
return { valid: true, frontend_locations };
|
|
164
|
+
}
|
|
71
165
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
166
|
+
|
|
167
|
+
/** Compare dot-separated numeric semver parts (e.g. "2.10.0" vs "2.9.0"). Not full semver (no prerelease). */
|
|
168
|
+
const semverPartsGte = (version, minimum) => {
|
|
169
|
+
const toNums = (v) =>
|
|
170
|
+
v.split('.').map((segment) => {
|
|
171
|
+
const n = parseInt(segment, 10);
|
|
172
|
+
return Number.isFinite(n) ? n : NaN;
|
|
173
|
+
});
|
|
174
|
+
const a = toNums(version);
|
|
175
|
+
const b = toNums(minimum);
|
|
176
|
+
if (a.some((n) => Number.isNaN(n)) || b.some((n) => Number.isNaN(n))) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
const len = Math.max(a.length, b.length);
|
|
180
|
+
for (let i = 0; i < len; i++) {
|
|
181
|
+
const ai = a[i] ?? 0;
|
|
182
|
+
const bi = b[i] ?? 0;
|
|
183
|
+
if (ai < bi) return false;
|
|
184
|
+
if (ai > bi) return true;
|
|
79
185
|
}
|
|
186
|
+
return true;
|
|
187
|
+
};
|
|
80
188
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
189
|
+
const validatePlatformVersion = (platform_version) => {
|
|
190
|
+
console.log('🔍 Validating platform version:', platform_version);
|
|
191
|
+
if(!platform_version || typeof platform_version !== 'string' || platform_version === null || platform_version === undefined) {
|
|
192
|
+
return { valid: false, errors: ['platform_version must be a string'] };
|
|
193
|
+
}
|
|
194
|
+
const gte = semverPartsGte(platform_version, '2.0.0');
|
|
195
|
+
if (gte === null) {
|
|
196
|
+
return { valid: false, errors: ['platform_version must be a semver-like string (e.g. "2.0.0") with numeric segments'] };
|
|
197
|
+
}
|
|
198
|
+
if (!gte) {
|
|
199
|
+
return { valid: false, errors: ['platform_version must be greater than or equal to "2.0.0"'] };
|
|
200
|
+
}
|
|
201
|
+
return { valid: true, platform_version };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const validateParentProduct = (parent_product) => {
|
|
205
|
+
console.log('🔍 Validating parent product:', parent_product);
|
|
206
|
+
if(!parent_product || typeof parent_product !== 'string' || parent_product === null || parent_product === undefined) {
|
|
207
|
+
return { valid: false, errors: ['parent_product must be a string'] };
|
|
208
|
+
}
|
|
209
|
+
if(parent_product.trim() === '') {
|
|
210
|
+
return { valid: false, errors: ['parent_product must be a non-empty string'] };
|
|
211
|
+
}
|
|
212
|
+
return { valid: true, parent_product };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const validateProductConfig = (product_config) => {
|
|
216
|
+
console.log('🔍 Validating product config:', product_config);
|
|
217
|
+
if(!product_config || typeof product_config !== 'object' || product_config === null || Array.isArray(product_config)) {
|
|
218
|
+
return { valid: false, errors: ['product_config must be a key-value map (object)'] };
|
|
219
|
+
}
|
|
220
|
+
if(Object.keys(product_config).length > 1) {
|
|
221
|
+
return { valid: false, errors: ['product_config must be a single key-value map (object)'] };
|
|
222
|
+
}
|
|
223
|
+
for (const [name, value] of Object.entries(product_config)) {
|
|
224
|
+
if(name != null && typeof name !== 'string') {
|
|
225
|
+
return { valid: false, errors: [`product_config["${name}"] must be an object with "name" string`] };
|
|
90
226
|
}
|
|
227
|
+
validateFrontendLocations(value.frontend_locations);
|
|
228
|
+
validateWhitelistedDomains(value.whitelisted_domains);
|
|
229
|
+
validateEventListenerFunctions(value.event_listener_functions);
|
|
230
|
+
validateBackendApiFunctions(value.backend_api_functions);
|
|
231
|
+
validateInstallationParameters(value.installation_parameters);
|
|
232
|
+
validateAppOauthConfig(value.app_oauth_config);
|
|
233
|
+
validateConfiguredFunctions({event_listener_functions: value.event_listener_functions, backend_api_functions: value.backend_api_functions});
|
|
234
|
+
|
|
91
235
|
}
|
|
236
|
+
}
|
|
92
237
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Split a string on commas that are not inside (), [], {}, or strings.
|
|
241
|
+
*/
|
|
242
|
+
const splitTopLevelCommas = (str) => {
|
|
243
|
+
const parts = [];
|
|
244
|
+
let buf = '';
|
|
245
|
+
let depth = 0;
|
|
246
|
+
let inString = null;
|
|
247
|
+
for (let i = 0; i < str.length; i++) {
|
|
248
|
+
const c = str[i];
|
|
249
|
+
if (inString) {
|
|
250
|
+
buf += c;
|
|
251
|
+
if (c === '\\' && i + 1 < str.length) {
|
|
252
|
+
buf += str[++i];
|
|
253
|
+
continue;
|
|
101
254
|
}
|
|
255
|
+
if (c === inString) inString = null;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
259
|
+
inString = c;
|
|
260
|
+
buf += c;
|
|
261
|
+
continue;
|
|
102
262
|
}
|
|
263
|
+
if (c === '{' || c === '(' || c === '[') depth++;
|
|
264
|
+
else if (c === '}' || c === ')' || c === ']') depth--;
|
|
265
|
+
if (c === ',' && depth === 0) {
|
|
266
|
+
parts.push(buf);
|
|
267
|
+
buf = '';
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
buf += c;
|
|
103
271
|
}
|
|
272
|
+
if (buf.trim()) parts.push(buf);
|
|
273
|
+
return parts;
|
|
274
|
+
};
|
|
104
275
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
if (value.description != null && typeof value.description !== 'string') {
|
|
129
|
-
errors.push(`installationParams["${name}"].description must be a string`);
|
|
130
|
-
}
|
|
276
|
+
/**
|
|
277
|
+
* Extract `module.exports = { ... }` object body (handles strings and nested braces).
|
|
278
|
+
*/
|
|
279
|
+
const extractModuleExportsObjectBody = (content) => {
|
|
280
|
+
const m = content.match(/module\.exports\s*=\s*\{/);
|
|
281
|
+
if (!m) return null;
|
|
282
|
+
let start = m.index + m[0].length;
|
|
283
|
+
let depth = 1;
|
|
284
|
+
let i = start;
|
|
285
|
+
while (i < content.length && depth > 0) {
|
|
286
|
+
const c = content[i];
|
|
287
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
288
|
+
const quote = c;
|
|
289
|
+
i++;
|
|
290
|
+
while (i < content.length) {
|
|
291
|
+
if (content[i] === '\\') {
|
|
292
|
+
i += 2;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (content[i] === quote) {
|
|
296
|
+
i++;
|
|
297
|
+
break;
|
|
131
298
|
}
|
|
299
|
+
i++;
|
|
132
300
|
}
|
|
301
|
+
continue;
|
|
133
302
|
}
|
|
303
|
+
if (c === '{') depth++;
|
|
304
|
+
else if (c === '}') depth--;
|
|
305
|
+
i++;
|
|
134
306
|
}
|
|
307
|
+
if (depth !== 0) return null;
|
|
308
|
+
return content.slice(start, i - 1);
|
|
309
|
+
};
|
|
135
310
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
311
|
+
/**
|
|
312
|
+
* Returns exported names from `app-backend/server.js` when using
|
|
313
|
+
* `module.exports = { foo, bar, baz: require(...) }`.
|
|
314
|
+
* @returns {string[]}
|
|
315
|
+
*/
|
|
316
|
+
const getAppBackendServerExportFunctionNames = () => {
|
|
317
|
+
const userDir = process.cwd();
|
|
318
|
+
const appBackendServerFile = path.join(userDir, 'app-backend', 'server.js');
|
|
319
|
+
if (!fs.existsSync(appBackendServerFile)) {
|
|
320
|
+
console.warn('⚠️ app-backend/server.js not found');
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
let content;
|
|
324
|
+
try {
|
|
325
|
+
content = fs.readFileSync(appBackendServerFile, 'utf8');
|
|
326
|
+
} catch (e) {
|
|
327
|
+
console.warn('⚠️ Could not read server.js:', e.message);
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const body = extractModuleExportsObjectBody(content);
|
|
332
|
+
if (body == null) {
|
|
333
|
+
console.warn('⚠️ No module.exports = { ... } found in server.js');
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const withoutBlockComments = body.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
338
|
+
const parts = splitTopLevelCommas(withoutBlockComments);
|
|
339
|
+
const names = [];
|
|
340
|
+
|
|
341
|
+
for (const part of parts) {
|
|
342
|
+
const trimmed = part.trim().replace(/\/\/.*$/gm, '').trim();
|
|
343
|
+
if (!trimmed) continue;
|
|
344
|
+
if (/^\.\.\./.test(trimmed)) continue;
|
|
345
|
+
|
|
346
|
+
const shorthand = trimmed.match(/^(\w+)\s*$/);
|
|
347
|
+
if (shorthand) {
|
|
348
|
+
names.push(shorthand[1]);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
const keyIdent = trimmed.match(/^(\w+)\s*:/);
|
|
352
|
+
if (keyIdent) {
|
|
353
|
+
names.push(keyIdent[1]);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const keyQuoted = trimmed.match(/^['"]([^'"]+)['"]\s*:/);
|
|
357
|
+
if (keyQuoted) {
|
|
358
|
+
names.push(keyQuoted[1]);
|
|
359
|
+
continue;
|
|
157
360
|
}
|
|
158
361
|
}
|
|
159
362
|
|
|
363
|
+
console.log('🔍 App backend server export names:', names);
|
|
364
|
+
return names;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Validates manifest.json in the current working directory.
|
|
369
|
+
* Returns { valid: true, manifest } on success, or { valid: false, errors } on failure.
|
|
370
|
+
*/
|
|
371
|
+
const validateManifestFile = () => {
|
|
372
|
+
const userDir = process.cwd();
|
|
373
|
+
const manifestPath = path.join(userDir, 'manifest.json');
|
|
374
|
+
const errors = [];
|
|
375
|
+
|
|
376
|
+
if (!fs.existsSync(manifestPath)) {
|
|
377
|
+
return { valid: false, errors: [`manifest.json not found in ${userDir}`] };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let manifest;
|
|
381
|
+
try {
|
|
382
|
+
const raw = fs.readFileSync(manifestPath, 'utf8');
|
|
383
|
+
manifest = JSON.parse(raw);
|
|
384
|
+
} catch (e) {
|
|
385
|
+
return { valid: false, errors: [`manifest.json is invalid JSON: ${e.message}`] };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (typeof manifest !== 'object' || manifest === null) {
|
|
389
|
+
return { valid: false, errors: ['manifest.json must be a JSON object'] };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
console.log('🔍 Viewing manifest.json:', manifest);
|
|
393
|
+
console.log('🔍 Validating manifest.json...');
|
|
394
|
+
|
|
395
|
+
validatePlatformVersion(manifest.platform_version);
|
|
396
|
+
validateParentProduct(manifest.parent_product);
|
|
397
|
+
validateProductConfig(manifest.product_config);
|
|
398
|
+
|
|
160
399
|
if (errors.length > 0) {
|
|
161
400
|
console.error('❌ Manifest validation failed:', errors);
|
|
162
401
|
return { valid: false, errors };
|
|
@@ -10,7 +10,7 @@ const zipAppProject = ({ projects }) => {
|
|
|
10
10
|
bundleFrontend({ projects });
|
|
11
11
|
|
|
12
12
|
console.log('📦 Zipping the app project...');
|
|
13
|
-
const projectsToZipKeys = ['app-frontend', 'app-backend', 'app-
|
|
13
|
+
const projectsToZipKeys = ['app-frontend', 'app-backend', 'app-install-frontend'];
|
|
14
14
|
const projectsToZip = projectsToZipKeys.reduce((acc, key) => {
|
|
15
15
|
if (projects[key]) {
|
|
16
16
|
acc[key] = projects[key];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Appnest Engine — Commands overview
|
|
2
|
+
|
|
3
|
+
Quick reference for **`appnest-engine`** CLI actions. All **`app`** commands are invoked as:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
appnest-engine app <action>
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Run these from your **app project directory** (where `app-backend` / `app-frontend` live) unless noted. The engine resolves paths from **`process.cwd()`** and passes **`ORIGINAL_CWD`** to the proxy and Vite builds.
|
|
10
|
+
|
|
11
|
+
For architecture and setup, see **[README.md](./README.md)**.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Command summary
|
|
16
|
+
|
|
17
|
+
| Action | Purpose |
|
|
18
|
+
|--------|---------|
|
|
19
|
+
| **`init`** | Scaffold or refresh the standard Appnest app layout in the current folder (base code from GitHub). Use once or when you need a fresh template. |
|
|
20
|
+
| **`precheck`** | Verify **Node.js ≥ 22** and that every engine + app path the CLI knows about exists on disk. Fails fast with guidance if something is missing. |
|
|
21
|
+
| **`install-packages`** | Run **`npm install`** inside each **engine** package: `appnest-backend`, `appnest-frontend`, `appnest-install-frontend`, `appnest-proxy`. Use after cloning the engine or changing engine dependencies. |
|
|
22
|
+
| **`start`** | Start the **proxy** only; the proxy then starts the backend wrapper, main Vite app, and (if present) the installation Vite app. This is the main **local dev** entry point (single port, unified routing). |
|
|
23
|
+
| **`bundle-frontend`** | Production **build** for both engine frontends (`npm run build` in `appnest-frontend` and `appnest-install-frontend`). Output is written into **your** `app-frontend/dist` and `app-install-frontend/dist` via `ORIGINAL_CWD`. |
|
|
24
|
+
| **`pack`** | Runs **`bundle-frontend`**, adjusts `app-frontend/dist/index.html` paths where needed, copies **`app-frontend`**, **`app-backend`**, **`app-install-frontend`**, and **`manifest.json`** into **`appnest-app-pack/`**, then zips that folder to **`app-pack-zip/app-pack-<timestamp>.zip`**. For deployment / distribution packs. |
|
|
25
|
+
| **`validate`** | Run linting (and related validation) on **`app-backend`** and **`app-frontend`**, and validate **`manifest.json`**. |
|
|
26
|
+
| **`ai-context`** | Download AI context from GitHub into **`appnest-ai-context/`** at project root. Replaces existing folder if present. |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Other invocations
|
|
31
|
+
|
|
32
|
+
| Invocation | Purpose |
|
|
33
|
+
|------------|---------|
|
|
34
|
+
| **`appnest-engine --version`** | Show engine version from root `package.json`. |
|
|
35
|
+
| **`appnest-engine help`** | Print the built-in short list of `app` actions. |
|
|
36
|
+
| **`appnest-engine app`** (no param) | Same as above — lists all app actions. |
|
|
37
|
+
| **`appnest-engine app help`** | Same as above. |
|
|
38
|
+
| **`appnest-engine app -h`** | Same as above (Commander built-in help). |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Notes
|
|
43
|
+
|
|
44
|
+
- **`install-packages`** does **not** run `npm install` in the user’s `app-backend` / `app-frontend`; those are your app’s own dependencies—install them in those folders as usual.
|
|
45
|
+
- **`app-install-frontend`** is **optional**; if the folder is missing, install UI is skipped and zip may warn for that path.
|
|
46
|
+
- Engine folder **`appnest-install-frontend`** serves the user folder **`app-install-frontend`** (names differ on purpose).
|
|
@@ -5,7 +5,7 @@ import path from 'path';
|
|
|
5
5
|
console.log('📂 User Directory from vite:', process.env.ORIGINAL_CWD);
|
|
6
6
|
const userProjectPath = process.env.ORIGINAL_CWD;
|
|
7
7
|
const projectPath = path.resolve(__dirname, '..');
|
|
8
|
-
console.log('📂 Resolved Appnest app-
|
|
8
|
+
console.log('📂 Resolved Appnest app-install-frontend Project Path:', projectPath);
|
|
9
9
|
const defaultClientScriptUrl =
|
|
10
10
|
'https://d345rit115hm0w.cloudfront.net/client123.js';
|
|
11
11
|
const clientScriptUrl =
|
|
@@ -19,7 +19,7 @@ if (!userProjectPath) {
|
|
|
19
19
|
console.error('❌ ORIGINAL_CWD not set!');
|
|
20
20
|
process.exit(1);
|
|
21
21
|
} else {
|
|
22
|
-
console.log('✅ In app-
|
|
22
|
+
console.log('✅ In app-install-frontend Using user project path:', userProjectPath);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// https://vite.dev/config/
|
|
@@ -46,7 +46,7 @@ export default defineConfig({
|
|
|
46
46
|
resolve: {
|
|
47
47
|
alias: {
|
|
48
48
|
// 👇 create a virtual import "@dev"
|
|
49
|
-
'@dev': path.resolve(userProjectPath, 'app-
|
|
49
|
+
'@dev': path.resolve(userProjectPath, 'app-install-frontend/src'),
|
|
50
50
|
// 👇 Force developer’s React imports to use *your* React
|
|
51
51
|
react: path.resolve(__dirname, 'node_modules/react'),
|
|
52
52
|
'react-dom': path.resolve(__dirname, 'node_modules/react-dom'),
|
|
@@ -58,7 +58,7 @@ export default defineConfig({
|
|
|
58
58
|
},
|
|
59
59
|
build: {
|
|
60
60
|
// Output dist folder inside the developer’s project
|
|
61
|
-
outDir: path.resolve(userProjectPath, 'app-
|
|
61
|
+
outDir: path.resolve(userProjectPath, 'app-install-frontend/dist'),
|
|
62
62
|
|
|
63
63
|
// 👇 optional but helps keep bundle stable
|
|
64
64
|
emptyOutDir: true,
|