@aravinthan_p/appnest-engine 1.0.20 → 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.
@@ -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
- * Validates manifest.json in the current working directory.
22
- * Returns { valid: true, manifest } on success, or { valid: false, errors } on failure.
23
- */
24
- const validateManifestFile = () => {
25
- const userDir = process.cwd();
26
- const manifestPath = path.join(userDir, 'manifest.json');
27
- const errors = [];
28
-
29
- if (!fs.existsSync(manifestPath)) {
30
- return { valid: false, errors: [`manifest.json not found in ${userDir}`] };
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
- let manifest;
34
- try {
35
- const raw = fs.readFileSync(manifestPath, 'utf8');
36
- manifest = JSON.parse(raw);
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
- if (typeof manifest !== 'object' || manifest === null) {
42
- return { valid: false, errors: ['manifest.json must be a JSON object'] };
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
- console.log('🔍 Validating manifest.json...');
46
- console.log('🔍 Manifest:', manifest);
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
- for (const key of REQUIRED_KEYS) {
49
- if (!(key in manifest)) {
50
- errors.push(`Missing required key: "${key}"`);
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
- if (manifest.parentProduct != null) {
55
- if (typeof manifest.parentProduct !== 'string' || !manifest.parentProduct.trim()) {
56
- errors.push('parentProduct must be a non-empty string (name)');
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
- if (manifest.location != null) {
61
- if (typeof manifest.location !== 'object' || manifest.location === null || Array.isArray(manifest.location)) {
62
- errors.push('location must be a key-value map (object)');
63
- } else {
64
- for (const [name, value] of Object.entries(manifest.location)) {
65
- if (typeof value !== 'object' || value === null || typeof value.url !== 'string') {
66
- errors.push(`location["${name}"] must be an object with "url" string`);
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
- if (manifest.whitelistedDomains != null) {
73
- if (!Array.isArray(manifest.whitelistedDomains)) {
74
- errors.push('whitelistedDomains must be an array of strings');
75
- } else {
76
- const bad = manifest.whitelistedDomains.some((d) => typeof d !== 'string');
77
- if (bad) errors.push('whitelistedDomains must contain only strings');
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
- if (manifest.productEvents != null) {
82
- if (typeof manifest.productEvents !== 'object' || manifest.productEvents === null || Array.isArray(manifest.productEvents)) {
83
- errors.push('productEvents must be a key-value map (object)');
84
- } else {
85
- for (const [name, value] of Object.entries(manifest.productEvents)) {
86
- if (typeof value !== 'object' || value === null || typeof value.handler !== 'string') {
87
- errors.push(`productEvents["${name}"] must be an object with "handler" string`);
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
- if (manifest.apiFunctions != null) {
94
- if (typeof manifest.apiFunctions !== 'object' || manifest.apiFunctions === null || Array.isArray(manifest.apiFunctions)) {
95
- errors.push('apiFunctions must be a key-value map (object)');
96
- } else {
97
- for (const [name, value] of Object.entries(manifest.apiFunctions)) {
98
- if (typeof value !== 'object' || value === null || typeof value.handler !== 'string') {
99
- errors.push(`apiFunctions["${name}"] must be an object with "handler" string`);
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
- if (manifest.customInstallationFrontend != null) {
106
- if (typeof manifest.customInstallationFrontend !== 'boolean') {
107
- errors.push('customInstallationFrontend must be a boolean');
108
- }
109
- }
110
-
111
- if (manifest.installationParams != null) {
112
- if (typeof manifest.installationParams !== 'object' || manifest.installationParams === null || Array.isArray(manifest.installationParams)) {
113
- errors.push('installationParams must be a key-value map (object)');
114
- } else {
115
- for (const [name, value] of Object.entries(manifest.installationParams)) {
116
- if (typeof value !== 'object' || value === null) {
117
- errors.push(`installationParams["${name}"] must be an object`);
118
- } else {
119
- if (value.type != null && !INSTALLATION_PARAM_TYPES.includes(value.type)) {
120
- errors.push(`installationParams["${name}"].type must be one of: ${INSTALLATION_PARAM_TYPES.join(', ')}`);
121
- }
122
- if (value.required != null && typeof value.required !== 'boolean') {
123
- errors.push(`installationParams["${name}"].required must be a boolean`);
124
- }
125
- if (value.display_name != null && typeof value.display_name !== 'string') {
126
- errors.push(`installationParams["${name}"].display_name must be a string`);
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
- if (manifest.oauth != null) {
137
- if (typeof manifest.oauth !== 'object' || manifest.oauth === null) {
138
- errors.push('oauth must be an object (or empty object {})');
139
- } else if (Object.keys(manifest.oauth).length > 0) {
140
- const o = manifest.oauth;
141
- if (o.client_id != null && typeof o.client_id !== 'string') errors.push('oauth.client_id must be a string');
142
- if (o.client_secret != null && typeof o.client_secret !== 'string') errors.push('oauth.client_secret must be a string');
143
- if (o.authorize_url != null && typeof o.authorize_url !== 'string') errors.push('oauth.authorize_url must be a string');
144
- if (o.token_url != null && typeof o.token_url !== 'string') errors.push('oauth.token_url must be a string');
145
- if (o.options != null) {
146
- if (typeof o.options !== 'object' || o.options === null) {
147
- errors.push('oauth.options must be an object');
148
- } else {
149
- if (o.options.response_type != null && typeof o.options.response_type !== 'string') errors.push('oauth.options.response_type must be a string');
150
- if (o.options.access_type != null && typeof o.options.access_type !== 'string') errors.push('oauth.options.access_type must be a string');
151
- if (o.options.prompt != null && typeof o.options.prompt !== 'string') errors.push('oauth.options.prompt must be a string');
152
- if (o.options.scopes != null && (!Array.isArray(o.options.scopes) || o.options.scopes.some((s) => typeof s !== 'string'))) {
153
- errors.push('oauth.options.scopes must be an array of strings');
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-installation-frontend'];
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).
@@ -4,7 +4,7 @@
4
4
  "version": "0.0.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "VITE_BASE_PATH=/app-installation-frontend/ vite",
7
+ "dev": "VITE_BASE_PATH=/app-install-frontend/ vite",
8
8
  "build": "vite build",
9
9
  "lint": "eslint .",
10
10
  "preview": "vite preview"
@@ -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-installation-frontend Project Path:', projectPath);
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-installation-frontend Using user project path:', userProjectPath);
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-installation-frontend/src'),
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-installation-frontend/dist'),
61
+ outDir: path.resolve(userProjectPath, 'app-install-frontend/dist'),
62
62
 
63
63
  // 👇 optional but helps keep bundle stable
64
64
  emptyOutDir: true,