claude-scionos 2.0.0 → 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 +24 -1
- package/CLAUDE.md +3 -3
- package/README.fr.md +24 -8
- package/README.md +25 -21
- package/index.js +187 -116
- package/package.json +9 -8
- package/src/detectors/claude-only.js +49 -61
- 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
|
|
@@ -55,7 +78,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
55
78
|
### Added
|
|
56
79
|
- Initial release
|
|
57
80
|
- Ephemeral and secure token handling (memory-only storage)
|
|
58
|
-
- Support for
|
|
81
|
+
- Support for ScioNos environment (`https://routerlab.ch`)
|
|
59
82
|
- Bilingual documentation (English and French)
|
|
60
83
|
- Command-line interface with `--version` flag
|
|
61
84
|
- Secure token input with masking
|
package/CLAUDE.md
CHANGED
|
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
4
4
|
|
|
5
5
|
## Project Overview
|
|
6
6
|
|
|
7
|
-
claude-scionos is a lightweight Node.js CLI wrapper that provides ephemeral and secure execution of the official Claude Code CLI. It's designed for the
|
|
7
|
+
claude-scionos is a lightweight Node.js CLI wrapper that provides ephemeral and secure execution of the official Claude Code CLI. It's designed for the ScioNos environment and focuses on in-memory credential management with zero disk persistence.
|
|
8
8
|
|
|
9
9
|
## Architecture
|
|
10
10
|
|
|
@@ -14,7 +14,7 @@ The project is a single-file Node.js application (`index.js`) that:
|
|
|
14
14
|
2. **Windows-specific check**: Verifies Git Bash installation (required by Claude Code on Windows)
|
|
15
15
|
3. **Secure token collection**: Uses `@inquirer/prompts` for masked password input
|
|
16
16
|
4. **Environment isolation**: Creates isolated environment variables with:
|
|
17
|
-
- `ANTHROPIC_BASE_URL`: Set to `https://routerlab.ch` (hardcoded
|
|
17
|
+
- `ANTHROPIC_BASE_URL`: Set to `https://routerlab.ch` (hardcoded ScioNos endpoint)
|
|
18
18
|
- `ANTHROPIC_AUTH_TOKEN`: User-provided token (memory only)
|
|
19
19
|
- `ANTHROPIC_API_KEY`: Explicitly set to empty string
|
|
20
20
|
5. **Process spawning**: Launches Claude Code CLI with inherited stdio and custom environment
|
|
@@ -77,7 +77,7 @@ Currently no automated tests are configured. When implementing tests, focus on:
|
|
|
77
77
|
|
|
78
78
|
## Important Constants
|
|
79
79
|
|
|
80
|
-
- `ANTHROPIC_BASE_URL`: `"https://routerlab.ch"` (
|
|
80
|
+
- `ANTHROPIC_BASE_URL`: `"https://routerlab.ch"` (ScioNos-specific endpoint)
|
|
81
81
|
- Minimum Node.js version: 22
|
|
82
82
|
- Package entry point: `index.js` with shebang for direct execution
|
|
83
83
|
|
package/README.fr.md
CHANGED
|
@@ -34,7 +34,7 @@ _[🇬🇧 Read in English](./README.md)_
|
|
|
34
34
|
|
|
35
35
|
**claude-scionos** est un exécuteur éphémère et sécurisé pour la CLI officielle [Claude Code](https://github.com/anthropics/claude-code). Il initialise toutes les variables d'environnement nécessaires **directement en mémoire**, garantissant qu'aucun fichier de configuration ni aucune donnée d'authentification n'est jamais écrit sur le disque.
|
|
36
36
|
|
|
37
|
-
L'objectif est d'offrir une couche d'exécution propre, isolée et professionnelle, entièrement compatible avec Claude Code, spécifiquement conçue pour **l'environnement
|
|
37
|
+
L'objectif est d'offrir une couche d'exécution propre, isolée et professionnelle, entièrement compatible avec Claude Code, spécifiquement conçue pour **l'environnement ScioNos**.
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
@@ -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
|
|
@@ -127,6 +131,18 @@ npx claude-scionos --version
|
|
|
127
131
|
npx claude-scionos -v
|
|
128
132
|
```
|
|
129
133
|
|
|
134
|
+
#### Compatibilité totale avec Claude Code
|
|
135
|
+
|
|
136
|
+
**`claude-scionos` est un wrapper transparent** — il accepte **tous les flags et commandes** supportés par la CLI officielle Claude Code.
|
|
137
|
+
|
|
138
|
+
Vous pouvez utiliser n'importe quel flag ou commande Claude Code, comme :
|
|
139
|
+
- `npx claude-scionos --model opus "expliquer ce code"`
|
|
140
|
+
- `npx claude-scionos --verbose --continue`
|
|
141
|
+
- `npx claude-scionos -p --output-format json "requête"`
|
|
142
|
+
- `npx claude-scionos --chrome --agents '{"reviewer":{...}}'`
|
|
143
|
+
|
|
144
|
+
Pour une liste complète des flags et commandes disponibles, consultez la [documentation officielle de la CLI Claude Code](https://code.claude.com/docs/cli-reference).
|
|
145
|
+
|
|
130
146
|
---
|
|
131
147
|
|
|
132
148
|
### 🔍 Fonctionnement
|
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ _[🇫🇷 Lire en français](./README.fr.md)_
|
|
|
34
34
|
|
|
35
35
|
**claude-scionos** is an ephemeral and secure runner for the official [Claude Code](https://github.com/anthropics/claude-code) CLI. It initializes all required environment variables **directly in memory**, ensuring that no configuration files or authentication data are ever written to disk.
|
|
36
36
|
|
|
37
|
-
The goal is to offer a clean, isolated, and professional execution layer fully compatible with Claude Code, specifically designed for the **
|
|
37
|
+
The goal is to offer a clean, isolated, and professional execution layer fully compatible with Claude Code, specifically designed for the **ScioNos environment**.
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
@@ -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
|
-
####
|
|
111
|
-
|
|
112
|
-
```bash
|
|
113
|
-
$ npx claude-scionos
|
|
106
|
+
#### Debugging
|
|
114
107
|
|
|
115
|
-
|
|
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
|
|
@@ -127,6 +119,18 @@ npx claude-scionos --version
|
|
|
127
119
|
npx claude-scionos -v
|
|
128
120
|
```
|
|
129
121
|
|
|
122
|
+
#### Full Claude Code Compatibility
|
|
123
|
+
|
|
124
|
+
**`claude-scionos` is a transparent wrapper** — it accepts **all flags and commands** supported by the official Claude Code CLI.
|
|
125
|
+
|
|
126
|
+
You can use any Claude Code flag or command, such as:
|
|
127
|
+
- `npx claude-scionos --model opus "explain this code"`
|
|
128
|
+
- `npx claude-scionos --verbose --continue`
|
|
129
|
+
- `npx claude-scionos -p --output-format json "query"`
|
|
130
|
+
- `npx claude-scionos --chrome --agents '{"reviewer":{...}}'`
|
|
131
|
+
|
|
132
|
+
For a complete list of available flags and commands, see the [official Claude Code CLI documentation](https://code.claude.com/docs/cli-reference).
|
|
133
|
+
|
|
130
134
|
---
|
|
131
135
|
|
|
132
136
|
### 🔍 How It Works
|
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,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-scionos",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Ephemeral and secure runner for Claude Code CLI in
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "Ephemeral and secure runner for Claude Code CLI in ScioNos environment",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
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\"",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"claude",
|
|
19
|
-
"
|
|
19
|
+
"scionos",
|
|
20
20
|
"cli",
|
|
21
21
|
"wrapper"
|
|
22
22
|
],
|
|
@@ -35,15 +35,16 @@
|
|
|
35
35
|
},
|
|
36
36
|
"private": false,
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@inquirer/prompts": "^8.0
|
|
38
|
+
"@inquirer/prompts": "^8.1.0",
|
|
39
39
|
"chalk": "^5.6.2",
|
|
40
40
|
"cross-spawn": "^7.0.6",
|
|
41
41
|
"update-notifier": "^7.3.1",
|
|
42
42
|
"which": "^6.0.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@eslint/js": "^9.39.
|
|
46
|
-
"eslint": "^9.39.
|
|
47
|
-
"globals": "^
|
|
45
|
+
"@eslint/js": "^9.39.2",
|
|
46
|
+
"eslint": "^9.39.2",
|
|
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`);
|
|
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
|
}
|
|
@@ -180,10 +169,9 @@ function checkGitBashOnWindows() {
|
|
|
180
169
|
/**
|
|
181
170
|
* Provides installation instructions based on OS and current state
|
|
182
171
|
* @param {Object} osInfo - OS detection result
|
|
183
|
-
* @param {Object} claudeStatus - Claude Code detection result
|
|
184
172
|
* @returns {string} Installation instructions
|
|
185
173
|
*/
|
|
186
|
-
function getInstallationInstructions(osInfo
|
|
174
|
+
function getInstallationInstructions(osInfo) {
|
|
187
175
|
let instructions = [];
|
|
188
176
|
|
|
189
177
|
if (osInfo.type === 'Windows') {
|
|
@@ -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
|
+
});
|