codeplay-common 4.6.9 → 4.7.1

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.
@@ -7,7 +7,7 @@ node finalrelease "1,2,7"
7
7
 
8
8
  import { existsSync, readFileSync, readdirSync, promises, constants, writeFileSync, unlinkSync, mkdirSync, renameSync } from 'fs';
9
9
  import { join, resolve } from 'path';
10
- import { execSync } from 'child_process';
10
+ import { execSync, execFileSync } from 'child_process';
11
11
  import { createInterface } from 'readline';
12
12
 
13
13
  import { fileURLToPath } from 'url';
@@ -16,135 +16,135 @@ import { dirname } from 'path';
16
16
  /* const path = require('path');
17
17
  const fs = require('fs'); */
18
18
 
19
- import fs from 'fs';
20
- import path from 'path';
21
-
22
- const __filename = fileURLToPath(import.meta.url);
23
- const __dirname = dirname(__filename);
24
-
25
- function stripJavaScriptComments(content) {
26
- let result = '';
27
- let state = 'code';
28
-
29
- for (let index = 0; index < content.length; index++) {
30
- const character = content[index];
31
- const nextCharacter = content[index + 1];
32
-
33
- if (state === 'lineComment') {
34
- if (character === '\n') {
35
- result += character;
36
- state = 'code';
37
- } else {
38
- result += ' ';
39
- }
40
- continue;
41
- }
42
-
43
- if (state === 'blockComment') {
44
- if (character === '*' && nextCharacter === '/') {
45
- result += ' ';
46
- index++;
47
- state = 'code';
48
- } else {
49
- result += character === '\n' ? '\n' : ' ';
50
- }
51
- continue;
52
- }
53
-
54
- if (state !== 'code') {
55
- result += character;
56
-
57
- if (character === '\\') {
58
- if (nextCharacter !== undefined) {
59
- result += nextCharacter;
60
- index++;
61
- }
62
- } else if (
63
- (state === 'singleQuote' && character === "'") ||
64
- (state === 'doubleQuote' && character === '"') ||
65
- (state === 'templateLiteral' && character === '`')
66
- ) {
67
- state = 'code';
68
- }
69
- continue;
70
- }
71
-
72
- if (character === '/' && nextCharacter === '/') {
73
- result += ' ';
74
- index++;
75
- state = 'lineComment';
76
- } else if (character === '/' && nextCharacter === '*') {
77
- result += ' ';
78
- index++;
79
- state = 'blockComment';
80
- } else {
81
- result += character;
82
-
83
- if (character === "'") state = 'singleQuote';
84
- else if (character === '"') state = 'doubleQuote';
85
- else if (character === '`') state = 'templateLiteral';
86
- }
87
- }
88
-
89
- return result;
90
- }
91
-
92
- function checkProductionAdmobAlias() {
93
- const possibleConfigFiles = ['vite.config.mjs', 'vite.config.js'];
94
- const viteConfigPath = possibleConfigFiles
95
- .map(configFile => resolve(__dirname, configFile))
96
- .find(filePath => existsSync(filePath));
97
-
98
- if (!viteConfigPath) {
99
- console.error('❌ FINAL RELEASE BLOCKED: No vite.config.mjs or vite.config.js file found.');
100
- process.exit(1);
101
- }
102
-
103
- const viteConfigContent = stripJavaScriptComments(readFileSync(viteConfigPath, 'utf8'));
104
- const aliasPattern = /(['"])@admob\1\s*:\s*path\.resolve\(\s*__dirname\s*,\s*(['"])([^'"\r\n]+)\2\s*\)/g;
105
- const activeAliases = [...viteConfigContent.matchAll(aliasPattern)].map(match => match[3]);
106
- const productionAliasPattern = /^\.\/src\/js\/Ads\/admob-emi-nextgen-\d+(?:\.\d+)+\.js$/;
107
-
108
- if (activeAliases.length === 0) {
109
- console.error('❌ FINAL RELEASE BLOCKED: The production @admob alias is commented out or missing in the Vite config.');
110
- console.error(" Enable: '@admob': path.resolve(__dirname, './src/js/Ads/admob-emi-nextgen-x.x.js')");
111
- process.exit(1);
112
- }
113
-
114
- if (activeAliases.length > 1) {
115
- console.error('❌ FINAL RELEASE BLOCKED: Multiple active @admob aliases were found in the Vite config.');
116
- console.error(' Keep exactly one active production admob-emi-nextgen-x.x.js alias.');
117
- process.exit(1);
118
- }
119
-
120
- const activeAlias = activeAliases[0];
121
- if (!productionAliasPattern.test(activeAlias)) {
122
- console.error(`❌ FINAL RELEASE BLOCKED: @admob currently points to '${activeAlias}'.`);
123
- console.error(" It must point to './src/js/Ads/admob-emi-nextgen-x.x.js', not a test AdMob file.");
124
- process.exit(1);
125
- }
126
-
127
- const productionAdmobPath = resolve(__dirname, activeAlias);
128
- if (!existsSync(productionAdmobPath)) {
129
- console.error(`❌ FINAL RELEASE BLOCKED: The production @admob file does not exist: ${productionAdmobPath}`);
130
- process.exit(1);
131
- }
132
-
133
- console.log(`✅ Production @admob alias verified: ${activeAlias}`);
134
- }
135
-
136
- // This must be the first release validation so test/no-ad builds cannot proceed.
137
- checkProductionAdmobAlias();
138
-
139
- // Define a mapping between store IDs and store names
140
- /* const storeNames = {
19
+ import fs from 'fs';
20
+ import path from 'path';
21
+
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = dirname(__filename);
24
+
25
+ function stripJavaScriptComments(content) {
26
+ let result = '';
27
+ let state = 'code';
28
+
29
+ for (let index = 0; index < content.length; index++) {
30
+ const character = content[index];
31
+ const nextCharacter = content[index + 1];
32
+
33
+ if (state === 'lineComment') {
34
+ if (character === '\n') {
35
+ result += character;
36
+ state = 'code';
37
+ } else {
38
+ result += ' ';
39
+ }
40
+ continue;
41
+ }
42
+
43
+ if (state === 'blockComment') {
44
+ if (character === '*' && nextCharacter === '/') {
45
+ result += ' ';
46
+ index++;
47
+ state = 'code';
48
+ } else {
49
+ result += character === '\n' ? '\n' : ' ';
50
+ }
51
+ continue;
52
+ }
53
+
54
+ if (state !== 'code') {
55
+ result += character;
56
+
57
+ if (character === '\\') {
58
+ if (nextCharacter !== undefined) {
59
+ result += nextCharacter;
60
+ index++;
61
+ }
62
+ } else if (
63
+ (state === 'singleQuote' && character === "'") ||
64
+ (state === 'doubleQuote' && character === '"') ||
65
+ (state === 'templateLiteral' && character === '`')
66
+ ) {
67
+ state = 'code';
68
+ }
69
+ continue;
70
+ }
71
+
72
+ if (character === '/' && nextCharacter === '/') {
73
+ result += ' ';
74
+ index++;
75
+ state = 'lineComment';
76
+ } else if (character === '/' && nextCharacter === '*') {
77
+ result += ' ';
78
+ index++;
79
+ state = 'blockComment';
80
+ } else {
81
+ result += character;
82
+
83
+ if (character === "'") state = 'singleQuote';
84
+ else if (character === '"') state = 'doubleQuote';
85
+ else if (character === '`') state = 'templateLiteral';
86
+ }
87
+ }
88
+
89
+ return result;
90
+ }
91
+
92
+ function checkProductionAdmobAlias() {
93
+ const possibleConfigFiles = ['vite.config.mjs', 'vite.config.js'];
94
+ const viteConfigPath = possibleConfigFiles
95
+ .map(configFile => resolve(__dirname, configFile))
96
+ .find(filePath => existsSync(filePath));
97
+
98
+ if (!viteConfigPath) {
99
+ console.error('❌ FINAL RELEASE BLOCKED: No vite.config.mjs or vite.config.js file found.');
100
+ process.exit(1);
101
+ }
102
+
103
+ const viteConfigContent = stripJavaScriptComments(readFileSync(viteConfigPath, 'utf8'));
104
+ const aliasPattern = /(['"])@admob\1\s*:\s*path\.resolve\(\s*__dirname\s*,\s*(['"])([^'"\r\n]+)\2\s*\)/g;
105
+ const activeAliases = [...viteConfigContent.matchAll(aliasPattern)].map(match => match[3]);
106
+ const productionAliasPattern = /^\.\/src\/js\/Ads\/admob-emi-nextgen-\d+(?:\.\d+)+\.js$/;
107
+
108
+ if (activeAliases.length === 0) {
109
+ console.error('❌ FINAL RELEASE BLOCKED: The production @admob alias is commented out or missing in the Vite config.');
110
+ console.error(" Enable: '@admob': path.resolve(__dirname, './src/js/Ads/admob-emi-nextgen-x.x.js')");
111
+ process.exit(1);
112
+ }
113
+
114
+ if (activeAliases.length > 1) {
115
+ console.error('❌ FINAL RELEASE BLOCKED: Multiple active @admob aliases were found in the Vite config.');
116
+ console.error(' Keep exactly one active production admob-emi-nextgen-x.x.js alias.');
117
+ process.exit(1);
118
+ }
119
+
120
+ const activeAlias = activeAliases[0];
121
+ if (!productionAliasPattern.test(activeAlias)) {
122
+ console.error(`❌ FINAL RELEASE BLOCKED: @admob currently points to '${activeAlias}'.`);
123
+ console.error(" It must point to './src/js/Ads/admob-emi-nextgen-x.x.js', not a test AdMob file.");
124
+ process.exit(1);
125
+ }
126
+
127
+ const productionAdmobPath = resolve(__dirname, activeAlias);
128
+ if (!existsSync(productionAdmobPath)) {
129
+ console.error(`❌ FINAL RELEASE BLOCKED: The production @admob file does not exist: ${productionAdmobPath}`);
130
+ process.exit(1);
131
+ }
132
+
133
+ console.log(`✅ Production @admob alias verified: ${activeAlias}`);
134
+ }
135
+
136
+ // This must be the first release validation so test/no-ad builds cannot proceed.
137
+ checkProductionAdmobAlias();
138
+
139
+ // Define a mapping between store IDs and store names
140
+ /* const storeNames = {
141
141
  "1": "PlayStore",
142
142
  "2": "SamsungStore",
143
143
  "7": "AmazonStore"
144
144
  }; */
