claude-scionos 2.0.1 → 2.2.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/CHANGELOG.md +23 -0
- package/README.fr.md +11 -7
- package/README.md +12 -20
- package/index.js +187 -116
- package/package.json +4 -3
- package/src/detectors/claude-only.js +48 -59
- package/tests/detectors.test.js +166 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.2.0] - 2026-01-06
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Auto-Installation**: Prompts users to automatically install Claude Code CLI (`npm install -g`) if missing.
|
|
12
|
+
- **Native Path Detection**: Now detects Claude Code installations in native paths (`~/.local/bin`, Windows Apps, etc.) per official docs.
|
|
13
|
+
- **SIGTERM Support**: Added handling for `SIGTERM` signals (Docker, CI/CD) to cleanly stop the child process.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **Crash on Config-Only**: Fixed a critical bug where the wrapper would crash if a configuration file existed but the CLI executable was missing.
|
|
17
|
+
- **Recursion Safety**: Now launches the detected absolute path of the executable instead of the generic command name, preventing potential loop issues.
|
|
18
|
+
- **Error Logging**: Errors are now correctly sent to `stderr` instead of `stdout`.
|
|
19
|
+
|
|
20
|
+
## [2.1.0] - 2026-01-06
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **Debug Mode**: New `--scionos-debug` flag for detailed diagnostic output
|
|
24
|
+
- **Test Infrastructure**: Added Vitest test suite covering core detection logic
|
|
25
|
+
- **Linting**: Fixed development environment and linting rules
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **Windows Path Handling**: Fixed an issue where `where claude` returned multiple paths on Windows
|
|
29
|
+
- **Signal Handling**: Improved `SIGINT` (Ctrl+C) handling to prevent wrapper from killing Claude prematurely
|
|
30
|
+
|
|
8
31
|
## [2.0.0] - 2025-12-12
|
|
9
32
|
|
|
10
33
|
### ⚠️ BREAKING CHANGES
|
package/README.fr.md
CHANGED
|
@@ -54,14 +54,10 @@ L'objectif est d'offrir une couche d'exécution propre, isolée et professionnel
|
|
|
54
54
|
Avant d'utiliser `claude-scionos`, assurez-vous d'avoir :
|
|
55
55
|
|
|
56
56
|
- **Node.js** version 22 ou supérieure ([Télécharger](https://nodejs.org/))
|
|
57
|
-
- La CLI **Claude Code** installée globalement :
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
npm install -g @anthropic-ai/claude-code
|
|
61
|
-
```
|
|
62
|
-
|
|
63
57
|
- Un **ANTHROPIC_AUTH_TOKEN** valide depuis [https://routerlab.ch/keys](https://routerlab.ch/keys)
|
|
64
58
|
|
|
59
|
+
*(Note : Si la CLI **Claude Code** n'est pas installée, l'outil vous proposera de l'installer automatiquement.)*
|
|
60
|
+
|
|
65
61
|
---
|
|
66
62
|
|
|
67
63
|
### 📥 Installation
|
|
@@ -102,11 +98,19 @@ npx claude-scionos
|
|
|
102
98
|
|
|
103
99
|
**Ce qui se passe :**
|
|
104
100
|
|
|
105
|
-
1. L'outil vérifie si la CLI Claude Code est installée
|
|
101
|
+
1. L'outil vérifie si la CLI Claude Code est installée (si non, propose l'**installation automatique**)
|
|
106
102
|
2. Vous invite à saisir votre `ANTHROPIC_AUTH_TOKEN`
|
|
107
103
|
3. Lance Claude Code avec le jeton stocké **uniquement en mémoire**
|
|
108
104
|
4. Nettoie automatiquement les informations d'identification à la sortie
|
|
109
105
|
|
|
106
|
+
#### Débogage
|
|
107
|
+
|
|
108
|
+
Si vous rencontrez des problèmes, utilisez le flag de débogage pour voir les informations détaillées :
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npx claude-scionos --scionos-debug
|
|
112
|
+
```
|
|
113
|
+
|
|
110
114
|
#### Exemple de session
|
|
111
115
|
|
|
112
116
|
```bash
|
package/README.md
CHANGED
|
@@ -54,14 +54,10 @@ The goal is to offer a clean, isolated, and professional execution layer fully c
|
|
|
54
54
|
Before using `claude-scionos`, ensure you have:
|
|
55
55
|
|
|
56
56
|
- **Node.js** version 22 or later ([Download](https://nodejs.org/))
|
|
57
|
-
- **Claude Code** CLI installed globally:
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
npm install -g @anthropic-ai/claude-code
|
|
61
|
-
```
|
|
62
|
-
|
|
63
57
|
- A valid **ANTHROPIC_AUTH_TOKEN** from [https://routerlab.ch/keys](https://routerlab.ch/keys)
|
|
64
58
|
|
|
59
|
+
*(Note: If **Claude Code** is not installed, the tool will offer to install it for you automatically.)*
|
|
60
|
+
|
|
65
61
|
---
|
|
66
62
|
|
|
67
63
|
### 📥 Installation
|
|
@@ -94,7 +90,7 @@ claude-scionos
|
|
|
94
90
|
|
|
95
91
|
#### Basic Usage
|
|
96
92
|
|
|
97
|
-
|
|
93
|
+
Run the command:
|
|
98
94
|
|
|
99
95
|
```bash
|
|
100
96
|
npx claude-scionos
|
|
@@ -102,24 +98,20 @@ npx claude-scionos
|
|
|
102
98
|
|
|
103
99
|
**What happens:**
|
|
104
100
|
|
|
105
|
-
1.
|
|
106
|
-
2.
|
|
107
|
-
3.
|
|
108
|
-
4.
|
|
101
|
+
1. Checks if Claude Code CLI is installed (if not, offers **automatic installation**)
|
|
102
|
+
2. Prompts for your `ANTHROPIC_AUTH_TOKEN`
|
|
103
|
+
3. Launches Claude Code with token stored **in memory only**
|
|
104
|
+
4. Automatically cleans credentials on exit
|
|
109
105
|
|
|
110
|
-
####
|
|
106
|
+
#### Debugging
|
|
111
107
|
|
|
112
|
-
|
|
113
|
-
$ npx claude-scionos
|
|
114
|
-
|
|
115
|
-
Claude Code (via ScioNos)
|
|
116
|
-
To retrieve your token, visit: https://routerlab.ch/keys
|
|
117
|
-
? Please enter your ANTHROPIC_AUTH_TOKEN: ********
|
|
108
|
+
If you encounter issues, you can run with the debug flag to see detailed diagnostic information:
|
|
118
109
|
|
|
119
|
-
|
|
110
|
+
```bash
|
|
111
|
+
npx claude-scionos --scionos-debug
|
|
120
112
|
```
|
|
121
113
|
|
|
122
|
-
#### Command
|
|
114
|
+
#### Command Line Options
|
|
123
115
|
|
|
124
116
|
```bash
|
|
125
117
|
# Display version
|
package/index.js
CHANGED
|
@@ -1,117 +1,188 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import { password } from '@inquirer/prompts';
|
|
5
|
-
import spawn from 'cross-spawn';
|
|
6
|
-
import updateNotifier from 'update-notifier';
|
|
7
|
-
import process from 'node:process';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
console.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
console.log(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
console.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
2
|
+
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { password, confirm } from '@inquirer/prompts';
|
|
5
|
+
import spawn from 'cross-spawn';
|
|
6
|
+
import updateNotifier from 'update-notifier';
|
|
7
|
+
import process from 'node:process';
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import { isClaudeCodeInstalled, detectOS, checkGitBashOnWindows, getInstallationInstructions } from './src/detectors/claude-only.js';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const pkg = require('./package.json');
|
|
14
|
+
|
|
15
|
+
// Initialize update notifier
|
|
16
|
+
updateNotifier({ pkg }).notify();
|
|
17
|
+
|
|
18
|
+
// 0. Handle Flags
|
|
19
|
+
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
20
|
+
console.log(pkg.version);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const isDebug = process.argv.includes('--scionos-debug');
|
|
25
|
+
|
|
26
|
+
// 1. Enhanced System Detection
|
|
27
|
+
console.log(chalk.cyan('🔍 Checking system configuration...'));
|
|
28
|
+
|
|
29
|
+
// Detect OS and environment
|
|
30
|
+
const osInfo = detectOS();
|
|
31
|
+
console.log(chalk.gray(`✓ OS: ${osInfo.type} (${osInfo.arch})`));
|
|
32
|
+
console.log(chalk.gray(`✓ Shell: ${osInfo.shell}`));
|
|
33
|
+
|
|
34
|
+
// Check Claude Code installation
|
|
35
|
+
let claudeStatus = isClaudeCodeInstalled();
|
|
36
|
+
|
|
37
|
+
if (!claudeStatus.installed) {
|
|
38
|
+
console.error(chalk.redBright('\n❌ Claude Code CLI not found'));
|
|
39
|
+
|
|
40
|
+
// Show detailed detection info
|
|
41
|
+
console.error(chalk.yellow('\nDetection Details:'));
|
|
42
|
+
console.error(chalk.gray(claudeStatus.details));
|
|
43
|
+
console.log(''); // spacer
|
|
44
|
+
|
|
45
|
+
const shouldInstall = await confirm({
|
|
46
|
+
message: 'Claude Code CLI is not installed. Do you want to install it via npm now?',
|
|
47
|
+
default: true
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (shouldInstall) {
|
|
51
|
+
try {
|
|
52
|
+
console.log(chalk.cyan('\n📦 Installing @anthropic-ai/claude-code globally...'));
|
|
53
|
+
execSync('npm install -g @anthropic-ai/claude-code', { stdio: 'inherit' });
|
|
54
|
+
console.log(chalk.green('✓ Installation successful!'));
|
|
55
|
+
|
|
56
|
+
// Re-detect to get the new path
|
|
57
|
+
claudeStatus = isClaudeCodeInstalled();
|
|
58
|
+
|
|
59
|
+
if (!claudeStatus.installed) {
|
|
60
|
+
// Fallback if install succeeded but path isn't picked up immediately
|
|
61
|
+
console.warn(chalk.yellow('⚠ Installation finished, but the executable was not found in the standard paths immediately.'));
|
|
62
|
+
console.warn(chalk.yellow('You may need to restart your terminal.'));
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.error(chalk.red(`\n❌ Installation failed: ${e.message}`));
|
|
67
|
+
console.error(chalk.yellow('Please try installing manually using the instructions below.'));
|
|
68
|
+
|
|
69
|
+
const instructions = getInstallationInstructions(osInfo, claudeStatus);
|
|
70
|
+
console.error(chalk.cyan(instructions));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
// User declined install
|
|
75
|
+
const instructions = getInstallationInstructions(osInfo, claudeStatus);
|
|
76
|
+
console.error(chalk.cyan(instructions));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Show Claude Code status
|
|
82
|
+
console.log(chalk.green('\n✓ Claude Code detected'));
|
|
83
|
+
console.log(chalk.gray(claudeStatus.details));
|
|
84
|
+
|
|
85
|
+
// 2. Check Git Bash on Windows (if needed)
|
|
86
|
+
let gitBashPath = null;
|
|
87
|
+
if (process.platform === 'win32') {
|
|
88
|
+
console.log(chalk.cyan('\n🔍 Checking Git Bash availability...'));
|
|
89
|
+
const gitBashStatus = checkGitBashOnWindows();
|
|
90
|
+
|
|
91
|
+
if (!gitBashStatus.available) {
|
|
92
|
+
console.log(chalk.red('\n❌ Git Bash is required on Windows'));
|
|
93
|
+
console.log(chalk.gray(gitBashStatus.message));
|
|
94
|
+
|
|
95
|
+
// Show Git Bash installation instructions
|
|
96
|
+
console.log(chalk.cyan('\n📥 Install Git for Windows:'));
|
|
97
|
+
console.log(chalk.white(' https://git-scm.com/downloads/win\n'));
|
|
98
|
+
console.log(chalk.cyan('⚙️ Or set the path manually:'));
|
|
99
|
+
console.log(chalk.white(' set CLAUDE_CODE_GIT_BASH_PATH=C:\\Program Files\\Git\\bin\\bash.exe'));
|
|
100
|
+
console.log(chalk.white(' (PowerShell: $env:CLAUDE_CODE_GIT_BASH_PATH="C:\\Program Files\\Git\\bin\\bash.exe")\n'));
|
|
101
|
+
console.log(chalk.yellow('💡 After installation, restart your terminal and try again.\n'));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
} else {
|
|
104
|
+
console.log(chalk.green('✓ Git Bash available'));
|
|
105
|
+
console.log(chalk.gray(gitBashStatus.message));
|
|
106
|
+
gitBashPath = gitBashStatus.path;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 3. Intro
|
|
111
|
+
console.clear();
|
|
112
|
+
console.log(chalk.cyan.bold("Claude Code (via ScioNos)"));
|
|
113
|
+
console.log(chalk.gray(`Running on ${osInfo.type} with ${osInfo.shell}`));
|
|
114
|
+
if (isDebug) console.log(chalk.yellow("🐞 Debug Mode Active"));
|
|
115
|
+
|
|
116
|
+
// 4. Token info
|
|
117
|
+
console.log(chalk.blueBright("To retrieve your token, visit: https://routerlab.ch/keys"));
|
|
118
|
+
|
|
119
|
+
// 5. Token input
|
|
120
|
+
const token = await password({
|
|
121
|
+
message: "Please enter your ANTHROPIC_AUTH_TOKEN:",
|
|
122
|
+
validate: (input) => {
|
|
123
|
+
if (!input || input.trim() === '') {
|
|
124
|
+
return "Token cannot be empty.";
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
},
|
|
128
|
+
mask: '*'
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// 6. Environment configuration
|
|
132
|
+
const env = {
|
|
133
|
+
...process.env,
|
|
134
|
+
ANTHROPIC_BASE_URL: "https://routerlab.ch",
|
|
135
|
+
ANTHROPIC_AUTH_TOKEN: token,
|
|
136
|
+
ANTHROPIC_API_KEY: "" // Force empty string
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// 7. Launch Claude Code
|
|
140
|
+
// Filter out our internal flag before passing args to Claude
|
|
141
|
+
const args = process.argv.slice(2).filter(arg => arg !== '--scionos-debug');
|
|
142
|
+
|
|
143
|
+
if (isDebug) {
|
|
144
|
+
console.log(chalk.yellow('\n--- DEBUG INFO ---'));
|
|
145
|
+
console.log(chalk.gray(`Platform: ${process.platform}`));
|
|
146
|
+
console.log(chalk.gray(`Claude Command: claude ${args.join(' ')}`));
|
|
147
|
+
console.log(chalk.gray(`Router URL: ${env.ANTHROPIC_BASE_URL}`));
|
|
148
|
+
console.log(chalk.gray(`Token Length: ${token.length} chars`));
|
|
149
|
+
if (gitBashPath) console.log(chalk.gray(`Git Bash: ${gitBashPath}`));
|
|
150
|
+
console.log(chalk.yellow('------------------\n'));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const child = spawn(claudeStatus.cliPath, args, {
|
|
154
|
+
stdio: 'inherit',
|
|
155
|
+
env: env
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Signal Handling
|
|
159
|
+
// We intentionally ignore SIGINT in the parent process.
|
|
160
|
+
// Because stdio is 'inherit', the child process (Claude) receives the Ctrl+C (SIGINT) directly from the TTY.
|
|
161
|
+
process.on('SIGINT', () => {
|
|
162
|
+
if (isDebug) {
|
|
163
|
+
console.error(chalk.yellow('\n[Wrapper] Received SIGINT. Waiting for Claude to exit...'));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Handle SIGTERM (e.g., Docker stop, kill)
|
|
168
|
+
process.on('SIGTERM', () => {
|
|
169
|
+
if (isDebug) {
|
|
170
|
+
console.error(chalk.yellow('\n[Wrapper] Received SIGTERM. Forwarding to Claude...'));
|
|
171
|
+
}
|
|
172
|
+
if (child) {
|
|
173
|
+
child.kill('SIGTERM');
|
|
174
|
+
}
|
|
175
|
+
process.exit(0);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
child.on('close', (code) => {
|
|
179
|
+
if (isDebug) {
|
|
180
|
+
console.error(chalk.yellow(`\n[Wrapper] Child process exited with code ${code}`));
|
|
181
|
+
}
|
|
182
|
+
process.exit(code);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
child.on('error', (err) => {
|
|
186
|
+
console.error(chalk.red(`Error launching Claude at ${claudeStatus.cliPath}: ${err.message}`));
|
|
187
|
+
process.exit(1);
|
|
188
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-scionos",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Ephemeral and secure runner for Claude Code CLI in ScioNos environment",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"claude-scionos": "index.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "
|
|
11
|
+
"test": "vitest run",
|
|
12
12
|
"lint": "eslint .",
|
|
13
13
|
"release:patch": "npm version patch -m \"Chore: Bump version to %s\"",
|
|
14
14
|
"release:minor": "npm version minor -m \"Chore: Bump version to %s\"",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@eslint/js": "^9.39.2",
|
|
46
46
|
"eslint": "^9.39.2",
|
|
47
|
-
"globals": "^17.0.0"
|
|
47
|
+
"globals": "^17.0.0",
|
|
48
|
+
"vitest": "^4.0.16"
|
|
48
49
|
}
|
|
49
50
|
}
|
|
@@ -13,79 +13,68 @@ function isClaudeCodeInstalled() {
|
|
|
13
13
|
const claudeDir = path.join(os.homedir(), '.claude');
|
|
14
14
|
const details = [];
|
|
15
15
|
|
|
16
|
+
// 1. Check for Configuration
|
|
17
|
+
let configFound = false;
|
|
16
18
|
if (fs.existsSync(claudeDir)) {
|
|
17
|
-
details.push(`✓ Found Claude directory: ${claudeDir}`);
|
|
18
|
-
|
|
19
|
-
// Check for settings.json to confirm it's actually Claude Code
|
|
20
19
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
21
20
|
if (fs.existsSync(settingsPath)) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// Try to read the config to see if it's configured
|
|
25
|
-
try {
|
|
26
|
-
const config = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
27
|
-
const hasBaseUrl = config.env?.ANTHROPIC_BASE_URL;
|
|
28
|
-
const hasApiKey = config.env?.ANTHROPIC_API_KEY;
|
|
29
|
-
|
|
30
|
-
if (hasBaseUrl && hasApiKey) {
|
|
31
|
-
details.push(`✓ Claude Code is configured with custom API`);
|
|
32
|
-
} else if (hasBaseUrl || hasApiKey) {
|
|
33
|
-
details.push(`⚠ Claude Code is partially configured`);
|
|
34
|
-
} else {
|
|
35
|
-
details.push(`ℹ Claude Code installed but not configured`);
|
|
36
|
-
}
|
|
37
|
-
} catch (error) {
|
|
38
|
-
details.push(`⚠ Could not read configuration file: ${error.message}`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
installed: true,
|
|
43
|
-
path: claudeDir,
|
|
44
|
-
configPath: settingsPath,
|
|
45
|
-
cliAvailable: false, // Will check next
|
|
46
|
-
details: details.join('\n ')
|
|
47
|
-
};
|
|
48
|
-
} else {
|
|
49
|
-
details.push(`⚠ Directory exists but no settings.json found`);
|
|
21
|
+
configFound = true;
|
|
22
|
+
details.push(`✓ Configuration found at: ${settingsPath}`);
|
|
50
23
|
}
|
|
51
24
|
}
|
|
52
25
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
const command = process.platform === 'win32' ? 'where claude' : 'which claude';
|
|
56
|
-
const claudePath = execSync(command, { encoding: 'utf8' }).trim();
|
|
26
|
+
// 2. Check for Executable (CLI)
|
|
27
|
+
let cliPath = null;
|
|
57
28
|
|
|
58
|
-
|
|
59
|
-
|
|
29
|
+
// 2a. Check Native Install Paths (per official docs)
|
|
30
|
+
const home = os.homedir();
|
|
31
|
+
const nativePaths = process.platform === 'win32'
|
|
32
|
+
? [path.join(home, '.local', 'bin', 'claude.exe'), path.join(home, 'AppData', 'Local', 'Microsoft', 'WindowsApps', 'claude.exe')]
|
|
33
|
+
: [path.join(home, '.local', 'bin', 'claude'), '/opt/homebrew/bin/claude', '/usr/local/bin/claude'];
|
|
34
|
+
|
|
35
|
+
for (const p of nativePaths) {
|
|
36
|
+
if (fs.existsSync(p)) {
|
|
37
|
+
cliPath = p;
|
|
38
|
+
details.push(`✓ Found native binary: ${p}`);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
60
42
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
43
|
+
// 2b. Check PATH if not found yet
|
|
44
|
+
if (!cliPath) {
|
|
45
|
+
try {
|
|
46
|
+
const command = process.platform === 'win32' ? 'where claude' : 'which claude';
|
|
47
|
+
const output = execSync(command, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
48
|
+
const foundPath = output.split(/\r?\n/)[0].trim();
|
|
49
|
+
if (foundPath && fs.existsSync(foundPath)) {
|
|
50
|
+
cliPath = foundPath;
|
|
51
|
+
details.push(`✓ Found in PATH: ${foundPath}`);
|
|
67
52
|
}
|
|
53
|
+
} catch (e) {
|
|
54
|
+
// Ignore error if not found in PATH
|
|
55
|
+
}
|
|
56
|
+
}
|
|
68
57
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
details: details.join('\n ')
|
|
78
|
-
};
|
|
58
|
+
// 3. Verify Version (if executable found)
|
|
59
|
+
if (cliPath) {
|
|
60
|
+
try {
|
|
61
|
+
// We use the full path to avoid recursion or alias issues
|
|
62
|
+
const version = execSync(`"${cliPath}" --version`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
63
|
+
details.push(`✓ Version: ${version}`);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
details.push(`⚠ Executable found but version check failed: ${error.message}`);
|
|
79
66
|
}
|
|
80
|
-
}
|
|
81
|
-
details.push(`✗
|
|
67
|
+
} else {
|
|
68
|
+
details.push(`✗ Executable 'claude' not found in PATH or standard locations.`);
|
|
82
69
|
}
|
|
83
70
|
|
|
84
71
|
return {
|
|
85
|
-
installed:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
72
|
+
installed: !!cliPath, // STRICT: Only true if the binary is found
|
|
73
|
+
configFound: configFound,
|
|
74
|
+
path: claudeDir,
|
|
75
|
+
configPath: configFound ? path.join(claudeDir, 'settings.json') : null,
|
|
76
|
+
cliAvailable: !!cliPath,
|
|
77
|
+
cliPath: cliPath,
|
|
89
78
|
details: details.join('\n ')
|
|
90
79
|
};
|
|
91
80
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import child_process from 'child_process';
|
|
6
|
+
import { isClaudeCodeInstalled, detectOS, checkGitBashOnWindows } from '../src/detectors/claude-only.js';
|
|
7
|
+
|
|
8
|
+
// Mock modules
|
|
9
|
+
vi.mock('fs');
|
|
10
|
+
vi.mock('os');
|
|
11
|
+
vi.mock('child_process');
|
|
12
|
+
|
|
13
|
+
describe('System Detectors', () => {
|
|
14
|
+
const originalPlatform = process.platform;
|
|
15
|
+
const originalEnv = process.env;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.resetAllMocks();
|
|
19
|
+
process.env = { ...originalEnv };
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
24
|
+
process.env = originalEnv;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('detectOS', () => {
|
|
28
|
+
it('should detect Windows', () => {
|
|
29
|
+
vi.mocked(os.platform).mockReturnValue('win32');
|
|
30
|
+
vi.mocked(os.arch).mockReturnValue('x64');
|
|
31
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
32
|
+
process.env.WINDIR = 'C:\\Windows'; // Simulate CMD
|
|
33
|
+
|
|
34
|
+
const result = detectOS();
|
|
35
|
+
expect(result.type).toBe('Windows');
|
|
36
|
+
expect(result.shell).toBe('Command Prompt (CMD)');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should detect macOS', () => {
|
|
40
|
+
vi.mocked(os.platform).mockReturnValue('darwin');
|
|
41
|
+
vi.mocked(os.arch).mockReturnValue('arm64');
|
|
42
|
+
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
|
43
|
+
process.env.SHELL = '/bin/zsh';
|
|
44
|
+
|
|
45
|
+
const result = detectOS();
|
|
46
|
+
expect(result.type).toBe('macOS');
|
|
47
|
+
expect(result.shell).toBe('Zsh');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('checkGitBashOnWindows', () => {
|
|
52
|
+
it('should return available if Git Bash exists in standard path on Windows', () => {
|
|
53
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
54
|
+
vi.mocked(fs.existsSync).mockImplementation((p) => p === 'C:\\Program Files\\Git\\bin\\bash.exe');
|
|
55
|
+
|
|
56
|
+
const result = checkGitBashOnWindows();
|
|
57
|
+
expect(result.available).toBe(true);
|
|
58
|
+
expect(result.path).toBe('C:\\Program Files\\Git\\bin\\bash.exe');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should return not available if no Git Bash found on Windows', () => {
|
|
62
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
63
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
64
|
+
|
|
65
|
+
const result = checkGitBashOnWindows();
|
|
66
|
+
expect(result.available).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should not require Git Bash on non-Windows', () => {
|
|
70
|
+
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
|
71
|
+
|
|
72
|
+
const result = checkGitBashOnWindows();
|
|
73
|
+
expect(result.available).toBe(true);
|
|
74
|
+
expect(result.message).toContain('Not required');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('isClaudeCodeInstalled', () => {
|
|
79
|
+
it('should detect Claude in PATH', () => {
|
|
80
|
+
// Mock homedir to avoid finding config file
|
|
81
|
+
vi.mocked(os.homedir).mockReturnValue('/home/user');
|
|
82
|
+
|
|
83
|
+
// Mock execSync to return a path
|
|
84
|
+
vi.mocked(child_process.execSync).mockImplementation((cmd) => {
|
|
85
|
+
if (cmd.includes('which claude') || cmd.includes('where claude')) {
|
|
86
|
+
return '/usr/local/bin/claude';
|
|
87
|
+
}
|
|
88
|
+
if (cmd.includes('--version')) {
|
|
89
|
+
return '0.0.1';
|
|
90
|
+
}
|
|
91
|
+
return '';
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Mock existsSync: false for config, true for CLI path
|
|
95
|
+
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
96
|
+
if (p === '/usr/local/bin/claude') return true;
|
|
97
|
+
return false;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const result = isClaudeCodeInstalled();
|
|
101
|
+
expect(result.installed).toBe(true);
|
|
102
|
+
expect(result.cliAvailable).toBe(true);
|
|
103
|
+
expect(result.cliPath).toBe('/usr/local/bin/claude');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should detect Claude from config file ONLY if CLI is also found', () => {
|
|
107
|
+
vi.mocked(os.homedir).mockReturnValue('/home/user');
|
|
108
|
+
// Simulate .claude directory and settings.json existence
|
|
109
|
+
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
110
|
+
if (p.endsWith('.claude')) return true;
|
|
111
|
+
if (p.endsWith('settings.json')) return true;
|
|
112
|
+
return false;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Mock reading config
|
|
116
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
|
117
|
+
env: { ANTHROPIC_BASE_URL: 'https://api.anthropic.com' }
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
// BUT execSync fails (CLI not found)
|
|
121
|
+
vi.mocked(child_process.execSync).mockImplementation(() => {
|
|
122
|
+
throw new Error('not found');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const result = isClaudeCodeInstalled();
|
|
126
|
+
// Updated logic: installed should be false if CLI is missing, even if config exists
|
|
127
|
+
expect(result.installed).toBe(false);
|
|
128
|
+
expect(result.configFound).toBe(true);
|
|
129
|
+
expect(result.cliAvailable).toBe(false);
|
|
130
|
+
expect(result.details).toContain('Executable \'claude\' not found');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should detect Claude in native path (Linux/Mac)', () => {
|
|
134
|
+
// Force Linux platform to ensure implementation looks for 'claude' not 'claude.exe'
|
|
135
|
+
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
136
|
+
|
|
137
|
+
// We simulate logic but path.join will use OS separators.
|
|
138
|
+
// On Windows, path.join produces backslashes.
|
|
139
|
+
// The code under test uses path.join.
|
|
140
|
+
const mockHome = '/home/user';
|
|
141
|
+
const expectedPath = path.join(mockHome, '.local', 'bin', 'claude');
|
|
142
|
+
|
|
143
|
+
vi.mocked(os.homedir).mockReturnValue(mockHome);
|
|
144
|
+
|
|
145
|
+
// Simulate native path existence
|
|
146
|
+
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
147
|
+
return p === expectedPath;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const result = isClaudeCodeInstalled();
|
|
151
|
+
expect(result.installed).toBe(true);
|
|
152
|
+
expect(result.cliPath).toBe(expectedPath);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should return false if neither CLI nor config found', () => {
|
|
156
|
+
vi.mocked(os.homedir).mockReturnValue('/home/user');
|
|
157
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
158
|
+
vi.mocked(child_process.execSync).mockImplementation(() => {
|
|
159
|
+
throw new Error('not found');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const result = isClaudeCodeInstalled();
|
|
163
|
+
expect(result.installed).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|