maiass 5.9.23 → 5.9.24
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/lib/bootstrap.js +102 -35
- package/lib/commit.js +6 -11
- package/lib/devlog.js +13 -9
- package/lib/secure-storage.js +22 -23
- package/lib/version-manager.js +82 -6
- package/package.json +1 -1
package/lib/bootstrap.js
CHANGED
|
@@ -58,6 +58,11 @@ function loadExistingValues() {
|
|
|
58
58
|
* @returns {string} Detected project type
|
|
59
59
|
*/
|
|
60
60
|
function detectProjectType() {
|
|
61
|
+
// Check for Swift/Xcode project — search root and one level deep
|
|
62
|
+
if (detectPbxproj() || fs.existsSync('Package.swift')) {
|
|
63
|
+
return 'swift';
|
|
64
|
+
}
|
|
65
|
+
|
|
61
66
|
// Check for WordPress
|
|
62
67
|
if (fs.existsSync('wp-config.php') || fs.existsSync('wp-content')) {
|
|
63
68
|
if (fs.existsSync('style.css')) {
|
|
@@ -66,9 +71,7 @@ function detectProjectType() {
|
|
|
66
71
|
return 'wordpress-theme';
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
|
-
|
|
70
|
-
const files = fs.readdirSync('.');
|
|
71
|
-
for (const file of files) {
|
|
74
|
+
for (const file of fs.readdirSync('.')) {
|
|
72
75
|
if (file.endsWith('.php')) {
|
|
73
76
|
const content = fs.readFileSync(file, 'utf8');
|
|
74
77
|
if (content.includes('Plugin Name:')) {
|
|
@@ -78,40 +81,77 @@ function detectProjectType() {
|
|
|
78
81
|
}
|
|
79
82
|
return 'wordpress-site';
|
|
80
83
|
}
|
|
81
|
-
|
|
84
|
+
|
|
82
85
|
// Check for Craft CMS
|
|
83
86
|
if (fs.existsSync('craft') || fs.existsSync('config/general.php')) {
|
|
84
87
|
return 'craft';
|
|
85
88
|
}
|
|
86
|
-
|
|
89
|
+
|
|
87
90
|
// Default to bespoke
|
|
88
91
|
return 'bespoke';
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Find the .pbxproj file by searching the current directory and one level of subdirectories.
|
|
96
|
+
* Xcode projects are often nested: ProjectRoot/MyApp/MyApp.xcodeproj/project.pbxproj
|
|
97
|
+
* Skips hidden dirs, node_modules, and build artefact folders.
|
|
98
|
+
* @returns {string|null} Relative path to the .pbxproj, or null
|
|
99
|
+
*/
|
|
100
|
+
function detectPbxproj() {
|
|
101
|
+
const SKIP = new Set(['node_modules', '.git', 'build', 'dist', 'Pods', 'DerivedData']);
|
|
102
|
+
|
|
103
|
+
function findInDir(dir, depth = 0) {
|
|
104
|
+
try {
|
|
105
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
106
|
+
// Check for .xcodeproj bundles at this level
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (entry.isDirectory() && entry.name.endsWith('.xcodeproj')) {
|
|
109
|
+
const pbxproj = path.join(dir, entry.name, 'project.pbxproj');
|
|
110
|
+
if (fs.existsSync(pbxproj)) {
|
|
111
|
+
// Return relative to cwd
|
|
112
|
+
return path.relative(process.cwd(), pbxproj);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Recurse one level into non-skipped subdirectories
|
|
117
|
+
if (depth < 2) {
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
if (entry.isDirectory() && !SKIP.has(entry.name) && !entry.name.startsWith('.')) {
|
|
120
|
+
const found = findInDir(path.join(dir, entry.name), depth + 1);
|
|
121
|
+
if (found) return found;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch {}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return findInDir(process.cwd());
|
|
130
|
+
}
|
|
131
|
+
|
|
91
132
|
/**
|
|
92
133
|
* Detect version source file
|
|
93
134
|
* @returns {string} Detected version file
|
|
94
135
|
*/
|
|
95
136
|
function detectVersionSource() {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (fs.existsSync('VERSION'))
|
|
103
|
-
return 'VERSION';
|
|
104
|
-
}
|
|
137
|
+
// Swift: prefer .pbxproj over anything else
|
|
138
|
+
const pbxproj = detectPbxproj();
|
|
139
|
+
if (pbxproj) return pbxproj;
|
|
140
|
+
|
|
141
|
+
if (fs.existsSync('package.json')) return 'package.json';
|
|
142
|
+
if (fs.existsSync('composer.json')) return 'composer.json';
|
|
143
|
+
if (fs.existsSync('VERSION')) return 'VERSION';
|
|
105
144
|
return 'package.json'; // Default
|
|
106
145
|
}
|
|
107
146
|
|
|
108
147
|
/**
|
|
109
148
|
* Infer version file type from filename/extension
|
|
110
149
|
* @param {string} filename - Version file name or path
|
|
111
|
-
* @returns {string} File type: 'json', 'php', or '
|
|
150
|
+
* @returns {string} File type: 'json', 'php', 'txt', or 'xcodeproj'
|
|
112
151
|
*/
|
|
113
152
|
function inferVersionFileType(filename) {
|
|
114
153
|
if (!filename) return 'txt';
|
|
154
|
+
if (filename.endsWith('.pbxproj')) return 'xcodeproj';
|
|
115
155
|
if (filename.endsWith('.json')) return 'json';
|
|
116
156
|
if (filename.endsWith('.php')) return 'php';
|
|
117
157
|
if (filename.endsWith('.css')) return 'php'; // CSS version headers use same pattern as PHP
|
|
@@ -155,7 +195,7 @@ export async function bootstrapProject() {
|
|
|
155
195
|
config.projectType = await configureProjectType(existing);
|
|
156
196
|
|
|
157
197
|
// Step 3: Version source
|
|
158
|
-
config.versionSource = await configureVersionSource(existing);
|
|
198
|
+
config.versionSource = await configureVersionSource(existing, config.projectType);
|
|
159
199
|
|
|
160
200
|
// Step 4: Features selection
|
|
161
201
|
config.features = await chooseFeatures(existing);
|
|
@@ -236,14 +276,15 @@ async function configureProjectType(existing) {
|
|
|
236
276
|
console.log(` ${colors.BCyan('3)')} wordpress-plugin - WordPress plugin with main PHP file versioning`);
|
|
237
277
|
console.log(` ${colors.BCyan('4)')} wordpress-site - Full WordPress installation`);
|
|
238
278
|
console.log(` ${colors.BCyan('5)')} craft - Craft CMS project`);
|
|
279
|
+
console.log(` ${colors.BCyan('6)')} swift - Swift/Xcode app (.pbxproj versioning)`);
|
|
239
280
|
console.log('');
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
const
|
|
243
|
-
|
|
281
|
+
|
|
282
|
+
const types = ['bespoke', 'wordpress-theme', 'wordpress-plugin', 'wordpress-site', 'craft', 'swift'];
|
|
283
|
+
const defaultChoice = types.indexOf(current) + 1 || 1;
|
|
284
|
+
const choice = await getLineInput(`Select project type [1-6, Enter for ${defaultChoice}=${current}]: `);
|
|
285
|
+
|
|
244
286
|
if (!choice) return current;
|
|
245
|
-
|
|
246
|
-
const types = ['bespoke', 'wordpress-theme', 'wordpress-plugin', 'wordpress-site', 'craft'];
|
|
287
|
+
|
|
247
288
|
const index = parseInt(choice) - 1;
|
|
248
289
|
|
|
249
290
|
if (index >= 0 && index < types.length) {
|
|
@@ -258,27 +299,53 @@ async function configureProjectType(existing) {
|
|
|
258
299
|
/**
|
|
259
300
|
* Step 3: Configure version source
|
|
260
301
|
*/
|
|
261
|
-
async function configureVersionSource(existing) {
|
|
302
|
+
async function configureVersionSource(existing, projectType = 'bespoke') {
|
|
262
303
|
console.log('');
|
|
263
304
|
console.log(colors.BCyan('📦 Version Source Configuration'));
|
|
264
305
|
console.log('');
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
306
|
+
|
|
307
|
+
const isSwift = projectType === 'swift';
|
|
308
|
+
const pbxproj = detectPbxproj();
|
|
309
|
+
|
|
310
|
+
// For Swift, use the found .pbxproj as default; fall back to non-Swift detection otherwise
|
|
311
|
+
const detected = isSwift ? (pbxproj || null) : detectVersionSource();
|
|
312
|
+
// When project type is swift and a pbxproj was found, only keep the existing setting
|
|
313
|
+
// if it's also a .pbxproj (i.e. don't let a stale 'VERSION' or 'package.json' override it)
|
|
314
|
+
const existingIsCompatible = !isSwift || !pbxproj || (existing.MAIASS_VERSION_PRIMARY_FILE || '').endsWith('.pbxproj');
|
|
315
|
+
const current = (existingIsCompatible ? existing.MAIASS_VERSION_PRIMARY_FILE : null) || detected || '';
|
|
316
|
+
|
|
269
317
|
console.log('MAIASS needs to know where your project version is stored.');
|
|
270
318
|
console.log('This file will be updated automatically when you bump versions.');
|
|
271
319
|
console.log('');
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
console.log(
|
|
320
|
+
|
|
321
|
+
if (isSwift) {
|
|
322
|
+
console.log(colors.BCyan('🍎 Swift/Xcode versioning'));
|
|
323
|
+
console.log(colors.Gray(' MAIASS manages MARKETING_VERSION (e.g. 1.2.3) in your .pbxproj.'));
|
|
324
|
+
console.log(colors.Gray(' CURRENT_PROJECT_VERSION (build number) is auto-incremented on each bump.'));
|
|
325
|
+
console.log(colors.Gray(' Both are read by Xcode directly — no Info.plist edits needed.'));
|
|
326
|
+
console.log('');
|
|
327
|
+
if (pbxproj) {
|
|
328
|
+
console.log(`${SYMBOLS.CHECKMARK} Found: ${colors.BGreen(pbxproj)}`);
|
|
329
|
+
} else {
|
|
330
|
+
console.log(`${SYMBOLS.WARNING} ${colors.BYellow('No .xcodeproj found in this directory.')}`);
|
|
331
|
+
console.log(colors.Gray(' Enter the path manually, e.g. MyApp.xcodeproj/project.pbxproj'));
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
if (detected) {
|
|
335
|
+
console.log(`${SYMBOLS.INFO} Detected version file: ${colors.BGreen(detected)}`);
|
|
336
|
+
}
|
|
337
|
+
if (existing.MAIASS_VERSION_PRIMARY_FILE) {
|
|
338
|
+
console.log(`${SYMBOLS.INFO} Current setting: ${colors.BGreen(existing.MAIASS_VERSION_PRIMARY_FILE)}`);
|
|
339
|
+
}
|
|
340
|
+
console.log(colors.Gray('Common options: package.json, composer.json, VERSION, style.css'));
|
|
276
341
|
}
|
|
277
342
|
console.log('');
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
343
|
+
|
|
344
|
+
const prompt = current
|
|
345
|
+
? `Version source file [Enter for ${current}]: `
|
|
346
|
+
: `Version source file: `;
|
|
347
|
+
const file = await getLineInput(prompt);
|
|
348
|
+
|
|
282
349
|
const result = file || current;
|
|
283
350
|
if (file) {
|
|
284
351
|
console.log(`${SYMBOLS.CHECKMARK} Version source set to: ${colors.BGreen(result)}`);
|
package/lib/commit.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// Commit functionality for MAIASS - port of maiass.sh commit behavior
|
|
2
2
|
import { execSync } from 'child_process';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
3
6
|
import { log, redact } from './logger.js';
|
|
4
7
|
import { SYMBOLS } from './symbols.js';
|
|
5
8
|
import { getGitInfo, getGitStatus } from './git-info.js';
|
|
@@ -823,20 +826,12 @@ async function handleStagedCommit(gitInfo, options = {}) {
|
|
|
823
826
|
if (jiraTicket && finalCommitMessage && !finalCommitMessage.startsWith(jiraTicket)) {
|
|
824
827
|
finalCommitMessage = `${jiraTicket} ${finalCommitMessage}`;
|
|
825
828
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
const fs = (await import('fs')).default;
|
|
829
|
-
const os = (await import('os')).default;
|
|
830
|
-
const path = (await import('path')).default;
|
|
829
|
+
// Write commit message to a temp file on all platforms — avoids shell quoting and injection risks
|
|
830
|
+
{
|
|
831
831
|
const tmpFile = path.join(os.tmpdir(), `maiass-commit-msg-${Date.now()}.txt`);
|
|
832
832
|
fs.writeFileSync(tmpFile, finalCommitMessage, { encoding: 'utf8' });
|
|
833
|
-
|
|
834
|
-
result = executeGitCommand(commitCommand, quietMode);
|
|
833
|
+
result = executeGitCommand(`git commit -F "${tmpFile}"`, quietMode);
|
|
835
834
|
fs.unlinkSync(tmpFile);
|
|
836
|
-
} else {
|
|
837
|
-
// Use echo/pipe for non-Windows
|
|
838
|
-
commitCommand = `echo ${JSON.stringify(commitMessage)} | git commit -F -`;
|
|
839
|
-
result = executeGitCommand(commitCommand, quietMode);
|
|
840
835
|
}
|
|
841
836
|
|
|
842
837
|
if (result === null) {
|
package/lib/devlog.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Development logging utility for MAIASS
|
|
2
2
|
// Node.js equivalent of the devlog.sh integration from maiass.sh
|
|
3
|
-
import {
|
|
3
|
+
import { execFile, execSync } from 'child_process';
|
|
4
4
|
import { existsSync } from 'fs';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import colors from './colors.js';
|
|
@@ -89,20 +89,24 @@ export function logThis(message, options = {}) {
|
|
|
89
89
|
return null;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
//
|
|
93
|
-
const
|
|
92
|
+
// Normalise message for single-line logging (no shell escaping needed — args array is used)
|
|
93
|
+
const normalisedMessage = message.replace(/\n/g, '; ');
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
// Build args array — no shell interpolation, so no injection risk
|
|
96
|
+
let executable, args;
|
|
96
97
|
if (process.platform === 'win32') {
|
|
97
|
-
|
|
98
|
+
executable = 'powershell.exe';
|
|
99
|
+
args = ['-ExecutionPolicy', 'Bypass', '-NonInteractive', '-Command',
|
|
100
|
+
`devlog -s '${normalisedMessage.replace(/'/g, "''")}' '?' '${project}' '${client}' '${jiraTicket}' '${subClient}'`];
|
|
98
101
|
} else {
|
|
99
|
-
|
|
102
|
+
executable = 'devlog.sh';
|
|
103
|
+
args = [normalisedMessage, '?', project, client, jiraTicket, subClient];
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
logger.debug(`Executing devlog
|
|
103
|
-
|
|
106
|
+
logger.debug(`Executing devlog: ${executable} ${args.join(' ')}`);
|
|
107
|
+
|
|
104
108
|
// Execute asynchronously - don't block the main workflow (fire-and-forget)
|
|
105
|
-
|
|
109
|
+
execFile(executable, args, { encoding: 'utf8' }, (error, stdout, stderr) => {
|
|
106
110
|
if (error) {
|
|
107
111
|
|
|
108
112
|
if (process.env.MAIASS_DEBUG === 'true') {
|
package/lib/secure-storage.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Windows uses encrypted file storage in AppData
|
|
4
4
|
// Compatible with bashmaiass approach but uses NODEMAIASS service names
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { execFileSync, spawnSync } from 'child_process';
|
|
7
7
|
import os from 'os';
|
|
8
8
|
import fs from 'fs';
|
|
9
9
|
import path from 'path';
|
|
@@ -98,8 +98,8 @@ export function storeSecureVariable(varName, varValue) {
|
|
|
98
98
|
|
|
99
99
|
try {
|
|
100
100
|
if (os.platform() === 'darwin') {
|
|
101
|
-
// macOS: Use keychain via security command
|
|
102
|
-
|
|
101
|
+
// macOS: Use keychain via security command — args array avoids shell injection
|
|
102
|
+
execFileSync('security', ['add-generic-password', '-U', '-s', serviceName, '-a', varName, '-w', varValue], {
|
|
103
103
|
stdio: 'pipe'
|
|
104
104
|
});
|
|
105
105
|
} else if (os.platform() === 'win32') {
|
|
@@ -139,12 +139,12 @@ export function storeSecureVariable(varName, varValue) {
|
|
|
139
139
|
logger.debug(`Stored ${varName} in Windows secure storage (encrypted file)`);
|
|
140
140
|
}
|
|
141
141
|
} else {
|
|
142
|
-
// Linux: Use secret-tool if available
|
|
142
|
+
// Linux: Use secret-tool if available — spawnSync with input avoids shell + pipe injection
|
|
143
143
|
try {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
execFileSync('which', ['secret-tool'], { stdio: 'pipe' });
|
|
145
|
+
spawnSync('secret-tool', ['store', '--label', `NODEMAIASS ${varName} (${serviceName})`, 'service', serviceName, 'key', varName], {
|
|
146
|
+
input: varValue,
|
|
147
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
148
148
|
});
|
|
149
149
|
} catch (error) {
|
|
150
150
|
if (debugMode) {
|
|
@@ -183,8 +183,8 @@ export function retrieveSecureVariable(varName) {
|
|
|
183
183
|
let value = null;
|
|
184
184
|
|
|
185
185
|
if (os.platform() === 'darwin') {
|
|
186
|
-
// macOS: Use keychain via security command
|
|
187
|
-
value =
|
|
186
|
+
// macOS: Use keychain via security command — args array avoids shell injection
|
|
187
|
+
value = execFileSync('security', ['find-generic-password', '-s', serviceName, '-a', varName, '-w'], {
|
|
188
188
|
stdio: 'pipe',
|
|
189
189
|
encoding: 'utf8'
|
|
190
190
|
}).trim();
|
|
@@ -216,10 +216,10 @@ export function retrieveSecureVariable(varName) {
|
|
|
216
216
|
return null;
|
|
217
217
|
}
|
|
218
218
|
} else {
|
|
219
|
-
// Linux: Use secret-tool if available
|
|
219
|
+
// Linux: Use secret-tool if available — args array avoids shell injection
|
|
220
220
|
try {
|
|
221
|
-
|
|
222
|
-
value =
|
|
221
|
+
execFileSync('which', ['secret-tool'], { stdio: 'pipe' });
|
|
222
|
+
value = execFileSync('secret-tool', ['lookup', 'service', serviceName, 'key', varName], {
|
|
223
223
|
stdio: 'pipe',
|
|
224
224
|
encoding: 'utf8'
|
|
225
225
|
}).trim();
|
|
@@ -259,8 +259,8 @@ export function removeSecureVariable(varName) {
|
|
|
259
259
|
|
|
260
260
|
try {
|
|
261
261
|
if (os.platform() === 'darwin') {
|
|
262
|
-
// macOS: Use keychain via security command
|
|
263
|
-
|
|
262
|
+
// macOS: Use keychain via security command — args array avoids shell injection
|
|
263
|
+
execFileSync('security', ['delete-generic-password', '-s', serviceName, '-a', varName], {
|
|
264
264
|
stdio: 'pipe'
|
|
265
265
|
});
|
|
266
266
|
} else if (os.platform() === 'win32') {
|
|
@@ -302,13 +302,12 @@ export function removeSecureVariable(varName) {
|
|
|
302
302
|
return false;
|
|
303
303
|
}
|
|
304
304
|
} else {
|
|
305
|
-
// Linux: secret-tool doesn't have direct delete,
|
|
305
|
+
// Linux: secret-tool doesn't have direct delete, so store empty value — spawnSync with input avoids shell injection
|
|
306
306
|
try {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
stdio: 'pipe',
|
|
311
|
-
shell: true
|
|
307
|
+
execFileSync('which', ['secret-tool'], { stdio: 'pipe' });
|
|
308
|
+
spawnSync('secret-tool', ['store', '--label', `NODEMAIASS ${varName} (${serviceName})`, 'service', serviceName, 'key', varName], {
|
|
309
|
+
input: '',
|
|
310
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
312
311
|
});
|
|
313
312
|
} catch (error) {
|
|
314
313
|
if (debugMode) {
|
|
@@ -391,14 +390,14 @@ export function loadSecureVariables() {
|
|
|
391
390
|
export function isSecureStorageAvailable() {
|
|
392
391
|
try {
|
|
393
392
|
if (os.platform() === 'darwin') {
|
|
394
|
-
|
|
393
|
+
execFileSync('which', ['security'], { stdio: 'pipe' });
|
|
395
394
|
return true;
|
|
396
395
|
} else if (os.platform() === 'win32') {
|
|
397
396
|
// Windows: Always available (uses encrypted file storage)
|
|
398
397
|
return true;
|
|
399
398
|
} else {
|
|
400
399
|
// Linux: Check for secret-tool
|
|
401
|
-
|
|
400
|
+
execFileSync('which', ['secret-tool'], { stdio: 'pipe' });
|
|
402
401
|
return true;
|
|
403
402
|
}
|
|
404
403
|
} catch (error) {
|
package/lib/version-manager.js
CHANGED
|
@@ -84,6 +84,28 @@ const VERSION_FILE_TYPES = {
|
|
|
84
84
|
return content.replace(/^\d+\.\d+\.\d+/, newVersion);
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
|
+
xcodeproj: {
|
|
88
|
+
extensions: ['.pbxproj'],
|
|
89
|
+
detect: (content) => /MARKETING_VERSION\s*=\s*\d+\.\d+(?:\.\d+)?\s*;/.test(content),
|
|
90
|
+
extract: (content) => {
|
|
91
|
+
// Match 1.0 or 1.0.0 style versions
|
|
92
|
+
const match = content.match(/MARKETING_VERSION\s*=\s*(\d+\.\d+(?:\.\d+)?)\s*;/);
|
|
93
|
+
return match ? match[1] : null;
|
|
94
|
+
},
|
|
95
|
+
update: (content, newVersion) => {
|
|
96
|
+
// Bump MARKETING_VERSION everywhere it appears (all targets)
|
|
97
|
+
content = content.replace(
|
|
98
|
+
/(MARKETING_VERSION\s*=\s*)\d+\.\d+(?:\.\d+)?(\s*;)/g,
|
|
99
|
+
`$1${newVersion}$2`
|
|
100
|
+
);
|
|
101
|
+
// Auto-increment CURRENT_PROJECT_VERSION (build number)
|
|
102
|
+
content = content.replace(
|
|
103
|
+
/(CURRENT_PROJECT_VERSION\s*=\s*)(\d+)(\s*;)/g,
|
|
104
|
+
(_, prefix, num, suffix) => `${prefix}${parseInt(num, 10) + 1}${suffix}`
|
|
105
|
+
);
|
|
106
|
+
return content;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
87
109
|
php: {
|
|
88
110
|
extensions: ['.php','pattern'],
|
|
89
111
|
detect: (content) => {
|
|
@@ -125,13 +147,14 @@ const VERSION_FILE_TYPES = {
|
|
|
125
147
|
export function parseVersion(version) {
|
|
126
148
|
if (!version) return null;
|
|
127
149
|
|
|
128
|
-
|
|
150
|
+
// Accept x.y.z or x.y (treat x.y as x.y.0)
|
|
151
|
+
const match = version.match(/^(\d+)\.(\d+)(?:\.(\d+))?(?:-(.+))?$/);
|
|
129
152
|
if (!match) return null;
|
|
130
|
-
|
|
153
|
+
|
|
131
154
|
return {
|
|
132
155
|
major: parseInt(match[1], 10),
|
|
133
156
|
minor: parseInt(match[2], 10),
|
|
134
|
-
patch: parseInt(match[3], 10),
|
|
157
|
+
patch: match[3] !== undefined ? parseInt(match[3], 10) : 0,
|
|
135
158
|
prerelease: match[4] || null,
|
|
136
159
|
raw: version
|
|
137
160
|
};
|
|
@@ -543,7 +566,11 @@ function extractVersionByType(content, type, lineStart) {
|
|
|
543
566
|
if (type === 'php' || type === 'pattern') {
|
|
544
567
|
return VERSION_FILE_TYPES.php.extract(content);
|
|
545
568
|
}
|
|
546
|
-
|
|
569
|
+
|
|
570
|
+
if (type === 'xcodeproj') {
|
|
571
|
+
return VERSION_FILE_TYPES.xcodeproj.extract(content);
|
|
572
|
+
}
|
|
573
|
+
|
|
547
574
|
// Unknown type - try generic version extraction
|
|
548
575
|
const match = content.match(/(\d+\.\d+\.\d+)/);
|
|
549
576
|
return match ? match[1] : null;
|
|
@@ -564,7 +591,10 @@ export function detectVersionFiles(projectPath = process.cwd()) {
|
|
|
564
591
|
const primaryFileRaw = process.env.MAIASS_VERSION_PRIMARY_FILE;
|
|
565
592
|
const primaryFile = primaryFileRaw ? primaryFileRaw.split('\\').join('/') : primaryFileRaw;
|
|
566
593
|
const primaryTypeEnv = process.env.MAIASS_VERSION_PRIMARY_TYPE;
|
|
567
|
-
const primaryType = primaryTypeEnv || (
|
|
594
|
+
const primaryType = primaryTypeEnv || (
|
|
595
|
+
primaryFile && primaryFile.endsWith('.pbxproj') ? 'xcodeproj' :
|
|
596
|
+
primaryFile && primaryFile.endsWith('.json') ? 'json' : 'txt'
|
|
597
|
+
);
|
|
568
598
|
const primaryLineStart = process.env.MAIASS_VERSION_PRIMARY_LINE_START || '';
|
|
569
599
|
|
|
570
600
|
if (primaryFile) {
|
|
@@ -602,9 +632,55 @@ export function detectVersionFiles(projectPath = process.cwd()) {
|
|
|
602
632
|
}
|
|
603
633
|
|
|
604
634
|
// Fallback: scan common version file patterns in project root
|
|
635
|
+
// Check for Xcode project first — .pbxproj lives inside *.xcodeproj bundle.
|
|
636
|
+
// Search root and up to 2 levels of subdirectories (nested project structures).
|
|
637
|
+
{
|
|
638
|
+
const SKIP = new Set(['node_modules', '.git', 'build', 'dist', 'Pods', 'DerivedData']);
|
|
639
|
+
|
|
640
|
+
function findPbxproj(dir, depth = 0) {
|
|
641
|
+
try {
|
|
642
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
643
|
+
for (const entry of entries) {
|
|
644
|
+
if (entry.isDirectory() && entry.name.endsWith('.xcodeproj')) {
|
|
645
|
+
const pbxprojPath = path.join(dir, entry.name, 'project.pbxproj');
|
|
646
|
+
if (fs.existsSync(pbxprojPath)) return pbxprojPath;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (depth < 2) {
|
|
650
|
+
for (const entry of entries) {
|
|
651
|
+
if (entry.isDirectory() && !SKIP.has(entry.name) && !entry.name.startsWith('.')) {
|
|
652
|
+
const found = findPbxproj(path.join(dir, entry.name), depth + 1);
|
|
653
|
+
if (found) return found;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
} catch {}
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const pbxprojPath = findPbxproj(projectPath);
|
|
662
|
+
if (pbxprojPath) {
|
|
663
|
+
try {
|
|
664
|
+
const content = fs.readFileSync(pbxprojPath, 'utf8');
|
|
665
|
+
const version = VERSION_FILE_TYPES.xcodeproj.extract(content);
|
|
666
|
+
if (version) {
|
|
667
|
+
versionFiles.push({
|
|
668
|
+
path: pbxprojPath,
|
|
669
|
+
filename: path.relative(projectPath, pbxprojPath),
|
|
670
|
+
type: 'xcodeproj',
|
|
671
|
+
currentVersion: version,
|
|
672
|
+
content,
|
|
673
|
+
isPrimary: true
|
|
674
|
+
});
|
|
675
|
+
return versionFiles; // .pbxproj is authoritative for Swift projects
|
|
676
|
+
}
|
|
677
|
+
} catch {}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
605
681
|
const filesToCheck = [
|
|
606
682
|
'package.json',
|
|
607
|
-
'composer.json',
|
|
683
|
+
'composer.json',
|
|
608
684
|
'VERSION',
|
|
609
685
|
'version.txt',
|
|
610
686
|
'style.css',
|