145
145
 
146
146
 
147
- const isOnlyOneAPK = true; // 🔥 change to false for normal behavior
147
+ const isOnlyOneAPK = false; // 🔥 change to false for normal behavior
148
148
 
149
149
  const storeNames = {
150
150
  "1": "PlayStore",
@@ -177,47 +177,89 @@ const androidManifestPath = join("android", "app", "src", "main", "AndroidManife
177
177
  const configPath = path.join(process.cwd(), 'capacitor.config.json');
178
178
  const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
179
179
 
180
- const _appUniqueId = config.android?.APP_UNIQUE_ID;
181
- const _appName=config.appName;
180
+ const _appUniqueId = config.android?.APP_UNIQUE_ID;
181
+ const _appName=config.appName;
182
182
  const _appPackageId=config.appId;
183
183
 
184
- const FLOWCHART_API_APP_ID = 228;
185
- const FLOWCHART_SHARED_PATH = join('public', 'src', 'shared.js');
186
- const FLOWCHART_PLAY_API_BASE = 'https://htmlcodeplay.com/appfiles/128FlowChart';
187
- const FLOWCHART_OTHER_STORE_API_BASE = 'https://htmlcodeplay.com/appfiles/128FlowChart/otherapi';
188
- const FLOWCHART_OTHER_STORE_API_HASH = 'd1a3774529b74206ae2117f9c25f258e485a1f7348620e769db19c2796aa1fdf';
189
- const flowchartSharedOriginal = existsSync(FLOWCHART_SHARED_PATH)
190
- ? readFileSync(FLOWCHART_SHARED_PATH, 'utf8')
191
- : null;
192
-
193
- function applyFlowchartStoreApi(storeId) {
194
- if (String(_appUniqueId) !== String(FLOWCHART_API_APP_ID) || !flowchartSharedOriginal) return false;
195
- const apiBase = String(storeId) === '1' ? FLOWCHART_PLAY_API_BASE : FLOWCHART_OTHER_STORE_API_BASE;
196
- const accessHash = String(storeId) === '1' ? '' : FLOWCHART_OTHER_STORE_API_HASH;
197
- const apiBaseMarker = /const FLOWCHART_API_BASE = "[^"]*";/;
198
- const accessHashMarker = /const FLOWCHART_API_ACCESS_HASH = "[^"]*";/;
199
-
200
- if (!apiBaseMarker.test(flowchartSharedOriginal) || !accessHashMarker.test(flowchartSharedOriginal)) {
201
- throw new Error(`Flowchart API configuration markers were not found in ${FLOWCHART_SHARED_PATH}.`);
184
+ const releaseSyncConfigPath = join(__dirname, 'buildCodeplay', 'release-sync.config.json');
185
+ let releaseSyncConfig = {};
186
+ if (existsSync(releaseSyncConfigPath)) {
187
+ try {
188
+ releaseSyncConfig = JSON.parse(readFileSync(releaseSyncConfigPath, 'utf8'));
189
+ } catch (error) {
190
+ console.warn(`⚠️ Could not read ${releaseSyncConfigPath}: ${error.message}`);
202
191
  }
203
-
204
- const updated = flowchartSharedOriginal
205
- .replace(apiBaseMarker, `const FLOWCHART_API_BASE = "${apiBase}";`)
206
- .replace(accessHashMarker, `const FLOWCHART_API_ACCESS_HASH = "${accessHash}";`);
207
- writeFileSync(FLOWCHART_SHARED_PATH, updated, 'utf8');
208
- console.log(`✅ App 228 API endpoint configured for Store ID ${storeId}.`);
209
- return true;
210
192
  }
193
+ const RELEASE_SYNC_URL = process.env.CODEPLAY_RELEASE_SYNC_URL || releaseSyncConfig.url || 'https://htmlcodeplay.com/codeplay/api/v1/release/sync';
194
+ const RELEASE_SYNC_TOKEN = process.env.CODEPLAY_RELEASE_SYNC_TOKEN || releaseSyncConfig.token || '';
195
+ const RELEASE_PLATFORM_IDS = { PlayStore: 1, SamsungStore: 13, AmazonStore: 17, MiStore: 15, HuaweiStore: 12, OppoStore: 14, VivoStore: 16 };
211
196
 
212
- function restoreFlowchartStoreApi() {
213
- if (String(_appUniqueId) === String(FLOWCHART_API_APP_ID) && flowchartSharedOriginal) {
214
- writeFileSync(FLOWCHART_SHARED_PATH, flowchartSharedOriginal, 'utf8');
197
+ function syncReleaseToCodeplay(storeName, finalVersionName, finalVersionCode, releasedDate) {
198
+ if (!RELEASE_SYNC_TOKEN) {
199
+ console.warn(`⚠️ Codeplay release sync skipped: add a token to ${releaseSyncConfigPath}.`);
200
+ return;
201
+ }
202
+ const payload = JSON.stringify({
203
+ app_name: _appName,
204
+ package_id: _appPackageId,
205
+ version_name: String(finalVersionName),
206
+ version_code: String(finalVersionCode),
207
+ released_date: releasedDate,
208
+ language_id: 0,
209
+ language_name: 'Capacitor+ Framework 7',
210
+ notes: 'Automated release update',
211
+ status: 1,
212
+ folder_name: path.basename(process.cwd()),
213
+ app_unique_id: String(_appUniqueId || ''),
214
+ platform_id: RELEASE_PLATFORM_IDS[storeName],
215
+ });
216
+ try {
217
+ const response = execFileSync('curl.exe', ['-sS', '--fail-with-body', '-X', 'POST', RELEASE_SYNC_URL,
218
+ '-H', 'Content-Type: application/json', '-H', `X-Release-Token: ${RELEASE_SYNC_TOKEN}`, '--data-binary', payload],
219
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
220
+ console.log(`✅ Codeplay sync (${storeName}): ${response.trim()}`);
221
+ } catch (error) {
222
+ console.warn(`⚠️ Codeplay sync failed for ${storeName}; continuing release builds: ${error.message}`);
215
223
  }
216
224
  }
217
225
 
226
+ const FLOWCHART_API_APP_ID = 228;
227
+ const FLOWCHART_SHARED_PATH = join('public', 'src', 'shared.js');
228
+ const FLOWCHART_PLAY_API_BASE = 'https://htmlcodeplay.com/appfiles/128FlowChart';
229
+ const FLOWCHART_OTHER_STORE_API_BASE = 'https://htmlcodeplay.com/appfiles/128FlowChart/otherapi';
230
+ const FLOWCHART_OTHER_STORE_API_HASH = 'd1a3774529b74206ae2117f9c25f258e485a1f7348620e769db19c2796aa1fdf';
231
+ const flowchartSharedOriginal = existsSync(FLOWCHART_SHARED_PATH)
232
+ ? readFileSync(FLOWCHART_SHARED_PATH, 'utf8')
233
+ : null;
234
+
235
+ function applyFlowchartStoreApi(storeId) {
236
+ if (String(_appUniqueId) !== String(FLOWCHART_API_APP_ID) || !flowchartSharedOriginal) return false;
237
+ const apiBase = String(storeId) === '1' ? FLOWCHART_PLAY_API_BASE : FLOWCHART_OTHER_STORE_API_BASE;
238
+ const accessHash = String(storeId) === '1' ? '' : FLOWCHART_OTHER_STORE_API_HASH;
239
+ const apiBaseMarker = /const FLOWCHART_API_BASE = "[^"]*";/;
240
+ const accessHashMarker = /const FLOWCHART_API_ACCESS_HASH = "[^"]*";/;
241
+
242
+ if (!apiBaseMarker.test(flowchartSharedOriginal) || !accessHashMarker.test(flowchartSharedOriginal)) {
243
+ throw new Error(`Flowchart API configuration markers were not found in ${FLOWCHART_SHARED_PATH}.`);
244
+ }
218
245
 
246
+ const updated = flowchartSharedOriginal
247
+ .replace(apiBaseMarker, `const FLOWCHART_API_BASE = "${apiBase}";`)
248
+ .replace(accessHashMarker, `const FLOWCHART_API_ACCESS_HASH = "${accessHash}";`);
249
+ writeFileSync(FLOWCHART_SHARED_PATH, updated, 'utf8');
250
+ console.log(`✅ App 228 API endpoint configured for Store ID ${storeId}.`);
251
+ return true;
252
+ }
253
+
254
+ function restoreFlowchartStoreApi() {
255
+ if (String(_appUniqueId) === String(FLOWCHART_API_APP_ID) && flowchartSharedOriginal) {
256
+ writeFileSync(FLOWCHART_SHARED_PATH, flowchartSharedOriginal, 'utf8');
257
+ }
258
+ }
219
259
 
220
- function fileExists(filePath) {
260
+
261
+
262
+ function fileExists(filePath) {
221
263
  return existsSync(filePath);
222
264
  }
223
265
 
@@ -679,7 +721,7 @@ const gradleFilePath = join("android", "app", "build.gradle");
679
721
  const gradleContent = readFileSync(gradleFilePath, 'utf8');
680
722
 
681
723
  // Return the character range of a Gradle block, allowing nested blocks inside it.
682
- function findGradleBlock(content, headerPattern) {
724
+ function findGradleBlock(content, headerPattern) {
683
725
  const headerMatch = content.match(headerPattern);
684
726
  if (!headerMatch) return null;
685
727
  const openingBrace = content.indexOf('{', headerMatch.index + headerMatch[0].length);
@@ -697,43 +739,43 @@ function findGradleBlock(content, headerPattern) {
697
739
  else if (character === '{') depth += 1;
698
740
  else if (character === '}' && --depth === 0) return { start: openingBrace + 1, end: index };
699
741
  }
700
- return null;
701
- }
702
-
703
- const amazonWindowExtensionsDependency = 'implementation "androidx.window.extensions.core:core:1.0.0"';
704
- const amazonWindowExtensionsDependencyPattern = /^[ \t]*implementation\s+["']androidx\.window\.extensions\.core:core:[^"']+["'][ \t]*(?:\r?\n)?/gm;
705
-
706
- function setAmazonWindowExtensionsDependency(enabled, { cleanup = false } = {}) {
707
- const currentGradleContent = readFileSync(gradleFilePath, 'utf8');
708
- let nextGradleContent = currentGradleContent.replace(amazonWindowExtensionsDependencyPattern, '');
709
-
710
- if (enabled) {
711
- const dependenciesBlock = findGradleBlock(nextGradleContent, /^\s*dependencies\s*/m);
712
- if (!dependenciesBlock) {
713
- throw new Error(`The dependencies block was not found in ${gradleFilePath}.`);
714
- }
715
-
716
- const newline = nextGradleContent.includes('\r\n') ? '\r\n' : '\n';
717
- const contentBeforeClosingBrace = nextGradleContent.slice(0, dependenciesBlock.end);
718
- const linePrefix = contentBeforeClosingBrace.endsWith('\n') ? '' : newline;
719
- nextGradleContent = contentBeforeClosingBrace
720
- + linePrefix
721
- + ` ${amazonWindowExtensionsDependency}${newline}`
722
- + nextGradleContent.slice(dependenciesBlock.end);
723
- }
724
-
725
- if (nextGradleContent !== currentGradleContent) {
726
- writeFileSync(gradleFilePath, nextGradleContent, 'utf8');
727
- }
728
-
729
- console.log(enabled
730
- ? '✅ Added Amazon Fire WebView compatibility dependency for Store ID 7.'
731
- : cleanup
732
- ? 'ℹ️ Removed temporary Amazon Fire WebView compatibility dependency after build cleanup.'
733
- : 'ℹ️ Amazon Fire WebView compatibility dependency is not included for this build.');
734
- }
735
-
736
- const versionCodeMatch = gradleContent.match(/versionCode\s+(\d+)/);
742
+ return null;
743
+ }
744
+
745
+ const amazonWindowExtensionsDependency = 'implementation "androidx.window.extensions.core:core:1.0.0"';
746
+ const amazonWindowExtensionsDependencyPattern = /^[ \t]*implementation\s+["']androidx\.window\.extensions\.core:core:[^"']+["'][ \t]*(?:\r?\n)?/gm;
747
+
748
+ function setAmazonWindowExtensionsDependency(enabled, { cleanup = false } = {}) {
749
+ const currentGradleContent = readFileSync(gradleFilePath, 'utf8');
750
+ let nextGradleContent = currentGradleContent.replace(amazonWindowExtensionsDependencyPattern, '');
751
+
752
+ if (enabled) {
753
+ const dependenciesBlock = findGradleBlock(nextGradleContent, /^\s*dependencies\s*/m);
754
+ if (!dependenciesBlock) {
755
+ throw new Error(`The dependencies block was not found in ${gradleFilePath}.`);
756
+ }
757
+
758
+ const newline = nextGradleContent.includes('\r\n') ? '\r\n' : '\n';
759
+ const contentBeforeClosingBrace = nextGradleContent.slice(0, dependenciesBlock.end);
760
+ const linePrefix = contentBeforeClosingBrace.endsWith('\n') ? '' : newline;
761
+ nextGradleContent = contentBeforeClosingBrace
762
+ + linePrefix
763
+ + ` ${amazonWindowExtensionsDependency}${newline}`
764
+ + nextGradleContent.slice(dependenciesBlock.end);
765
+ }
766
+
767
+ if (nextGradleContent !== currentGradleContent) {
768
+ writeFileSync(gradleFilePath, nextGradleContent, 'utf8');
769
+ }
770
+
771
+ console.log(enabled
772
+ ? '✅ Added Amazon Fire WebView compatibility dependency for Store ID 7.'
773
+ : cleanup
774
+ ? 'ℹ️ Removed temporary Amazon Fire WebView compatibility dependency after build cleanup.'
775
+ : 'ℹ️ Amazon Fire WebView compatibility dependency is not included for this build.');
776
+ }
777
+
778
+ const versionCodeMatch = gradleContent.match(/versionCode\s+(\d+)/);
737
779
  const versionNameMatch = gradleContent.match(/versionName\s+"([^"]+)"/);
738
780
 
739
781
  const versionCode = versionCodeMatch ? versionCodeMatch[1] : '';
@@ -885,11 +927,11 @@ rl.question('Enter new versionCode (press enter to keep current): ', (newVersion
885
927
  if (!originalMinSdkVersion) {
886
928
  originalMinSdkVersion = currentMinSdkVersion;
887
929
  }
888
- try {
889
- applyFlowchartStoreApi(id);
890
- const shouldIncludeAmazonWindowExtensions = String(id) === '7' && amazon === true;
891
- setAmazonWindowExtensionsDependency(shouldIncludeAmazonWindowExtensions);
892
- // Modify the minSdkVersion based on the store
930
+ try {
931
+ applyFlowchartStoreApi(id);
932
+ const shouldIncludeAmazonWindowExtensions = String(id) === '7' && amazon === true;
933
+ setAmazonWindowExtensionsDependency(shouldIncludeAmazonWindowExtensions);
934
+ // Modify the minSdkVersion based on the store
893
935
 
894
936
 
895
937
  if(currentMinSdkVersion==23 || currentMinSdkVersion==24)
@@ -994,26 +1036,27 @@ rl.question('Enter new versionCode (press enter to keep current): ', (newVersion
994
1036
  const checkFullPath = join("AAB", newFileName.replace(/\.aab$/, `.${newExt}`));
995
1037
 
996
1038
  // Rename the output file
997
- if (existsSync(oldFilePath)) {
998
- renameSync(oldFilePath, checkFullPath);
999
- console.log(`✅ Renamed output ${newExt.toUpperCase()} file to: ${path.basename(checkFullPath)}`);
1000
- } else {
1039
+ if (existsSync(oldFilePath)) {
1040
+ renameSync(oldFilePath, checkFullPath);
1041
+ console.log(`✅ Renamed output ${newExt.toUpperCase()} file to: ${path.basename(checkFullPath)}`);
1042
+ syncReleaseToCodeplay(storeName, finalVersionName, finalVersionCode, new Date().toISOString().slice(0, 10));
1043
+ } else {
1001
1044
  console.error(`❌ ${newExt.toUpperCase()} file not found after build.`);
1002
1045
  }
1003
1046
 
1004
- } catch (error) {
1005
- console.error(`❌ Error during build for Store ID ${id}:`, error);
1006
- restoreFlowchartStoreApi();
1007
- setAmazonWindowExtensionsDependency(false, {
1008
- cleanup: String(id) === '7' && amazon === true,
1009
- });
1010
- process.exit(1);
1011
- } finally {
1012
- restoreFlowchartStoreApi();
1013
- setAmazonWindowExtensionsDependency(false, {
1014
- cleanup: String(id) === '7' && amazon === true,
1015
- });
1016
- }
1047
+ } catch (error) {
1048
+ console.error(`❌ Error during build for Store ID ${id}:`, error);
1049
+ restoreFlowchartStoreApi();
1050
+ setAmazonWindowExtensionsDependency(false, {
1051
+ cleanup: String(id) === '7' && amazon === true,
1052
+ });
1053
+ process.exit(1);
1054
+ } finally {
1055
+ restoreFlowchartStoreApi();
1056
+ setAmazonWindowExtensionsDependency(false, {
1057
+ cleanup: String(id) === '7' && amazon === true,
1058
+ });
1059
+ }
1017
1060
  });
1018
1061
 
1019
1062
  rl.close(); // Close the readline interface after all operations
@@ -1058,9 +1101,9 @@ function managePackages(store) {
1058
1101
  uninstall = 'cordova-plugin-samsungiap';
1059
1102
 
1060
1103
  // Update AndroidManifest.xml for PlayStore
1061
- if (playstore && store === "PlayStore")
1062
- updateAndroidManifest(store,
1063
- '<uses-permission android:name="com.android.vending.BILLING" />');
1104
+ if (playstore && store === "PlayStore")
1105
+ updateAndroidManifest(store,
1106
+ '<uses-permission android:name="com.android.vending.BILLING" />');
1064
1107
 
1065
1108
  } else if (samsung && store === "SamsungStore") {
1066
1109
  install = 'cordova-plugin-samsungiap';
@@ -86,8 +86,8 @@ const analysisFrameDirectory = path.join(temporaryDirectory, 'ai-analysis');
86
86
  const endCardBannerPath = path.join(outputVideoDirectory, `${appFilePrefix}-ai-end-card-banner.png`);
87
87
  const promoCopySchemaPath = path.join(temporaryDirectory, 'promo-copy-schema.json');
88
88
  const promoCopyResultPath = path.join(temporaryDirectory, 'promo-copy-result.json');
89
- const ffmpegFallbackDirectory = String(process.env.FFMPEG_BIN || '').trim()
90
- || path.join(projectDirectory, 'tools', 'ffmpeg');
89
+ const ffmpegFallbackDirectory = String(process.env.FFMPEG_BIN || '').trim()
90
+ || path.join(projectDirectory, 'tools', 'ffmpeg');
91
91
  const minimumSceneCount = 7;
92
92
  const minimumSourceDuration = 12;
93
93
  let expectedDuration = 34;
@@ -209,15 +209,15 @@ async function ensureEndCardBanner() {
209
209
  + '- Keep all logos and badge text sharp, correctly proportioned, and completely visible.\n'
210
210
  + '- Match the background colors and visual style to the actual app icon.\n'
211
211
  + '- Create a polished, professional background related to the app’s purpose.\n'
212
- + '- Keep the background subtle so it does not compete with the icon, title, feature text, or download badges.\n'
212
+ + '- Keep the background subtle so it does not compete with the icon, title, feature text, or download badges.\n'
213
213
  + '- Use a balanced landscape composition:\n'
214
214
  + ' - Large actual app icon on the left.\n'
215
215
  + ' - App Store badge followed by Google Play badge on the right.\n'
216
216
  + ' - Maintain comfortable margins and clear space around every element.\n'
217
- + '- Add the application title prominently.\n'
218
- + '- Add concise promotional text describing the application’s actual features, presenting each feature one by one.\n'
219
- + '- Keep the title and feature text sharp, readable, accurate, and visually balanced with the icon and download badges.\n'
220
- + '- Do not invent unsupported features or add unrelated marketing text.\n'
217
+ + '- Add the application title prominently.\n'
218
+ + '- Add concise promotional text describing the application’s actual features, presenting each feature one by one.\n'
219
+ + '- Keep the title and feature text sharp, readable, accurate, and visually balanced with the icon and download badges.\n'
220
+ + '- Do not invent unsupported features or add unrelated marketing text.\n'
221
221
  + '- Do not add watermarks, fake UI, generated symbols, or unrelated decorations.\n'
222
222
  + '- Rounded corners and a subtle shadow may be applied around the app icon, but its internal artwork must remain unchanged.\n'
223
223
  + '- Use image generation only for the background.\n'
@@ -467,12 +467,12 @@ function commandExists(command) {
467
467
  return !result.error && result.status === 0;
468
468
  }
469
469
 
470
- function getExecutable(command) {
471
- const executableName = process.platform === 'win32' ? `${command}.exe` : command;
472
- const fallbackPath = path.join(ffmpegFallbackDirectory, executableName);
473
- if (fs.existsSync(fallbackPath)) return fallbackPath;
474
- return commandExists(command) ? command : '';
475
- }
470
+ function getExecutable(command) {
471
+ const executableName = process.platform === 'win32' ? `${command}.exe` : command;
472
+ const fallbackPath = path.join(ffmpegFallbackDirectory, executableName);
473
+ if (fs.existsSync(fallbackPath)) return fallbackPath;
474
+ return commandExists(command) ? command : '';
475
+ }
476
476
 
477
477
  function detectRedundantStaticRanges(ffmpegCommand, inputPath, sourceDuration) {
478
478
  const minimumStaticDuration = Math.max(2, Math.min(4, sourceDuration / 60));
@@ -640,7 +640,7 @@ function resolvePromoLayout(requestedLayout) {
640
640
  }
641
641
 
642
642
  function getPythonExecutable() {
643
- for (const candidate of ['py', 'python', 'python3']) {
643
+ for (const candidate of ['py', 'python', 'python3']) {
644
644
  const result = spawnSync(candidate, ['-m', 'edge_tts', '--version'], {
645
645
  encoding: 'utf8',
646
646
  windowsHide: true,
@@ -915,7 +915,7 @@ Rules:
915
915
  - footer_keywords must contain exactly 3 distinct, natural app-search phrases, each 2-24 characters.
916
916
  - Generate a YouTube upload title and description from the same verified features.
917
917
  - YouTube title: 40-100 characters, include ${JSON.stringify(appName)} and one strong natural search phrase, without clickbait.
918
- - YouTube description: 350-1500 characters with readable paragraphs. Put the clearest app summary and primary search phrase in the opening two lines, explain the demonstrated features, add a simple call to action, then insert a newline and finish with exactly 3 relevant hashtags on that separate final line. The hashtags must not be appended to the preceding paragraph.
918
+ - YouTube description: 350-1500 characters with readable paragraphs. Put the clearest app summary and primary search phrase in the opening two lines, explain the demonstrated features, add a simple call to action, then insert a newline and finish with exactly 3 relevant hashtags on that separate final line. The hashtags must not be appended to the preceding paragraph.
919
919
  - Do not add download links or URLs; the script will append the verified Google Play URL built from the configured package ID. Do not add chapter timestamps or unsupported availability claims.
920
920
  - Keep YouTube text natural and useful; do not repeat keywords unnaturally.
921
921
  - Avoid prices, ratings, awards, competitor names, unsupported superlatives, and words such as best, guaranteed, or free.
@@ -1025,14 +1025,14 @@ function validatePromoCopy(value) {
1025
1025
  throw new Error(`The final scene must mention the app name ${JSON.stringify(appName)}.`);
1026
1026
  }
1027
1027
 
1028
- const youtubeTitle = cleanGeneratedString(value.youtube.title);
1029
- let youtubeDescription = cleanGeneratedDescription(value.youtube.description);
1030
- const trailingHashtagGroup = youtubeDescription.match(
1031
- /(?:^|\s)(#[\p{L}\p{N}_]+\s+#[\p{L}\p{N}_]+\s+#[\p{L}\p{N}_]+)\s*$/u,
1032
- );
1033
- if (trailingHashtagGroup) {
1034
- youtubeDescription = `${youtubeDescription.slice(0, trailingHashtagGroup.index).trimEnd()}\n${trailingHashtagGroup[1]}`;
1035
- }
1028
+ const youtubeTitle = cleanGeneratedString(value.youtube.title);
1029
+ let youtubeDescription = cleanGeneratedDescription(value.youtube.description);
1030
+ const trailingHashtagGroup = youtubeDescription.match(
1031
+ /(?:^|\s)(#[\p{L}\p{N}_]+\s+#[\p{L}\p{N}_]+\s+#[\p{L}\p{N}_]+)\s*$/u,
1032
+ );
1033
+ if (trailingHashtagGroup) {
1034
+ youtubeDescription = `${youtubeDescription.slice(0, trailingHashtagGroup.index).trimEnd()}\n${trailingHashtagGroup[1]}`;
1035
+ }
1036
1036
  if (youtubeTitle.length < 40 || youtubeTitle.length > 100) {
1037
1037
  throw new Error('YouTube title must be 40-100 characters.');
1038
1038
  }