@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 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
+ };