erosolar-cli 1.7.191 → 1.7.192
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/dist/core/toolRuntime.d.ts +19 -7
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +11 -9
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +1 -0
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/systemPrompt.d.ts.map +1 -1
- package/dist/shell/systemPrompt.js +3 -1
- package/dist/shell/systemPrompt.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +5 -0
- package/dist/shell/terminalInputAdapter.js.map +1 -1
- package/dist/tools/devTools.d.ts.map +1 -1
- package/dist/tools/devTools.js +554 -0
- package/dist/tools/devTools.js.map +1 -1
- package/package.json +1 -1
package/dist/tools/devTools.js
CHANGED
|
@@ -488,6 +488,560 @@ export function createDevTools(workingDir) {
|
|
|
488
488
|
},
|
|
489
489
|
},
|
|
490
490
|
// ========================================================================
|
|
491
|
+
// PyPI Publishing Tools
|
|
492
|
+
// ========================================================================
|
|
493
|
+
{
|
|
494
|
+
name: 'python_publish',
|
|
495
|
+
description: 'Publish Python package to PyPI. Handles the full workflow: check auth, commit changes, bump version, build, and publish.',
|
|
496
|
+
parameters: {
|
|
497
|
+
type: 'object',
|
|
498
|
+
properties: {
|
|
499
|
+
versionBump: {
|
|
500
|
+
type: 'string',
|
|
501
|
+
enum: ['patch', 'minor', 'major'],
|
|
502
|
+
description: 'Version bump type (default: patch)',
|
|
503
|
+
},
|
|
504
|
+
commitMessage: {
|
|
505
|
+
type: 'string',
|
|
506
|
+
description: 'Commit message for uncommitted changes (if any)',
|
|
507
|
+
},
|
|
508
|
+
dryRun: {
|
|
509
|
+
type: 'boolean',
|
|
510
|
+
description: 'If true, performs all steps except the actual publish',
|
|
511
|
+
},
|
|
512
|
+
testPypi: {
|
|
513
|
+
type: 'boolean',
|
|
514
|
+
description: 'Publish to TestPyPI instead of PyPI (default: false)',
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
additionalProperties: false,
|
|
518
|
+
},
|
|
519
|
+
handler: async (args) => {
|
|
520
|
+
const versionBump = typeof args['versionBump'] === 'string' ? args['versionBump'] : 'patch';
|
|
521
|
+
const commitMessage = typeof args['commitMessage'] === 'string' ? args['commitMessage'] : undefined;
|
|
522
|
+
const dryRun = args['dryRun'] === true;
|
|
523
|
+
const testPypi = args['testPypi'] === true;
|
|
524
|
+
const steps = [];
|
|
525
|
+
const errors = [];
|
|
526
|
+
try {
|
|
527
|
+
// Step 1: Detect Python project type and check auth
|
|
528
|
+
steps.push('## Step 1: Detecting project type and checking authentication');
|
|
529
|
+
const hasPoetry = existsSync(join(workingDir, 'pyproject.toml'));
|
|
530
|
+
const hasSetupPy = existsSync(join(workingDir, 'setup.py'));
|
|
531
|
+
const hasSetupCfg = existsSync(join(workingDir, 'setup.cfg'));
|
|
532
|
+
let projectType = 'unknown';
|
|
533
|
+
let currentVersion = '';
|
|
534
|
+
if (hasPoetry) {
|
|
535
|
+
const pyprojectContent = readFileSync(join(workingDir, 'pyproject.toml'), 'utf-8');
|
|
536
|
+
if (pyprojectContent.includes('[tool.poetry]')) {
|
|
537
|
+
projectType = 'poetry';
|
|
538
|
+
const versionMatch = pyprojectContent.match(/version\s*=\s*"([^"]+)"/);
|
|
539
|
+
if (versionMatch)
|
|
540
|
+
currentVersion = versionMatch[1] || '';
|
|
541
|
+
steps.push(`✓ Detected Poetry project (v${currentVersion})`);
|
|
542
|
+
// Check Poetry auth
|
|
543
|
+
try {
|
|
544
|
+
await execAsync('poetry config pypi-token.pypi', { cwd: workingDir, timeout: 10000 });
|
|
545
|
+
steps.push('✓ Poetry PyPI token configured');
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
steps.push('⚠ PyPI token may not be configured. Run: poetry config pypi-token.pypi <token>');
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (projectType === 'unknown' && (hasSetupPy || hasSetupCfg)) {
|
|
553
|
+
projectType = 'setuptools';
|
|
554
|
+
// Try to get version from setup.py or setup.cfg
|
|
555
|
+
if (hasSetupPy) {
|
|
556
|
+
const setupContent = readFileSync(join(workingDir, 'setup.py'), 'utf-8');
|
|
557
|
+
const versionMatch = setupContent.match(/version\s*=\s*['"]([^'"]+)['"]/);
|
|
558
|
+
if (versionMatch)
|
|
559
|
+
currentVersion = versionMatch[1] || '';
|
|
560
|
+
}
|
|
561
|
+
steps.push(`✓ Detected setuptools project${currentVersion ? ` (v${currentVersion})` : ''}`);
|
|
562
|
+
// Check twine auth
|
|
563
|
+
try {
|
|
564
|
+
await execAsync('twine --version', { cwd: workingDir, timeout: 10000 });
|
|
565
|
+
steps.push('✓ Twine is available');
|
|
566
|
+
}
|
|
567
|
+
catch {
|
|
568
|
+
errors.push('✗ Twine not found. Install with: pip install twine');
|
|
569
|
+
return [...steps, '', '## Errors', ...errors].join('\n');
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (projectType === 'unknown') {
|
|
573
|
+
errors.push('✗ Could not detect Python project type. Need pyproject.toml (Poetry) or setup.py/setup.cfg (setuptools)');
|
|
574
|
+
return [...steps, '', '## Errors', ...errors].join('\n');
|
|
575
|
+
}
|
|
576
|
+
// Step 2: Check for uncommitted changes
|
|
577
|
+
steps.push('');
|
|
578
|
+
steps.push('## Step 2: Checking git status');
|
|
579
|
+
try {
|
|
580
|
+
const { stdout: gitStatus } = await execAsync('git status --porcelain', { cwd: workingDir, timeout: 10000 });
|
|
581
|
+
if (gitStatus.trim()) {
|
|
582
|
+
if (commitMessage) {
|
|
583
|
+
steps.push(`Found uncommitted changes, committing with message: "${commitMessage}"`);
|
|
584
|
+
await execAsync('git add .', { cwd: workingDir, timeout: 30000 });
|
|
585
|
+
await execAsync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, { cwd: workingDir, timeout: 30000 });
|
|
586
|
+
steps.push('✓ Changes committed');
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
errors.push('✗ Uncommitted changes found. Provide a commitMessage or commit manually first.');
|
|
590
|
+
gitStatus.trim().split('\n').forEach(line => errors.push(` ${line}`));
|
|
591
|
+
return [...steps, '', '## Errors', ...errors].join('\n');
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
steps.push('✓ Working directory clean');
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
catch (e) {
|
|
599
|
+
steps.push(`Warning: Could not check git status: ${e.message}`);
|
|
600
|
+
}
|
|
601
|
+
// Step 3: Bump version
|
|
602
|
+
steps.push('');
|
|
603
|
+
steps.push('## Step 3: Bumping version');
|
|
604
|
+
try {
|
|
605
|
+
if (projectType === 'poetry') {
|
|
606
|
+
const { stdout: versionOut } = await execAsync(`poetry version ${versionBump}`, {
|
|
607
|
+
cwd: workingDir,
|
|
608
|
+
timeout: 30000,
|
|
609
|
+
});
|
|
610
|
+
steps.push(`✓ ${versionOut.trim()}`);
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
// For setuptools, use bump2version if available, otherwise manual
|
|
614
|
+
try {
|
|
615
|
+
await execAsync(`bump2version ${versionBump}`, { cwd: workingDir, timeout: 30000 });
|
|
616
|
+
steps.push(`✓ Version bumped with bump2version`);
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
steps.push(`⚠ bump2version not available. Please manually update version in setup.py/setup.cfg`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
catch (e) {
|
|
624
|
+
errors.push(`✗ Version bump failed: ${e.message}`);
|
|
625
|
+
return [...steps, '', '## Errors', ...errors].join('\n');
|
|
626
|
+
}
|
|
627
|
+
// Step 4: Build
|
|
628
|
+
steps.push('');
|
|
629
|
+
steps.push('## Step 4: Building package');
|
|
630
|
+
try {
|
|
631
|
+
if (projectType === 'poetry') {
|
|
632
|
+
await execAsync('poetry build', {
|
|
633
|
+
cwd: workingDir,
|
|
634
|
+
timeout: 120000,
|
|
635
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
636
|
+
});
|
|
637
|
+
steps.push('✓ Built with Poetry');
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
// Clean old builds
|
|
641
|
+
await execAsync('rm -rf dist/ build/ *.egg-info', { cwd: workingDir, timeout: 10000, shell: '/bin/bash' });
|
|
642
|
+
await execAsync('python -m build', {
|
|
643
|
+
cwd: workingDir,
|
|
644
|
+
timeout: 120000,
|
|
645
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
646
|
+
});
|
|
647
|
+
steps.push('✓ Built with python -m build');
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
catch (e) {
|
|
651
|
+
errors.push(`✗ Build failed: ${e.message}`);
|
|
652
|
+
return [...steps, '', '## Errors', ...errors].join('\n');
|
|
653
|
+
}
|
|
654
|
+
// Step 5: Publish
|
|
655
|
+
steps.push('');
|
|
656
|
+
steps.push('## Step 5: Publishing to PyPI');
|
|
657
|
+
const repository = testPypi ? 'testpypi' : 'pypi';
|
|
658
|
+
if (dryRun) {
|
|
659
|
+
steps.push(`⚠ DRY RUN - would publish to ${repository}`);
|
|
660
|
+
steps.push('Files that would be uploaded:');
|
|
661
|
+
try {
|
|
662
|
+
const { stdout: distFiles } = await execAsync('ls -la dist/', { cwd: workingDir, timeout: 10000 });
|
|
663
|
+
steps.push(distFiles);
|
|
664
|
+
}
|
|
665
|
+
catch {
|
|
666
|
+
steps.push(' (could not list dist files)');
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
try {
|
|
671
|
+
if (projectType === 'poetry') {
|
|
672
|
+
const publishCmd = testPypi
|
|
673
|
+
? 'poetry publish -r testpypi'
|
|
674
|
+
: 'poetry publish';
|
|
675
|
+
await execAsync(publishCmd, {
|
|
676
|
+
cwd: workingDir,
|
|
677
|
+
timeout: 120000,
|
|
678
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
const uploadUrl = testPypi
|
|
683
|
+
? '--repository-url https://test.pypi.org/legacy/'
|
|
684
|
+
: '';
|
|
685
|
+
await execAsync(`twine upload ${uploadUrl} dist/*`, {
|
|
686
|
+
cwd: workingDir,
|
|
687
|
+
timeout: 120000,
|
|
688
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
steps.push(`✓ Published to ${repository}`);
|
|
692
|
+
}
|
|
693
|
+
catch (e) {
|
|
694
|
+
errors.push(`✗ Publish failed: ${e.message}`);
|
|
695
|
+
return [...steps, '', '## Errors', ...errors].join('\n');
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// Step 6: Git commit version bump and push
|
|
699
|
+
steps.push('');
|
|
700
|
+
steps.push('## Step 6: Committing version bump and pushing');
|
|
701
|
+
try {
|
|
702
|
+
const { stdout: gitStatus } = await execAsync('git status --porcelain', { cwd: workingDir, timeout: 10000 });
|
|
703
|
+
if (gitStatus.trim()) {
|
|
704
|
+
await execAsync('git add .', { cwd: workingDir, timeout: 30000 });
|
|
705
|
+
await execAsync('git commit -m "Bump version"', { cwd: workingDir, timeout: 30000 });
|
|
706
|
+
steps.push('✓ Version bump committed');
|
|
707
|
+
}
|
|
708
|
+
await execAsync('git push && git push --tags', { cwd: workingDir, timeout: 60000 });
|
|
709
|
+
steps.push('✓ Pushed to remote');
|
|
710
|
+
}
|
|
711
|
+
catch (e) {
|
|
712
|
+
steps.push(`Warning: Could not push to git: ${e.message}`);
|
|
713
|
+
}
|
|
714
|
+
// Summary
|
|
715
|
+
steps.push('');
|
|
716
|
+
steps.push('## Summary');
|
|
717
|
+
steps.push(`✓ Python package ${dryRun ? '(dry run)' : 'published'} to ${repository}`);
|
|
718
|
+
return steps.join('\n');
|
|
719
|
+
}
|
|
720
|
+
catch (error) {
|
|
721
|
+
errors.push(`Unexpected error: ${error.message}`);
|
|
722
|
+
return [...steps, '', '## Errors', ...errors].join('\n');
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
name: 'pypi_check_auth',
|
|
728
|
+
description: 'Check PyPI authentication status for both Poetry and twine',
|
|
729
|
+
parameters: {
|
|
730
|
+
type: 'object',
|
|
731
|
+
properties: {},
|
|
732
|
+
additionalProperties: false,
|
|
733
|
+
},
|
|
734
|
+
handler: async () => {
|
|
735
|
+
const output = ['# PyPI Authentication Status', ''];
|
|
736
|
+
// Check Poetry
|
|
737
|
+
output.push('## Poetry');
|
|
738
|
+
try {
|
|
739
|
+
const { stdout: poetryConfig } = await execAsync('poetry config --list 2>/dev/null | grep pypi', {
|
|
740
|
+
cwd: workingDir,
|
|
741
|
+
timeout: 10000,
|
|
742
|
+
shell: '/bin/bash',
|
|
743
|
+
});
|
|
744
|
+
if (poetryConfig.includes('pypi-token')) {
|
|
745
|
+
output.push('✓ PyPI token configured in Poetry');
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
output.push('✗ No PyPI token in Poetry');
|
|
749
|
+
output.push(' Set with: poetry config pypi-token.pypi <your-token>');
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
catch {
|
|
753
|
+
output.push('Poetry not available or not configured');
|
|
754
|
+
}
|
|
755
|
+
// Check twine/pip
|
|
756
|
+
output.push('');
|
|
757
|
+
output.push('## Twine/pip');
|
|
758
|
+
try {
|
|
759
|
+
await execAsync('twine --version', { cwd: workingDir, timeout: 10000 });
|
|
760
|
+
output.push('✓ Twine is installed');
|
|
761
|
+
// Check for .pypirc
|
|
762
|
+
const homedir = process.env['HOME'] || '';
|
|
763
|
+
if (existsSync(join(homedir, '.pypirc'))) {
|
|
764
|
+
output.push('✓ .pypirc found in home directory');
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
output.push('⚠ No .pypirc found. Twine will prompt for credentials.');
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
catch {
|
|
771
|
+
output.push('✗ Twine not installed');
|
|
772
|
+
output.push(' Install with: pip install twine');
|
|
773
|
+
}
|
|
774
|
+
// Check for TWINE_* env vars
|
|
775
|
+
output.push('');
|
|
776
|
+
output.push('## Environment Variables');
|
|
777
|
+
const twineUser = process.env['TWINE_USERNAME'];
|
|
778
|
+
const twinePass = process.env['TWINE_PASSWORD'];
|
|
779
|
+
if (twineUser) {
|
|
780
|
+
output.push(`✓ TWINE_USERNAME set: ${twineUser}`);
|
|
781
|
+
}
|
|
782
|
+
if (twinePass) {
|
|
783
|
+
output.push('✓ TWINE_PASSWORD is set');
|
|
784
|
+
}
|
|
785
|
+
if (!twineUser && !twinePass) {
|
|
786
|
+
output.push('No TWINE_* environment variables set');
|
|
787
|
+
}
|
|
788
|
+
return output.join('\n');
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
// ========================================================================
|
|
792
|
+
// Cargo (Rust) Publishing Tools
|
|
793
|
+
// ========================================================================
|
|
794
|
+
{
|
|
795
|
+
name: 'cargo_publish',
|
|
796
|
+
description: 'Publish Rust crate to crates.io. Handles the full workflow: check auth, commit changes, bump version, build, and publish.',
|
|
797
|
+
parameters: {
|
|
798
|
+
type: 'object',
|
|
799
|
+
properties: {
|
|
800
|
+
versionBump: {
|
|
801
|
+
type: 'string',
|
|
802
|
+
enum: ['patch', 'minor', 'major'],
|
|
803
|
+
description: 'Version bump type (default: patch)',
|
|
804
|
+
},
|
|
805
|
+
commitMessage: {
|
|
806
|
+
type: 'string',
|
|
807
|
+
description: 'Commit message for uncommitted changes (if any)',
|
|
808
|
+
},
|
|
809
|
+
dryRun: {
|
|
810
|
+
type: 'boolean',
|
|
811
|
+
description: 'If true, performs all steps except the actual publish',
|
|
812
|
+
},
|
|
813
|
+
allowDirty: {
|
|
814
|
+
type: 'boolean',
|
|
815
|
+
description: 'Allow publishing with uncommitted changes (default: false)',
|
|
816
|
+
},
|
|
817
|
+
},
|
|
818
|
+
additionalProperties: false,
|
|
819
|
+
},
|
|
820
|
+
handler: async (args) => {
|
|
821
|
+
const versionBump = typeof args['versionBump'] === 'string' ? args['versionBump'] : 'patch';
|
|
822
|
+
const commitMessage = typeof args['commitMessage'] === 'string' ? args['commitMessage'] : undefined;
|
|
823
|
+
const dryRun = args['dryRun'] === true;
|
|
824
|
+
const allowDirty = args['allowDirty'] === true;
|
|
825
|
+
const steps = [];
|
|
826
|
+
const errors = [];
|
|
827
|
+
try {
|
|
828
|
+
// Step 1: Check Cargo.toml exists and auth
|
|
829
|
+
steps.push('## Step 1: Checking project and authentication');
|
|
830
|
+
const cargoTomlPath = join(workingDir, 'Cargo.toml');
|
|
831
|
+
if (!existsSync(cargoTomlPath)) {
|
|
832
|
+
errors.push('✗ Cargo.toml not found. This is not a Rust project.');
|
|
833
|
+
return [...steps, '', '## Errors', ...errors].join('\n');
|
|
834
|
+
}
|
|
835
|
+
const cargoContent = readFileSync(cargoTomlPath, 'utf-8');
|
|
836
|
+
const nameMatch = cargoContent.match(/name\s*=\s*"([^"]+)"/);
|
|
837
|
+
const versionMatch = cargoContent.match(/version\s*=\s*"([^"]+)"/);
|
|
838
|
+
const crateName = nameMatch ? nameMatch[1] : 'unknown';
|
|
839
|
+
const currentVersion = versionMatch ? versionMatch[1] : '0.0.0';
|
|
840
|
+
steps.push(`✓ Found crate: ${crateName} v${currentVersion}`);
|
|
841
|
+
// Check cargo login
|
|
842
|
+
try {
|
|
843
|
+
const { stdout: whoami } = await execAsync('cargo owner --list 2>/dev/null | head -1', {
|
|
844
|
+
cwd: workingDir,
|
|
845
|
+
timeout: 30000,
|
|
846
|
+
shell: '/bin/bash',
|
|
847
|
+
});
|
|
848
|
+
if (whoami.trim()) {
|
|
849
|
+
steps.push(`✓ Logged in to crates.io`);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
steps.push('⚠ Could not verify crates.io login. Make sure you ran `cargo login`');
|
|
854
|
+
}
|
|
855
|
+
// Step 2: Check for uncommitted changes
|
|
856
|
+
steps.push('');
|
|
857
|
+
steps.push('## Step 2: Checking git status');
|
|
858
|
+
try {
|
|
859
|
+
const { stdout: gitStatus } = await execAsync('git status --porcelain', { cwd: workingDir, timeout: 10000 });
|
|
860
|
+
if (gitStatus.trim()) {
|
|
861
|
+
if (commitMessage) {
|
|
862
|
+
steps.push(`Found uncommitted changes, committing with message: "${commitMessage}"`);
|
|
863
|
+
await execAsync('git add .', { cwd: workingDir, timeout: 30000 });
|
|
864
|
+
await execAsync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, { cwd: workingDir, timeout: 30000 });
|
|
865
|
+
steps.push('✓ Changes committed');
|
|
866
|
+
}
|
|
867
|
+
else if (!allowDirty) {
|
|
868
|
+
errors.push('✗ Uncommitted changes found. Provide a commitMessage, commit manually, or use allowDirty=true.');
|
|
869
|
+
gitStatus.trim().split('\n').forEach(line => errors.push(` ${line}`));
|
|
870
|
+
return [...steps, '', '## Errors', ...errors].join('\n');
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
steps.push('⚠ Uncommitted changes present (allowDirty=true)');
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
steps.push('✓ Working directory clean');
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
catch (e) {
|
|
881
|
+
steps.push(`Warning: Could not check git status: ${e.message}`);
|
|
882
|
+
}
|
|
883
|
+
// Step 3: Bump version
|
|
884
|
+
steps.push('');
|
|
885
|
+
steps.push('## Step 3: Bumping version');
|
|
886
|
+
// Parse current version
|
|
887
|
+
const versionParts = (currentVersion || '0.0.0').split('.').map(p => parseInt(p, 10) || 0);
|
|
888
|
+
let [major = 0, minor = 0, patch = 0] = versionParts;
|
|
889
|
+
switch (versionBump) {
|
|
890
|
+
case 'major':
|
|
891
|
+
major++;
|
|
892
|
+
minor = 0;
|
|
893
|
+
patch = 0;
|
|
894
|
+
break;
|
|
895
|
+
case 'minor':
|
|
896
|
+
minor++;
|
|
897
|
+
patch = 0;
|
|
898
|
+
break;
|
|
899
|
+
case 'patch':
|
|
900
|
+
default:
|
|
901
|
+
patch++;
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
const newVersion = `${major}.${minor}.${patch}`;
|
|
905
|
+
// Update Cargo.toml
|
|
906
|
+
const updatedCargo = cargoContent.replace(/version\s*=\s*"[^"]+"/, `version = "${newVersion}"`);
|
|
907
|
+
const fs = await import('node:fs/promises');
|
|
908
|
+
await fs.writeFile(cargoTomlPath, updatedCargo, 'utf-8');
|
|
909
|
+
steps.push(`✓ Version bumped: ${currentVersion} → ${newVersion}`);
|
|
910
|
+
// Update Cargo.lock
|
|
911
|
+
try {
|
|
912
|
+
await execAsync('cargo check', { cwd: workingDir, timeout: 120000 });
|
|
913
|
+
steps.push('✓ Cargo.lock updated');
|
|
914
|
+
}
|
|
915
|
+
catch (e) {
|
|
916
|
+
steps.push(`Warning: cargo check had issues: ${e.message}`);
|
|
917
|
+
}
|
|
918
|
+
// Step 4: Build and test
|
|
919
|
+
steps.push('');
|
|
920
|
+
steps.push('## Step 4: Building and testing');
|
|
921
|
+
try {
|
|
922
|
+
await execAsync('cargo build --release', {
|
|
923
|
+
cwd: workingDir,
|
|
924
|
+
timeout: 300000,
|
|
925
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
926
|
+
});
|
|
927
|
+
steps.push('✓ Release build successful');
|
|
928
|
+
await execAsync('cargo test', {
|
|
929
|
+
cwd: workingDir,
|
|
930
|
+
timeout: 300000,
|
|
931
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
932
|
+
});
|
|
933
|
+
steps.push('✓ Tests passed');
|
|
934
|
+
}
|
|
935
|
+
catch (e) {
|
|
936
|
+
errors.push(`✗ Build/test failed: ${e.message}`);
|
|
937
|
+
return [...steps, '', '## Errors', ...errors].join('\n');
|
|
938
|
+
}
|
|
939
|
+
// Step 5: Publish
|
|
940
|
+
steps.push('');
|
|
941
|
+
steps.push('## Step 5: Publishing to crates.io');
|
|
942
|
+
if (dryRun) {
|
|
943
|
+
steps.push('⚠ DRY RUN - would publish to crates.io');
|
|
944
|
+
try {
|
|
945
|
+
const { stdout: dryRunOut } = await execAsync('cargo publish --dry-run', {
|
|
946
|
+
cwd: workingDir,
|
|
947
|
+
timeout: 120000,
|
|
948
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
949
|
+
});
|
|
950
|
+
steps.push('Dry run output:');
|
|
951
|
+
steps.push(dryRunOut.substring(0, 2000));
|
|
952
|
+
}
|
|
953
|
+
catch (e) {
|
|
954
|
+
steps.push(`Dry run notes: ${e.message}`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
try {
|
|
959
|
+
const publishFlags = allowDirty ? '--allow-dirty' : '';
|
|
960
|
+
await execAsync(`cargo publish ${publishFlags}`, {
|
|
961
|
+
cwd: workingDir,
|
|
962
|
+
timeout: 180000,
|
|
963
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
964
|
+
});
|
|
965
|
+
steps.push(`✓ Published ${crateName}@${newVersion} to crates.io`);
|
|
966
|
+
}
|
|
967
|
+
catch (e) {
|
|
968
|
+
errors.push(`✗ Publish failed: ${e.message}`);
|
|
969
|
+
return [...steps, '', '## Errors', ...errors].join('\n');
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
// Step 6: Git commit and push
|
|
973
|
+
steps.push('');
|
|
974
|
+
steps.push('## Step 6: Committing and pushing');
|
|
975
|
+
try {
|
|
976
|
+
await execAsync('git add Cargo.toml Cargo.lock', { cwd: workingDir, timeout: 10000 });
|
|
977
|
+
await execAsync(`git commit -m "Release v${newVersion}"`, { cwd: workingDir, timeout: 30000 });
|
|
978
|
+
await execAsync(`git tag -a "v${newVersion}" -m "Release v${newVersion}"`, { cwd: workingDir, timeout: 10000 });
|
|
979
|
+
steps.push(`✓ Created tag v${newVersion}`);
|
|
980
|
+
await execAsync('git push && git push --tags', { cwd: workingDir, timeout: 60000 });
|
|
981
|
+
steps.push('✓ Pushed to remote');
|
|
982
|
+
}
|
|
983
|
+
catch (e) {
|
|
984
|
+
steps.push(`Warning: Could not push to git: ${e.message}`);
|
|
985
|
+
}
|
|
986
|
+
// Summary
|
|
987
|
+
steps.push('');
|
|
988
|
+
steps.push('## Summary');
|
|
989
|
+
steps.push(`✓ ${crateName}@${newVersion} ${dryRun ? '(dry run)' : 'published to crates.io'}`);
|
|
990
|
+
return steps.join('\n');
|
|
991
|
+
}
|
|
992
|
+
catch (error) {
|
|
993
|
+
errors.push(`Unexpected error: ${error.message}`);
|
|
994
|
+
return [...steps, '', '## Errors', ...errors].join('\n');
|
|
995
|
+
}
|
|
996
|
+
},
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
name: 'cargo_check_auth',
|
|
1000
|
+
description: 'Check crates.io authentication status',
|
|
1001
|
+
parameters: {
|
|
1002
|
+
type: 'object',
|
|
1003
|
+
properties: {},
|
|
1004
|
+
additionalProperties: false,
|
|
1005
|
+
},
|
|
1006
|
+
handler: async () => {
|
|
1007
|
+
const output = ['# Crates.io Authentication Status', ''];
|
|
1008
|
+
// Check if cargo is available
|
|
1009
|
+
try {
|
|
1010
|
+
const { stdout: cargoVersion } = await execAsync('cargo --version', { cwd: workingDir, timeout: 10000 });
|
|
1011
|
+
output.push(`✓ ${cargoVersion.trim()}`);
|
|
1012
|
+
}
|
|
1013
|
+
catch {
|
|
1014
|
+
output.push('✗ Cargo not found. Install Rust from https://rustup.rs');
|
|
1015
|
+
return output.join('\n');
|
|
1016
|
+
}
|
|
1017
|
+
// Check credentials file
|
|
1018
|
+
const homedir = process.env['HOME'] || '';
|
|
1019
|
+
const credentialsPath = join(homedir, '.cargo', 'credentials.toml');
|
|
1020
|
+
const legacyCredentialsPath = join(homedir, '.cargo', 'credentials');
|
|
1021
|
+
output.push('');
|
|
1022
|
+
output.push('## Credentials');
|
|
1023
|
+
if (existsSync(credentialsPath) || existsSync(legacyCredentialsPath)) {
|
|
1024
|
+
output.push('✓ Credentials file found');
|
|
1025
|
+
output.push(' To update token, run: cargo login');
|
|
1026
|
+
}
|
|
1027
|
+
else {
|
|
1028
|
+
output.push('✗ No credentials file found');
|
|
1029
|
+
output.push(' Run: cargo login <your-api-token>');
|
|
1030
|
+
output.push(' Get token from: https://crates.io/settings/tokens');
|
|
1031
|
+
}
|
|
1032
|
+
// Check CARGO_REGISTRY_TOKEN env var
|
|
1033
|
+
output.push('');
|
|
1034
|
+
output.push('## Environment Variables');
|
|
1035
|
+
if (process.env['CARGO_REGISTRY_TOKEN']) {
|
|
1036
|
+
output.push('✓ CARGO_REGISTRY_TOKEN is set');
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
output.push('CARGO_REGISTRY_TOKEN not set (optional)');
|
|
1040
|
+
}
|
|
1041
|
+
return output.join('\n');
|
|
1042
|
+
},
|
|
1043
|
+
},
|
|
1044
|
+
// ========================================================================
|
|
491
1045
|
// Git Workflow Tools
|
|
492
1046
|
// ========================================================================
|
|
493
1047
|
{
|