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 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 SNIA environment (`https://hubs02225.snia.ch`)
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 SNIA environment and focuses on in-memory credential management with zero disk persistence.
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 SNIA endpoint)
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"` (SNIA-specific endpoint)
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 SNIA**.
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 **SNIA environment**.
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
- Simply execute the command:
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. The tool checks if Claude Code CLI is installed
106
- 2. Prompts you to enter your `ANTHROPIC_AUTH_TOKEN`
107
- 3. Launches Claude Code with the token stored **only in memory**
108
- 4. Automatically cleans up credentials when you exit
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
- #### Example Session
111
-
112
- ```bash
113
- $ npx claude-scionos
106
+ #### Debugging
114
107
 
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
- # Claude Code starts...
110
+ ```bash
111
+ npx claude-scionos --scionos-debug
120
112
  ```
121
113
 
122
- #### Command-Line Options
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 { createRequire } from 'node:module';
9
- import { isClaudeCodeInstalled, detectOS, checkGitBashOnWindows, getInstallationInstructions } from './src/detectors/claude-only.js';
10
-
11
- const require = createRequire(import.meta.url);
12
- const pkg = require('./package.json');
13
-
14
- // Initialize update notifier
15
- updateNotifier({ pkg }).notify();
16
-
17
- // 0. Handle --version / -v flag
18
- if (process.argv.includes('--version') || process.argv.includes('-v')) {
19
- console.log(pkg.version);
20
- process.exit(0);
21
- }
22
-
23
- // 1. Enhanced System Detection
24
- console.log(chalk.cyan('🔍 Checking system configuration...'));
25
-
26
- // Detect OS and environment
27
- const osInfo = detectOS();
28
- console.log(chalk.gray(`✓ OS: ${osInfo.type} (${osInfo.arch})`));
29
- console.log(chalk.gray(`✓ Shell: ${osInfo.shell}`));
30
-
31
- // Check Claude Code installation
32
- const claudeStatus = isClaudeCodeInstalled();
33
-
34
- if (!claudeStatus.installed) {
35
- console.error(chalk.redBright('\n❌ Claude Code not found'));
36
-
37
- // Show detailed detection info
38
- console.log(chalk.yellow('\nDetection Details:'));
39
- console.log(chalk.gray(claudeStatus.details));
40
-
41
- // Show installation instructions
42
- const instructions = getInstallationInstructions(osInfo, claudeStatus);
43
- console.log(chalk.cyan(instructions));
44
-
45
- process.exit(1);
46
- }
47
-
48
- // Show Claude Code status
49
- console.log(chalk.green('\n✓ Claude Code detected'));
50
- console.log(chalk.gray(claudeStatus.details));
51
-
52
- // 2. Check Git Bash on Windows (if needed)
53
- if (process.platform === 'win32') {
54
- console.log(chalk.cyan('\n🔍 Checking Git Bash availability...'));
55
- const gitBashStatus = checkGitBashOnWindows();
56
-
57
- if (!gitBashStatus.available) {
58
- console.log(chalk.red('\n❌ Git Bash is required on Windows'));
59
- console.log(chalk.gray(gitBashStatus.message));
60
-
61
- // Show Git Bash installation instructions
62
- console.log(chalk.cyan('\n📥 Install Git for Windows:'));
63
- console.log(chalk.white(' https://git-scm.com/downloads/win\n'));
64
- console.log(chalk.cyan('⚙️ Or set the path manually:'));
65
- console.log(chalk.white(' set CLAUDE_CODE_GIT_BASH_PATH=C:\\Program Files\\Git\\bin\\bash.exe'));
66
- console.log(chalk.white(' (PowerShell: $env:CLAUDE_CODE_GIT_BASH_PATH="C:\\Program Files\\Git\\bin\\bash.exe")\n'));
67
- console.log(chalk.yellow('💡 After installation, restart your terminal and try again.\n'));
68
- process.exit(1);
69
- } else {
70
- console.log(chalk.green('✓ Git Bash available'));
71
- console.log(chalk.gray(gitBashStatus.message));
72
- }
73
- }
74
-
75
- // 3. Intro
76
- console.clear();
77
- console.log(chalk.cyan.bold("Claude Code (via ScioNos)"));
78
- console.log(chalk.gray(`Running on ${osInfo.type} with ${osInfo.shell}`));
79
-
80
- // 4. Token info
81
- console.log(chalk.blueBright("To retrieve your token, visit: https://routerlab.ch/keys"));
82
-
83
- // 5. Token input
84
- const token = await password({
85
- message: "Please enter your ANTHROPIC_AUTH_TOKEN:",
86
- validate: (input) => {
87
- if (!input || input.trim() === '') {
88
- return "Token cannot be empty.";
89
- }
90
- return true;
91
- },
92
- mask: '*'
93
- });
94
-
95
- // 6. Environment configuration
96
- const env = {
97
- ...process.env,
98
- ANTHROPIC_BASE_URL: "https://routerlab.ch",
99
- ANTHROPIC_AUTH_TOKEN: token,
100
- ANTHROPIC_API_KEY: "" // Force empty string
101
- };
102
-
103
- // 7. Launch Claude Code
104
- const args = process.argv.slice(2);
105
- const child = spawn('claude', args, {
106
- stdio: 'inherit',
107
- env: env
108
- });
109
-
110
- child.on('close', (code) => {
111
- process.exit(code);
112
- });
113
-
114
- child.on('error', (err) => {
115
- console.error(chalk.red(`Error launching Claude: ${err.message}`));
116
- process.exit(1);
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.0.0",
4
- "description": "Ephemeral and secure runner for Claude Code CLI in SNIA environment",
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": "echo \"Error: no test specified\" && exit 1",
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
- "snia",
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.2",
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.1",
46
- "eslint": "^9.39.1",
47
- "globals": "^16.5.0"
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
- details.push(`✓ Configuration file found: ${settingsPath}`);
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
- // Also check for the claude CLI command in PATH
54
- try {
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
- if (claudePath) {
59
- details.push(`✓ Claude CLI found in PATH: ${claudePath}`);
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
- // Try to get version to confirm it's working
62
- try {
63
- const version = execSync('claude --version', { encoding: 'utf8' }).trim();
64
- details.push(`✓ Version: ${version}`);
65
- } catch (error) {
66
- details.push(`⚠ CLI found but version check failed`);
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
- return {
70
- installed: true,
71
- path: claudeDir && fs.existsSync(claudeDir) ? claudeDir : null,
72
- configPath: claudeDir && fs.existsSync(path.join(claudeDir, 'settings.json'))
73
- ? path.join(claudeDir, 'settings.json')
74
- : null,
75
- cliAvailable: true,
76
- cliPath: claudePath,
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
- } catch (error) {
81
- details.push(`✗ Claude CLI not found in PATH`);
67
+ } else {
68
+ details.push(`✗ Executable 'claude' not found in PATH or standard locations.`);
82
69
  }
83
70
 
84
71
  return {
85
- installed: false,
86
- path: null,
87
- configPath: null,
88
- cliAvailable: false,
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, claudeStatus) {
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
+ });