@solidxai/solidctl 0.1.28-beta.3 → 0.1.28-beta.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.
@@ -6,12 +6,37 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.registerReleaseCommand = registerReleaseCommand;
7
7
  const child_process_1 = require("child_process");
8
8
  const fs_1 = __importDefault(require("fs"));
9
+ const inquirer_1 = __importDefault(require("inquirer"));
10
+ const os_1 = __importDefault(require("os"));
9
11
  const path_1 = __importDefault(require("path"));
10
12
  const DEFAULT_CONFIG = {
11
13
  mainBranch: 'main',
12
14
  devBranch: 'dev',
13
15
  reverseMerge: true,
14
16
  };
17
+ const SANDBOX_GATED_RELEASE_PROJECTS = new Set(['solid-core-module', 'solid-core-ui']);
18
+ const SANDBOX_BASE_API_URL = 'https://api.demo.solidxai.com';
19
+ const SANDBOX_TEST_REQUEST_COMPANY_NAME = 'Logicloop Ventures Ltd';
20
+ const SANDBOX_STATUS_PAGE_BASE_URL = 'https://demo.solidxai.com/admin/core/sandbox-builder/sandbox/form';
21
+ const SANDBOX_POLL_INTERVAL_MS = 15_000;
22
+ const SANDBOX_POLL_TIMEOUT_MS = 90 * 60 * 1000;
23
+ const SANDBOX_PENDING_STATUSES = new Set([
24
+ 'PENDING',
25
+ 'VERIFYING',
26
+ 'PROVISIONING',
27
+ 'ACTIVE',
28
+ 'TESTING',
29
+ 'EXPIRING',
30
+ 'DELETING',
31
+ ]);
32
+ const SANDBOX_SUCCESS_STATUSES = new Set(['TEST_PASSED']);
33
+ const SANDBOX_FAILURE_STATUSES = new Set([
34
+ 'VERIFICATION_FAILED',
35
+ 'TEST_FAILED',
36
+ 'STOPPED',
37
+ 'FAILED',
38
+ 'DELETED',
39
+ ]);
15
40
  function loadConfig() {
16
41
  const configPaths = [
17
42
  path_1.default.join(process.cwd(), 'solidctl.config.json'),
@@ -34,9 +59,78 @@ function loadConfig() {
34
59
  }
35
60
  return DEFAULT_CONFIG;
36
61
  }
62
+ function readPackageJson(packageJsonPath = path_1.default.join(process.cwd(), 'package.json')) {
63
+ if (!fs_1.default.existsSync(packageJsonPath)) {
64
+ return undefined;
65
+ }
66
+ try {
67
+ return JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf-8'));
68
+ }
69
+ catch {
70
+ return undefined;
71
+ }
72
+ }
73
+ function readRequiredPackageJson(packageJsonPath) {
74
+ const packageJson = readPackageJson(packageJsonPath);
75
+ if (!packageJson) {
76
+ console.error(`Could not read package.json at ${packageJsonPath}`);
77
+ process.exit(1);
78
+ }
79
+ return packageJson;
80
+ }
81
+ function resolveReleaseProject() {
82
+ const cwdName = path_1.default.basename(process.cwd());
83
+ const packageJson = readPackageJson();
84
+ const packageName = packageJson?.name;
85
+ const solidApiPackageJsonPath = path_1.default.join(process.cwd(), 'solid-api', 'package.json');
86
+ const solidApiPackageName = readPackageJson(solidApiPackageJsonPath)?.name;
87
+ switch (cwdName) {
88
+ case 'solidctl':
89
+ if (packageName === '@solidxai/solidctl') {
90
+ console.log(`Release project resolved: solidctl (${packageName})`);
91
+ return { type: 'solidctl', cwdName, packageName, versionSourcePath: path_1.default.join(process.cwd(), 'package.json') };
92
+ }
93
+ break;
94
+ case 'solid-core-module':
95
+ if (packageName === '@solidxai/core') {
96
+ console.log(`Release project resolved: solid-core-module (${packageName})`);
97
+ return { type: 'solid-core-module', cwdName, packageName, versionSourcePath: path_1.default.join(process.cwd(), 'package.json') };
98
+ }
99
+ break;
100
+ case 'solid-core-ui':
101
+ if (packageName === '@solidxai/core-ui') {
102
+ console.log(`Release project resolved: solid-core-ui (${packageName})`);
103
+ return { type: 'solid-core-ui', cwdName, packageName, versionSourcePath: path_1.default.join(process.cwd(), 'package.json') };
104
+ }
105
+ break;
106
+ case 'solid-library-management':
107
+ if (solidApiPackageName === '@library-management/solid-api') {
108
+ console.log(`Release project resolved: solid-library-management (${solidApiPackageName})`);
109
+ return {
110
+ type: 'solid-library-management',
111
+ cwdName,
112
+ packageName: solidApiPackageName,
113
+ versionSourcePath: solidApiPackageJsonPath,
114
+ };
115
+ }
116
+ break;
117
+ }
118
+ console.error(`❌ Could not resolve release project from folder "${cwdName}" and package name "${packageName || solidApiPackageName || 'unknown'}".`);
119
+ console.error(' Supported release folders are solidctl, solid-core-module, solid-core-ui, and solid-library-management.');
120
+ process.exit(1);
121
+ }
37
122
  function getCurrentBranch() {
38
123
  return (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
39
124
  }
125
+ function getRequiredBranch(preid, mainBranch, devBranch) {
126
+ if (preid === 'alpha') {
127
+ return 'predev';
128
+ }
129
+ if (preid) {
130
+ return devBranch;
131
+ }
132
+ return mainBranch;
133
+ }
40
134
  function exec(cmd, dryRun) {
41
135
  if (dryRun) {
42
136
  console.log(`[dry-run] ${cmd}`);
@@ -45,6 +139,539 @@ function exec(cmd, dryRun) {
45
139
  (0, child_process_1.execSync)(cmd, { stdio: 'inherit' });
46
140
  return '';
47
141
  }
142
+ function sleep(ms) {
143
+ return new Promise((resolve) => setTimeout(resolve, ms));
144
+ }
145
+ function shouldRunSandboxReleaseGate(project, preid) {
146
+ if (!SANDBOX_GATED_RELEASE_PROJECTS.has(project.type)) {
147
+ return false;
148
+ }
149
+ return preid === undefined || preid === 'beta';
150
+ }
151
+ function buildSandboxStatusPageUrl(sandboxId) {
152
+ return `${SANDBOX_STATUS_PAGE_BASE_URL}/${sandboxId}?viewMode=view&activeTab=page-provisioning-logs`;
153
+ }
154
+ function buildSandboxTestRunsPageUrl(sandboxId) {
155
+ return `${SANDBOX_STATUS_PAGE_BASE_URL}/${sandboxId}?viewMode=view&activeTab=page-test-runs`;
156
+ }
157
+ function formatTimestamp(date) {
158
+ return date.toLocaleString('en-IN', {
159
+ year: 'numeric',
160
+ month: 'short',
161
+ day: '2-digit',
162
+ hour: '2-digit',
163
+ minute: '2-digit',
164
+ second: '2-digit',
165
+ hour12: true,
166
+ });
167
+ }
168
+ function formatDuration(durationMs) {
169
+ const totalSeconds = Math.max(0, Math.floor(durationMs / 1000));
170
+ const hours = Math.floor(totalSeconds / 3600);
171
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
172
+ const seconds = totalSeconds % 60;
173
+ if (hours > 0) {
174
+ return `${hours}h ${minutes}m ${seconds}s`;
175
+ }
176
+ if (minutes > 0) {
177
+ return `${minutes}m ${seconds}s`;
178
+ }
179
+ return `${seconds}s`;
180
+ }
181
+ function getExpectedVersion(project, versionType, preid) {
182
+ if (!project.versionSourcePath) {
183
+ return undefined;
184
+ }
185
+ const packageJson = readRequiredPackageJson(project.versionSourcePath);
186
+ if (!packageJson.version) {
187
+ console.error(`package.json is missing a version at ${project.versionSourcePath}`);
188
+ process.exit(1);
189
+ }
190
+ return planNextVersion(packageJson.version, versionType, preid);
191
+ }
192
+ function formatSandboxReleaseName(project, plannedVersion) {
193
+ const label = project.packageName || project.type;
194
+ return plannedVersion ? `Release validation for ${label} ${plannedVersion}` : `Release validation for ${label}`;
195
+ }
196
+ function extractApiErrorMessage(payload, fallbackMessage) {
197
+ if (!payload || typeof payload !== 'object') {
198
+ return fallbackMessage;
199
+ }
200
+ const candidate = payload;
201
+ if (typeof candidate.error === 'string' && candidate.error.trim().length > 0) {
202
+ return candidate.error;
203
+ }
204
+ if (Array.isArray(candidate.message) && candidate.message.length > 0) {
205
+ return candidate.message.join(', ');
206
+ }
207
+ if (typeof candidate.message === 'string' && candidate.message.trim().length > 0) {
208
+ return candidate.message;
209
+ }
210
+ return fallbackMessage;
211
+ }
212
+ async function requestJson(url, init, fallbackMessage) {
213
+ const response = await fetch(url, init);
214
+ const rawBody = await response.text();
215
+ const parsedBody = rawBody ? JSON.parse(rawBody) : undefined;
216
+ if (!response.ok) {
217
+ throw new Error(extractApiErrorMessage(parsedBody, fallbackMessage));
218
+ }
219
+ return parsedBody;
220
+ }
221
+ async function promptSandboxReleaseCredentials() {
222
+ const answers = await inquirer_1.default.prompt([
223
+ {
224
+ type: 'input',
225
+ name: 'releaserName',
226
+ prefix: '',
227
+ message: 'Your full name:',
228
+ validate: (value) => (value.trim().length > 0 ? true : 'Name is required.'),
229
+ },
230
+ {
231
+ type: 'input',
232
+ name: 'releaserEmail',
233
+ prefix: '',
234
+ message: 'Your email address:',
235
+ validate: (value) => {
236
+ const trimmedValue = value.trim();
237
+ if (!trimmedValue) {
238
+ return 'Email is required.';
239
+ }
240
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmedValue) ? true : 'Enter a valid email address.';
241
+ },
242
+ },
243
+ {
244
+ type: 'input',
245
+ name: 'releaserMobile',
246
+ prefix: '',
247
+ message: 'Your mobile number:',
248
+ validate: (value) => (value.trim().length > 0 ? true : 'Mobile number is required.'),
249
+ },
250
+ {
251
+ type: 'input',
252
+ name: 'username',
253
+ prefix: '',
254
+ message: 'Sandbox microservice username:',
255
+ validate: (value) => (value.trim().length > 0 ? true : 'Username is required.'),
256
+ },
257
+ {
258
+ type: 'password',
259
+ name: 'password',
260
+ prefix: '',
261
+ message: 'Sandbox microservice password:',
262
+ mask: '*',
263
+ validate: (value) => (value.trim().length > 0 ? true : 'Password is required.'),
264
+ },
265
+ ]);
266
+ return {
267
+ releaserName: answers.releaserName.trim(),
268
+ releaserEmail: answers.releaserEmail.trim(),
269
+ releaserMobile: answers.releaserMobile.trim(),
270
+ username: answers.username.trim(),
271
+ password: answers.password,
272
+ };
273
+ }
274
+ async function authenticateSandboxReleaseUser(credentials) {
275
+ const response = await requestJson(`${SANDBOX_BASE_API_URL}/api/iam/authenticate`, {
276
+ method: 'POST',
277
+ headers: {
278
+ 'Content-Type': 'application/json',
279
+ },
280
+ body: JSON.stringify({
281
+ username: credentials.username,
282
+ password: credentials.password,
283
+ }),
284
+ }, 'Failed to authenticate with the sandbox microservice.');
285
+ const accessToken = response.data?.accessToken;
286
+ if (!accessToken) {
287
+ throw new Error('Sandbox microservice authentication did not return an access token.');
288
+ }
289
+ return accessToken;
290
+ }
291
+ async function launchReleaseValidationSandbox(project, accessToken, credentials, plannedVersion) {
292
+ const response = await requestJson(`${SANDBOX_BASE_API_URL}/api/sandbox/test-request`, {
293
+ method: 'POST',
294
+ headers: {
295
+ 'Content-Type': 'application/json',
296
+ Authorization: `Bearer ${accessToken}`,
297
+ },
298
+ body: JSON.stringify({
299
+ name: credentials.releaserName,
300
+ emailAddress: credentials.releaserEmail,
301
+ companyName: SANDBOX_TEST_REQUEST_COMPANY_NAME,
302
+ mobile: credentials.releaserMobile,
303
+ }),
304
+ }, 'Failed to create the sandbox test request.');
305
+ if (!response.data?.id) {
306
+ throw new Error('Sandbox test request did not return a sandbox id.');
307
+ }
308
+ return response.data;
309
+ }
310
+ async function fetchSandboxStatus(sandboxId) {
311
+ const response = await requestJson(`${SANDBOX_BASE_API_URL}/api/sandbox/${sandboxId}`, {
312
+ method: 'GET',
313
+ headers: {
314
+ Accept: 'application/json',
315
+ },
316
+ }, `Failed to fetch sandbox status for sandbox ${sandboxId}.`);
317
+ return response.data;
318
+ }
319
+ async function teardownSandbox(sandboxId, accessToken) {
320
+ await requestJson(`${SANDBOX_BASE_API_URL}/api/sandbox/${sandboxId}`, {
321
+ method: 'DELETE',
322
+ headers: {
323
+ Accept: 'application/json',
324
+ Authorization: `Bearer ${accessToken}`,
325
+ },
326
+ }, `Failed to initiate teardown for sandbox ${sandboxId}.`);
327
+ }
328
+ function printSandboxLaunchMessage(sandbox) {
329
+ const sandboxName = sandbox.displayName || sandbox.slug || `sandbox-${sandbox.id}`;
330
+ const sandboxStatus = sandbox.status || 'UNKNOWN';
331
+ const statusPageUrl = buildSandboxStatusPageUrl(sandbox.id);
332
+ console.log(`Test sandbox launched: ${sandboxName}`);
333
+ console.log(`Provisioning logs: ${statusPageUrl}`);
334
+ console.log(`The test sandbox has been provisioned and we will wait for the automated test cases to finish before continuing with the release. In the meantime, you can open the sandbox microservice status page above to monitor provisioning progress and review details.`);
335
+ console.log(`Current sandbox status: ${sandboxStatus}`);
336
+ }
337
+ async function waitForSandboxTerminalStatus(sandboxId) {
338
+ const startedAt = Date.now();
339
+ let lastLoggedStatus;
340
+ while (Date.now() - startedAt < SANDBOX_POLL_TIMEOUT_MS) {
341
+ const sandbox = await fetchSandboxStatus(sandboxId);
342
+ const currentStatus = sandbox.status || 'UNKNOWN';
343
+ if (currentStatus !== lastLoggedStatus) {
344
+ console.log(`Sandbox ${sandbox.id} status: ${currentStatus}`);
345
+ lastLoggedStatus = currentStatus;
346
+ }
347
+ if (SANDBOX_SUCCESS_STATUSES.has(currentStatus) || SANDBOX_FAILURE_STATUSES.has(currentStatus)) {
348
+ return sandbox;
349
+ }
350
+ if (!SANDBOX_PENDING_STATUSES.has(currentStatus)) {
351
+ return sandbox;
352
+ }
353
+ await sleep(SANDBOX_POLL_INTERVAL_MS);
354
+ }
355
+ throw new Error(`Timed out waiting for sandbox ${sandboxId} to reach a terminal status after ${Math.floor(SANDBOX_POLL_TIMEOUT_MS / 1000)} seconds.`);
356
+ }
357
+ async function runSandboxReleaseGate(project, dryRun, preid, plannedVersion) {
358
+ if (!shouldRunSandboxReleaseGate(project, preid)) {
359
+ return;
360
+ }
361
+ if (dryRun) {
362
+ console.log('[dry-run] Would prompt for sandbox microservice credentials, launch a validation sandbox, and wait for TEST_PASSED before publishing.');
363
+ return;
364
+ }
365
+ console.log(`${project.type} releases require sandbox validation before publishing.`);
366
+ const credentials = await promptSandboxReleaseCredentials();
367
+ const accessToken = await authenticateSandboxReleaseUser(credentials);
368
+ const sandbox = await launchReleaseValidationSandbox(project, accessToken, credentials, plannedVersion);
369
+ printSandboxLaunchMessage(sandbox);
370
+ const finalSandbox = await waitForSandboxTerminalStatus(sandbox.id);
371
+ const finalStatus = finalSandbox.status || 'UNKNOWN';
372
+ console.log('Teardown initiated...');
373
+ try {
374
+ await teardownSandbox(finalSandbox.id, accessToken);
375
+ }
376
+ catch (error) {
377
+ console.error(`Warning: teardown could not be initiated for sandbox ${finalSandbox.id}.`, error instanceof Error ? error.message : error);
378
+ }
379
+ if (SANDBOX_SUCCESS_STATUSES.has(finalStatus)) {
380
+ console.log(`Sandbox validation passed with status ${finalStatus}. Continuing with release...`);
381
+ return;
382
+ }
383
+ const testRunsPageUrl = buildSandboxTestRunsPageUrl(finalSandbox.id);
384
+ const failureReason = finalSandbox.failureReason ? ` Reason: ${finalSandbox.failureReason}` : '';
385
+ throw new Error(`Sandbox validation failed with status ${finalStatus}. Cancelling release.${failureReason} Review the failed test runs here: ${testRunsPageUrl}`);
386
+ }
387
+ function getReleaseOptions(options) {
388
+ const config = loadConfig();
389
+ return {
390
+ mainBranch: options.mainBranch || config.mainBranch,
391
+ devBranch: options.devBranch || config.devBranch,
392
+ reverseMerge: options.merge !== false && config.reverseMerge,
393
+ dryRun: options.dryRun || false,
394
+ force: options.force || false,
395
+ preid: options.preid,
396
+ isPrerelease: !!options.preid,
397
+ };
398
+ }
399
+ function validateReleaseBranch(preid, mainBranch, devBranch, force) {
400
+ const currentBranch = getCurrentBranch();
401
+ const requiredBranch = preid ? getRequiredBranch(preid, mainBranch, devBranch) : mainBranch;
402
+ if (currentBranch !== requiredBranch) {
403
+ if (force) {
404
+ console.log(`Not on ${requiredBranch} branch (on ${currentBranch}), but --force flag set. Continuing...`);
405
+ return currentBranch;
406
+ }
407
+ if (preid === 'alpha') {
408
+ console.error(`Must be on predev branch to publish alpha pre-releases. Currently on: ${currentBranch}`);
409
+ }
410
+ else if (preid) {
411
+ console.error(`Must be on ${devBranch} branch to publish ${preid} pre-releases. Currently on: ${currentBranch}`);
412
+ }
413
+ else {
414
+ console.error(`Must be on ${mainBranch} branch to publish stable releases. Currently on: ${currentBranch}`);
415
+ }
416
+ console.error(' Use --force to override this check.');
417
+ process.exit(1);
418
+ }
419
+ return currentBranch;
420
+ }
421
+ function getVersionCommand(versionType, preid) {
422
+ if (preid) {
423
+ if (versionType === 'patch' || versionType === 'prerelease') {
424
+ return `npm version prerelease --preid=${preid}`;
425
+ }
426
+ if (versionType === 'preminor' || versionType === 'minor') {
427
+ return `npm version preminor --preid=${preid}`;
428
+ }
429
+ if (versionType === 'premajor' || versionType === 'major') {
430
+ return `npm version premajor --preid=${preid}`;
431
+ }
432
+ return `npm version prerelease --preid=${preid}`;
433
+ }
434
+ return `npm version ${versionType}`;
435
+ }
436
+ function parseVersion(version) {
437
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+)\.(\d+))?$/);
438
+ if (!match) {
439
+ throw new Error(`Unsupported version format: ${version}`);
440
+ }
441
+ return {
442
+ major: Number(match[1]),
443
+ minor: Number(match[2]),
444
+ patch: Number(match[3]),
445
+ prereleaseId: match[4],
446
+ prereleaseNumber: match[5] === undefined ? undefined : Number(match[5]),
447
+ };
448
+ }
449
+ function formatVersion(parsed) {
450
+ const base = `${parsed.major}.${parsed.minor}.${parsed.patch}`;
451
+ if (parsed.prereleaseId === undefined || parsed.prereleaseNumber === undefined) {
452
+ return base;
453
+ }
454
+ return `${base}-${parsed.prereleaseId}.${parsed.prereleaseNumber}`;
455
+ }
456
+ function planNextVersion(currentVersion, versionType, preid) {
457
+ const parsed = parseVersion(currentVersion);
458
+ if (!preid) {
459
+ if (versionType === 'minor') {
460
+ return formatVersion({ major: parsed.major, minor: parsed.minor + 1, patch: 0 });
461
+ }
462
+ if (versionType === 'major') {
463
+ return formatVersion({ major: parsed.major + 1, minor: 0, patch: 0 });
464
+ }
465
+ return formatVersion({ major: parsed.major, minor: parsed.minor, patch: parsed.patch + 1 });
466
+ }
467
+ if (versionType === 'minor' || versionType === 'preminor') {
468
+ return formatVersion({
469
+ major: parsed.major,
470
+ minor: parsed.minor + 1,
471
+ patch: 0,
472
+ prereleaseId: preid,
473
+ prereleaseNumber: 0,
474
+ });
475
+ }
476
+ if (versionType === 'major' || versionType === 'premajor') {
477
+ return formatVersion({
478
+ major: parsed.major + 1,
479
+ minor: 0,
480
+ patch: 0,
481
+ prereleaseId: preid,
482
+ prereleaseNumber: 0,
483
+ });
484
+ }
485
+ if (parsed.prereleaseId) {
486
+ return formatVersion({
487
+ major: parsed.major,
488
+ minor: parsed.minor,
489
+ patch: parsed.patch,
490
+ prereleaseId: preid,
491
+ prereleaseNumber: parsed.prereleaseId === preid ? (parsed.prereleaseNumber ?? 0) + 1 : 0,
492
+ });
493
+ }
494
+ return formatVersion({
495
+ major: parsed.major,
496
+ minor: parsed.minor,
497
+ patch: parsed.patch + 1,
498
+ prereleaseId: preid,
499
+ prereleaseNumber: 0,
500
+ });
501
+ }
502
+ function getMovingDockerTag(preid) {
503
+ if (preid) {
504
+ return preid;
505
+ }
506
+ return 'latest';
507
+ }
508
+ function copyDirectoryForDockerBuild(sourcePath, destinationPath) {
509
+ fs_1.default.cpSync(sourcePath, destinationPath, {
510
+ recursive: true,
511
+ filter: (currentPath) => {
512
+ const baseName = path_1.default.basename(currentPath);
513
+ return !['.git', 'node_modules', 'dist', 'coverage', '.DS_Store', 'logs', '.venv', '.pytest_cache'].includes(baseName);
514
+ },
515
+ });
516
+ }
517
+ function createSolidLibraryManagementDockerfile() {
518
+ return `FROM node:20-bookworm
519
+
520
+ ENV DEBIAN_FRONTEND=noninteractive
521
+ WORKDIR /workspace/agent
522
+
523
+ RUN apt-get update \\
524
+ && apt-get install -y python3 python3-pip python3-venv supervisor \\
525
+ && rm -rf /var/lib/apt/lists/*
526
+
527
+ RUN npm install -g @angular-devkit/schematics-cli
528
+
529
+ COPY agent /workspace/agent
530
+
531
+ RUN python3 -m venv /opt/agent-venv
532
+ RUN /bin/sh -lc ". /opt/agent-venv/bin/activate && cd /workspace/agent && pip install --no-cache-dir -e /workspace/agent/vendor/mini-swe-agent && pip install --no-cache-dir -e '.[full]'"
533
+ RUN mkdir -p /opt/prebuilt/agent-ui-dist
534
+ RUN cd /workspace/agent/agent-ui && npm i --legacy-peer-deps && npm run build -- --outDir /opt/prebuilt/agent-ui-dist --emptyOutDir
535
+ `;
536
+ }
537
+ async function runSharedReleaseFlow(versionType, options, project) {
538
+ const { mainBranch, devBranch, reverseMerge, dryRun, force, preid, isPrerelease } = getReleaseOptions(options);
539
+ const releaseStartedAt = new Date();
540
+ try {
541
+ console.log(`Release started at: ${formatTimestamp(releaseStartedAt)}`);
542
+ const currentBranch = validateReleaseBranch(preid, mainBranch, devBranch, force);
543
+ const plannedVersion = getExpectedVersion(project, versionType, preid);
544
+ if (dryRun) {
545
+ console.log('Dry run mode - no changes will be made\n');
546
+ }
547
+ await runSandboxReleaseGate(project, dryRun, preid, plannedVersion);
548
+ const versionCmd = getVersionCommand(versionType, preid);
549
+ if (isPrerelease) {
550
+ console.log(`Updating package version (pre-release: ${preid})...`);
551
+ }
552
+ else {
553
+ console.log(`Updating package version (${versionType})...`);
554
+ }
555
+ exec(versionCmd, dryRun);
556
+ console.log('Pushing to git (with tags)...');
557
+ exec('git push --follow-tags', dryRun);
558
+ console.log('Publishing package...');
559
+ if (isPrerelease) {
560
+ exec(`npm publish --tag ${preid}`, dryRun);
561
+ }
562
+ else {
563
+ exec('npm publish', dryRun);
564
+ }
565
+ console.log('Published successfully!\n');
566
+ if (!isPrerelease && reverseMerge) {
567
+ console.log(`Merging ${mainBranch} into ${devBranch}...`);
568
+ exec(`git checkout ${devBranch}`, dryRun);
569
+ exec(`git pull origin ${devBranch}`, dryRun);
570
+ try {
571
+ exec(`git merge ${mainBranch} -m "chore: merge ${mainBranch} after publish"`, dryRun);
572
+ exec(`git push origin ${devBranch}`, dryRun);
573
+ console.log(`Successfully merged ${mainBranch} into ${devBranch}\n`);
574
+ }
575
+ catch {
576
+ console.error('\nMerge conflict detected. Please resolve manually.');
577
+ console.error(` You are now on the ${devBranch} branch.`);
578
+ process.exit(1);
579
+ }
580
+ exec(`git checkout ${mainBranch}`, dryRun);
581
+ console.log(`Back on ${mainBranch} branch`);
582
+ }
583
+ else if (!isPrerelease && !reverseMerge) {
584
+ console.log('Skipping reverse merge (--no-merge)');
585
+ }
586
+ else {
587
+ console.log(`Staying on ${currentBranch} branch`);
588
+ }
589
+ const releaseEndedAt = new Date();
590
+ console.log(`Release finished at: ${formatTimestamp(releaseEndedAt)}`);
591
+ console.log(`Total release duration: ${formatDuration(releaseEndedAt.getTime() - releaseStartedAt.getTime())}`);
592
+ console.log('\nAll done!');
593
+ }
594
+ catch (error) {
595
+ const releaseEndedAt = new Date();
596
+ console.error(`Release finished at: ${formatTimestamp(releaseEndedAt)}`);
597
+ console.error(`Total release duration: ${formatDuration(releaseEndedAt.getTime() - releaseStartedAt.getTime())}`);
598
+ console.error('Error:', error instanceof Error ? error.message : error);
599
+ process.exit(1);
600
+ }
601
+ }
602
+ function runSolidLibraryManagementReleaseFlow(versionType, options, project) {
603
+ const { mainBranch, devBranch, dryRun, force, preid, isPrerelease } = getReleaseOptions(options);
604
+ const solidApiPackageJsonPath = project.versionSourcePath || path_1.default.join(process.cwd(), 'solid-api', 'package.json');
605
+ const solidApiPackageJson = readRequiredPackageJson(solidApiPackageJsonPath);
606
+ const currentVersion = solidApiPackageJson.version;
607
+ const agentRepoPath = process.env.SOLIDX_AI_AGENT_PATH;
608
+ const imageRepository = 'solidxaiorg/solid-library-management-sandbox-base-image';
609
+ if (!currentVersion) {
610
+ console.error(`solid-api package.json is missing a version at ${solidApiPackageJsonPath}`);
611
+ process.exit(1);
612
+ }
613
+ if (!agentRepoPath) {
614
+ console.error('SOLIDX_AI_AGENT_PATH is not set. This release flow needs the local agent checkout.');
615
+ process.exit(1);
616
+ }
617
+ if (!fs_1.default.existsSync(agentRepoPath)) {
618
+ console.error(`SOLIDX_AI_AGENT_PATH does not exist: ${agentRepoPath}`);
619
+ process.exit(1);
620
+ }
621
+ const currentBranch = validateReleaseBranch(preid, mainBranch, devBranch, force);
622
+ const plannedVersion = planNextVersion(currentVersion, versionType, preid);
623
+ const movingDockerTag = getMovingDockerTag(preid);
624
+ const versionCmd = getVersionCommand(versionType, preid);
625
+ const buildContextRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'solid-library-management-release-'));
626
+ const dockerfilePath = path_1.default.join(buildContextRoot, 'Dockerfile');
627
+ const agentCopyPath = path_1.default.join(buildContextRoot, 'agent');
628
+ const versionImageTag = `${imageRepository}:${plannedVersion}`;
629
+ const movingImageTag = `${imageRepository}:${movingDockerTag}`;
630
+ try {
631
+ console.log(`Preparing solid-library-management sandbox base image release (${plannedVersion})...`);
632
+ console.log(`Docker image repository: ${imageRepository}`);
633
+ console.log(`Docker base image: node:20-bookworm`);
634
+ console.log(`Docker tags to publish: ${plannedVersion}, ${movingDockerTag}`);
635
+ console.log(`Agent source path: ${agentRepoPath}`);
636
+ if (dryRun) {
637
+ console.log('Dry run mode - no changes will be made\n');
638
+ }
639
+ console.log(`Updating solid-api version (${isPrerelease ? `pre-release: ${preid}` : versionType})...`);
640
+ exec(`cd solid-api && ${versionCmd}`, dryRun);
641
+ if (!dryRun) {
642
+ const actualVersion = readRequiredPackageJson(solidApiPackageJsonPath).version;
643
+ if (!actualVersion) {
644
+ throw new Error('Failed to determine solid-api version after npm version.');
645
+ }
646
+ if (actualVersion !== plannedVersion) {
647
+ throw new Error(`Expected solid-api version ${plannedVersion}, but found ${actualVersion} after npm version.`);
648
+ }
649
+ }
650
+ console.log('Creating Docker build context...');
651
+ if (!dryRun) {
652
+ copyDirectoryForDockerBuild(agentRepoPath, agentCopyPath);
653
+ fs_1.default.writeFileSync(dockerfilePath, createSolidLibraryManagementDockerfile(), 'utf-8');
654
+ }
655
+ console.log('Building sandbox base image...');
656
+ exec(`docker build --progress=plain -t ${versionImageTag} -t ${movingImageTag} ${buildContextRoot}`, dryRun);
657
+ console.log('Pushing git commit and tags...');
658
+ exec('git push --follow-tags', dryRun);
659
+ console.log('Publishing Docker image...');
660
+ exec(`docker push ${versionImageTag}`, dryRun);
661
+ exec(`docker push ${movingImageTag}`, dryRun);
662
+ console.log(`Staying on ${currentBranch} branch`);
663
+ console.log('\nDocker image release completed!');
664
+ }
665
+ catch (error) {
666
+ console.error('Error:', error instanceof Error ? error.message : error);
667
+ process.exit(1);
668
+ }
669
+ finally {
670
+ if (!dryRun) {
671
+ fs_1.default.rmSync(buildContextRoot, { recursive: true, force: true });
672
+ }
673
+ }
674
+ }
48
675
  function registerReleaseCommand(program) {
49
676
  program
50
677
  .command('release [version-type]')
@@ -62,12 +689,12 @@ Examples:
62
689
  $ solidctl release minor # minor: 0.0.12 → 0.1.0
63
690
  $ solidctl release major # major: 0.0.12 → 1.0.0
64
691
 
65
- Pre-releases (from dev branch):
66
- $ solidctl release --preid=alpha # 0.0.12 → 0.0.13-alpha.0
692
+ Pre-releases:
693
+ $ solidctl release --preid=alpha # from predev: 0.0.12 → 0.0.13-alpha.0
67
694
  $ solidctl release --preid=alpha # 0.0.13-alpha.0 → 0.0.13-alpha.1
68
- $ solidctl release minor --preid=alpha # 0.0.12 → 0.1.0-alpha.0
69
- $ solidctl release --preid=beta # 0.0.13-alpha.1 → 0.0.13-beta.0
70
- $ solidctl release --preid=rc # 0.0.13-beta.1 → 0.0.13-rc.0
695
+ $ solidctl release minor --preid=alpha # from predev: 0.0.12 → 0.1.0-alpha.0
696
+ $ solidctl release --preid=beta # from dev: 0.0.13-alpha.1 → 0.0.13-beta.0
697
+ $ solidctl release --preid=rc # from dev: 0.0.13-beta.1 → 0.0.13-rc.0
71
698
 
72
699
  Options:
73
700
  $ solidctl release --dry-run # Preview without making changes
@@ -86,95 +713,21 @@ Configuration:
86
713
  }
