maiass 5.7.31
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/LICENSE +26 -0
- package/README.md +347 -0
- package/build.js +127 -0
- package/lib/account-info.js +476 -0
- package/lib/colors.js +49 -0
- package/lib/commit.js +885 -0
- package/lib/config-command.js +310 -0
- package/lib/config-manager.js +344 -0
- package/lib/config.js +150 -0
- package/lib/devlog.js +182 -0
- package/lib/env-display.js +162 -0
- package/lib/git-info.js +509 -0
- package/lib/header.js +152 -0
- package/lib/input-utils.js +116 -0
- package/lib/logger.js +285 -0
- package/lib/machine-fingerprint.js +229 -0
- package/lib/maiass-command.js +79 -0
- package/lib/maiass-pipeline.js +1204 -0
- package/lib/maiass-variables.js +152 -0
- package/lib/secure-storage.js +256 -0
- package/lib/symbols.js +200 -0
- package/lib/token-validator.js +184 -0
- package/lib/version-command.js +256 -0
- package/lib/version-manager.js +902 -0
- package/maiass-standalone.cjs +148 -0
- package/maiass.cjs +34 -0
- package/maiass.mjs +167 -0
- package/package.json +45 -0
- package/setup-env.js +83 -0
|
@@ -0,0 +1,1204 @@
|
|
|
1
|
+
import { log, logger } from './logger.js';
|
|
2
|
+
import colors from './colors.js';
|
|
3
|
+
import { SYMBOLS } from './symbols.js';
|
|
4
|
+
import {
|
|
5
|
+
getGitInfo,
|
|
6
|
+
getCurrentBranch,
|
|
7
|
+
isGitRepository,
|
|
8
|
+
getGitStatus,
|
|
9
|
+
classifyBranch
|
|
10
|
+
} from './git-info.js';
|
|
11
|
+
import { commitThis } from './commit.js';
|
|
12
|
+
import { logMerge } from './devlog.js';
|
|
13
|
+
import { getCurrentVersion, detectVersionFiles, bumpVersion, updateVersionFiles } from './version-manager.js';
|
|
14
|
+
import { handleVersionCommand } from './version-command.js';
|
|
15
|
+
import { getSingleCharInput, getLineInput } from './input-utils.js';
|
|
16
|
+
import { updateChangelog as updateChangelogNew } from './changelog.js';
|
|
17
|
+
import { displayHeader } from './header.js';
|
|
18
|
+
import { execSync } from 'child_process';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import fs from 'fs';
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
import chalk from 'chalk';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get color for credit display based on remaining credits (matches bashmaiass)
|
|
26
|
+
* @param {number} credits - Remaining credits
|
|
27
|
+
* @returns {Function} Chalk color function
|
|
28
|
+
*/
|
|
29
|
+
function getCreditColor(credits) {
|
|
30
|
+
if (credits >= 1000) {
|
|
31
|
+
return chalk.hex('#00AA00'); // Green
|
|
32
|
+
} else if (credits > 600) {
|
|
33
|
+
const ratio = (credits - 600) / 400;
|
|
34
|
+
const r = Math.round(0 + (255 - 0) * (1 - ratio));
|
|
35
|
+
const g = Math.round(170 + (255 - 170) * (1 - ratio));
|
|
36
|
+
return chalk.hex(`#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}00`);
|
|
37
|
+
} else if (credits > 300) {
|
|
38
|
+
const ratio = (credits - 300) / 300;
|
|
39
|
+
const g = Math.round(165 + (255 - 165) * ratio);
|
|
40
|
+
return chalk.hex(`#FF${g.toString(16).padStart(2, '0')}00`);
|
|
41
|
+
} else {
|
|
42
|
+
const ratio = credits / 300;
|
|
43
|
+
const g = Math.round(0 + 165 * ratio);
|
|
44
|
+
return chalk.hex(`#FF${g.toString(16).padStart(2, '0')}00`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Get nodemaiass's own version from its package.json
|
|
49
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
50
|
+
const __dirname = path.dirname(__filename);
|
|
51
|
+
const maiassPackageJson = JSON.parse(
|
|
52
|
+
fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8')
|
|
53
|
+
);
|
|
54
|
+
const MAIASS_VERSION = maiassPackageJson.version;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if a git remote exists
|
|
58
|
+
* @param {string} remoteName - Name of the remote (e.g., 'origin')
|
|
59
|
+
* @returns {Promise<boolean>} Whether remote exists
|
|
60
|
+
*/
|
|
61
|
+
async function checkRemoteExists(remoteName) {
|
|
62
|
+
try {
|
|
63
|
+
const result = execSync(`git remote get-url ${remoteName}`, {
|
|
64
|
+
encoding: 'utf8',
|
|
65
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
66
|
+
});
|
|
67
|
+
return result.trim().length > 0;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Update changelog with new version and commit messages
|
|
75
|
+
* @param {string} newVersion - New version number
|
|
76
|
+
* @param {string} currentVersion - Current version (to get commits since this tag)
|
|
77
|
+
*/
|
|
78
|
+
async function updateChangelog(newVersion, currentVersion) {
|
|
79
|
+
const changelogPath = process.env.MAIASS_CHANGELOG_PATH || 'CHANGELOG.md';
|
|
80
|
+
const changelogName = process.env.MAIASS_CHANGELOG_NAME || 'CHANGELOG.md';
|
|
81
|
+
const fs = await import('fs/promises');
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// Determine the latest version from the top of the changelog file (not git tags)
|
|
85
|
+
let lastVersion = null;
|
|
86
|
+
let changelogLines = [];
|
|
87
|
+
try {
|
|
88
|
+
await fs.access(changelogPath);
|
|
89
|
+
const changelogContent = await fs.readFile(changelogPath, 'utf8');
|
|
90
|
+
changelogLines = changelogContent.split('\n');
|
|
91
|
+
for (const line of changelogLines) {
|
|
92
|
+
const match = line.match(/^##\s+(\d+\.\d+\.\d+)/);
|
|
93
|
+
if (match) {
|
|
94
|
+
lastVersion = match[1];
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// changelog does not exist yet
|
|
100
|
+
}
|
|
101
|
+
if (!lastVersion) {
|
|
102
|
+
// Fallback: try package.json or all commits
|
|
103
|
+
try {
|
|
104
|
+
const pkg = JSON.parse(await fs.readFile('package.json', 'utf8'));
|
|
105
|
+
if (pkg.version) lastVersion = pkg.version;
|
|
106
|
+
} catch {}
|
|
107
|
+
}
|
|
108
|
+
log.blue(SYMBOLS.GEAR, lastVersion ? `Getting commits since last changelog version: ${lastVersion}` : 'No previous version found, getting all commits');
|
|
109
|
+
// Get commit messages since last version for main changelog
|
|
110
|
+
const gitLogCommand = lastVersion
|
|
111
|
+
? `git log ${lastVersion}..HEAD --pretty=format:"%B"`
|
|
112
|
+
: `git log --pretty=format:"%B"`;
|
|
113
|
+
const mainLogResult = executeGitCommand(gitLogCommand, true);
|
|
114
|
+
let hasCommits = mainLogResult.success && mainLogResult.output.trim();
|
|
115
|
+
|
|
116
|
+
if (!hasCommits) {
|
|
117
|
+
log.warning(SYMBOLS.INFO, 'No commits found since last release');
|
|
118
|
+
// Still proceed to update changelog with version entry
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Get commit messages for internal changelog (with author info, patch, and date)
|
|
122
|
+
// Use git log to get patch version, date, author, and subject for each commit
|
|
123
|
+
const internalGitLogCommand = lastVersion
|
|
124
|
+
? `git log ${lastVersion}..HEAD --pretty=format:"%h|%ad|%an|%s" --date=short`
|
|
125
|
+
: `git log --pretty=format:"%h|%ad|%an|%s" --date=short`;
|
|
126
|
+
const internalLogResult = executeGitCommand(internalGitLogCommand, true);
|
|
127
|
+
if (!internalLogResult.success) {
|
|
128
|
+
log.warning(SYMBOLS.WARNING, 'Failed to get commits for internal changelog');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let commitMessages = [];
|
|
132
|
+
let internalCommitMessages = [];
|
|
133
|
+
|
|
134
|
+
// Only process commits if we have any
|
|
135
|
+
if (hasCommits) {
|
|
136
|
+
// Group commit messages by date (using git log with date)
|
|
137
|
+
const gitLogByDateCommand = lastVersion
|
|
138
|
+
? `git log ${lastVersion}..HEAD --pretty=format:"%ad|%B" --date=short`
|
|
139
|
+
: `git log --pretty=format:"%ad|%B" --date=short`;
|
|
140
|
+
const logByDateResult = executeGitCommand(gitLogByDateCommand, true);
|
|
141
|
+
let groupedByDate = {};
|
|
142
|
+
if (logByDateResult.success && logByDateResult.output.trim()) {
|
|
143
|
+
logByDateResult.output.split('\n\n').forEach(entry => {
|
|
144
|
+
// Each entry: 'YYYY-MM-DD|commit message'
|
|
145
|
+
const [date, ...msgParts] = entry.split('|');
|
|
146
|
+
const msg = msgParts.join('|').trim();
|
|
147
|
+
if (!msg) return;
|
|
148
|
+
if (/^merge branch/i.test(msg)) return; // Exclude merge branch commits
|
|
149
|
+
// Remove JIRA ticket numbers
|
|
150
|
+
const cleanedMsg = msg.replace(/\b[A-Z]+-[0-9]+\b:? ?/g, '');
|
|
151
|
+
if (!groupedByDate[date]) groupedByDate[date] = [];
|
|
152
|
+
groupedByDate[date].push(cleanedMsg);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
// Now format grouped commits for changelog
|
|
156
|
+
commitMessages = [];
|
|
157
|
+
Object.entries(groupedByDate).forEach(([date, msgs]) => {
|
|
158
|
+
// Format date as '23 July 2025'
|
|
159
|
+
const formattedDate = new Date(date).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' });
|
|
160
|
+
commitMessages.push(`### ${formattedDate}`);
|
|
161
|
+
msgs.forEach(m => {
|
|
162
|
+
// Format as bullet points (with sub-bullets if multi-line)
|
|
163
|
+
const lines = m.split('\n').filter(line => line.trim());
|
|
164
|
+
if (lines.length === 1) {
|
|
165
|
+
commitMessages.push(`- ${lines[0]}`);
|
|
166
|
+
} else if (lines.length > 1) {
|
|
167
|
+
commitMessages.push(`- ${lines[0]}`);
|
|
168
|
+
lines.slice(1).forEach(l => commitMessages.push(`\t- ${l}`));
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
commitMessages.push(''); // Blank line after each date group
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Process internal changelog commits: group by patch version and date, include author and subject
|
|
176
|
+
if (hasCommits && internalLogResult.success) {
|
|
177
|
+
// Get all patch versions and their commit hashes between lastVersion and HEAD
|
|
178
|
+
const patchLogCommand = lastVersion
|
|
179
|
+
? `git log ${lastVersion}..HEAD --pretty=format:"%h|%ad|%an|%s" --date=short`
|
|
180
|
+
: `git log --pretty=format:"%h|%ad|%an|%s" --date=short`;
|
|
181
|
+
const patchLogResult = executeGitCommand(patchLogCommand, true);
|
|
182
|
+
let patchGroups = {};
|
|
183
|
+
if (patchLogResult.success && patchLogResult.output.trim()) {
|
|
184
|
+
patchLogResult.output.split('\n').forEach(line => {
|
|
185
|
+
if (!line.trim()) return;
|
|
186
|
+
const parts = line.split('|');
|
|
187
|
+
if (parts.length < 4) return;
|
|
188
|
+
const [hash, date, author, subject] = parts;
|
|
189
|
+
if (/^merge branch/i.test(subject) || /^(ncl|bump|bumping|fixing merge conflicts|version bump|merged)/i.test(subject)) return;
|
|
190
|
+
// Try to extract patch version from subject if present, else group all under newVersion
|
|
191
|
+
// (This logic can be improved if commit messages always contain version, else just group all under newVersion)
|
|
192
|
+
let patch = newVersion;
|
|
193
|
+
// Optionally, extract patch version from subject, but fallback to newVersion
|
|
194
|
+
// Format commit entry
|
|
195
|
+
const commitEntry = `(${author}) ${subject}\n\t- hash: ${hash} date: ${date}`;
|
|
196
|
+
if (!patchGroups[patch]) patchGroups[patch] = { date, commits: [] };
|
|
197
|
+
patchGroups[patch].commits.push(commitEntry);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
// Flatten patchGroups into internalCommitMessages in patch version order (most recent first)
|
|
201
|
+
internalCommitMessages = [];
|
|
202
|
+
Object.entries(patchGroups).forEach(([patch, group]) => {
|
|
203
|
+
internalCommitMessages.push(`## ${patch}`);
|
|
204
|
+
internalCommitMessages.push(group.date);
|
|
205
|
+
internalCommitMessages.push('');
|
|
206
|
+
group.commits.forEach(c => internalCommitMessages.push(c));
|
|
207
|
+
internalCommitMessages.push('');
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Format commit messages with proper bullets (matching bash script logic)
|
|
212
|
+
let formattedCommits = '';
|
|
213
|
+
let shouldUpdateChangelog = true;
|
|
214
|
+
|
|
215
|
+
if (commitMessages.length === 0) {
|
|
216
|
+
log.warning(SYMBOLS.INFO, 'No relevant commits found for changelog - updating version header only');
|
|
217
|
+
// Don't add any commit entries when no meaningful commits exist
|
|
218
|
+
formattedCommits = '';
|
|
219
|
+
shouldUpdateChangelog = true;
|
|
220
|
+
} else {
|
|
221
|
+
formattedCommits = commitMessages.map(commit => {
|
|
222
|
+
const lines = commit.split('\n').filter(line => line.trim());
|
|
223
|
+
if (lines.length === 0) return '';
|
|
224
|
+
|
|
225
|
+
const firstLine = lines[0].trim();
|
|
226
|
+
|
|
227
|
+
// Check if it's a single-line commit or multi-line commit
|
|
228
|
+
if (lines.length === 1) {
|
|
229
|
+
// Single line commit - just add main bullet if needed
|
|
230
|
+
return firstLine.startsWith('- ') ? firstLine : `- ${firstLine}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Multi-line commit - handle differently based on format
|
|
234
|
+
if (firstLine.startsWith('- ')) {
|
|
235
|
+
// AI commit with bullets - treat first line as main subject, rest as sub-bullets
|
|
236
|
+
const subject = firstLine;
|
|
237
|
+
const bodyLines = lines.slice(1);
|
|
238
|
+
|
|
239
|
+
const formattedBody = bodyLines.map(line => {
|
|
240
|
+
const trimmed = line.trim();
|
|
241
|
+
if (!trimmed) return '';
|
|
242
|
+
|
|
243
|
+
// If line already starts with bullet, just indent it
|
|
244
|
+
if (trimmed.startsWith('- ')) {
|
|
245
|
+
return `\t${trimmed}`;
|
|
246
|
+
} else {
|
|
247
|
+
// Non-bullet line, just indent without adding bullet
|
|
248
|
+
return `\t${trimmed}`;
|
|
249
|
+
}
|
|
250
|
+
}).filter(line => line);
|
|
251
|
+
|
|
252
|
+
return [subject, ...formattedBody].join('\n');
|
|
253
|
+
} else {
|
|
254
|
+
// Manual commit - add bullets to all lines
|
|
255
|
+
return lines.map((line, index) => {
|
|
256
|
+
const trimmed = line.trim();
|
|
257
|
+
if (index === 0) {
|
|
258
|
+
return `- ${trimmed}`; // Main bullet for subject
|
|
259
|
+
} else if (trimmed) {
|
|
260
|
+
// Check if line already has bullet to avoid double bullets
|
|
261
|
+
if (trimmed.startsWith('- ')) {
|
|
262
|
+
return `\t${trimmed}`; // Just indent, don't add extra bullet
|
|
263
|
+
} else {
|
|
264
|
+
return `\t- ${trimmed}`; // Add indented bullet
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return '';
|
|
268
|
+
}).filter(line => line).join('\n');
|
|
269
|
+
}
|
|
270
|
+
}).filter(commit => commit).join('\n');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Create date string (matching bash script format)
|
|
274
|
+
const date = new Date().toLocaleDateString('en-GB', {
|
|
275
|
+
day: 'numeric',
|
|
276
|
+
month: 'long',
|
|
277
|
+
year: 'numeric'
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Create date string with weekday for internal changelog (format: "24 November 2025 (Monday)")
|
|
281
|
+
const now = new Date();
|
|
282
|
+
const weekday = now.toLocaleDateString('en-GB', { weekday: 'long' });
|
|
283
|
+
const dateWithWeekday = `${date} (${weekday.charAt(0).toUpperCase() + weekday.slice(1)})`;
|
|
284
|
+
|
|
285
|
+
// Handle existing changelog or create new one
|
|
286
|
+
let updatedContent;
|
|
287
|
+
try {
|
|
288
|
+
await fs.access(changelogPath);
|
|
289
|
+
const currentContent = await fs.readFile(changelogPath, 'utf8');
|
|
290
|
+
|
|
291
|
+
// Split current content into lines for slicing
|
|
292
|
+
const lines = currentContent.split('\n');
|
|
293
|
+
// Find the index of the second version heading (previous version)
|
|
294
|
+
let restStartIdx = -1;
|
|
295
|
+
for (let i = 1; i < lines.length; ++i) {
|
|
296
|
+
if (/^##\s+\d+\.\d+\.\d+/.test(lines[i])) {
|
|
297
|
+
restStartIdx = i;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const restOfContent = restStartIdx !== -1 ? lines.slice(restStartIdx).join('\n') : '';
|
|
302
|
+
// Always prepend new entry, never duplicate previous section
|
|
303
|
+
if (formattedCommits.trim()) {
|
|
304
|
+
updatedContent = `## ${newVersion}\n${date}\n\n${formattedCommits}\n\n${restOfContent}`;
|
|
305
|
+
} else {
|
|
306
|
+
updatedContent = `## ${newVersion}\n${date}\n\n${restOfContent}`;
|
|
307
|
+
}
|
|
308
|
+
} catch (error) {
|
|
309
|
+
// Changelog doesn't exist - create new one
|
|
310
|
+
if (formattedCommits.trim()) {
|
|
311
|
+
updatedContent = `## ${newVersion}\n${date}\n\n${formattedCommits}\n`;
|
|
312
|
+
} else {
|
|
313
|
+
updatedContent = `## ${newVersion}\n${date}\n`;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Replace all newlines with OS-specific line endings for cross-platform compatibility
|
|
318
|
+
const os = await import('os');
|
|
319
|
+
const finalContent = updatedContent.replace(/\n/g, os.EOL);
|
|
320
|
+
await fs.writeFile(changelogPath, finalContent, 'utf8');
|
|
321
|
+
log.success(SYMBOLS.CHECKMARK, `Updated ${changelogName}`);
|
|
322
|
+
|
|
323
|
+
} catch (error) {
|
|
324
|
+
log.warning(SYMBOLS.WARNING, `Failed to update changelog: ${error.message}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Execute git command safely
|
|
330
|
+
* @param {string} command - Git command to execute
|
|
331
|
+
* @param {boolean} silent - Whether to suppress errors
|
|
332
|
+
* @returns {Object} Command result
|
|
333
|
+
*/
|
|
334
|
+
function executeGitCommand(command, silent = false) {
|
|
335
|
+
try {
|
|
336
|
+
// In brief mode, always pipe output to avoid showing git details
|
|
337
|
+
const verbosity = process.env.MAIASS_VERBOSITY || 'normal';
|
|
338
|
+
const shouldPipeOutput = silent || verbosity === 'brief';
|
|
339
|
+
|
|
340
|
+
const result = execSync(command, {
|
|
341
|
+
encoding: 'utf8',
|
|
342
|
+
stdio: shouldPipeOutput ? 'pipe' : ['pipe', 'pipe', 'ignore']
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Log git output for debugging even in brief mode
|
|
346
|
+
logger.debug(`Git command: ${command}`);
|
|
347
|
+
if (result.trim()) {
|
|
348
|
+
logger.debug(`Git output: ${result.trim()}`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
success: true,
|
|
353
|
+
output: result.trim(),
|
|
354
|
+
error: null
|
|
355
|
+
};
|
|
356
|
+
} catch (error) {
|
|
357
|
+
logger.debug(`Git command failed: ${command}`);
|
|
358
|
+
logger.debug(`Git error: ${error.message}`);
|
|
359
|
+
if (error.stdout) {
|
|
360
|
+
logger.debug(`Git stdout: ${error.stdout}`);
|
|
361
|
+
}
|
|
362
|
+
if (error.stderr) {
|
|
363
|
+
logger.debug(`Git stderr: ${error.stderr}`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (!silent) {
|
|
367
|
+
logger.error(SYMBOLS.CROSS, `Git command failed: ${command}`);
|
|
368
|
+
logger.error(SYMBOLS.CROSS, `Error: ${error.message}`);
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
success: false,
|
|
372
|
+
output: '',
|
|
373
|
+
error: error.message
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Check if a git branch exists locally
|
|
380
|
+
* @param {string} branchName - Name of branch to check
|
|
381
|
+
* @returns {boolean} True if branch exists
|
|
382
|
+
*/
|
|
383
|
+
function branchExists(branchName) {
|
|
384
|
+
const result = executeGitCommand(`git branch --list ${branchName}`, true);
|
|
385
|
+
logger.debug(`DEBUG: branchExists(${branchName}) - success: ${result.success}, output: '${result.output}', trimmed: '${result.output.trim()}'`);
|
|
386
|
+
return result.success && result.output.trim() !== '';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Check if a git remote exists
|
|
391
|
+
* @param {string} remoteName - Name of remote to check
|
|
392
|
+
* @returns {boolean} True if remote exists
|
|
393
|
+
*/
|
|
394
|
+
function remoteExists(remoteName = 'origin') {
|
|
395
|
+
const result = executeGitCommand(`git remote get-url ${remoteName}`, true);
|
|
396
|
+
return result.success;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Switch to a different git branch
|
|
401
|
+
* @param {string} branchName - Name of branch to switch to
|
|
402
|
+
* @returns {Promise<boolean>} True if switch was successful
|
|
403
|
+
*/
|
|
404
|
+
async function switchToBranch(branchName) {
|
|
405
|
+
logger.info(SYMBOLS.GEAR, `Switching to ${branchName} branch...`);
|
|
406
|
+
|
|
407
|
+
const result = executeGitCommand(`git checkout ${branchName}`);
|
|
408
|
+
|
|
409
|
+
if (result.success) {
|
|
410
|
+
logger.success(SYMBOLS.CHECKMARK, `Switched to ${branchName} branch`);
|
|
411
|
+
return true;
|
|
412
|
+
} else {
|
|
413
|
+
console.error(colors.Red(`${SYMBOLS.CROSS} Failed to switch to ${branchName}: ${result.error}`));
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Validate and handle branch strategy for MAIASS workflow
|
|
420
|
+
* @param {Object} options - Pipeline options
|
|
421
|
+
* @returns {Promise<Object>} Branch validation result
|
|
422
|
+
*/
|
|
423
|
+
async function validateAndHandleBranching(options = {}) {
|
|
424
|
+
const { force = false, autoSwitch = true } = options;
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
// Get current git information
|
|
428
|
+
const gitInfo = await getGitInfo();
|
|
429
|
+
|
|
430
|
+
if (!gitInfo.isRepository) {
|
|
431
|
+
console.error(colors.Red(`${SYMBOLS.CROSS} Not in a git repository`));
|
|
432
|
+
return { success: false, error: 'Not in git repository' };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const currentBranch = gitInfo.currentBranch;
|
|
436
|
+
const developBranch = process.env.MAIASS_DEVELOPBRANCH || 'develop';
|
|
437
|
+
const mainBranch = process.env.MAIASS_MAINBRANCH || 'main';
|
|
438
|
+
const stagingBranch = process.env.MAIASS_STAGINGBRANCH || 'staging';
|
|
439
|
+
|
|
440
|
+
// Classify current branch
|
|
441
|
+
const branchClassification = classifyBranch(currentBranch, {
|
|
442
|
+
developBranch,
|
|
443
|
+
mainBranch,
|
|
444
|
+
stagingBranch
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Handle different branch scenarios
|
|
448
|
+
if (currentBranch === mainBranch || branchClassification.isRelease) {
|
|
449
|
+
log.warning(SYMBOLS.WARNING, `You are currently on the ${currentBranch} branch`);
|
|
450
|
+
log.space();
|
|
451
|
+
|
|
452
|
+
if (!force) {
|
|
453
|
+
const reply = await getSingleCharInput(`Do you want to continue on ${developBranch}? [y/N] `);
|
|
454
|
+
if (reply !== 'y') {
|
|
455
|
+
log.warning(SYMBOLS.INFO, 'Operation cancelled by user');
|
|
456
|
+
return { success: false, cancelled: true };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Switch to develop branch
|
|
461
|
+
if (autoSwitch) {
|
|
462
|
+
const switched = await switchToBranch(developBranch);
|
|
463
|
+
if (!switched) {
|
|
464
|
+
return { success: false, error: `Failed to switch to ${developBranch}` };
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
} else if (currentBranch === stagingBranch) {
|
|
468
|
+
log.blue(SYMBOLS.GEAR, `Currently on staging branch, switching to ${developBranch}...`);
|
|
469
|
+
|
|
470
|
+
if (autoSwitch) {
|
|
471
|
+
const switched = await switchToBranch(developBranch);
|
|
472
|
+
if (!switched) {
|
|
473
|
+
return { success: false, error: `Failed to switch to ${developBranch}` };
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
} else if (currentBranch !== developBranch) {
|
|
477
|
+
// On feature branch or other branch
|
|
478
|
+
log.blue(SYMBOLS.INFO, `Currently on feature branch: ${currentBranch}`);
|
|
479
|
+
log.blue(SYMBOLS.INFO, 'MAIASS workflow will proceed on current branch');
|
|
480
|
+
// Only show version management note if we're doing version management
|
|
481
|
+
log.space();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
success: true,
|
|
486
|
+
originalBranch: currentBranch,
|
|
487
|
+
currentBranch: getCurrentBranch(),
|
|
488
|
+
developBranch,
|
|
489
|
+
mainBranch,
|
|
490
|
+
stagingBranch,
|
|
491
|
+
branchClassification
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Handle the commit workflow phase
|
|
497
|
+
* @param {Object} branchInfo - Branch information from validation
|
|
498
|
+
* @param {Object} options - Pipeline options
|
|
499
|
+
* @returns {Promise<Object>} Commit result
|
|
500
|
+
*/
|
|
501
|
+
async function handleCommitWorkflow(branchInfo, options = {}) {
|
|
502
|
+
const { commitsOnly = false, autoStage = false, silent = false } = options;
|
|
503
|
+
|
|
504
|
+
logger.header(SYMBOLS.INFO, 'Commit Workflow Phase');
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
// Run the commit workflow
|
|
508
|
+
const commitSuccess = await commitThis({
|
|
509
|
+
autoStage,
|
|
510
|
+
commitsOnly,
|
|
511
|
+
silent
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// Check if commit workflow succeeded
|
|
515
|
+
if (!commitSuccess) {
|
|
516
|
+
return { success: false, cancelled: true, error: 'Commit workflow cancelled by user' };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// After commit workflow, check if working directory is clean for version management
|
|
520
|
+
// If we're not in commits-only mode, we need a clean working directory to proceed
|
|
521
|
+
if (!commitsOnly) {
|
|
522
|
+
const postCommitGitInfo = getGitInfo();
|
|
523
|
+
const postCommitStatus = postCommitGitInfo.status;
|
|
524
|
+
|
|
525
|
+
if (postCommitStatus.unstagedCount > 0 || postCommitStatus.untrackedCount > 0) {
|
|
526
|
+
// Working directory is not clean, cannot proceed with version management
|
|
527
|
+
console.log();
|
|
528
|
+
log.warning(SYMBOLS.WARNING, 'Working directory has uncommitted changes');
|
|
529
|
+
log.info(SYMBOLS.INFO, 'Cannot proceed with version management - pipeline stopped');
|
|
530
|
+
log.info(SYMBOLS.INFO, `Current branch: ${getCurrentBranch()}`);
|
|
531
|
+
log.success('', 'Thank you for using MAIASS.');
|
|
532
|
+
return { success: true, stoppedDueToUncommittedChanges: true };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return { success: true };
|
|
537
|
+
} catch (error) {
|
|
538
|
+
console.error(colors.Red(`${SYMBOLS.CROSS} Commit workflow failed: ${error.message}`));
|
|
539
|
+
return { success: false, error: error.message };
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Handle the merge to develop workflow
|
|
545
|
+
* @param {Object} branchInfo - Branch information from validation
|
|
546
|
+
* @param {Object} commitResult - Result from commit workflow
|
|
547
|
+
* @param {Object} options - Pipeline options
|
|
548
|
+
* @returns {Promise<Object>} Merge result
|
|
549
|
+
*/
|
|
550
|
+
async function handleMergeToDevelop(branchInfo, commitResult, options = {}) {
|
|
551
|
+
const { force = false, silent = false, originalGitInfo = null, autoSwitch = true, versionBump = null, tag = false } = options;
|
|
552
|
+
const { currentBranch, developBranch, originalBranch } = branchInfo;
|
|
553
|
+
|
|
554
|
+
// If we're already on develop or this was commits-only, skip merge
|
|
555
|
+
if (currentBranch === developBranch || originalBranch === developBranch) {
|
|
556
|
+
log.blue(SYMBOLS.INFO, `Already on ${developBranch} branch, skipping merge`);
|
|
557
|
+
return { success: true, skipped: true };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
// Check if develop branch exists
|
|
562
|
+
if (!branchExists(developBranch)) {
|
|
563
|
+
log.warning(SYMBOLS.WARNING, `Branch '${developBranch}' does not exist`);
|
|
564
|
+
log.blue(SYMBOLS.INFO, `Using simplified workflow on current branch: ${currentBranch}`);
|
|
565
|
+
return { success: true, simplified: true };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Determine tagging strategy for this version bump
|
|
569
|
+
const taggingDecision = shouldTagRelease(versionBump, tag);
|
|
570
|
+
|
|
571
|
+
// Prompt user for merge (and tagging if needed)
|
|
572
|
+
if (!force) {
|
|
573
|
+
log.blue(SYMBOLS.INFO, `Ready to merge changes to ${developBranch} branch`);
|
|
574
|
+
// Only show branch details if we're doing version management
|
|
575
|
+
if (autoSwitch) {
|
|
576
|
+
logger.BWhite(` ${colors.Gray('Current branch:')} ${colors.White(currentBranch)}`);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
let mergePrompt = `Merge to ${developBranch} for version management? [Y/n] `;
|
|
580
|
+
let tagPrompt = null;
|
|
581
|
+
|
|
582
|
+
// If this is a patch that needs prompting for tagging, ask about it
|
|
583
|
+
if (taggingDecision.needsPrompt && versionBump === 'patch') {
|
|
584
|
+
tagPrompt = `Create release branch and tag for this patch version? [y/N] `;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let reply;
|
|
588
|
+
let tagReply = 'n'; // Default to no tagging for patches
|
|
589
|
+
|
|
590
|
+
if (silent) {
|
|
591
|
+
console.log(colors.BCyan(mergePrompt) + colors.BGreen('Y'));
|
|
592
|
+
reply = 'Y';
|
|
593
|
+
if (tagPrompt) {
|
|
594
|
+
console.log(colors.BCyan(tagPrompt) + colors.BGreen('N'));
|
|
595
|
+
tagReply = 'n';
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
reply = await getSingleCharInput(mergePrompt);
|
|
599
|
+
if (reply !== 'n' && tagPrompt) {
|
|
600
|
+
tagReply = await getSingleCharInput(tagPrompt);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (reply === 'n') {
|
|
605
|
+
log.warning(SYMBOLS.INFO, `Merge cancelled - staying on ${currentBranch}`);
|
|
606
|
+
return { success: true, cancelled: true };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Update tagging decision based on user response
|
|
610
|
+
if (taggingDecision.needsPrompt) {
|
|
611
|
+
taggingDecision.shouldTag = tagReply === 'y';
|
|
612
|
+
taggingDecision.reason = tagReply === 'y' ? 'user requested' : 'user declined';
|
|
613
|
+
taggingDecision.needsPrompt = false;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Perform merge
|
|
618
|
+
// change this to use logger
|
|
619
|
+
logger.info(SYMBOLS.MERGING, `Merging ${currentBranch} into ${developBranch}...`);
|
|
620
|
+
|
|
621
|
+
// Switch to develop
|
|
622
|
+
const switched = await switchToBranch(developBranch);
|
|
623
|
+
if (!switched) {
|
|
624
|
+
return { success: false, error: `Failed to switch to ${developBranch}` };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Pull latest changes if remote exists
|
|
628
|
+
if (remoteExists()) {
|
|
629
|
+
logger.info(SYMBOLS.PULLING, 'Pulling latest changes from remote...');
|
|
630
|
+
const pullResult = executeGitCommand(`git pull origin ${developBranch}`);
|
|
631
|
+
if (!pullResult.success) {
|
|
632
|
+
logger.error(SYMBOLS.CROSS, `Git operation failed: pull`);
|
|
633
|
+
logger.error(SYMBOLS.CROSS, `Please resolve any conflicts or issues before proceeding`);
|
|
634
|
+
return { success: false, error: pullResult.error };
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Merge feature branch
|
|
639
|
+
logger.info(SYMBOLS.MERGING, `Merging ${currentBranch} into ${developBranch}...`);
|
|
640
|
+
const mergeResult = executeGitCommand(`git merge --no-ff ${currentBranch}`);
|
|
641
|
+
if (!mergeResult.success) {
|
|
642
|
+
console.error(colors.Red(`${SYMBOLS.CROSS} Git operation failed: merge`));
|
|
643
|
+
console.error(colors.Red(`${SYMBOLS.CROSS} Please resolve any conflicts or issues before proceeding`));
|
|
644
|
+
return { success: false, error: mergeResult.error };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
log.success(SYMBOLS.CHECKMARK, `Successfully merged ${currentBranch} into ${developBranch}`);
|
|
648
|
+
|
|
649
|
+
// Log the merge to devlog.sh (equivalent to logthis in maiass.sh)
|
|
650
|
+
logMerge(currentBranch, developBranch, originalGitInfo, 'Merged');
|
|
651
|
+
|
|
652
|
+
return {
|
|
653
|
+
success: true,
|
|
654
|
+
merged: true,
|
|
655
|
+
taggingDecision: taggingDecision
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Determine if a version should be tagged based on version bump type and configuration
|
|
661
|
+
* @param {string} versionBump - Type of version bump (major, minor, patch)
|
|
662
|
+
* @param {boolean} forceTag - CLI flag to force tagging
|
|
663
|
+
* @returns {Object} Tagging decision and reason
|
|
664
|
+
*/
|
|
665
|
+
function shouldTagRelease(versionBump, forceTag = false) {
|
|
666
|
+
const tagReleases = process.env.MAIASS_TAG_RELEASES || 'ask';
|
|
667
|
+
|
|
668
|
+
// CLI flag overrides everything
|
|
669
|
+
if (forceTag) {
|
|
670
|
+
return { shouldTag: true, reason: 'CLI flag', needsPrompt: false };
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Major and minor releases are always tagged
|
|
674
|
+
if (versionBump === 'major' || versionBump === 'minor') {
|
|
675
|
+
return { shouldTag: true, reason: 'major/minor release', needsPrompt: false };
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Handle patch releases based on configuration
|
|
679
|
+
switch (tagReleases.toLowerCase()) {
|
|
680
|
+
case 'all':
|
|
681
|
+
return { shouldTag: true, reason: 'MAIASS_TAG_RELEASES=all', needsPrompt: false };
|
|
682
|
+
case 'none':
|
|
683
|
+
return { shouldTag: false, reason: 'MAIASS_TAG_RELEASES=none', needsPrompt: false };
|
|
684
|
+
case 'ask':
|
|
685
|
+
default:
|
|
686
|
+
return { shouldTag: false, reason: 'patch release', needsPrompt: true };
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Handle version management phase - create release branch, bump version, merge back
|
|
692
|
+
* @param {Object} branchInfo - Branch information
|
|
693
|
+
* @param {Object} mergeResult - Merge operation result
|
|
694
|
+
* @param {Object} options - Version management options
|
|
695
|
+
* @returns {Promise<Object>} Version management result
|
|
696
|
+
*/
|
|
697
|
+
async function handleVersionManagement(branchInfo, mergeResult, options = {}) {
|
|
698
|
+
const { versionBump, dryRun = false, tag = false, force = false, silent = false, originalGitInfo = null } = options;
|
|
699
|
+
const { developBranch } = branchInfo;
|
|
700
|
+
|
|
701
|
+
logger.header(SYMBOLS.INFO, 'Version Management Phase');
|
|
702
|
+
|
|
703
|
+
// Check if we have version files
|
|
704
|
+
const versionInfo = await getCurrentVersion();
|
|
705
|
+
|
|
706
|
+
if (!versionInfo.hasVersionFiles) {
|
|
707
|
+
log.warning(SYMBOLS.WARNING, 'No version files detected');
|
|
708
|
+
log.blue(SYMBOLS.INFO, 'Skipping version management');
|
|
709
|
+
return { success: true, skipped: true };
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Must be on develop branch for version management
|
|
713
|
+
const currentBranch = getCurrentBranch();
|
|
714
|
+
if (currentBranch !== developBranch) {
|
|
715
|
+
console.error(colors.Red(`${SYMBOLS.CROSS} Version management must be done on ${developBranch} branch`));
|
|
716
|
+
console.error(colors.Red(`${SYMBOLS.CROSS} Current branch: ${currentBranch}`));
|
|
717
|
+
return { success: false, error: `Not on ${developBranch} branch` };
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Determine new version
|
|
721
|
+
const newVersion = bumpVersion(versionInfo.current, versionBump);
|
|
722
|
+
if (!newVersion) {
|
|
723
|
+
console.error(colors.Red(`${SYMBOLS.CROSS} Failed to determine new version`));
|
|
724
|
+
return { success: false, error: 'Version calculation failed' };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
log.branded(`Setting version based on argument: ${versionBump}`, colors.White);
|
|
728
|
+
log.branded(`Current version: ${versionInfo.current}`, colors.White);
|
|
729
|
+
logger.success(SYMBOLS.CHECKMARK, `New version: ${newVersion}`);
|
|
730
|
+
|
|
731
|
+
if (dryRun) {
|
|
732
|
+
console.log(colors.BBlue(`${SYMBOLS.INFO} Dry run - version management skipped`));
|
|
733
|
+
return { success: true, dryRun: true, version: newVersion };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Determine if we should use release branch workflow or simple version bump
|
|
737
|
+
const taggingDecision = mergeResult.taggingDecision || shouldTagRelease(versionBump, tag);
|
|
738
|
+
const useReleaseBranch = taggingDecision.shouldTag;
|
|
739
|
+
|
|
740
|
+
if (useReleaseBranch) {
|
|
741
|
+
// Full release branch workflow with tagging
|
|
742
|
+
logger.info(SYMBOLS.INFO, `Using release branch workflow (${taggingDecision.reason})`);
|
|
743
|
+
return await handleReleaseBranchWorkflow(newVersion, versionInfo, developBranch, originalGitInfo);
|
|
744
|
+
} else {
|
|
745
|
+
// Simple version bump without release branch
|
|
746
|
+
logger.info(SYMBOLS.INFO, `Using simple version bump (${taggingDecision.reason})`);
|
|
747
|
+
return await handleSimpleVersionBump(newVersion, versionInfo, developBranch, branchInfo.originalBranch);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Handle simple version bump without release branch or tagging
|
|
753
|
+
* @param {string} newVersion - New version to set
|
|
754
|
+
* @param {Object} versionInfo - Current version information
|
|
755
|
+
* @param {string} developBranch - Name of develop branch
|
|
756
|
+
* @param {string} originalBranch - Original branch to return to
|
|
757
|
+
* @returns {Promise<Object>} Version bump result
|
|
758
|
+
*/
|
|
759
|
+
async function handleSimpleVersionBump(newVersion, versionInfo, developBranch, originalBranch) {
|
|
760
|
+
try {
|
|
761
|
+
// Update version files directly on develop
|
|
762
|
+
logger.info(SYMBOLS.INFO, 'Updating version files...');
|
|
763
|
+
const versionResult = await updateVersionFiles(newVersion, versionInfo.files);
|
|
764
|
+
if (!versionResult.success) {
|
|
765
|
+
logger.error(SYMBOLS.CROSS, 'Failed to update version files');
|
|
766
|
+
return { success: false, error: 'Version file update failed' };
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Update changelog
|
|
770
|
+
logger.info(SYMBOLS.INFO, 'Updating changelog...');
|
|
771
|
+
const changelogPath = process.env.MAIASS_CHANGELOG_PATH || '.';
|
|
772
|
+
await updateChangelogNew(changelogPath, newVersion);
|
|
773
|
+
|
|
774
|
+
// Commit version changes
|
|
775
|
+
logger.info(SYMBOLS.GEAR, 'Committing version changes...');
|
|
776
|
+
const addResult = executeGitCommand('git add -A');
|
|
777
|
+
if (!addResult.success) {
|
|
778
|
+
logger.error(SYMBOLS.CROSS, 'Git operation failed: add -A');
|
|
779
|
+
return { success: false, error: 'Git add failed' };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const { success: committed } = executeGitCommand(`git commit -m "Bumped version to ${newVersion}"`);
|
|
783
|
+
if (!committed) {
|
|
784
|
+
logger.error(SYMBOLS.CROSS, 'Git operation failed: commit');
|
|
785
|
+
return { success: false, error: 'Version commit failed' };
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Push to remote if exists
|
|
789
|
+
const hasRemote = await checkRemoteExists('origin');
|
|
790
|
+
if (hasRemote) {
|
|
791
|
+
logger.info(SYMBOLS.PUSHING, `Pushing version update to remote...`);
|
|
792
|
+
const { success: pushed } = executeGitCommand(`git push origin ${developBranch}`);
|
|
793
|
+
if (!pushed) {
|
|
794
|
+
logger.warning(SYMBOLS.WARNING, 'Failed to push to remote, but version update succeeded locally');
|
|
795
|
+
} else {
|
|
796
|
+
logger.success(SYMBOLS.CHECKMARK, `Pushed version update to remote`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Return to original branch
|
|
801
|
+
if (originalBranch !== developBranch) {
|
|
802
|
+
logger.info(SYMBOLS.INFO, `Returning to original branch: ${originalBranch}`);
|
|
803
|
+
const switched = await switchToBranch(originalBranch);
|
|
804
|
+
if (!switched) {
|
|
805
|
+
logger.warning(SYMBOLS.WARNING, `Failed to switch back to ${originalBranch}`);
|
|
806
|
+
} else {
|
|
807
|
+
logger.success(SYMBOLS.CHECKMARK, `Switched back to ${originalBranch}`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
logger.success(SYMBOLS.CHECKMARK, `Version updated to ${newVersion} on ${developBranch}`);
|
|
812
|
+
return {
|
|
813
|
+
success: true,
|
|
814
|
+
version: newVersion,
|
|
815
|
+
previousVersion: versionInfo.current,
|
|
816
|
+
workflow: 'simple'
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
} catch (error) {
|
|
820
|
+
logger.error(SYMBOLS.CROSS, `Simple version bump failed: ${error.message}`);
|
|
821
|
+
return { success: false, error: error.message };
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Handle full release branch workflow with tagging
|
|
827
|
+
* @param {string} newVersion - New version to set
|
|
828
|
+
* @param {Object} versionInfo - Current version information
|
|
829
|
+
* @param {string} developBranch - Name of develop branch
|
|
830
|
+
* @param {Object} originalGitInfo - Original git information
|
|
831
|
+
* @returns {Promise<Object>} Release workflow result
|
|
832
|
+
*/
|
|
833
|
+
async function handleReleaseBranchWorkflow(newVersion, versionInfo, developBranch, originalGitInfo) {
|
|
834
|
+
try {
|
|
835
|
+
// Create release branch
|
|
836
|
+
const releaseBranch = `release/${newVersion}`;
|
|
837
|
+
logger.info(SYMBOLS.INFO, `Creating release branch: ${releaseBranch}`);
|
|
838
|
+
|
|
839
|
+
// Create and switch to release branch
|
|
840
|
+
const { success: branchCreated } = executeGitCommand(`git checkout -b ${releaseBranch}`);
|
|
841
|
+
if (!branchCreated) {
|
|
842
|
+
logger.error(SYMBOLS.CROSS, 'Git operation failed: checkout -b');
|
|
843
|
+
return { success: false, error: 'Release branch creation failed' };
|
|
844
|
+
}
|
|
845
|
+
logger.success(SYMBOLS.CHECKMARK, `Created and switched to ${releaseBranch}`);
|
|
846
|
+
|
|
847
|
+
// Update version files on release branch
|
|
848
|
+
logger.info(SYMBOLS.INFO, 'Updating version files...');
|
|
849
|
+
const versionResult = await updateVersionFiles(newVersion, versionInfo.files);
|
|
850
|
+
if (!versionResult.success) {
|
|
851
|
+
logger.error(SYMBOLS.CROSS, 'Failed to update version files');
|
|
852
|
+
return { success: false, error: 'Version file update failed' };
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Update changelog
|
|
856
|
+
logger.info(SYMBOLS.INFO, 'Updating changelog...');
|
|
857
|
+
const changelogPath = process.env.MAIASS_CHANGELOG_PATH || '.';
|
|
858
|
+
await updateChangelogNew(changelogPath, newVersion);
|
|
859
|
+
|
|
860
|
+
// Commit version changes
|
|
861
|
+
logger.info(SYMBOLS.GEAR, 'Committing version changes...');
|
|
862
|
+
const addResult = executeGitCommand('git add -A');
|
|
863
|
+
if (!addResult.success) {
|
|
864
|
+
logger.error(SYMBOLS.CROSS, 'Git operation failed: add -A');
|
|
865
|
+
return { success: false, error: 'Git add failed' };
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const { success: committed } = executeGitCommand(`git commit -m "Bumped version to ${newVersion}"`);
|
|
869
|
+
if (!committed) {
|
|
870
|
+
logger.error(SYMBOLS.CROSS, 'Git operation failed: commit');
|
|
871
|
+
return { success: false, error: 'Version commit failed' };
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Create version tag
|
|
875
|
+
logger.info(SYMBOLS.GEAR, `Creating version tag...`);
|
|
876
|
+
const { success: tagged } = executeGitCommand(`git tag -a ${newVersion} -m "Release version ${newVersion}"`);
|
|
877
|
+
if (!tagged) {
|
|
878
|
+
logger.error(SYMBOLS.CROSS, 'Git operation failed: tag');
|
|
879
|
+
return { success: false, error: 'Git tag failed' };
|
|
880
|
+
}
|
|
881
|
+
logger.success(SYMBOLS.CHECKMARK, `Tagged release as ${newVersion}`);
|
|
882
|
+
|
|
883
|
+
// Push release branch if remote exists
|
|
884
|
+
const hasRemote = await checkRemoteExists('origin');
|
|
885
|
+
if (hasRemote) {
|
|
886
|
+
logger.info(SYMBOLS.PUSHING, `Pushing release branch to remote...`);
|
|
887
|
+
const { success: pushed } = executeGitCommand(`git push --set-upstream origin ${releaseBranch}`);
|
|
888
|
+
if (!pushed) {
|
|
889
|
+
logger.error(SYMBOLS.CROSS, 'Git operation failed: push');
|
|
890
|
+
return { success: false, error: 'Git push failed' };
|
|
891
|
+
}
|
|
892
|
+
logger.success(SYMBOLS.CHECKMARK, `Pushed ${releaseBranch} to remote`);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Switch back to develop and merge release
|
|
896
|
+
logger.info(SYMBOLS.MERGING, `Merging release back to ${developBranch}...`);
|
|
897
|
+
const { success: switchedToDevelop } = executeGitCommand(`git checkout ${developBranch}`);
|
|
898
|
+
if (!switchedToDevelop) {
|
|
899
|
+
logger.error(SYMBOLS.CROSS, `Git operation failed: checkout ${developBranch}`);
|
|
900
|
+
return { success: false, error: 'Failed to switch to develop' };
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Pull latest develop if remote exists
|
|
904
|
+
if (hasRemote) {
|
|
905
|
+
logger.info(SYMBOLS.PULLING, `Pulling latest ${developBranch}...`);
|
|
906
|
+
const pullResult = executeGitCommand(`git pull origin ${developBranch}`);
|
|
907
|
+
if (!pullResult.success) {
|
|
908
|
+
logger.error(SYMBOLS.CROSS, 'Git operation failed: pull');
|
|
909
|
+
return { success: false, error: 'Git pull failed' };
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Merge release branch back to develop
|
|
914
|
+
const { success: merged } = executeGitCommand(`git merge --no-ff ${releaseBranch}`);
|
|
915
|
+
if (!merged) {
|
|
916
|
+
logger.error(SYMBOLS.CROSS, 'Git operation failed: merge --no-ff');
|
|
917
|
+
return { success: false, error: 'Release merge failed' };
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
logger.success(SYMBOLS.CHECKMARK, `Merged ${releaseBranch} back to ${developBranch}`);
|
|
921
|
+
|
|
922
|
+
// Log the release merge to devlog.sh
|
|
923
|
+
logMerge(releaseBranch, developBranch, originalGitInfo, 'Merged');
|
|
924
|
+
|
|
925
|
+
// Push updated develop and tags if remote exists
|
|
926
|
+
if (hasRemote) {
|
|
927
|
+
logger.info(SYMBOLS.PUSHING, `Pushing updated ${developBranch}...`);
|
|
928
|
+
const { success: pushedDevelop } = await executeGitCommand(`git push origin ${developBranch}`);
|
|
929
|
+
if (pushedDevelop) {
|
|
930
|
+
logger.success(SYMBOLS.CHECKMARK, `Pushed updated ${developBranch} to remote`);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Push tags
|
|
934
|
+
await executeGitCommand('git push --tags');
|
|
935
|
+
logger.success(SYMBOLS.CHECKMARK, `Pushed tags to remote`);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
logger.success(SYMBOLS.CHECKMARK, `Release workflow completed: ${versionInfo.current} → ${newVersion}`);
|
|
939
|
+
return {
|
|
940
|
+
success: true,
|
|
941
|
+
version: newVersion,
|
|
942
|
+
releaseBranch,
|
|
943
|
+
previousVersion: versionInfo.current,
|
|
944
|
+
workflow: 'release-branch'
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
} catch (error) {
|
|
948
|
+
logger.error(SYMBOLS.CROSS, `Release workflow failed: ${error.message}`);
|
|
949
|
+
return { success: false, error: error.message };
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Check for MAIASS version updates from npm registry
|
|
955
|
+
* @param {string} currentVersion - Current version to compare against
|
|
956
|
+
* @returns {Promise<Object>} Update check result
|
|
957
|
+
*/
|
|
958
|
+
async function checkForUpdates(currentVersion) {
|
|
959
|
+
try {
|
|
960
|
+
// Set a timeout to prevent hanging
|
|
961
|
+
const controller = new AbortController();
|
|
962
|
+
const timeoutId = setTimeout(() => controller.abort(), 3000); // 3 second timeout
|
|
963
|
+
|
|
964
|
+
const response = await fetch('https://registry.npmjs.org/maiass-dev', {
|
|
965
|
+
headers: {
|
|
966
|
+
'Accept': 'application/vnd.npm.install-v1+json',
|
|
967
|
+
'User-Agent': 'MAIASS-Version-Checker'
|
|
968
|
+
},
|
|
969
|
+
signal: controller.signal
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
clearTimeout(timeoutId);
|
|
973
|
+
|
|
974
|
+
if (!response.ok) {
|
|
975
|
+
return { updateAvailable: false, error: `HTTP ${response.status}` };
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const registryData = await response.json();
|
|
979
|
+
const latestVersion = registryData['dist-tags']?.latest;
|
|
980
|
+
const releaseUrl = 'https://www.npmjs.com/package/maiass-dev';
|
|
981
|
+
|
|
982
|
+
if (!latestVersion) {
|
|
983
|
+
return { updateAvailable: false, error: 'No latest version found in npm registry' };
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Simple version comparison (assumes semantic versioning)
|
|
987
|
+
const current = currentVersion.split('.').map(num => parseInt(num, 10));
|
|
988
|
+
const latest = latestVersion.split('.').map(num => parseInt(num, 10));
|
|
989
|
+
|
|
990
|
+
let updateAvailable = false;
|
|
991
|
+
for (let i = 0; i < Math.max(current.length, latest.length); i++) {
|
|
992
|
+
const currentPart = current[i] || 0;
|
|
993
|
+
const latestPart = latest[i] || 0;
|
|
994
|
+
if (latestPart > currentPart) {
|
|
995
|
+
updateAvailable = true;
|
|
996
|
+
break;
|
|
997
|
+
} else if (latestPart < currentPart) {
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
return {
|
|
1003
|
+
updateAvailable,
|
|
1004
|
+
currentVersion,
|
|
1005
|
+
latestVersion,
|
|
1006
|
+
releaseUrl,
|
|
1007
|
+
error: null
|
|
1008
|
+
};
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
// Handle timeout and other errors gracefully
|
|
1011
|
+
const errorMsg = error.name === 'AbortError' ? 'Request timeout' : error.message;
|
|
1012
|
+
return { updateAvailable: false, error: errorMsg };
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Display version information and update status
|
|
1018
|
+
* @param {string} currentVersion - Current MAIASS version
|
|
1019
|
+
*/
|
|
1020
|
+
async function displayVersionInfo(currentVersion) {
|
|
1021
|
+
// Display current version
|
|
1022
|
+
logger.info(SYMBOLS.INFO, `MAIASS ${colors.Cyan(currentVersion)}`);
|
|
1023
|
+
|
|
1024
|
+
// Note: Update checking is disabled for nodemaiass as it's in active development
|
|
1025
|
+
// and not following npm registry releases. Bashmaiass uses Homebrew for updates.
|
|
1026
|
+
// TODO: Re-enable when nodemaiass is published to npm with stable releases
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Main MAIASS pipeline orchestrator
|
|
1031
|
+
* @param {Object} options - Pipeline options
|
|
1032
|
+
* @returns {Promise<Object>} Pipeline result
|
|
1033
|
+
*/
|
|
1034
|
+
export async function runMaiassPipeline(options = {}) {
|
|
1035
|
+
const {
|
|
1036
|
+
commitsOnly = false,
|
|
1037
|
+
autoStage = false,
|
|
1038
|
+
versionBump = null,
|
|
1039
|
+
dryRun = false,
|
|
1040
|
+
tag = false,
|
|
1041
|
+
force = false,
|
|
1042
|
+
silent = false
|
|
1043
|
+
} = options;
|
|
1044
|
+
|
|
1045
|
+
// Display branded header with MAIASS's own version (not project version)
|
|
1046
|
+
displayHeader(MAIASS_VERSION);
|
|
1047
|
+
|
|
1048
|
+
// Get git info early to show current branch
|
|
1049
|
+
const originalGitInfo = getGitInfo();
|
|
1050
|
+
// Use console.log directly to bypass verbosity filtering (always show branch like bashmaiass)
|
|
1051
|
+
console.log(`${colors.BSoftPink('|))')} ${colors.White(`Currently on branch: ${colors.BWhite(originalGitInfo.currentBranch)}`)}`);
|
|
1052
|
+
console.log();
|
|
1053
|
+
|
|
1054
|
+
// Get PROJECT version for version management (not MAIASS version)
|
|
1055
|
+
const versionInfo = await getCurrentVersion();
|
|
1056
|
+
const currentVersion = versionInfo?.current || 'unknown';
|
|
1057
|
+
|
|
1058
|
+
// Display project version info in debug mode only
|
|
1059
|
+
logger.debug('Project Version Information', { current: currentVersion, source: versionInfo.source });
|
|
1060
|
+
logger.debug('MAIASS Version', MAIASS_VERSION);
|
|
1061
|
+
|
|
1062
|
+
try {
|
|
1063
|
+
// CRITICAL: originalGitInfo captures the JIRA ticket from the initial branch (e.g., feature/VEL-405_something)
|
|
1064
|
+
// before we switch to develop/release branches that don't have JIRA tickets
|
|
1065
|
+
|
|
1066
|
+
// Phase 1: Branch Detection and Validation
|
|
1067
|
+
logger.debug('Phase 1: Branch Detection and Validation');
|
|
1068
|
+
const branchInfo = await validateAndHandleBranching({ force, autoSwitch: !commitsOnly });
|
|
1069
|
+
|
|
1070
|
+
if (!branchInfo.success) {
|
|
1071
|
+
return branchInfo;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Phase 2: Commit Workflow
|
|
1075
|
+
logger.debug('Phase 2: Commit Workflow');
|
|
1076
|
+
const commitResult = await handleCommitWorkflow(branchInfo, { commitsOnly, autoStage, silent });
|
|
1077
|
+
|
|
1078
|
+
if (!commitResult.success) {
|
|
1079
|
+
// Check if it was cancelled by user vs actual failure
|
|
1080
|
+
if (commitResult.cancelled) {
|
|
1081
|
+
// User chose not to proceed - this is not an error
|
|
1082
|
+
return { success: true, cancelled: true, phase: 'commit-cancelled' };
|
|
1083
|
+
}
|
|
1084
|
+
return commitResult;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// If commit workflow stopped due to uncommitted changes, exit gracefully
|
|
1088
|
+
if (commitResult.stoppedDueToUncommittedChanges) {
|
|
1089
|
+
return { success: true, phase: 'stopped-dirty-working-directory' };
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// If commits-only mode, stop here
|
|
1093
|
+
if (commitsOnly) {
|
|
1094
|
+
console.log();
|
|
1095
|
+
console.log(colors.BGreen(`${SYMBOLS.CHECKMARK} Commits-only mode completed successfully`));
|
|
1096
|
+
console.log(colors.BBlue(`${SYMBOLS.INFO} Current branch: ${getCurrentBranch()}`));
|
|
1097
|
+
return { success: true, phase: 'commits-only' };
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
console.log();
|
|
1101
|
+
|
|
1102
|
+
// Phase 3: Merge to Develop
|
|
1103
|
+
log.blue(SYMBOLS.INFO, 'Phase 3: Merge to Develop');
|
|
1104
|
+
const mergeResult = await handleMergeToDevelop(branchInfo, commitResult, {
|
|
1105
|
+
force,
|
|
1106
|
+
silent,
|
|
1107
|
+
originalGitInfo,
|
|
1108
|
+
autoSwitch: !commitsOnly,
|
|
1109
|
+
versionBump,
|
|
1110
|
+
tag
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
if (!mergeResult.success) {
|
|
1114
|
+
return mergeResult;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// If merge was cancelled by user, stop here gracefully
|
|
1118
|
+
if (mergeResult.cancelled) {
|
|
1119
|
+
console.log();
|
|
1120
|
+
logger.success(SYMBOLS.CHECKMARK, `Workflow completed on ${getCurrentBranch()}`);
|
|
1121
|
+
logger.info(SYMBOLS.INFO, 'Thank you for using MAIASS!');
|
|
1122
|
+
return { success: true, cancelled: true, phase: 'merge-cancelled' };
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
console.log('');
|
|
1126
|
+
|
|
1127
|
+
// Phase 4: Version Management
|
|
1128
|
+
log.blue(SYMBOLS.INFO, 'Phase 4: Version Management');
|
|
1129
|
+
const versionResult = await handleVersionManagement(branchInfo, mergeResult, {
|
|
1130
|
+
versionBump,
|
|
1131
|
+
dryRun,
|
|
1132
|
+
tag,
|
|
1133
|
+
force,
|
|
1134
|
+
silent,
|
|
1135
|
+
originalGitInfo
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
if (!versionResult.success) {
|
|
1139
|
+
return versionResult;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// Cache branch info to avoid multiple git calls that might hang
|
|
1143
|
+
const finalBranch = getCurrentBranch();
|
|
1144
|
+
const originalBranch = branchInfo.originalBranch;
|
|
1145
|
+
|
|
1146
|
+
console.log('');
|
|
1147
|
+
// Match bashmaiass colors: green checkmark, aqua text, bold white branch
|
|
1148
|
+
console.log(`${colors.BSoftPink('|))')} ${colors.BGreen('✅ Done!')} ${colors.Aqua('You are on')} ${colors.BWhite(finalBranch)} ${colors.Aqua('branch')}`);
|
|
1149
|
+
|
|
1150
|
+
// Display credit summary if AI was used
|
|
1151
|
+
const subscriptionId = process.env.MAIASS_SUBSCRIPTION_ID;
|
|
1152
|
+
if (commitResult && commitResult.aiUsed) {
|
|
1153
|
+
console.log('📊 Credit Summary:');
|
|
1154
|
+
if (commitResult.creditsUsed) {
|
|
1155
|
+
console.log(` Credits used this session: ${colors.SoftPink(commitResult.creditsUsed)} (${commitResult.aiModel || 'gpt-3.5-turbo'})`);
|
|
1156
|
+
}
|
|
1157
|
+
if (commitResult.creditsRemaining !== undefined) {
|
|
1158
|
+
const creditColor = getCreditColor(commitResult.creditsRemaining);
|
|
1159
|
+
console.log(`${creditColor(`Credits remaining: ${commitResult.creditsRemaining}`)}`);
|
|
1160
|
+
}
|
|
1161
|
+
console.log('');
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Display thank you message with top-up link
|
|
1165
|
+
console.log(colors.Cyan('═'.repeat(40)));
|
|
1166
|
+
console.log(`${colors.BSoftPink('|))')} Thank you for using MAIASS! ✨`);
|
|
1167
|
+
console.log(colors.Cyan('═'.repeat(40)));
|
|
1168
|
+
|
|
1169
|
+
// Show top-up link if we have a subscription ID
|
|
1170
|
+
if (subscriptionId && subscriptionId.startsWith('sub_anon_')) {
|
|
1171
|
+
console.log(`💳 ${colors.BMagenta('Need more credits?')}`);
|
|
1172
|
+
const topupEndpoint = process.env.MAIASS_TOPUP_ENDPOINT || 'https://maiass.net/top-up';
|
|
1173
|
+
console.log(colors.Blue(`${topupEndpoint}/${subscriptionId}`));
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
return {
|
|
1177
|
+
success: true,
|
|
1178
|
+
branchInfo,
|
|
1179
|
+
commitResult,
|
|
1180
|
+
mergeResult,
|
|
1181
|
+
versionResult
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
log.error(SYMBOLS.CROSS, `Pipeline failed: ${error.message}`);
|
|
1186
|
+
if (process.env.MAIASS_DEBUG === 'true') {
|
|
1187
|
+
log.error(SYMBOLS.GRAY, error.stack);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// Write debug file when pipeline fails
|
|
1191
|
+
try {
|
|
1192
|
+
const { writeDebugFile } = await import('./logger.js');
|
|
1193
|
+
const debugFile = writeDebugFile();
|
|
1194
|
+
if (debugFile) {
|
|
1195
|
+
log.error(SYMBOLS.INFO, `Debug information written to: ${debugFile}`);
|
|
1196
|
+
}
|
|
1197
|
+
} catch (debugError) {
|
|
1198
|
+
// Don't let debug file writing block pipeline completion
|
|
1199
|
+
log.warning(SYMBOLS.WARNING, `Failed to write debug file: ${debugError.message}`);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
return { success: false, error: error.message };
|
|
1203
|
+
}
|
|
1204
|
+
}
|