maiass 5.9.22 → 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 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
- // Check for plugin
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
- if (fs.existsSync('package.json')) {
97
- return 'package.json';
98
- }
99
- if (fs.existsSync('composer.json')) {
100
- return 'composer.json';
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 'txt'
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 defaultChoice = ['bespoke', 'wordpress-theme', 'wordpress-plugin', 'wordpress-site', 'craft'].indexOf(current) + 1;
242
- const choice = await getLineInput(`Select project type [1-5, Enter for ${defaultChoice}=${current}]: `);
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 detected = detectVersionSource();
267
- const current = existing.MAIASS_VERSION_PRIMARY_FILE || detected;
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
- console.log(`${SYMBOLS.INFO} Detected version file: ${colors.BGreen(detected)}`);
274
- if (existing.MAIASS_VERSION_PRIMARY_FILE) {
275
- console.log(`${SYMBOLS.INFO} Current setting: ${colors.BGreen(existing.MAIASS_VERSION_PRIMARY_FILE)}`);
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
- console.log(colors.Gray('Common options: package.json, composer.json, VERSION, style.css'));
280
- const file = await getLineInput(`Version source file [Enter for ${current}]: `);
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
- if (process.platform === 'win32') {
827
- // Write commit message to a temporary file to avoid quoting/newline issues
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
- commitCommand = `git commit -F "${tmpFile}"`;
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 { exec, execSync } from 'child_process';
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
- // Escape the message for shell execution
93
- const escapedMessage = message.replace(/"/g, '\\"').replace(/\n/g, '; ');
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
- let command;
95
+ // Build args array — no shell interpolation, so no injection risk
96
+ let executable, args;
96
97
  if (process.platform === 'win32') {
97
- command = `powershell.exe -ExecutionPolicy Bypass -NonInteractive -Command "devlog -s '${escapedMessage}' '?' '${project}' '${client}' '${jiraTicket}' '${subClient}'"`;
98
+ executable = 'powershell.exe';
99
+ args = ['-ExecutionPolicy', 'Bypass', '-NonInteractive', '-Command',
100
+ `devlog -s '${normalisedMessage.replace(/'/g, "''")}' '?' '${project}' '${client}' '${jiraTicket}' '${subClient}'`];
98
101
  } else {
99
- command = `devlog.sh "${escapedMessage}" "?" "${project}" "${client}" "${jiraTicket}" "${subClient}"`;
102
+ executable = 'devlog.sh';
103
+ args = [normalisedMessage, '?', project, client, jiraTicket, subClient];
100
104
  }
101
105
 
102
- logger.debug(`Executing devlog command: ${command}`);
103
-
106
+ logger.debug(`Executing devlog: ${executable} ${args.join(' ')}`);
107
+
104
108
  // Execute asynchronously - don't block the main workflow (fire-and-forget)
105
- exec(command, { encoding: 'utf8' }, (error, stdout, stderr) => {
109
+ execFile(executable, args, { encoding: 'utf8' }, (error, stdout, stderr) => {
106
110
  if (error) {
107
111
 
108
112
  if (process.env.MAIASS_DEBUG === 'true') {
@@ -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 { execSync } from 'child_process';
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
- execSync(`security add-generic-password -U -s "${serviceName}" -a "${varName}" -w "${varValue}"`, {
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
- execSync('which secret-tool', { stdio: 'pipe' });
145
- execSync(`echo -n "${varValue}" | secret-tool store --label="NODEMAIASS ${varName} (${serviceName})" service "${serviceName}" key "${varName}"`, {
146
- stdio: 'pipe',
147
- shell: true
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 = execSync(`security find-generic-password -s "${serviceName}" -a "${varName}" -w`, {
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
- execSync('which secret-tool', { stdio: 'pipe' });
222
- value = execSync(`secret-tool lookup service "${serviceName}" key "${varName}"`, {
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
- execSync(`security delete-generic-password -s "${serviceName}" -a "${varName}"`, {
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, but we can try to clear it
305
+ // Linux: secret-tool doesn't have direct delete, so store empty value spawnSync with input avoids shell injection
306
306
  try {
307
- execSync('which secret-tool', { stdio: 'pipe' });
308
- // secret-tool doesn't have a delete command, so we store an empty value
309
- execSync(`echo -n "" | secret-tool store --label="NODEMAIASS ${varName} (${serviceName})" service "${serviceName}" key "${varName}"`, {
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
- execSync('which security', { stdio: 'pipe' });
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
- execSync('which secret-tool', { stdio: 'pipe' });
400
+ execFileSync('which', ['secret-tool'], { stdio: 'pipe' });
402
401
  return true;
403
402
  }
404
403
  } catch (error) {
@@ -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
- const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
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 || (primaryFile && primaryFile.endsWith('.json') ? 'json' : 'txt');
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',
package/maiass.mjs CHANGED
@@ -192,7 +192,7 @@ if (args.includes('--help') || args.includes('-h') || command === 'help') {
192
192
  '.env.maiass.local',
193
193
  `# .env.maiass.local — personal/local MAIASS settings (never committed)\n` +
194
194
  `# Generated on first run: ${new Date().toISOString()}\n` +
195
- `# Override any .env.maiass value here, e.g. MAIASS_AI_HOST=http://localhost:8787\n`,
195
+ `# Use this file for personal overrides, e.g. MAIASS_AI_MODE=autosuggest\n`,
196
196
  'utf8'
197
197
  );
198
198
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "maiass",
3
3
  "type": "module",
4
- "version": "5.9.22",
4
+ "version": "5.9.24",
5
5
  "description": "MAIASS - Modular AI-Augmented Semantic Scribe - Intelligent Git workflow automation",
6
6
  "main": "maiass.mjs",
7
7
  "bin": {