@xaidenlabs/uso 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/bin/index.js +55 -0
- package/package.json +29 -0
- package/src/commands/doctor.js +127 -0
- package/src/commands/init.js +211 -0
- package/src/commands/uninstall.js +242 -0
- package/src/commands/verify.js +195 -0
- package/src/commands/workflow.js +38 -0
- package/src/platforms/linux.js +36 -0
- package/src/platforms/macos.js +30 -0
- package/src/platforms/windows.js +102 -0
- package/src/utils/logger.js +15 -0
- package/src/utils/paths.js +22 -0
- package/src/utils/wallet.js +86 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Xaiden Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# 🦀 USO ☀️
|
|
2
|
+
### The **Magic Button** for Solana! ✨
|
|
3
|
+
|
|
4
|
+
Do you want to build cool blockchain apps on Solana?
|
|
5
|
+
Usually, installing all the tools is super hard and boring.
|
|
6
|
+
**USO does it all for you with just ONE CLICK!**
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 🚀 Step 1: Get USO
|
|
11
|
+
First, let's put USO on your computer.Open your **Terminal** (or Command Prompt) and type this magic spell:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g @xaidenlabs/uso
|
|
15
|
+
```
|
|
16
|
+
*(If that doesn't work, ensure you have Node.js installed!)*
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 🩺 Step 2: Check Your Computer
|
|
21
|
+
Is your computer ready? Ask the Doctor!
|
|
22
|
+
```bash
|
|
23
|
+
uso doctor
|
|
24
|
+
```
|
|
25
|
+
It will check everything for you.
|
|
26
|
+
- ✅ means GOOD!
|
|
27
|
+
- ❌ means MISSING (Don't worry, USO will fix it!)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## ✨ Step 3: Install Everything!
|
|
32
|
+
This is the best part. To install **Rust**, **Solana**, and **Anchor** all at once, just type:
|
|
33
|
+
```bash
|
|
34
|
+
uso init
|
|
35
|
+
```
|
|
36
|
+
- It will download everything.
|
|
37
|
+
- It might ask for permission (click **Yes**!).
|
|
38
|
+
- **IMPORTANT**: When it's done, close your terminal and open it again!
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 🎯 Step 4: Install Just One Thing (Optional)
|
|
43
|
+
Maybe you only want one toy? You can do that too!
|
|
44
|
+
|
|
45
|
+
- **Want just Rust?** 🦀
|
|
46
|
+
```bash
|
|
47
|
+
uso install rust
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- **Want just Solana?** ☀️
|
|
51
|
+
```bash
|
|
52
|
+
uso install solana
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- **Want just Anchor?** ⚓
|
|
56
|
+
```bash
|
|
57
|
+
uso install anchor
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## ✅ Step 5: Test It!
|
|
63
|
+
Let's make sure everything works perfectly.
|
|
64
|
+
```bash
|
|
65
|
+
uso verify
|
|
66
|
+
```
|
|
67
|
+
If this says "Success", you are ready to build the future! 🚀
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 🗑️ Step 6: Removing It (Uninstall)
|
|
72
|
+
If you don't want these tools anymore, USO can clean up for you.
|
|
73
|
+
|
|
74
|
+
- **Remove Everything:**
|
|
75
|
+
```bash
|
|
76
|
+
uso uninstall
|
|
77
|
+
```
|
|
78
|
+
*(Be careful! It might ask to delete your wallet with your crypto keys!)*
|
|
79
|
+
|
|
80
|
+
- **Remove Just One Thing:**
|
|
81
|
+
```bash
|
|
82
|
+
uso uninstall solana
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## ❓ Help! It's not working!
|
|
88
|
+
|
|
89
|
+
**"Permission Denied" or "Error"?**
|
|
90
|
+
- Try running your terminal as **Administrator** (Right-click -> Run as Administrator).
|
|
91
|
+
|
|
92
|
+
**"Command not found"?**
|
|
93
|
+
- Did you close and reopen your terminal after installing? Do that now!
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
**Made with ❤️ for You.**
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { program } = require('commander');
|
|
3
|
+
const { init } = require('../src/commands/init');
|
|
4
|
+
const { doctor } = require('../src/commands/doctor');
|
|
5
|
+
const { verify } = require('../src/commands/verify');
|
|
6
|
+
const { build, test, deploy, clean } = require('../src/commands/workflow');
|
|
7
|
+
const { uninstall } = require('../src/commands/uninstall');
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name('uso')
|
|
11
|
+
.description('Universal Solana Orchestrator - One-command setup for all OS')
|
|
12
|
+
.version('1.0.0');
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.command('init [component]')
|
|
16
|
+
.alias('install')
|
|
17
|
+
.description('Install Rust, Solana CLI, Anchor Framework, or specific component (rust, solana, anchor)')
|
|
18
|
+
.action(init);
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command('doctor')
|
|
22
|
+
.description('Check if the environment is ready for Solana development')
|
|
23
|
+
.action(doctor);
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.command('verify')
|
|
27
|
+
.description('Verify installation by building a test Anchor project')
|
|
28
|
+
.action(verify);
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.command('build')
|
|
32
|
+
.description('Build the Anchor project (wraps "anchor build")')
|
|
33
|
+
.action(build);
|
|
34
|
+
|
|
35
|
+
program
|
|
36
|
+
.command('test')
|
|
37
|
+
.description('Run Anchor tests (wraps "anchor test")')
|
|
38
|
+
.action(test);
|
|
39
|
+
|
|
40
|
+
program
|
|
41
|
+
.command('deploy')
|
|
42
|
+
.description('Deploy the program (wraps "anchor deploy")')
|
|
43
|
+
.action(deploy);
|
|
44
|
+
|
|
45
|
+
program
|
|
46
|
+
.command('clean')
|
|
47
|
+
.description('Clean the project (wraps "anchor clean")')
|
|
48
|
+
.action(clean);
|
|
49
|
+
|
|
50
|
+
program
|
|
51
|
+
.command('uninstall [component]')
|
|
52
|
+
.description('Uninstall uso components (rust, solana, anchor) or all')
|
|
53
|
+
.action(uninstall);
|
|
54
|
+
|
|
55
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xaidenlabs/uso",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Universal Solana Orchestrator - A one-command setup tool for Solana and Anchor development environments on Windows, macOS, and Linux.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"uso": "./bin/index.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"solana",
|
|
13
|
+
"anchor",
|
|
14
|
+
"web3",
|
|
15
|
+
"rust",
|
|
16
|
+
"install",
|
|
17
|
+
"setup",
|
|
18
|
+
"developer-tools"
|
|
19
|
+
],
|
|
20
|
+
"author": "Xaiden Labs",
|
|
21
|
+
"license": "ISC",
|
|
22
|
+
"type": "commonjs",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"chalk": "^4.1.2",
|
|
25
|
+
"commander": "^14.0.3",
|
|
26
|
+
"ora": "^5.4.1",
|
|
27
|
+
"shelljs": "^0.10.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const shell = require('shelljs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { log } = require('../utils/logger');
|
|
6
|
+
|
|
7
|
+
const checkGit = (silent = false) => {
|
|
8
|
+
const installed = !!shell.which('git');
|
|
9
|
+
if (!silent) {
|
|
10
|
+
if (installed) log.success("✅ Git installed");
|
|
11
|
+
else log.error("❌ Git not found");
|
|
12
|
+
}
|
|
13
|
+
return installed;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const checkRust = (silent = false) => {
|
|
17
|
+
const rustc = shell.exec('rustc --version', { silent: true });
|
|
18
|
+
const installed = rustc.code === 0;
|
|
19
|
+
if (!silent) {
|
|
20
|
+
if (installed) log.success(`✅ Rust installed (${rustc.stdout.trim()})`);
|
|
21
|
+
else log.error("❌ Rust not found");
|
|
22
|
+
}
|
|
23
|
+
return installed;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const checkSolana = (silent = false) => {
|
|
27
|
+
// 1. Try PATH first
|
|
28
|
+
let solana = shell.exec('solana --version', { silent: true });
|
|
29
|
+
if (solana.code === 0) {
|
|
30
|
+
if (!silent) log.success(`✅ Solana CLI installed (${solana.stdout.trim()})`);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 2. Try default Windows path if on Windows
|
|
35
|
+
if (os.platform() === 'win32') {
|
|
36
|
+
const home = os.homedir();
|
|
37
|
+
// Modern path (Agave/Solana)
|
|
38
|
+
const defaultPath = path.join(home, '.local', 'share', 'solana', 'install', 'active_release', 'bin', 'solana.exe');
|
|
39
|
+
|
|
40
|
+
if (fs.existsSync(defaultPath)) {
|
|
41
|
+
if (!silent) log.success(`✅ Solana CLI installed (Found at ${defaultPath})`);
|
|
42
|
+
// Optionally we could try to get version from it, but existence is enough to skip install
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!silent) log.error("❌ Solana CLI not found");
|
|
48
|
+
return false;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const checkAnchor = (silent = false) => {
|
|
52
|
+
const anchor = shell.exec('anchor --version', { silent: true });
|
|
53
|
+
const installed = anchor.code === 0;
|
|
54
|
+
if (!silent) {
|
|
55
|
+
if (installed) log.success(`✅ Anchor installed (${anchor.stdout.trim()})`);
|
|
56
|
+
else log.error("❌ Anchor not found");
|
|
57
|
+
}
|
|
58
|
+
return installed;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const checkCppTools = (silent = false) => {
|
|
62
|
+
if (os.platform() !== 'win32') return true; // Not needed on others
|
|
63
|
+
|
|
64
|
+
// 1. Check PATH
|
|
65
|
+
const hasCl = !!shell.which('cl');
|
|
66
|
+
if (hasCl) {
|
|
67
|
+
if (!silent) log.success("✅ C++ Build Tools (cl.exe) found in PATH");
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 2. Check via vswhere
|
|
72
|
+
const programFiles = process.env['ProgramFiles(x86)'] || process.env['ProgramFiles'];
|
|
73
|
+
const vswherePath = path.join(programFiles, 'Microsoft Visual Studio', 'Installer', 'vswhere.exe');
|
|
74
|
+
|
|
75
|
+
if (fs.existsSync(vswherePath)) {
|
|
76
|
+
// Use quotes around * to prevent shell globbing issues
|
|
77
|
+
// We look for ANY product that installs VC tools.
|
|
78
|
+
// We relax the requirement to just 'installationPath' to confirm VS is present,
|
|
79
|
+
// as 'Desktop development with C++' is the standard workload.
|
|
80
|
+
// But to be safe, we should check for the VC component if possible.
|
|
81
|
+
// Tests showed that -products * returned the install path successfully.
|
|
82
|
+
|
|
83
|
+
const cmd = `"${vswherePath}" -latest -products "*" -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`;
|
|
84
|
+
const result = shell.exec(cmd, { silent: true });
|
|
85
|
+
|
|
86
|
+
if (result.code === 0 && result.stdout.trim().length > 0) {
|
|
87
|
+
if (!silent) log.success("✅ C++ Build Tools detected (via vswhere).");
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Fallback: Check for just the workload, or even just the existence of VS Build Tools
|
|
92
|
+
// usage: vswhere -latest -products * -requires Microsoft.VisualStudio.Workload.VCTools
|
|
93
|
+
const cmd2 = `"${vswherePath}" -latest -products "*" -requires Microsoft.VisualStudio.Workload.VCTools -property installationPath`;
|
|
94
|
+
const result2 = shell.exec(cmd2, { silent: true });
|
|
95
|
+
|
|
96
|
+
if (result2.code === 0 && result2.stdout.trim().length > 0) {
|
|
97
|
+
if (!silent) log.success("✅ C++ Build Tools detected (via vswhere workload).");
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!silent) {
|
|
103
|
+
log.warn("⚠️ C++ Build Tools (cl.exe) not found.");
|
|
104
|
+
log.warn(" Rust requires 'Desktop development with C++' to compile.");
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const doctor = async () => {
|
|
110
|
+
const platform = os.platform();
|
|
111
|
+
log.header(`🩺 Running Doctor for ${platform}...`);
|
|
112
|
+
|
|
113
|
+
checkGit();
|
|
114
|
+
checkRust();
|
|
115
|
+
checkSolana();
|
|
116
|
+
checkAnchor();
|
|
117
|
+
checkCppTools();
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
doctor,
|
|
122
|
+
checkGit,
|
|
123
|
+
checkRust,
|
|
124
|
+
checkSolana,
|
|
125
|
+
checkAnchor,
|
|
126
|
+
checkCppTools
|
|
127
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const shell = require('shelljs');
|
|
3
|
+
const { log, spinner } = require('../utils/logger');
|
|
4
|
+
const { installWindows } = require('../platforms/windows');
|
|
5
|
+
const { installMacOS } = require('../platforms/macos');
|
|
6
|
+
const { installLinux } = require('../platforms/linux');
|
|
7
|
+
const { getCargoBinPath } = require('../utils/paths');
|
|
8
|
+
const { checkRust, checkSolana, checkAnchor } = require('./doctor');
|
|
9
|
+
const { ensureWalletInteractive } = require('../utils/wallet');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
|
|
13
|
+
const init = async (component) => {
|
|
14
|
+
const platform = os.platform();
|
|
15
|
+
|
|
16
|
+
if (component) {
|
|
17
|
+
component = component.toLowerCase();
|
|
18
|
+
log.info(`🎯 Targeted installation: ${component}`);
|
|
19
|
+
|
|
20
|
+
if (component === 'rust') {
|
|
21
|
+
if (checkRust(true)) {
|
|
22
|
+
log.success("✅ Rust is already installed.");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
log.info("🦀 Installing Rust...");
|
|
26
|
+
let success = false;
|
|
27
|
+
if (platform === 'win32') success = await installWindows(true, false);
|
|
28
|
+
else if (platform === 'darwin') success = await installMacOS(true, false);
|
|
29
|
+
else success = await installLinux(true, false);
|
|
30
|
+
|
|
31
|
+
if (success) log.success("✅ Rust installed successfully.");
|
|
32
|
+
else log.error("❌ Rust installation failed.");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (component === 'solana') {
|
|
37
|
+
if (checkSolana(true)) {
|
|
38
|
+
log.success("✅ Solana CLI is already installed.");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
log.info("☀️ Installing Solana CLI...");
|
|
42
|
+
let success = false;
|
|
43
|
+
if (platform === 'win32') success = await installWindows(false, true);
|
|
44
|
+
else if (platform === 'darwin') success = await installMacOS(false, true);
|
|
45
|
+
else success = await installLinux(false, true);
|
|
46
|
+
|
|
47
|
+
if (success) log.success("✅ Solana CLI installed successfully.");
|
|
48
|
+
else log.error("❌ Solana CLI installation failed.");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (component === 'anchor') {
|
|
53
|
+
if (checkAnchor(true)) {
|
|
54
|
+
log.success("✅ Anchor is already installed.");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Fall through to Anchor installation logic below, but skip others
|
|
58
|
+
} else if (component !== 'anchor') { // If it's not rust, solana, or anchor
|
|
59
|
+
log.error(`❌ Unknown component: ${component}. Available: rust, solana, anchor`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// --- FULL INSTALLATION / ANCHOR ONLY FLOW ---
|
|
65
|
+
|
|
66
|
+
// If authenticating for just Anchor, we assume Rust/Solana are prerequisites or we skip them
|
|
67
|
+
let installRust = !checkRust(true);
|
|
68
|
+
let installSolana = !checkSolana(true);
|
|
69
|
+
const hasAnchor = checkAnchor(true);
|
|
70
|
+
|
|
71
|
+
if (component === 'anchor') {
|
|
72
|
+
// ensuring prerequisites for anchor
|
|
73
|
+
if (installRust) {
|
|
74
|
+
log.warn("⚠️ Rust is required for Anchor but not installed.");
|
|
75
|
+
// asking or just failing? For granular, let's just fail or warn.
|
|
76
|
+
// But let's proceed to install Anchor logic which handles cargo check
|
|
77
|
+
}
|
|
78
|
+
installRust = false; // Don't run platform installers for these
|
|
79
|
+
installSolana = false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Validating state for full install
|
|
83
|
+
if (!component && !installRust && !installSolana && hasAnchor) {
|
|
84
|
+
log.success("🎉 Everything is already installed!");
|
|
85
|
+
await ensureWalletInteractive();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!component) {
|
|
90
|
+
log.header("🔍 Checking current environment state...");
|
|
91
|
+
log.info(`\n📦 Missing components:`);
|
|
92
|
+
if (installRust) log.error(" - Rust");
|
|
93
|
+
if (installSolana) log.error(" - Solana CLI");
|
|
94
|
+
if (!hasAnchor) log.error(" - Anchor");
|
|
95
|
+
console.log("");
|
|
96
|
+
|
|
97
|
+
const spin = spinner('Starting Installation...').start();
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
let success = false;
|
|
101
|
+
|
|
102
|
+
if (platform === 'win32') {
|
|
103
|
+
spin.stop();
|
|
104
|
+
success = await installWindows(installRust, installSolana);
|
|
105
|
+
} else if (platform === 'darwin') {
|
|
106
|
+
spin.stop();
|
|
107
|
+
success = await installMacOS(installRust, installSolana);
|
|
108
|
+
} else {
|
|
109
|
+
spin.stop();
|
|
110
|
+
success = await installLinux(installRust, installSolana);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!success) {
|
|
114
|
+
log.error("❌ Platform-specific installation failed.");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
} catch (e) {
|
|
118
|
+
spin.stop();
|
|
119
|
+
log.error(e.message);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Install Anchor (Universal) - Runs if component='anchor' OR full install
|
|
125
|
+
if (!hasAnchor && (!component || component === 'anchor')) {
|
|
126
|
+
log.info("⚓ Installing Anchor Framework...");
|
|
127
|
+
|
|
128
|
+
const cargoBin = getCargoBinPath();
|
|
129
|
+
const cargoExe = platform === 'win32' ? 'cargo.exe' : 'cargo';
|
|
130
|
+
const avmExe = platform === 'win32' ? 'avm.exe' : 'avm';
|
|
131
|
+
|
|
132
|
+
// Resolve cargo command
|
|
133
|
+
let cargoCmd = cargoExe;
|
|
134
|
+
if (fs.existsSync(path.join(cargoBin, cargoExe))) {
|
|
135
|
+
cargoCmd = `"${path.join(cargoBin, cargoExe)}"`;
|
|
136
|
+
} else if (!installRust && checkRust(true)) {
|
|
137
|
+
cargoCmd = 'cargo';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
log.subHeader(`Using cargo: ${cargoCmd}`);
|
|
141
|
+
|
|
142
|
+
// Try installing AVM first (Preferred)
|
|
143
|
+
log.info(" Attempting AVM install...");
|
|
144
|
+
const avmInstall = shell.exec(`${cargoCmd} install --git https://github.com/coral-xyz/anchor avm --locked --force`);
|
|
145
|
+
|
|
146
|
+
if (avmInstall.code === 0) {
|
|
147
|
+
// Use AVM
|
|
148
|
+
let avmCmd = avmExe;
|
|
149
|
+
if (fs.existsSync(path.join(cargoBin, avmExe))) {
|
|
150
|
+
avmCmd = `"${path.join(cargoBin, avmExe)}"`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
log.subHeader(`Using avm: ${avmCmd}`);
|
|
154
|
+
const avmUse = shell.exec(`${avmCmd} install latest`);
|
|
155
|
+
|
|
156
|
+
// Detect Permission Error for AVM
|
|
157
|
+
if (avmUse.code !== 0) {
|
|
158
|
+
const output = avmUse.stderr + avmUse.stdout;
|
|
159
|
+
if (output.includes("os error 1314") && platform === 'win32') {
|
|
160
|
+
log.warn("⚠️ AVM Permission denied (Symlink creation failed).");
|
|
161
|
+
log.info("🛡️ Triggering Run as Administrator (UAC) for Anchor...");
|
|
162
|
+
const absAvmPath = path.join(cargoBin, avmExe);
|
|
163
|
+
const elevateCmd = `powershell -Command "Start-Process -FilePath '${absAvmPath}' -ArgumentList 'install latest' -Verb RunAs -Wait; Start-Process -FilePath '${absAvmPath}' -ArgumentList 'use latest' -Verb RunAs -Wait"`;
|
|
164
|
+
const elevatedRun = shell.exec(elevateCmd);
|
|
165
|
+
|
|
166
|
+
if (elevatedRun.code === 0) {
|
|
167
|
+
log.success("✅ Anchor installed (Elevated).");
|
|
168
|
+
} else {
|
|
169
|
+
log.error("❌ Elevated installation failed. Trying fallback...");
|
|
170
|
+
fallbackDirectInstall(cargoCmd);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
} else {
|
|
174
|
+
log.warn("⚠️ AVM installation failed. Trying fallback...");
|
|
175
|
+
fallbackDirectInstall(cargoCmd);
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
shell.exec(`${avmCmd} use latest`);
|
|
179
|
+
log.success("✅ Anchor installed.");
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
log.error("❌ Failed to install AVM via Cargo.");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
} else if (component === 'anchor' && hasAnchor) {
|
|
186
|
+
log.success("✅ Anchor is already installed.");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if (!component) {
|
|
191
|
+
log.header('\n✅ Uso Setup Complete!');
|
|
192
|
+
await ensureWalletInteractive();
|
|
193
|
+
|
|
194
|
+
if (installRust || installSolana || !hasAnchor) {
|
|
195
|
+
log.warn('👉 Please RESTART your terminal/VS Code to ensure all PATH variables are updated.');
|
|
196
|
+
}
|
|
197
|
+
log.info('🚀 Try running: uso verify');
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const fallbackDirectInstall = (cargoCmd) => {
|
|
202
|
+
log.info("👉 Falling back to direct 'anchor-cli' installation...");
|
|
203
|
+
const directInstall = shell.exec(`${cargoCmd} install --git https://github.com/coral-xyz/anchor anchor-cli --locked --force`);
|
|
204
|
+
if (directInstall.code !== 0) {
|
|
205
|
+
log.error("❌ Failed to install Anchor via fallback method.");
|
|
206
|
+
} else {
|
|
207
|
+
log.success("✅ Anchor installed (Direct Cargo).");
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
module.exports = { init };
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
const shell = require('shelljs');
|
|
2
|
+
const { log, spinner } = require('../utils/logger');
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const askQuestion = (query) => {
|
|
9
|
+
const rl = readline.createInterface({
|
|
10
|
+
input: process.stdin,
|
|
11
|
+
output: process.stdout,
|
|
12
|
+
});
|
|
13
|
+
return new Promise(resolve => rl.question(query, ans => {
|
|
14
|
+
rl.close();
|
|
15
|
+
resolve(ans);
|
|
16
|
+
}));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Runs a command and attempts to elevate privileges if it fails with a permission error.
|
|
21
|
+
*/
|
|
22
|
+
const runOrElevate = (command, description) => {
|
|
23
|
+
log.info(`Running: ${description}...`);
|
|
24
|
+
|
|
25
|
+
// We run without silent:true initially to let the user see output,
|
|
26
|
+
// but detecting the error code is what matters.
|
|
27
|
+
// actually, to detect the specific string "os error 1314", we need to capture output.
|
|
28
|
+
// So we run silently first? Or we just run and if it fails, we assume it *might* be elevation if on Windows?
|
|
29
|
+
// Let's run synchronously and capture output.
|
|
30
|
+
|
|
31
|
+
const result = shell.exec(command, { silent: true });
|
|
32
|
+
|
|
33
|
+
if (result.code === 0) {
|
|
34
|
+
console.log(result.stdout);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Print the error output to the user
|
|
39
|
+
console.log(result.stdout);
|
|
40
|
+
console.error(result.stderr);
|
|
41
|
+
|
|
42
|
+
const output = result.stderr + result.stdout;
|
|
43
|
+
|
|
44
|
+
// Check for common permission errors
|
|
45
|
+
// "os error 1314" is specific to Windows symlink privilege
|
|
46
|
+
if ((output.includes("os error 1314") || output.includes("EPERM") || output.includes("permission denied")) && os.platform() === 'win32') {
|
|
47
|
+
log.warn(`⚠️ Permission denied during: ${description}`);
|
|
48
|
+
log.info("🛡️ Triggering Run as Administrator (UAC) to retry...");
|
|
49
|
+
|
|
50
|
+
// Construct PowerShell command to run cmd /c <command> as admin
|
|
51
|
+
// We need to be careful with quoting.
|
|
52
|
+
const escapedCommand = command.replace(/'/g, "''"); // Basic PowerShell escaping for single quotes
|
|
53
|
+
const elevateCmd = `powershell -Command "Start-Process -FilePath 'cmd.exe' -ArgumentList '/c ${escapedCommand}' -Verb RunAs -Wait"`;
|
|
54
|
+
|
|
55
|
+
const elevatedRun = shell.exec(elevateCmd);
|
|
56
|
+
|
|
57
|
+
if (elevatedRun.code === 0) {
|
|
58
|
+
log.success(`✅ ${description} completed (Elevated).`);
|
|
59
|
+
return true;
|
|
60
|
+
} else {
|
|
61
|
+
log.error(`❌ Elevated execution failed for: ${description}`);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
log.error(`❌ Command failed: ${description}`);
|
|
67
|
+
return false;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const uninstall = async (component) => {
|
|
71
|
+
log.header("🗑️ USO Uninstallation & Cleanup");
|
|
72
|
+
|
|
73
|
+
if (component) {
|
|
74
|
+
component = component.toLowerCase();
|
|
75
|
+
log.info(`🎯 Targeted uninstallation: ${component}`);
|
|
76
|
+
|
|
77
|
+
if (component === 'anchor') {
|
|
78
|
+
const anchorInstalled = shell.which('anchor');
|
|
79
|
+
if (anchorInstalled) {
|
|
80
|
+
log.info("Removing Anchor...");
|
|
81
|
+
// Try avm uninstall first if available
|
|
82
|
+
if (shell.which('avm')) {
|
|
83
|
+
runOrElevate('avm uninstall latest', 'Uninstall Anchor (AVM)');
|
|
84
|
+
}
|
|
85
|
+
runOrElevate('cargo uninstall anchor-cli', 'Uninstall anchor-cli');
|
|
86
|
+
runOrElevate('cargo uninstall avm', 'Uninstall avm');
|
|
87
|
+
log.success("Anchor removal steps completed.");
|
|
88
|
+
} else {
|
|
89
|
+
log.success("✅ Anchor is not installed.");
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (component === 'solana') {
|
|
95
|
+
// Check PATH first
|
|
96
|
+
let solanaInstalled = shell.which('solana');
|
|
97
|
+
const localShareSolana = path.join(os.homedir(), '.local', 'share', 'solana');
|
|
98
|
+
|
|
99
|
+
// If not found in PATH, check default location
|
|
100
|
+
if (!solanaInstalled && fs.existsSync(localShareSolana)) {
|
|
101
|
+
solanaInstalled = true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (solanaInstalled) {
|
|
105
|
+
log.info("Removing Solana CLI...");
|
|
106
|
+
|
|
107
|
+
if (fs.existsSync(localShareSolana)) {
|
|
108
|
+
try {
|
|
109
|
+
fs.rmSync(localShareSolana, { recursive: true, force: true });
|
|
110
|
+
log.success(`Removed ${localShareSolana}`);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
log.warn(`Failed to remove ${localShareSolana} directly: ${err.message}`);
|
|
113
|
+
log.info("Trying to remove via elevated command...");
|
|
114
|
+
runOrElevate(`rmdir /s /q "${localShareSolana}"`, `Remove folder ${localShareSolana}`);
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
log.warn(`Could not find Solana folder at ${localShareSolana}. It might be removed already.`);
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
log.success("✅ Solana CLI is not installed.");
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (component === 'rust') {
|
|
126
|
+
const rustInstalled = shell.which('rustc');
|
|
127
|
+
if (rustInstalled) {
|
|
128
|
+
log.info("Running rustup self uninstall...");
|
|
129
|
+
runOrElevate('rustup self uninstall -y', 'Uninstall Rust');
|
|
130
|
+
} else {
|
|
131
|
+
log.success("✅ Rust is not installed.");
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
log.error(`❌ Unknown component: ${component}. Available: rust, solana, anchor`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- FULL INTERACTIVE UNINSTALL ---
|
|
141
|
+
|
|
142
|
+
log.warn("This process allows you to remove components installed by uso.");
|
|
143
|
+
log.warn("Please be careful, especially with wallet removal!");
|
|
144
|
+
|
|
145
|
+
const proceed = await askQuestion("👉 Do you want to proceed with uninstallation? (y/N): ");
|
|
146
|
+
if (proceed.toLowerCase() !== 'y') {
|
|
147
|
+
log.info("Operation cancelled.");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 1. Uninstall Anchor
|
|
152
|
+
const anchorInstalled = shell.which('anchor');
|
|
153
|
+
if (anchorInstalled) {
|
|
154
|
+
const removeAnchor = await askQuestion("\n⚓ Remove Anchor Framework? (y/N): ");
|
|
155
|
+
if (removeAnchor.toLowerCase() === 'y') {
|
|
156
|
+
log.info("Removing Anchor...");
|
|
157
|
+
// Try avm uninstall first if available
|
|
158
|
+
if (shell.which('avm')) {
|
|
159
|
+
runOrElevate('avm uninstall latest', 'Uninstall Anchor (AVM)');
|
|
160
|
+
}
|
|
161
|
+
runOrElevate('cargo uninstall anchor-cli', 'Uninstall anchor-cli');
|
|
162
|
+
runOrElevate('cargo uninstall avm', 'Uninstall avm');
|
|
163
|
+
log.success("Anchor removal steps completed.");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 2. Uninstall Solana
|
|
168
|
+
let solanaInstalled = shell.which('solana');
|
|
169
|
+
const localShareSolana = path.join(os.homedir(), '.local', 'share', 'solana');
|
|
170
|
+
|
|
171
|
+
// If not found in PATH, check default location (like doctor does)
|
|
172
|
+
if (!solanaInstalled && fs.existsSync(localShareSolana)) {
|
|
173
|
+
solanaInstalled = true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (solanaInstalled) {
|
|
177
|
+
const removeSolana = await askQuestion("\n☀️ Remove Solana CLI? (y/N): ");
|
|
178
|
+
if (removeSolana.toLowerCase() === 'y') {
|
|
179
|
+
log.info("Removing Solana CLI...");
|
|
180
|
+
|
|
181
|
+
// Default locations
|
|
182
|
+
// const localShareSolana = path.join(os.homedir(), '.local', 'share', 'solana'); // Already defined
|
|
183
|
+
|
|
184
|
+
if (fs.existsSync(localShareSolana)) {
|
|
185
|
+
try {
|
|
186
|
+
fs.rmSync(localShareSolana, { recursive: true, force: true });
|
|
187
|
+
log.success(`Removed ${localShareSolana}`);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
log.warn(`Failed to remove ${localShareSolana} directly: ${err.message}`);
|
|
190
|
+
log.info("Trying to remove via elevated command...");
|
|
191
|
+
runOrElevate(`rmdir /s /q "${localShareSolana}"`, `Remove folder ${localShareSolana}`);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
log.warn(`Could not find Solana at ${localShareSolana}. It might be already removed.`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 3. Uninstall Rust
|
|
200
|
+
const rustInstalled = shell.which('rustc');
|
|
201
|
+
if (rustInstalled) {
|
|
202
|
+
const removeRust = await askQuestion("\n🦀 Remove Rust? (y/N): ");
|
|
203
|
+
if (removeRust.toLowerCase() === 'y') {
|
|
204
|
+
log.info("Running rustup self uninstall...");
|
|
205
|
+
runOrElevate('rustup self uninstall -y', 'Uninstall Rust');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 4. WALLET REMOVAL (DANGER)
|
|
210
|
+
const walletPath = path.join(os.homedir(), '.config', 'solana', 'id.json');
|
|
211
|
+
if (fs.existsSync(walletPath)) {
|
|
212
|
+
log.error("\n⚠️ DANGER ZONE ⚠️");
|
|
213
|
+
log.warn(`Found a Solana wallet at: ${walletPath}`);
|
|
214
|
+
log.warn("If you delete this without a backup, your funds will be LOST FOREVER.");
|
|
215
|
+
|
|
216
|
+
const removeWallet = await askQuestion("💥 Do you REALLY want to delete this wallet? (type 'DELETE' to confirm): ");
|
|
217
|
+
if (removeWallet === 'DELETE') {
|
|
218
|
+
try {
|
|
219
|
+
fs.unlinkSync(walletPath);
|
|
220
|
+
log.success("Wallet deleted.");
|
|
221
|
+
|
|
222
|
+
// Clean up parent config dir if empty
|
|
223
|
+
const configDir = path.dirname(walletPath);
|
|
224
|
+
try {
|
|
225
|
+
if (fs.readdirSync(configDir).length === 0) {
|
|
226
|
+
fs.rmSync(configDir, { recursive: true, force: true });
|
|
227
|
+
}
|
|
228
|
+
} catch (e) { }
|
|
229
|
+
} catch (err) {
|
|
230
|
+
log.error(`Failed to delete wallet: ${err.message}`);
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
log.info("Skipping wallet deletion.");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
log.header("\n✅ Cleanup complete.");
|
|
238
|
+
log.info("To remove the 'uso' tool itself, run:");
|
|
239
|
+
log.info(" npm uninstall -g uso");
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
module.exports = { uninstall };
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const shell = require('shelljs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const { log, spinner } = require('../utils/logger');
|
|
7
|
+
const { ensureWalletInteractive, resolveSolanaKeygen } = require('../utils/wallet');
|
|
8
|
+
const { getCargoBinPath } = require('../utils/paths');
|
|
9
|
+
|
|
10
|
+
const getSolanaBin = () => {
|
|
11
|
+
if (shell.which('solana')) return 'solana';
|
|
12
|
+
if (os.platform() === 'win32') {
|
|
13
|
+
const defaultPath = path.join(os.homedir(), '.local', 'share', 'solana', 'install', 'active_release', 'bin', 'solana.exe');
|
|
14
|
+
if (fs.existsSync(defaultPath)) return `"${defaultPath}"`;
|
|
15
|
+
}
|
|
16
|
+
return 'solana';
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const getAnchorBin = () => {
|
|
20
|
+
if (shell.which('anchor')) return 'anchor';
|
|
21
|
+
return 'anchor';
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const askBuildConfirmation = async () => {
|
|
25
|
+
const rl = readline.createInterface({
|
|
26
|
+
input: process.stdin,
|
|
27
|
+
output: process.stdout
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
log.info("");
|
|
32
|
+
log.info("ℹ️ The next step verifies your toolchain by building a real project.");
|
|
33
|
+
log.warn("⚠️ NOTE: If this is your first time, it will download ~200MB of platform tools.");
|
|
34
|
+
log.warn(" This can take 5-10 minutes depending on your internet.");
|
|
35
|
+
|
|
36
|
+
rl.question("👉 Do you want to proceed with the full build verification? [Y/n] ", (answer) => {
|
|
37
|
+
rl.close();
|
|
38
|
+
const a = answer.toLowerCase();
|
|
39
|
+
if (a === 'n' || a === 'no') {
|
|
40
|
+
resolve(false);
|
|
41
|
+
} else {
|
|
42
|
+
resolve(true);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const verify = async () => {
|
|
49
|
+
log.header("🧪 Verifying Solana Setup...");
|
|
50
|
+
|
|
51
|
+
const anchorCmd = getAnchorBin();
|
|
52
|
+
|
|
53
|
+
// 1. Check/Create Wallet
|
|
54
|
+
const walletReady = await ensureWalletInteractive();
|
|
55
|
+
|
|
56
|
+
if (!walletReady) {
|
|
57
|
+
log.error("❌ Verification cancelled. A wallet is required to build/test.");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Fix PATH for cargo-build-sbf logic (Windows)
|
|
62
|
+
if (os.platform() === 'win32') {
|
|
63
|
+
const solanaBase = path.join(os.homedir(), '.local', 'share', 'solana', 'install');
|
|
64
|
+
const activeRelease = path.join(solanaBase, 'active_release');
|
|
65
|
+
const activeBin = path.join(activeRelease, 'bin');
|
|
66
|
+
|
|
67
|
+
let realBin = activeBin;
|
|
68
|
+
try {
|
|
69
|
+
if (fs.existsSync(activeRelease)) {
|
|
70
|
+
const realRelease = fs.realpathSync(activeRelease);
|
|
71
|
+
realBin = path.join(realRelease, 'bin');
|
|
72
|
+
}
|
|
73
|
+
} catch (e) { }
|
|
74
|
+
|
|
75
|
+
const pathsToAdd = [activeBin];
|
|
76
|
+
if (realBin !== activeBin) pathsToAdd.push(realBin);
|
|
77
|
+
|
|
78
|
+
let pathKey = 'PATH';
|
|
79
|
+
for (const key of Object.keys(process.env)) {
|
|
80
|
+
if (key.toUpperCase() === 'PATH') {
|
|
81
|
+
pathKey = key;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let currentPath = process.env[pathKey] || '';
|
|
87
|
+
let modified = false;
|
|
88
|
+
|
|
89
|
+
pathsToAdd.forEach(p => {
|
|
90
|
+
if (fs.existsSync(p) && !currentPath.includes(p)) {
|
|
91
|
+
currentPath = `${p};${currentPath}`;
|
|
92
|
+
modified = true;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (modified) {
|
|
97
|
+
process.env[pathKey] = currentPath;
|
|
98
|
+
log.success("✅ Solana bin temporarily added to PATH.");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 2. Ask for confirmation before building
|
|
103
|
+
const proceed = await askBuildConfirmation();
|
|
104
|
+
if (!proceed) {
|
|
105
|
+
log.info("\n⏭️ Skipping build verification.");
|
|
106
|
+
log.success("✅ Basic tools are installed.");
|
|
107
|
+
log.info("👉 You can run 'npx uso doctor' for a quick version check.");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 3. Build Project
|
|
112
|
+
log.info("🔨 Building verification project...");
|
|
113
|
+
|
|
114
|
+
const templateDir = path.resolve(__dirname, '../../anchor_project');
|
|
115
|
+
const tempDir = path.join(os.tmpdir(), 'uso-verification-' + Date.now());
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
shell.cp('-R', templateDir, tempDir);
|
|
119
|
+
|
|
120
|
+
let attempts = 0;
|
|
121
|
+
const maxAttempts = 3;
|
|
122
|
+
let success = false;
|
|
123
|
+
let finalError = "";
|
|
124
|
+
|
|
125
|
+
while (attempts < maxAttempts && !success) {
|
|
126
|
+
attempts++;
|
|
127
|
+
if (attempts > 1) {
|
|
128
|
+
log.warn(`⚠️ Retrying build (Attempt ${attempts}/${maxAttempts})...`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const buildResult = shell.exec(`cd "${tempDir}" && ${anchorCmd} build`, {
|
|
132
|
+
silent: false,
|
|
133
|
+
env: process.env
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
success = buildResult.code === 0;
|
|
137
|
+
let stderr = buildResult.stderr + buildResult.stdout;
|
|
138
|
+
|
|
139
|
+
if (!success) {
|
|
140
|
+
finalError = stderr;
|
|
141
|
+
|
|
142
|
+
if (stderr.includes('TimedOut') || stderr.includes('reqwest::Error') || stderr.includes('Failed to install platform-tools')) {
|
|
143
|
+
log.warn(`⚠️ Network timeout detected. Retrying...`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (os.platform() === 'win32' && stderr.includes('os error 1314')) {
|
|
148
|
+
log.warn("⚠️ Privilege error detected (os error 1314).");
|
|
149
|
+
log.info("ℹ️ First-time Solana build requires Administrator privileges to install tools.");
|
|
150
|
+
log.info("🛡️ Retrying with Elevated Permissions (Please accept UAC prompt)...");
|
|
151
|
+
|
|
152
|
+
let absAnchor = anchorCmd;
|
|
153
|
+
if (anchorCmd === 'anchor') {
|
|
154
|
+
if (shell.which('anchor')) absAnchor = shell.which('anchor').toString();
|
|
155
|
+
else absAnchor = path.join(getCargoBinPath(), 'anchor.exe');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const psCommand = `cd '${tempDir}'; & '${absAnchor}' build; if ($LASTEXITCODE -ne 0) { Read-Host 'Build Failed. Press Enter to exit...' }`;
|
|
159
|
+
const elevateCmd = `powershell -Command "Start-Process powershell -ArgumentList \\"-NoExit\\", \\"-Command\\", \\"${psCommand.replace(/"/g, '\\`"')}\\" -Verb RunAs -Wait"`;
|
|
160
|
+
|
|
161
|
+
shell.exec(elevateCmd);
|
|
162
|
+
|
|
163
|
+
const idlPath = path.join(tempDir, 'target', 'idl', 'uso_verifier.json');
|
|
164
|
+
if (fs.existsSync(idlPath)) {
|
|
165
|
+
success = true;
|
|
166
|
+
log.success("✅ Elevated build passed.");
|
|
167
|
+
} else {
|
|
168
|
+
log.error("❌ Elevated build failed or was cancelled.");
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (success) {
|
|
178
|
+
log.success("✅ Verification Successful!");
|
|
179
|
+
log.success("🎉 Your Solana environment is fully operational.");
|
|
180
|
+
log.info(" - Rust is working");
|
|
181
|
+
log.info(" - Solana CLI is working");
|
|
182
|
+
log.info(" - Anchor is working");
|
|
183
|
+
log.info(" - Wallet is configured");
|
|
184
|
+
} else {
|
|
185
|
+
log.error("❌ Verification Failed.");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
} catch (err) {
|
|
189
|
+
log.error("❌ Verification Error: " + err.message);
|
|
190
|
+
} finally {
|
|
191
|
+
shell.rm('-rf', tempDir);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
module.exports = { verify };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const shell = require('shelljs');
|
|
2
|
+
const { log, spinner } = require('../utils/logger');
|
|
3
|
+
|
|
4
|
+
const runProxyCommand = async (command, args = []) => {
|
|
5
|
+
// Check if anchor is available
|
|
6
|
+
if (!shell.which('anchor')) {
|
|
7
|
+
log.error("❌ Anchor is not found in PATH.");
|
|
8
|
+
log.warn("👉 Run 'uso init' (or 'uso install') to set up your environment.");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const fullCommand = `anchor ${command} ${args.join(' ')}`;
|
|
13
|
+
log.header(`🚀 Running: ${fullCommand}`);
|
|
14
|
+
|
|
15
|
+
const execution = shell.exec(fullCommand);
|
|
16
|
+
|
|
17
|
+
if (execution.code === 0) {
|
|
18
|
+
log.success(`✅ '${command}' completed successfully.`);
|
|
19
|
+
} else {
|
|
20
|
+
log.error(`❌ '${command}' failed.`);
|
|
21
|
+
// We don't exit process here strictly, but let the user know
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const build = () => runProxyCommand('build');
|
|
26
|
+
const test = () => runProxyCommand('test');
|
|
27
|
+
const deploy = () => runProxyCommand('deploy');
|
|
28
|
+
const clean = () => runProxyCommand('clean');
|
|
29
|
+
|
|
30
|
+
// Generic run command for other anchor commands?
|
|
31
|
+
// For now, explicit functions are safer for help generation.
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
build,
|
|
35
|
+
test,
|
|
36
|
+
deploy,
|
|
37
|
+
clean
|
|
38
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const shell = require('shelljs');
|
|
2
|
+
const { log } = require('../utils/logger');
|
|
3
|
+
|
|
4
|
+
const installLinux = async (shouldInstallRust, shouldInstallSolana) => {
|
|
5
|
+
log.header("🐧 Linux detected.");
|
|
6
|
+
|
|
7
|
+
// 1. Install dependencies (Quick check, or just run update? 'update' is harmless mostly)
|
|
8
|
+
// We can assume if Rust/Solana are missing, deps might be too.
|
|
9
|
+
// If both are present, we might skip this? For safety, we only run if installing something.
|
|
10
|
+
if (shouldInstallRust || shouldInstallSolana) {
|
|
11
|
+
log.info("🐧 Checking Linux dependencies (libudev, pkg-config)...");
|
|
12
|
+
shell.exec('sudo apt-get update && sudo apt-get install -y libudev-dev pkg-config build-essential');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 2. Install Rust
|
|
16
|
+
if (shouldInstallRust) {
|
|
17
|
+
log.info("🦀 Installing Rust...");
|
|
18
|
+
shell.exec('curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y');
|
|
19
|
+
log.success("✅ Rust installed.");
|
|
20
|
+
} else {
|
|
21
|
+
log.info("🦀 Rust is already installed. Skipping.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 3. Install Solana CLI
|
|
25
|
+
if (shouldInstallSolana) {
|
|
26
|
+
log.info("☀️ Installing Solana CLI...");
|
|
27
|
+
shell.exec('sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"');
|
|
28
|
+
log.success("✅ Solana CLI installed.");
|
|
29
|
+
} else {
|
|
30
|
+
log.info("☀️ Solana CLI is already installed. Skipping.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return true;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
module.exports = { installLinux };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const shell = require('shelljs');
|
|
2
|
+
const { log } = require('../utils/logger');
|
|
3
|
+
|
|
4
|
+
const installMacOS = async (shouldInstallRust, shouldInstallSolana) => {
|
|
5
|
+
log.header("🍎 macOS detected.");
|
|
6
|
+
|
|
7
|
+
// 1. Install Rust
|
|
8
|
+
if (shouldInstallRust) {
|
|
9
|
+
log.info("🦀 Installing Rust...");
|
|
10
|
+
const rustInstall = shell.exec('curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y');
|
|
11
|
+
if (rustInstall.code !== 0) return false;
|
|
12
|
+
log.success("✅ Rust installed.");
|
|
13
|
+
} else {
|
|
14
|
+
log.info("🦀 Rust is already installed. Skipping.");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 2. Install Solana CLI
|
|
18
|
+
if (shouldInstallSolana) {
|
|
19
|
+
log.info("☀️ Installing Solana CLI...");
|
|
20
|
+
const solanaInstall = shell.exec('sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"');
|
|
21
|
+
if (solanaInstall.code !== 0) return false;
|
|
22
|
+
log.success("✅ Solana CLI installed.");
|
|
23
|
+
} else {
|
|
24
|
+
log.info("☀️ Solana CLI is already installed. Skipping.");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return true;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
module.exports = { installMacOS };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const shell = require('shelljs');
|
|
2
|
+
const { log, spinner } = require('../utils/logger');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const installWindows = async (shouldInstallRust, shouldInstallSolana) => {
|
|
7
|
+
log.header("🪟 Windows detected.");
|
|
8
|
+
|
|
9
|
+
// 1. Check for C++ Build Tools
|
|
10
|
+
if (shouldInstallRust) {
|
|
11
|
+
const hasCl = shell.which('cl');
|
|
12
|
+
if (!hasCl) {
|
|
13
|
+
log.warn("⚠️ Visual Studio C++ Build Tools (cl.exe) not found!");
|
|
14
|
+
log.warn("👉 Please install them from: https://visualstudio.microsoft.com/visual-cpp-build-tools/");
|
|
15
|
+
log.warn(" Make sure to select 'Desktop development with C++' workload before installing Rust.");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 2. Install Rust
|
|
20
|
+
if (shouldInstallRust) {
|
|
21
|
+
log.info("🦀 Installing Rust (rustup-init.exe)...");
|
|
22
|
+
shell.exec('powershell -Command "Invoke-WebRequest -Uri https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe -OutFile rustup-init.exe"');
|
|
23
|
+
|
|
24
|
+
const rustInstall = shell.exec('powershell -Command "./rustup-init.exe -y"');
|
|
25
|
+
|
|
26
|
+
if (rustInstall.code !== 0) {
|
|
27
|
+
log.warn("⚠️ Rust installer finished with a non-zero code. It might have succeeded if you saw 'Rust is installed now'.");
|
|
28
|
+
} else {
|
|
29
|
+
log.success("✅ Rust installed.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (fs.existsSync('rustup-init.exe')) {
|
|
33
|
+
shell.rm('rustup-init.exe');
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
log.info("🦀 Rust is already installed. Skipping.");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 3. Install Solana CLI (Agave)
|
|
40
|
+
if (shouldInstallSolana) {
|
|
41
|
+
log.info("☀️ Installing Solana CLI (Agave)...");
|
|
42
|
+
log.info(" Downloading solana-install-init.exe...");
|
|
43
|
+
|
|
44
|
+
const downloadCmd = 'powershell -Command "Invoke-WebRequest -Uri https://release.anza.xyz/stable/solana-install-init-x86_64-pc-windows-msvc.exe -OutFile solana-install.exe"';
|
|
45
|
+
const dlResult = shell.exec(downloadCmd);
|
|
46
|
+
|
|
47
|
+
if (dlResult.code !== 0) {
|
|
48
|
+
log.error("❌ Failed to download Solana installer.");
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Try regular install first
|
|
53
|
+
log.info(" Running Solana Installer...");
|
|
54
|
+
const installResult = shell.exec('solana-install.exe stable');
|
|
55
|
+
|
|
56
|
+
// Check for Symlink Error (1314) for potential auto-elevation
|
|
57
|
+
if (installResult.code !== 0) {
|
|
58
|
+
const output = installResult.stderr + installResult.stdout;
|
|
59
|
+
if (output.includes("os error 1314")) {
|
|
60
|
+
log.warn("⚠️ Permission denied (Symlink creation failed).");
|
|
61
|
+
log.info("🛡️ Triggering Run as Administrator (UAC)...");
|
|
62
|
+
log.info("👉 Please click 'Yes' in the popup window to allow the installer.");
|
|
63
|
+
|
|
64
|
+
const absPath = path.resolve('solana-install.exe');
|
|
65
|
+
// Use Start-Process with -Verb RunAs to trigger elevation
|
|
66
|
+
// -Wait ensures we actually wait for it to finish
|
|
67
|
+
const elevateCmd = `powershell -Command "Start-Process -FilePath '${absPath}' -ArgumentList 'stable' -Verb RunAs -Wait"`;
|
|
68
|
+
|
|
69
|
+
const elevatedRun = shell.exec(elevateCmd);
|
|
70
|
+
|
|
71
|
+
if (elevatedRun.code === 0) {
|
|
72
|
+
// We can't easily capture stdout from the spawned high-privilege window,
|
|
73
|
+
// so we assume success if the process exited cleanly and verify existence.
|
|
74
|
+
// A basic verification: check if we can run solana
|
|
75
|
+
log.success("✅ Solana Installer finished (Elevated).");
|
|
76
|
+
} else {
|
|
77
|
+
log.error("❌ Elevated installation failed or was cancelled.");
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
log.error("❌ Solana CLI installation failed.");
|
|
82
|
+
// Don't return false hard here as we might want to continue, but usually this is fatal
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
log.success("✅ Solana CLI installed.");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (fs.existsSync('solana-install.exe')) {
|
|
89
|
+
shell.rm('solana-install.exe');
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
log.info("☀️ Solana CLI is already installed. Skipping.");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (shouldInstallRust || shouldInstallSolana) {
|
|
96
|
+
log.warn("⚠️ NOTE: You may need to restart your terminal for PATH changes to take effect.");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return true;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
module.exports = { installWindows };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const ora = require('ora');
|
|
3
|
+
|
|
4
|
+
const log = {
|
|
5
|
+
info: (msg) => console.log(chalk.blue(msg)),
|
|
6
|
+
success: (msg) => console.log(chalk.green(msg)),
|
|
7
|
+
warn: (msg) => console.log(chalk.yellow(msg)),
|
|
8
|
+
error: (msg) => console.log(chalk.red(msg)),
|
|
9
|
+
header: (msg) => console.log(chalk.bold.cyan(msg)),
|
|
10
|
+
subHeader: (msg) => console.log(chalk.cyan(msg)),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const spinner = (text) => ora(text);
|
|
14
|
+
|
|
15
|
+
module.exports = { log, spinner };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const shell = require('shelljs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
const getCargoBinPath = () => {
|
|
7
|
+
return path.join(os.homedir(), '.cargo', 'bin');
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const addToPathWindows = (newPath) => {
|
|
11
|
+
// This is tricky from Node. We can try to use setx but it's permanent and impactful.
|
|
12
|
+
// For now, we will just warn the user to restart their terminal which usually picks up standard install paths.
|
|
13
|
+
// Rustup and Solana installers usually handle the permanent PATH update in Registry.
|
|
14
|
+
// We just need to make sure the CURRENT session knows about it if possible, but that's hard in a child process.
|
|
15
|
+
// Best advice: Restart Terminal.
|
|
16
|
+
return;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
getCargoBinPath,
|
|
21
|
+
addToPathWindows
|
|
22
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const shell = require('shelljs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const { log } = require('./logger');
|
|
7
|
+
|
|
8
|
+
const resolveSolanaKeygen = () => {
|
|
9
|
+
// 1. Try PATH first
|
|
10
|
+
if (shell.which('solana-keygen')) return 'solana-keygen';
|
|
11
|
+
|
|
12
|
+
// 2. Try default Windows path
|
|
13
|
+
if (os.platform() === 'win32') {
|
|
14
|
+
const home = os.homedir();
|
|
15
|
+
const defaultPath = path.join(home, '.local', 'share', 'solana', 'install', 'active_release', 'bin', 'solana-keygen.exe');
|
|
16
|
+
if (fs.existsSync(defaultPath)) return `"${defaultPath}"`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Fallback
|
|
20
|
+
return 'solana-keygen';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Checks for wallet and prompts user to create one if missing.
|
|
25
|
+
* Returns true if wallet exists (or was created), false if user declined.
|
|
26
|
+
*/
|
|
27
|
+
const ensureWalletInteractive = async () => {
|
|
28
|
+
const walletDir = path.join(os.homedir(), '.config', 'solana');
|
|
29
|
+
const walletPath = path.join(walletDir, 'id.json');
|
|
30
|
+
|
|
31
|
+
if (fs.existsSync(walletPath)) {
|
|
32
|
+
log.info("🔑 Wallet found.");
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const rl = readline.createInterface({
|
|
37
|
+
input: process.stdin,
|
|
38
|
+
output: process.stdout
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
log.info("");
|
|
43
|
+
log.warn("⚠️ No Solana wallet found.");
|
|
44
|
+
rl.question("👉 Do you want to generate a new Solana wallet? [y/N] ", (answer) => {
|
|
45
|
+
rl.close();
|
|
46
|
+
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
47
|
+
log.info("🔑 Generating wallet...");
|
|
48
|
+
if (!fs.existsSync(walletDir)) fs.mkdirSync(walletDir, { recursive: true });
|
|
49
|
+
|
|
50
|
+
const keygenCmd = resolveSolanaKeygen();
|
|
51
|
+
|
|
52
|
+
// Use spawnSync to allow interactive input (passphrase)
|
|
53
|
+
const { spawnSync } = require('child_process');
|
|
54
|
+
|
|
55
|
+
// We need to strip quotes for spawn
|
|
56
|
+
let cmd = keygenCmd;
|
|
57
|
+
if (cmd.startsWith('"') && cmd.endsWith('"')) cmd = cmd.slice(1, -1);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// We use 'new' command which might prompt for passphrase
|
|
61
|
+
spawnSync(cmd, ['new', '--outfile', walletPath], { stdio: 'inherit', shell: true });
|
|
62
|
+
|
|
63
|
+
if (fs.existsSync(walletPath)) {
|
|
64
|
+
log.success("✅ Wallet generated.");
|
|
65
|
+
resolve(true);
|
|
66
|
+
} else {
|
|
67
|
+
// User might have cancelled via Ctrl+C in the subprocess
|
|
68
|
+
log.warn("❌ Creation cancelled or failed.");
|
|
69
|
+
resolve(false);
|
|
70
|
+
}
|
|
71
|
+
} catch (e) {
|
|
72
|
+
log.error("❌ Failed to generate wallet: " + e.message);
|
|
73
|
+
resolve(false);
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
log.info(" Skipping wallet generation.");
|
|
77
|
+
resolve(false);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
resolveSolanaKeygen,
|
|
85
|
+
ensureWalletInteractive
|
|
86
|
+
};
|