@solidxai/solidctl 0.1.28-beta.4 → 0.1.28-beta.40

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,25 @@ 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 net_1 = __importDefault(require("net"));
11
+ const os_1 = __importDefault(require("os"));
9
12
  const path_1 = __importDefault(require("path"));
10
13
  const DEFAULT_CONFIG = {
11
14
  mainBranch: 'main',
12
15
  devBranch: 'dev',
13
16
  reverseMerge: true,
14
17
  };
18
+ const LOCAL_RELEASE_TEST_PROJECTS = new Set(['solid-core-module', 'solid-core-ui']);
19
+ const RELEASE_TEST_PROJECT_PATH_ENV = 'SOLIDX_RELEASE_TEST_CONSUMING_PRJ_PATH';
20
+ const DEFAULT_RELEASE_VALIDATION_API_PORT = 8080;
21
+ const DEFAULT_RELEASE_VALIDATION_UI_PORT = 5173;
22
+ const RELEASE_VALIDATION_READY_TIMEOUT_MS = 5 * 60 * 1000;
23
+ const RELEASE_VALIDATION_READY_POLL_INTERVAL_MS = 2_000;
24
+ const RELEASE_VALIDATION_POST_READY_DELAY_MS = 10_000;
25
+ const RELEASE_VALIDATION_POST_STOP_DELAY_MS = 3_000;
26
+ const RELEASE_VALIDATION_TEARDOWN_RETRY_COUNT = 3;
27
+ const RELEASE_VALIDATION_TEARDOWN_RETRY_DELAY_MS = 3_000;
15
28
  function loadConfig() {
16
29
  const configPaths = [
17
30
  path_1.default.join(process.cwd(), 'solidctl.config.json'),
@@ -34,9 +47,81 @@ function loadConfig() {
34
47
  }
35
48
  return DEFAULT_CONFIG;
36
49
  }
50
+ function readPackageJson(packageJsonPath = path_1.default.join(process.cwd(), 'package.json')) {
51
+ if (!fs_1.default.existsSync(packageJsonPath)) {
52
+ return undefined;
53
+ }
54
+ try {
55
+ return JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf-8'));
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ }
61
+ function readTextFileIfExists(filePath) {
62
+ if (!fs_1.default.existsSync(filePath)) {
63
+ return undefined;
64
+ }
65
+ return fs_1.default.readFileSync(filePath, 'utf-8');
66
+ }
67
+ function readRequiredPackageJson(packageJsonPath) {
68
+ const packageJson = readPackageJson(packageJsonPath);
69
+ if (!packageJson) {
70
+ console.error(`Could not read package.json at ${packageJsonPath}`);
71
+ process.exit(1);
72
+ }
73
+ return packageJson;
74
+ }
75
+ function resolveReleaseProject() {
76
+ const cwdName = path_1.default.basename(process.cwd());
77
+ const packageJson = readPackageJson();
78
+ const packageName = packageJson?.name;
79
+ const solidApiPackageJsonPath = path_1.default.join(process.cwd(), 'solid-api', 'package.json');
80
+ const solidApiPackageName = readPackageJson(solidApiPackageJsonPath)?.name;
81
+ switch (cwdName) {
82
+ case 'solidctl':
83
+ if (packageName === '@solidxai/solidctl') {
84
+ console.log(`Release project resolved: solidctl (${packageName})`);
85
+ return { type: 'solidctl', cwdName, packageName, versionSourcePath: path_1.default.join(process.cwd(), 'package.json') };
86
+ }
87
+ break;
88
+ case 'solid-core-module':
89
+ if (packageName === '@solidxai/core') {
90
+ console.log(`Release project resolved: solid-core-module (${packageName})`);
91
+ return { type: 'solid-core-module', cwdName, packageName, versionSourcePath: path_1.default.join(process.cwd(), 'package.json') };
92
+ }
93
+ break;
94
+ case 'solid-core-ui':
95
+ if (packageName === '@solidxai/core-ui') {
96
+ console.log(`Release project resolved: solid-core-ui (${packageName})`);
97
+ return { type: 'solid-core-ui', cwdName, packageName, versionSourcePath: path_1.default.join(process.cwd(), 'package.json') };
98
+ }
99
+ break;
100
+ case 'solid-library-management':
101
+ if (solidApiPackageName === '@library-management/solid-api') {
102
+ console.log(`Release project resolved: solid-library-management (${solidApiPackageName})`);
103
+ return {
104
+ type: 'solid-library-management',
105
+ cwdName,
106
+ packageName: solidApiPackageName,
107
+ versionSourcePath: solidApiPackageJsonPath,
108
+ };
109
+ }
110
+ break;
111
+ }
112
+ console.error(`❌ Could not resolve release project from folder "${cwdName}" and package name "${packageName || solidApiPackageName || 'unknown'}".`);
113
+ console.error(' Supported release folders are solidctl, solid-core-module, solid-core-ui, and solid-library-management.');
114
+ process.exit(1);
115
+ }
37
116
  function getCurrentBranch() {
38
117
  return (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
39
118
  }
119
+ function getRequiredBranch(preid, mainBranch, devBranch) {
120
+ if (preid) {
121
+ return devBranch;
122
+ }
123
+ return mainBranch;
124
+ }
40
125
  function exec(cmd, dryRun) {
41
126
  if (dryRun) {
42
127
  console.log(`[dry-run] ${cmd}`);
@@ -45,6 +130,706 @@ function exec(cmd, dryRun) {
45
130
  (0, child_process_1.execSync)(cmd, { stdio: 'inherit' });
46
131
  return '';
47
132
  }
133
+ function sleep(ms) {
134
+ return new Promise((resolve) => setTimeout(resolve, ms));
135
+ }
136
+ function formatTimestamp(date) {
137
+ return date.toLocaleString('en-IN', {
138
+ year: 'numeric',
139
+ month: 'short',
140
+ day: '2-digit',
141
+ hour: '2-digit',
142
+ minute: '2-digit',
143
+ second: '2-digit',
144
+ hour12: true,
145
+ });
146
+ }
147
+ function formatDuration(durationMs) {
148
+ const totalSeconds = Math.max(0, Math.floor(durationMs / 1000));
149
+ const hours = Math.floor(totalSeconds / 3600);
150
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
151
+ const seconds = totalSeconds % 60;
152
+ if (hours > 0) {
153
+ return `${hours}h ${minutes}m ${seconds}s`;
154
+ }
155
+ if (minutes > 0) {
156
+ return `${minutes}m ${seconds}s`;
157
+ }
158
+ return `${seconds}s`;
159
+ }
160
+ function formatLogTimestamp(date) {
161
+ const year = date.getFullYear();
162
+ const month = String(date.getMonth() + 1).padStart(2, '0');
163
+ const day = String(date.getDate()).padStart(2, '0');
164
+ const hours = String(date.getHours()).padStart(2, '0');
165
+ const minutes = String(date.getMinutes()).padStart(2, '0');
166
+ const seconds = String(date.getSeconds()).padStart(2, '0');
167
+ return `${year}${month}${day}-${hours}${minutes}${seconds}`;
168
+ }
169
+ function parseEnvFile(filePath) {
170
+ const content = readTextFileIfExists(filePath);
171
+ if (!content) {
172
+ return {};
173
+ }
174
+ const env = {};
175
+ for (const rawLine of content.split(/\r?\n/)) {
176
+ const line = rawLine.trim();
177
+ if (!line || line.startsWith('#')) {
178
+ continue;
179
+ }
180
+ const separatorIndex = line.indexOf('=');
181
+ if (separatorIndex === -1) {
182
+ continue;
183
+ }
184
+ const key = line.slice(0, separatorIndex).trim();
185
+ let value = line.slice(separatorIndex + 1).trim();
186
+ if ((value.startsWith('"') && value.endsWith('"')) ||
187
+ (value.startsWith("'") && value.endsWith("'"))) {
188
+ value = value.slice(1, -1);
189
+ }
190
+ env[key] = value;
191
+ }
192
+ return env;
193
+ }
194
+ function extractPortFromUrl(urlValue) {
195
+ if (!urlValue) {
196
+ return undefined;
197
+ }
198
+ try {
199
+ const parsedUrl = new URL(urlValue);
200
+ if (parsedUrl.port) {
201
+ return Number(parsedUrl.port);
202
+ }
203
+ return parsedUrl.protocol === 'https:' ? 443 : 80;
204
+ }
205
+ catch {
206
+ return undefined;
207
+ }
208
+ }
209
+ function parsePortValue(rawValue) {
210
+ if (!rawValue) {
211
+ return undefined;
212
+ }
213
+ const parsed = Number(rawValue);
214
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
215
+ }
216
+ function inferApiPort(projectRoot) {
217
+ const envPath = path_1.default.join(projectRoot, 'solid-api', '.env');
218
+ const env = parseEnvFile(envPath);
219
+ return (parsePortValue(env.PORT) ??
220
+ extractPortFromUrl(env.TEST_API_BASE_URL) ??
221
+ extractPortFromUrl(env.BASE_URL) ??
222
+ DEFAULT_RELEASE_VALIDATION_API_PORT);
223
+ }
224
+ function inferUiPort(projectRoot) {
225
+ const packageJsonPath = path_1.default.join(projectRoot, 'solid-ui', 'package.json');
226
+ const packageJsonContent = readTextFileIfExists(packageJsonPath);
227
+ if (!packageJsonContent) {
228
+ return DEFAULT_RELEASE_VALIDATION_UI_PORT;
229
+ }
230
+ try {
231
+ const packageJson = JSON.parse(packageJsonContent);
232
+ const scripts = packageJson.scripts || {};
233
+ const candidateScripts = [scripts.dev, scripts['solidx:dev']].filter(Boolean);
234
+ for (const script of candidateScripts) {
235
+ const match = script.match(/(?:^|\s)--port(?:=|\s+)(\d+)/);
236
+ if (match) {
237
+ return Number(match[1]);
238
+ }
239
+ }
240
+ }
241
+ catch {
242
+ return DEFAULT_RELEASE_VALIDATION_UI_PORT;
243
+ }
244
+ return DEFAULT_RELEASE_VALIDATION_UI_PORT;
245
+ }
246
+ function resolveReleaseValidationTargets(projectRoot) {
247
+ const apiPort = inferApiPort(projectRoot);
248
+ const uiPort = inferUiPort(projectRoot);
249
+ return {
250
+ apiPort,
251
+ uiPort,
252
+ apiBaseUrl: `http://localhost:${apiPort}`,
253
+ uiBaseUrl: `http://localhost:${uiPort}`,
254
+ };
255
+ }
256
+ function getExpectedVersion(project, versionType, preid) {
257
+ if (!project.versionSourcePath) {
258
+ return undefined;
259
+ }
260
+ const packageJson = readRequiredPackageJson(project.versionSourcePath);
261
+ if (!packageJson.version) {
262
+ console.error(`package.json is missing a version at ${project.versionSourcePath}`);
263
+ process.exit(1);
264
+ }
265
+ return planNextVersion(packageJson.version, versionType, preid);
266
+ }
267
+ function shouldRunLocalReleaseValidation(project) {
268
+ return LOCAL_RELEASE_TEST_PROJECTS.has(project.type);
269
+ }
270
+ function validateConsumingProjectRoot(projectRoot) {
271
+ const requiredPaths = ['solid-api/package.json', 'solid-ui/package.json'];
272
+ for (const requiredPath of requiredPaths) {
273
+ const absolutePath = path_1.default.join(projectRoot, requiredPath);
274
+ if (!fs_1.default.existsSync(absolutePath)) {
275
+ throw new Error(`Release test consuming project is missing ${requiredPath}: ${projectRoot}`);
276
+ }
277
+ }
278
+ }
279
+ async function resolveReleaseTestConsumingProjectPath() {
280
+ const configuredPath = process.env[RELEASE_TEST_PROJECT_PATH_ENV]?.trim();
281
+ if (configuredPath) {
282
+ const resolvedPath = path_1.default.resolve(configuredPath);
283
+ validateConsumingProjectRoot(resolvedPath);
284
+ return resolvedPath;
285
+ }
286
+ const answers = await inquirer_1.default.prompt([
287
+ {
288
+ type: 'input',
289
+ name: 'projectPath',
290
+ prefix: '',
291
+ message: `Path to the SolidX release test consuming project (${RELEASE_TEST_PROJECT_PATH_ENV}):`,
292
+ validate: (value) => {
293
+ const trimmedValue = value.trim();
294
+ if (!trimmedValue) {
295
+ return 'Project path is required.';
296
+ }
297
+ const resolvedPath = path_1.default.resolve(trimmedValue);
298
+ try {
299
+ validateConsumingProjectRoot(resolvedPath);
300
+ return true;
301
+ }
302
+ catch (error) {
303
+ return error instanceof Error ? error.message : 'Invalid project path.';
304
+ }
305
+ },
306
+ },
307
+ ]);
308
+ return path_1.default.resolve(answers.projectPath.trim());
309
+ }
310
+ function runReleaseValidationCommand(command, dryRun, cwd, failureMessage) {
311
+ console.log(`▶ ${command}`);
312
+ if (dryRun) {
313
+ console.log(`[dry-run] (${cwd}) ${command}`);
314
+ return true;
315
+ }
316
+ try {
317
+ (0, child_process_1.execSync)(command, {
318
+ cwd,
319
+ stdio: 'inherit',
320
+ env: process.env,
321
+ });
322
+ return true;
323
+ }
324
+ catch (error) {
325
+ const details = error instanceof Error ? error.message : typeof error === 'string' ? error : JSON.stringify(error);
326
+ console.error(`${failureMessage} ${details}`);
327
+ return false;
328
+ }
329
+ }
330
+ function getNpxCommand() {
331
+ return process.platform === 'win32' ? 'npx.cmd' : 'npx';
332
+ }
333
+ function getReleaseValidationLogPath(cwd) {
334
+ const logsDir = path_1.default.join(cwd, 'logs', 'solidctl', 'release-validation');
335
+ fs_1.default.mkdirSync(logsDir, { recursive: true });
336
+ return path_1.default.join(logsDir, `start-dev-${formatLogTimestamp(new Date())}.log`);
337
+ }
338
+ function startConsumingProject(cwd, dryRun) {
339
+ const commandText = 'npx @solidxai/solidctl@beta start:dev --plain';
340
+ if (dryRun) {
341
+ console.log(`[dry-run] (${cwd}) ${commandText}`);
342
+ return { child: null, logPath: null, logStream: null };
343
+ }
344
+ const logPath = getReleaseValidationLogPath(cwd);
345
+ const logStream = fs_1.default.createWriteStream(logPath, { flags: 'a' });
346
+ console.log(`Starting consuming project from ${cwd}`);
347
+ console.log(`Consuming project logs: ${logPath}`);
348
+ const child = (0, child_process_1.spawn)(getNpxCommand(), ['@solidxai/solidctl@beta', 'start:dev', '--plain'], {
349
+ cwd,
350
+ stdio: ['ignore', 'pipe', 'pipe'],
351
+ env: process.env,
352
+ detached: process.platform !== 'win32',
353
+ shell: process.platform === 'win32',
354
+ });
355
+ child.stdout?.pipe(logStream);
356
+ child.stderr?.pipe(logStream);
357
+ return { child, logPath, logStream };
358
+ }
359
+ function waitForChildExit(child, timeoutMs) {
360
+ return new Promise((resolve) => {
361
+ let resolved = false;
362
+ const timeout = setTimeout(() => {
363
+ if (!resolved) {
364
+ resolved = true;
365
+ resolve(false);
366
+ }
367
+ }, timeoutMs);
368
+ child.once('exit', () => {
369
+ if (!resolved) {
370
+ clearTimeout(timeout);
371
+ resolved = true;
372
+ resolve(true);
373
+ }
374
+ });
375
+ });
376
+ }
377
+ async function stopConsumingProject(runningProject) {
378
+ if (!runningProject?.child) {
379
+ return;
380
+ }
381
+ const { child, logStream } = runningProject;
382
+ if (child.exitCode !== null || child.killed) {
383
+ logStream?.end();
384
+ return;
385
+ }
386
+ console.log('Stopping consuming project...');
387
+ if (process.platform !== 'win32' && child.pid) {
388
+ process.kill(-child.pid, 'SIGINT');
389
+ }
390
+ else {
391
+ child.kill('SIGINT');
392
+ }
393
+ if (await waitForChildExit(child, 10_000)) {
394
+ logStream?.end();
395
+ return;
396
+ }
397
+ if (process.platform !== 'win32' && child.pid) {
398
+ process.kill(-child.pid, 'SIGTERM');
399
+ }
400
+ else {
401
+ child.kill('SIGTERM');
402
+ }
403
+ if (await waitForChildExit(child, 10_000)) {
404
+ logStream?.end();
405
+ return;
406
+ }
407
+ if (process.platform !== 'win32' && child.pid) {
408
+ process.kill(-child.pid, 'SIGKILL');
409
+ }
410
+ else {
411
+ child.kill('SIGKILL');
412
+ }
413
+ logStream?.end();
414
+ }
415
+ function isPortOpen(port, host) {
416
+ return new Promise((resolve) => {
417
+ const socket = net_1.default.connect({ port, host });
418
+ const finalize = (result) => {
419
+ socket.removeAllListeners();
420
+ socket.destroy();
421
+ resolve(result);
422
+ };
423
+ socket.setTimeout(2_000);
424
+ socket.once('connect', () => finalize(true));
425
+ socket.once('timeout', () => finalize(false));
426
+ socket.once('error', () => finalize(false));
427
+ });
428
+ }
429
+ async function isLocalPortOpen(port) {
430
+ const hosts = ['127.0.0.1', '::1', 'localhost'];
431
+ for (const host of hosts) {
432
+ if (await isPortOpen(port, host)) {
433
+ return true;
434
+ }
435
+ }
436
+ return false;
437
+ }
438
+ async function waitForReleaseValidationServices(runningProject, targets) {
439
+ const startedAt = Date.now();
440
+ console.log(`Waiting for consuming project services on ports ${targets.apiPort} and ${targets.uiPort}...`);
441
+ while (Date.now() - startedAt < RELEASE_VALIDATION_READY_TIMEOUT_MS) {
442
+ const child = runningProject?.child;
443
+ if (child && child.exitCode !== null) {
444
+ const logSuffix = runningProject?.logPath ? ` Check logs at ${runningProject.logPath}.` : '';
445
+ throw new Error(`Consuming project start:dev exited early with code ${child.exitCode}.${logSuffix}`);
446
+ }
447
+ const [apiReady, uiReady] = await Promise.all([
448
+ isLocalPortOpen(targets.apiPort),
449
+ isLocalPortOpen(targets.uiPort),
450
+ ]);
451
+ if (apiReady && uiReady) {
452
+ console.log(`Validation services are ready on ports ${targets.apiPort} and ${targets.uiPort}.`);
453
+ console.log(`Waiting ${Math.floor(RELEASE_VALIDATION_POST_READY_DELAY_MS / 1000)}s for services to stabilize...`);
454
+ await sleep(RELEASE_VALIDATION_POST_READY_DELAY_MS);
455
+ return;
456
+ }
457
+ console.log(`Still waiting for services... api:${apiReady ? 'up' : 'down'} ui:${uiReady ? 'up' : 'down'}`);
458
+ await sleep(RELEASE_VALIDATION_READY_POLL_INTERVAL_MS);
459
+ }
460
+ throw new Error(`Timed out waiting for consuming project services on ports ${targets.apiPort} and ${targets.uiPort}.${runningProject?.logPath ? ` Check logs at ${runningProject.logPath}.` : ''}`);
461
+ }
462
+ async function runReleaseValidationTeardown(cwd, dryRun) {
463
+ if (dryRun) {
464
+ runReleaseValidationCommand('npx @solidxai/solidctl@latest test data --teardown', true, cwd, 'Warning: release test data teardown failed.');
465
+ return;
466
+ }
467
+ for (let attempt = 1; attempt <= RELEASE_VALIDATION_TEARDOWN_RETRY_COUNT; attempt += 1) {
468
+ const succeeded = runReleaseValidationCommand('npx @solidxai/solidctl@latest test data --teardown', false, cwd, `Warning: release test data teardown failed on attempt ${attempt}.`);
469
+ if (succeeded) {
470
+ return;
471
+ }
472
+ if (attempt < RELEASE_VALIDATION_TEARDOWN_RETRY_COUNT) {
473
+ console.log(`Waiting ${Math.floor(RELEASE_VALIDATION_TEARDOWN_RETRY_DELAY_MS / 1000)}s before retrying teardown...`);
474
+ await sleep(RELEASE_VALIDATION_TEARDOWN_RETRY_DELAY_MS);
475
+ }
476
+ }
477
+ }
478
+ async function runLocalReleaseValidation(project, dryRun) {
479
+ if (!shouldRunLocalReleaseValidation(project)) {
480
+ return;
481
+ }
482
+ const consumingProjectRoot = await resolveReleaseTestConsumingProjectPath();
483
+ const validationTargets = resolveReleaseValidationTargets(consumingProjectRoot);
484
+ console.log(`Running local release validation from ${consumingProjectRoot}`);
485
+ console.log(`Validation targets: api=${validationTargets.apiBaseUrl} ui=${validationTargets.uiBaseUrl}`);
486
+ const commands = [
487
+ {
488
+ command: 'npx @solidxai/solidctl@latest local-upgrade',
489
+ failureMessage: 'Warning: local-upgrade failed.',
490
+ },
491
+ {
492
+ command: 'npx @solidxai/solidctl@latest build',
493
+ failureMessage: 'Warning: consuming project build failed.',
494
+ },
495
+ {
496
+ command: 'npx @solidxai/solidctl@latest test data --setup',
497
+ failureMessage: 'Warning: release test data setup failed.',
498
+ },
499
+ {
500
+ command: 'npx @solidxai/solidctl@latest seed',
501
+ failureMessage: 'Warning: release seed failed.',
502
+ },
503
+ {
504
+ command: 'npx @solidxai/solidctl@latest test data --load',
505
+ failureMessage: 'Warning: release test data load failed.',
506
+ },
507
+ ];
508
+ const testCommands = [];
509
+ if (project.type === 'solid-core-module') {
510
+ testCommands.push({
511
+ command: `npx --yes @solidxai/solidctl@beta test run --module library-management --include-tags api-all-flows --api-base-url ${validationTargets.apiBaseUrl} --ui-base-url ${validationTargets.uiBaseUrl} --headless false`,
512
+ failureMessage: 'Warning: solid-core-module local release tests failed. Continuing with release.',
513
+ });
514
+ }
515
+ else if (project.type === 'solid-core-ui') {
516
+ console.log('Skipping local release UI tests for solid-core-ui for now.');
517
+ }
518
+ let consumingProjectProcess = null;
519
+ try {
520
+ for (const item of commands) {
521
+ runReleaseValidationCommand(item.command, dryRun, consumingProjectRoot, item.failureMessage);
522
+ }
523
+ if (testCommands.length > 0) {
524
+ consumingProjectProcess = startConsumingProject(consumingProjectRoot, dryRun);
525
+ if (!dryRun) {
526
+ await waitForReleaseValidationServices(consumingProjectProcess, validationTargets);
527
+ }
528
+ for (const item of testCommands) {
529
+ runReleaseValidationCommand(item.command, dryRun, consumingProjectRoot, item.failureMessage);
530
+ }
531
+ }
532
+ }
533
+ finally {
534
+ await stopConsumingProject(consumingProjectProcess);
535
+ if (!dryRun && consumingProjectProcess?.child) {
536
+ console.log(`Waiting ${Math.floor(RELEASE_VALIDATION_POST_STOP_DELAY_MS / 1000)}s after shutdown before teardown...`);
537
+ await sleep(RELEASE_VALIDATION_POST_STOP_DELAY_MS);
538
+ }
539
+ await runReleaseValidationTeardown(consumingProjectRoot, dryRun);
540
+ }
541
+ }
542
+ function getReleaseOptions(options) {
543
+ const config = loadConfig();
544
+ return {
545
+ mainBranch: options.mainBranch || config.mainBranch,
546
+ devBranch: options.devBranch || config.devBranch,
547
+ reverseMerge: options.merge !== false && config.reverseMerge,
548
+ dryRun: options.dryRun || false,
549
+ force: options.force || false,
550
+ preid: options.preid,
551
+ isPrerelease: !!options.preid,
552
+ };
553
+ }
554
+ function validateReleaseBranch(preid, mainBranch, devBranch, force) {
555
+ const currentBranch = getCurrentBranch();
556
+ const requiredBranch = preid ? getRequiredBranch(preid, mainBranch, devBranch) : mainBranch;
557
+ if (currentBranch !== requiredBranch) {
558
+ if (force) {
559
+ console.log(`Not on ${requiredBranch} branch (on ${currentBranch}), but --force flag set. Continuing...`);
560
+ return currentBranch;
561
+ }
562
+ if (preid === 'alpha') {
563
+ console.error(`Must be on ${devBranch} branch to publish alpha pre-releases. Currently on: ${currentBranch}`);
564
+ }
565
+ else if (preid) {
566
+ console.error(`Must be on ${devBranch} branch to publish ${preid} pre-releases. Currently on: ${currentBranch}`);
567
+ }
568
+ else {
569
+ console.error(`Must be on ${mainBranch} branch to publish stable releases. Currently on: ${currentBranch}`);
570
+ }
571
+ console.error(' Use --force to override this check.');
572
+ process.exit(1);
573
+ }
574
+ return currentBranch;
575
+ }
576
+ function getVersionCommand(versionType, preid) {
577
+ if (preid) {
578
+ if (versionType === 'patch' || versionType === 'prerelease') {
579
+ return `npm version prerelease --preid=${preid}`;
580
+ }
581
+ if (versionType === 'preminor' || versionType === 'minor') {
582
+ return `npm version preminor --preid=${preid}`;
583
+ }
584
+ if (versionType === 'premajor' || versionType === 'major') {
585
+ return `npm version premajor --preid=${preid}`;
586
+ }
587
+ return `npm version prerelease --preid=${preid}`;
588
+ }
589
+ return `npm version ${versionType}`;
590
+ }
591
+ function parseVersion(version) {
592
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+)\.(\d+))?$/);
593
+ if (!match) {
594
+ throw new Error(`Unsupported version format: ${version}`);
595
+ }
596
+ return {
597
+ major: Number(match[1]),
598
+ minor: Number(match[2]),
599
+ patch: Number(match[3]),
600
+ prereleaseId: match[4],
601
+ prereleaseNumber: match[5] === undefined ? undefined : Number(match[5]),
602
+ };
603
+ }
604
+ function formatVersion(parsed) {
605
+ const base = `${parsed.major}.${parsed.minor}.${parsed.patch}`;
606
+ if (parsed.prereleaseId === undefined || parsed.prereleaseNumber === undefined) {
607
+ return base;
608
+ }
609
+ return `${base}-${parsed.prereleaseId}.${parsed.prereleaseNumber}`;
610
+ }
611
+ function planNextVersion(currentVersion, versionType, preid) {
612
+ const parsed = parseVersion(currentVersion);
613
+ if (!preid) {
614
+ if (versionType === 'minor') {
615
+ return formatVersion({ major: parsed.major, minor: parsed.minor + 1, patch: 0 });
616
+ }
617
+ if (versionType === 'major') {
618
+ return formatVersion({ major: parsed.major + 1, minor: 0, patch: 0 });
619
+ }
620
+ return formatVersion({ major: parsed.major, minor: parsed.minor, patch: parsed.patch + 1 });
621
+ }
622
+ if (versionType === 'minor' || versionType === 'preminor') {
623
+ return formatVersion({
624
+ major: parsed.major,
625
+ minor: parsed.minor + 1,
626
+ patch: 0,
627
+ prereleaseId: preid,
628
+ prereleaseNumber: 0,
629
+ });
630
+ }
631
+ if (versionType === 'major' || versionType === 'premajor') {
632
+ return formatVersion({
633
+ major: parsed.major + 1,
634
+ minor: 0,
635
+ patch: 0,
636
+ prereleaseId: preid,
637
+ prereleaseNumber: 0,
638
+ });
639
+ }
640
+ if (parsed.prereleaseId) {
641
+ return formatVersion({
642
+ major: parsed.major,
643
+ minor: parsed.minor,
644
+ patch: parsed.patch,
645
+ prereleaseId: preid,
646
+ prereleaseNumber: parsed.prereleaseId === preid ? (parsed.prereleaseNumber ?? 0) + 1 : 0,
647
+ });
648
+ }
649
+ return formatVersion({
650
+ major: parsed.major,
651
+ minor: parsed.minor,
652
+ patch: parsed.patch + 1,
653
+ prereleaseId: preid,
654
+ prereleaseNumber: 0,
655
+ });
656
+ }
657
+ function getMovingDockerTag(preid) {
658
+ if (preid) {
659
+ return preid;
660
+ }
661
+ return 'latest';
662
+ }
663
+ function copyDirectoryForDockerBuild(sourcePath, destinationPath) {
664
+ fs_1.default.cpSync(sourcePath, destinationPath, {
665
+ recursive: true,
666
+ filter: (currentPath) => {
667
+ const baseName = path_1.default.basename(currentPath);
668
+ return !['.git', 'node_modules', 'dist', 'coverage', '.DS_Store', 'logs', '.venv', '.pytest_cache'].includes(baseName);
669
+ },
670
+ });
671
+ }
672
+ function createSolidLibraryManagementDockerfile() {
673
+ return `FROM node:20-bookworm
674
+
675
+ ENV DEBIAN_FRONTEND=noninteractive
676
+ WORKDIR /workspace/agent
677
+
678
+ RUN apt-get update \\
679
+ && apt-get install -y python3 python3-pip python3-venv supervisor \\
680
+ && rm -rf /var/lib/apt/lists/*
681
+
682
+ RUN npm install -g @angular-devkit/schematics-cli
683
+
684
+ COPY agent /workspace/agent
685
+
686
+ RUN python3 -m venv /opt/agent-venv
687
+ 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]'"
688
+ RUN mkdir -p /opt/prebuilt/agent-ui-dist
689
+ RUN cd /workspace/agent/agent-ui && npm i --legacy-peer-deps && npm run build -- --outDir /opt/prebuilt/agent-ui-dist --emptyOutDir
690
+ `;
691
+ }
692
+ async function runSharedReleaseFlow(versionType, options, project) {
693
+ const { mainBranch, devBranch, reverseMerge, dryRun, force, preid, isPrerelease } = getReleaseOptions(options);
694
+ const releaseStartedAt = new Date();
695
+ try {
696
+ console.log(`Release started at: ${formatTimestamp(releaseStartedAt)}`);
697
+ const currentBranch = validateReleaseBranch(preid, mainBranch, devBranch, force);
698
+ const plannedVersion = getExpectedVersion(project, versionType, preid);
699
+ if (dryRun) {
700
+ console.log('Dry run mode - no changes will be made\n');
701
+ }
702
+ if (plannedVersion) {
703
+ console.log(`Planned version: ${plannedVersion}`);
704
+ }
705
+ await runLocalReleaseValidation(project, dryRun);
706
+ const versionCmd = getVersionCommand(versionType, preid);
707
+ if (isPrerelease) {
708
+ console.log(`Updating package version (pre-release: ${preid})...`);
709
+ }
710
+ else {
711
+ console.log(`Updating package version (${versionType})...`);
712
+ }
713
+ exec(versionCmd, dryRun);
714
+ console.log('Pushing to git (with tags)...');
715
+ exec('git push --follow-tags', dryRun);
716
+ console.log('Publishing package...');
717
+ if (isPrerelease) {
718
+ exec(`npm publish --tag ${preid}`, dryRun);
719
+ }
720
+ else {
721
+ exec('npm publish', dryRun);
722
+ }
723
+ console.log('Published successfully!\n');
724
+ if (!isPrerelease && reverseMerge) {
725
+ console.log(`Merging ${mainBranch} into ${devBranch}...`);
726
+ exec(`git checkout ${devBranch}`, dryRun);
727
+ exec(`git pull origin ${devBranch}`, dryRun);
728
+ try {
729
+ exec(`git merge ${mainBranch} -m "chore: merge ${mainBranch} after publish"`, dryRun);
730
+ exec(`git push origin ${devBranch}`, dryRun);
731
+ console.log(`Successfully merged ${mainBranch} into ${devBranch}\n`);
732
+ }
733
+ catch {
734
+ console.error('\nMerge conflict detected. Please resolve manually.');
735
+ console.error(` You are now on the ${devBranch} branch.`);
736
+ process.exit(1);
737
+ }
738
+ exec(`git checkout ${mainBranch}`, dryRun);
739
+ console.log(`Back on ${mainBranch} branch`);
740
+ }
741
+ else if (!isPrerelease && !reverseMerge) {
742
+ console.log('Skipping reverse merge (--no-merge)');
743
+ }
744
+ else {
745
+ console.log(`Staying on ${currentBranch} branch`);
746
+ }
747
+ const releaseEndedAt = new Date();
748
+ console.log(`Release finished at: ${formatTimestamp(releaseEndedAt)}`);
749
+ console.log(`Total release duration: ${formatDuration(releaseEndedAt.getTime() - releaseStartedAt.getTime())}`);
750
+ console.log('\nAll done!');
751
+ }
752
+ catch (error) {
753
+ const releaseEndedAt = new Date();
754
+ console.error(`Release finished at: ${formatTimestamp(releaseEndedAt)}`);
755
+ console.error(`Total release duration: ${formatDuration(releaseEndedAt.getTime() - releaseStartedAt.getTime())}`);
756
+ console.error('Error:', error instanceof Error ? error.message : error);
757
+ process.exit(1);
758
+ }
759
+ }
760
+ function runSolidLibraryManagementReleaseFlow(versionType, options, project) {
761
+ const { mainBranch, devBranch, dryRun, force, preid, isPrerelease } = getReleaseOptions(options);
762
+ const solidApiPackageJsonPath = project.versionSourcePath || path_1.default.join(process.cwd(), 'solid-api', 'package.json');
763
+ const solidApiPackageJson = readRequiredPackageJson(solidApiPackageJsonPath);
764
+ const currentVersion = solidApiPackageJson.version;
765
+ const agentRepoPath = process.env.SOLIDX_AI_AGENT_PATH;
766
+ const imageRepository = 'solidxaiorg/solid-library-management-sandbox-base-image';
767
+ if (!currentVersion) {
768
+ console.error(`solid-api package.json is missing a version at ${solidApiPackageJsonPath}`);
769
+ process.exit(1);
770
+ }
771
+ if (!agentRepoPath) {
772
+ console.error('SOLIDX_AI_AGENT_PATH is not set. This release flow needs the local agent checkout.');
773
+ process.exit(1);
774
+ }
775
+ if (!fs_1.default.existsSync(agentRepoPath)) {
776
+ console.error(`SOLIDX_AI_AGENT_PATH does not exist: ${agentRepoPath}`);
777
+ process.exit(1);
778
+ }
779
+ const currentBranch = validateReleaseBranch(preid, mainBranch, devBranch, force);
780
+ const plannedVersion = planNextVersion(currentVersion, versionType, preid);
781
+ const movingDockerTag = getMovingDockerTag(preid);
782
+ const versionCmd = getVersionCommand(versionType, preid);
783
+ const buildContextRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'solid-library-management-release-'));
784
+ const dockerfilePath = path_1.default.join(buildContextRoot, 'Dockerfile');
785
+ const agentCopyPath = path_1.default.join(buildContextRoot, 'agent');
786
+ const versionImageTag = `${imageRepository}:${plannedVersion}`;
787
+ const movingImageTag = `${imageRepository}:${movingDockerTag}`;
788
+ try {
789
+ console.log(`Preparing solid-library-management sandbox base image release (${plannedVersion})...`);
790
+ console.log(`Docker image repository: ${imageRepository}`);
791
+ console.log(`Docker base image: node:20-bookworm`);
792
+ console.log(`Docker tags to publish: ${plannedVersion}, ${movingDockerTag}`);
793
+ console.log(`Agent source path: ${agentRepoPath}`);
794
+ if (dryRun) {
795
+ console.log('Dry run mode - no changes will be made\n');
796
+ }
797
+ console.log(`Updating solid-api version (${isPrerelease ? `pre-release: ${preid}` : versionType})...`);
798
+ exec(`cd solid-api && ${versionCmd}`, dryRun);
799
+ if (!dryRun) {
800
+ const actualVersion = readRequiredPackageJson(solidApiPackageJsonPath).version;
801
+ if (!actualVersion) {
802
+ throw new Error('Failed to determine solid-api version after npm version.');
803
+ }
804
+ if (actualVersion !== plannedVersion) {
805
+ throw new Error(`Expected solid-api version ${plannedVersion}, but found ${actualVersion} after npm version.`);
806
+ }
807
+ }
808
+ console.log('Creating Docker build context...');
809
+ if (!dryRun) {
810
+ copyDirectoryForDockerBuild(agentRepoPath, agentCopyPath);
811
+ fs_1.default.writeFileSync(dockerfilePath, createSolidLibraryManagementDockerfile(), 'utf-8');
812
+ }
813
+ console.log('Building sandbox base image...');
814
+ exec(`docker build --progress=plain -t ${versionImageTag} -t ${movingImageTag} ${buildContextRoot}`, dryRun);
815
+ console.log('Pushing git commit and tags...');
816
+ exec('git push --follow-tags', dryRun);
817
+ console.log('Publishing Docker image...');
818
+ exec(`docker push ${versionImageTag}`, dryRun);
819
+ exec(`docker push ${movingImageTag}`, dryRun);
820
+ console.log(`Staying on ${currentBranch} branch`);
821
+ console.log('\nDocker image release completed!');
822
+ }
823
+ catch (error) {
824
+ console.error('Error:', error instanceof Error ? error.message : error);
825
+ process.exit(1);
826
+ }
827
+ finally {
828
+ if (!dryRun) {
829
+ fs_1.default.rmSync(buildContextRoot, { recursive: true, force: true });
830
+ }
831
+ }
832
+ }
48
833
  function registerReleaseCommand(program) {
49
834
  program
50
835
  .command('release [version-type]')
@@ -62,18 +847,23 @@ Examples:
62
847
  $ solidctl release minor # minor: 0.0.12 → 0.1.0
63
848
  $ solidctl release major # major: 0.0.12 → 1.0.0
64
849
 
65
- Pre-releases (from dev branch):
66
- $ solidctl release --preid=alpha # 0.0.12 → 0.0.13-alpha.0
850
+ Pre-releases:
851
+ $ solidctl release --preid=alpha # from dev: 0.0.12 → 0.0.13-alpha.0
67
852
  $ 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
853
+ $ solidctl release minor --preid=alpha # from dev: 0.0.12 → 0.1.0-alpha.0
854
+ $ solidctl release --preid=beta # from dev: 0.0.13-alpha.1 → 0.0.13-beta.0
855
+ $ solidctl release --preid=rc # from dev: 0.0.13-beta.1 → 0.0.13-rc.0
71
856
 
72
857
  Options:
73
858
  $ solidctl release --dry-run # Preview without making changes
74
859
  $ solidctl release --force # Override branch checks
75
860
  $ solidctl release --no-merge # Skip main → dev merge after stable release
76
861
 
862
+ Local release validation:
863
+ For solid-core-module and solid-core-ui releases, solidctl now runs local validation commands
864
+ from the consuming project pointed to by ${RELEASE_TEST_PROJECT_PATH_ENV}. If that env var is
865
+ missing, the release command will prompt for the consuming project path.
866
+
77
867
  Configuration:
78
868
  Add to package.json or solidctl.config.json:
79
869
  {
@@ -86,95 +876,21 @@ Configuration:
86
876
  }
87
877
  }
88
878
  `)
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);
879
+ .action(async (versionType = 'patch', options) => {
880
+ const project = resolveReleaseProject();
881
+ switch (project.type) {
882
+ case 'solidctl':
883
+ await runSharedReleaseFlow(versionType, options, project);
884
+ break;
885
+ case 'solid-core-module':
886
+ await runSharedReleaseFlow(versionType, options, project);
887
+ break;
888
+ case 'solid-core-ui':
889
+ await runSharedReleaseFlow(versionType, options, project);
890
+ break;
891
+ case 'solid-library-management':
892
+ runSolidLibraryManagementReleaseFlow(versionType, options, project);
893
+ break;
178
894
  }
179
895
  });
180
896
  }