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.
@@ -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
  {