87
714
  }
88
715
  `)
89
- .action((versionType = 'patch', options) => {
90
- const config = loadConfig();
91
- const mainBranch = options.mainBranch || config.mainBranch;
92
- const devBranch = options.devBranch || config.devBranch;
93
- const reverseMerge = options.merge !== false && config.reverseMerge;
94
- const dryRun = options.dryRun || false;
95
- const force = options.force || false;
96
- const preid = options.preid;
97
- const isPrerelease = !!preid;
98
- try {
99
- const currentBranch = getCurrentBranch();
100
- const requiredBranch = isPrerelease ? devBranch : mainBranch;
101
- if (currentBranch !== requiredBranch) {
102
- if (force) {
103
- console.log(`⚠️ Not on ${requiredBranch} branch (on ${currentBranch}), but --force flag set. Continuing...`);
104
- }
105
- else {
106
- if (isPrerelease) {
107
- console.error(`❌ Must be on ${devBranch} branch to publish pre-releases. Currently on: ${currentBranch}`);
108
- }
109
- else {
110
- console.error(`❌ Must be on ${mainBranch} branch to publish stable releases. Currently on: ${currentBranch}`);
111
- }
112
- console.error(` Use --force to override this check.`);
113
- process.exit(1);
114
- }
115
- }
116
- if (dryRun) {
117
- console.log('🧪 Dry run mode - no changes will be made\n');
118
- }
119
- let versionCmd;
120
- if (isPrerelease) {
121
- if (versionType === 'patch' || versionType === 'prerelease') {
122
- versionCmd = `npm version prerelease --preid=${preid}`;
123
- }
124
- else if (versionType === 'preminor' || versionType === 'minor') {
125
- versionCmd = `npm version preminor --preid=${preid}`;
126
- }
127
- else if (versionType === 'premajor' || versionType === 'major') {
128
- versionCmd = `npm version premajor --preid=${preid}`;
129
- }
130
- else {
131
- versionCmd = `npm version prerelease --preid=${preid}`;
132
- }
133
- console.log(`🔄 Updating package version (pre-release: ${preid})...`);
134
- }
135
- else {
136
- versionCmd = `npm version ${versionType}`;
137
- console.log(`🔄 Updating package version (${versionType})...`);
138
- }
139
- exec(versionCmd, dryRun);
140
- console.log('📦 Pushing to git (with tags)...');
141
- exec('git push --follow-tags', dryRun);
142
- console.log('📦 Publishing package...');
143
- if (isPrerelease) {
144
- exec(`npm publish --tag ${preid}`, dryRun);
145
- }
146
- else {
147
- exec('npm publish', dryRun);
148
- }
149
- console.log('✅ Published successfully!\n');
150
- if (!isPrerelease && reverseMerge) {
151
- console.log(`🔀 Merging ${mainBranch} into ${devBranch}...`);
152
- exec(`git checkout ${devBranch}`, dryRun);
153
- exec(`git pull origin ${devBranch}`, dryRun);
154
- try {
155
- exec(`git merge ${mainBranch} -m "chore: merge ${mainBranch} after publish"`, dryRun);
156
- exec(`git push origin ${devBranch}`, dryRun);
157
- console.log(`✅ Successfully merged ${mainBranch} into ${devBranch}\n`);
158
- }
159
- catch {
160
- console.error(`\n⚠️ Merge conflict detected. Please resolve manually.`);
161
- console.error(` You are now on the ${devBranch} branch.`);
162
- process.exit(1);
163
- }
164
- exec(`git checkout ${mainBranch}`, dryRun);
165
- console.log(`📍 Back on ${mainBranch} branch`);
166
- }
167
- else if (!isPrerelease && !reverseMerge) {
168
- console.log(`⏭️ Skipping reverse merge (--no-merge)`);
169
- }
170
- else {
171
- console.log(`📍 Staying on ${currentBranch} branch`);
172
- }
173
- console.log('\n🎉 All done!');
174
- }
175
- catch (error) {
176
- console.error('❌ Error:', error instanceof Error ? error.message : error);
177
- process.exit(1);
716
+ .action(async (versionType = 'patch', options) => {
717
+ const project = resolveReleaseProject();
718
+ switch (project.type) {
719
+ case 'solidctl':
720
+ await runSharedReleaseFlow(versionType, options, project);
721
+ break;
722
+ case 'solid-core-module':
723
+ await runSharedReleaseFlow(versionType, options, project);
724
+ break;
725
+ case 'solid-core-ui':
726
+ await runSharedReleaseFlow(versionType, options, project);
727
+ break;
728
+ case 'solid-library-management':
729
+ runSolidLibraryManagementReleaseFlow(versionType, options, project);
730
+ break;
178
731
  }
179
732
  });
180
733
  }