mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 23:18:13 +02:00
Renommage AJEAN (3/3) : textes, aide, README et variables d'environnement
Surface visible passee en AJEAN : aide de la CLI, messages, invite du chat terminal, commentaires, README, metadonnees Windows du binaire (InternalName / OriginalFilename). Les reglages s'appellent desormais AJEAN_* (AJEAN_HOME, AJEAN_MODEL_DIRS, AJEAN_DL_CONNS, AJEAN_LINK_URL, AJEAN_ACME_EMAIL, AJEAN_SERVICE), avec l'ancien nom JEAN_* toujours lu en second via envAny(). Ces variables sont posees a la main dans des shells, des units, des docker-compose et des scripts de deploiement qu'on ne peut pas retrouver : cesser de les lire ferait retomber silencieusement un reglage sur sa valeur par defaut. Migration one-shot du dossier de donnees retentee dans la fenetre ou l'installateur Windows a arrete toutes les instances, seule fenetre ou le rename peut aboutir. Le chemin du binaire est recalcule juste apres, sinon on reinstallerait dans le dossier qu'on vient de deplacer. Deux choses volontairement NON renommees, parce que les renommer casserait des utilisateurs sans rien apporter : - "model": "jean", le nom expose sur l'endpoint compatible OpenAI. Des integrations externes le configurent en dur. - les cles localStorage (jean-theme, jean.sys...). Les renommer reinitialiserait theme, preferences d'affichage et prompt systeme dans le navigateur de chaque utilisateur. Le plist launchd recevait /usr/local/bin/jean en dur : le chemin est desormais injecte, sinon le daemon macOS aurait refuse de demarrer apres le renommage. index.html regenere depuis ui/src via go generate.
This commit is contained in:
1 parent
3720b90173
commit
993be881d5
44 files changed
+319
-236
No files matched your search
@@ -7,7 +7,7 @@
|
|||||||
AJEAN fournit tout ce qui entoure le modèle : l'interface de chat, les outils de l'assistant, la gestion du service et celle du matériel. Le moteur d'inférence est [llama.cpp](https://github.com/ggml-org/llama.cpp), qu'AJEAN compile lui-même pour la machine sur laquelle il tourne.
|
AJEAN fournit tout ce qui entoure le modèle : l'interface de chat, les outils de l'assistant, la gestion du service et celle du matériel. Le moteur d'inférence est [llama.cpp](https://github.com/ggml-org/llama.cpp), qu'AJEAN compile lui-même pour la machine sur laquelle il tourne.
|
||||||
|
|
||||||
```
|
```
|
||||||
télécharger le binaire → jean llamacpp install → jean edit → jean start → c'est parti
|
télécharger le binaire → ajean llamacpp install → ajean edit → ajean start → c'est parti
|
||||||
```
|
```
|
||||||
|
|
||||||
Aucune dépendance à l'exécution, aucun flag CMake à retenir, aucun conteneur. Vous obtenez une interface de chat complète et un endpoint compatible OpenAI pour vos outils tiers.
|
Aucune dépendance à l'exécution, aucun flag CMake à retenir, aucun conteneur. Vous obtenez une interface de chat complète et un endpoint compatible OpenAI pour vos outils tiers.
|
||||||
@@ -18,7 +18,7 @@ Aucune dépendance à l'exécution, aucun flag CMake à retenir, aucun conteneur
|
|||||||
|
|
||||||
**Un assistant, pas seulement un modèle.** L'interface web offre un chat avec raisonnement affiché, mémoire persistante, compactage automatique du contexte quand la conversation s'allonge, prompt système éditable, et des réglages d'apparence synchronisés entre appareils.
|
**Un assistant, pas seulement un modèle.** L'interface web offre un chat avec raisonnement affiché, mémoire persistante, compactage automatique du contexte quand la conversation s'allonge, prompt système éditable, et des réglages d'apparence synchronisés entre appareils.
|
||||||
|
|
||||||
**Des outils réels.** `jean agent on` active d'un seul coup toutes les capacités du modèle sur la machine :
|
**Des outils réels.** `ajean agent on` active d'un seul coup toutes les capacités du modèle sur la machine :
|
||||||
|
|
||||||
| Outil | Rôle |
|
| Outil | Rôle |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -28,13 +28,13 @@ Aucune dépendance à l'exécution, aucun flag CMake à retenir, aucun conteneur
|
|||||||
| web_* | recherche et lecture de pages, via Crawl4AI |
|
| web_* | recherche et lecture de pages, via Crawl4AI |
|
||||||
| mcp__* | les outils des serveurs MCP configurés |
|
| mcp__* | les outils des serveurs MCP configurés |
|
||||||
|
|
||||||
**Le matériel et le moteur, gérés pour vous.** `jean llamacpp install` clone et compile llama.cpp avec les bons flags pour *cette* machine : CUDA (capacité de calcul détectée par GPU, donc le multi-GPU fonctionne), ROCm, Metal, Vulkan, ou repli CPU. `jean llamacpp update` récupère le dernier commit, arrête le service le temps de recompiler, puis le redémarre.
|
**Le matériel et le moteur, gérés pour vous.** `ajean llamacpp install` clone et compile llama.cpp avec les bons flags pour *cette* machine : CUDA (capacité de calcul détectée par GPU, donc le multi-GPU fonctionne), ROCm, Metal, Vulkan, ou repli CPU. `ajean llamacpp update` récupère le dernier commit, arrête le service le temps de recompiler, puis le redémarre.
|
||||||
|
|
||||||
**Un service, pas un script.** `jean install` écrit l'unité systemd, la règle sudoers et les dossiers. Ensuite `start` / `stop` / `status` / `logs`. Windows et macOS ont leurs équivalents natifs (voir plus bas).
|
**Un service, pas un script.** `ajean install` écrit l'unité systemd, la règle sudoers et les dossiers. Ensuite `start` / `stop` / `status` / `logs`. Windows et macOS ont leurs équivalents natifs (voir plus bas).
|
||||||
|
|
||||||
**Plusieurs modèles, un clic.** Les presets gardent chacun leur configuration complète ; basculer de l'un à l'autre recharge le modèle sans toucher à un fichier. Les `.gguf` peuvent vivre sur n'importe quel disque.
|
**Plusieurs modèles, un clic.** Les presets gardent chacun leur configuration complète ; basculer de l'un à l'autre recharge le modèle sans toucher à un fichier. Les `.gguf` peuvent vivre sur n'importe quel disque.
|
||||||
|
|
||||||
**Accessible de partout.** `jean link` ouvre une connexion sortante vers [ajean.link](https://ajean.link), donc aucun port à ouvrir, et cela fonctionne même en CGNAT. Le chat est chiffré de bout en bout : le relais ne voit jamais les conversations.
|
**Accessible de partout.** `ajean link` ouvre une connexion sortante vers [ajean.link](https://ajean.link), donc aucun port à ouvrir, et cela fonctionne même en CGNAT. Le chat est chiffré de bout en bout : le relais ne voit jamais les conversations.
|
||||||
|
|
||||||
## Démarrage
|
## Démarrage
|
||||||
|
|
||||||
@@ -42,8 +42,8 @@ Aucune dépendance à l'exécution, aucun flag CMake à retenir, aucun conteneur
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -L -o jean https://github.com/nathaninline/ajean/releases/latest/download/jean-linux-amd64
|
curl -L -o jean https://github.com/nathaninline/ajean/releases/latest/download/jean-linux-amd64
|
||||||
chmod +x jean
|
chmod +x ajean
|
||||||
sudo mv jean /usr/local/bin/jean
|
sudo mv ajean /usr/local/bin/ajean
|
||||||
```
|
```
|
||||||
|
|
||||||
(Autres cibles sur la page [Releases](../../releases) : Linux, macOS et Windows, en amd64 et arm64.)
|
(Autres cibles sur la page [Releases](../../releases) : Linux, macOS et Windows, en amd64 et arm64.)
|
||||||
@@ -51,8 +51,8 @@ sudo mv jean /usr/local/bin/jean
|
|||||||
### 2. Installation et compilation du moteur
|
### 2. Installation et compilation du moteur
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo jean install # unité systemd, sudoers, dossiers
|
sudo ajean install # unité systemd, sudoers, dossiers
|
||||||
jean llamacpp install # compile llama.cpp pour le GPU présent
|
ajean llamacpp install # compile llama.cpp pour le GPU présent
|
||||||
```
|
```
|
||||||
|
|
||||||
Nécessite `git` et `cmake`, plus le toolkit de l'accélérateur (CUDA, ROCm…) pour l'accélération GPU.
|
Nécessite `git` et `cmake`, plus le toolkit de l'accélérateur (CUDA, ROCm…) pour l'accélération GPU.
|
||||||
@@ -60,10 +60,10 @@ Nécessite `git` et `cmake`, plus le toolkit de l'accélérateur (CUDA, ROCm…)
|
|||||||
### 3. Démarrage
|
### 3. Démarrage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
jean edit # régler MODEL=/chemin/vers/le-modele.gguf
|
ajean edit # régler MODEL=/chemin/vers/le-modele.gguf
|
||||||
jean start
|
ajean start
|
||||||
jean test # vérifier que le modèle répond
|
ajean test # vérifier que le modèle répond
|
||||||
jean web # http://<hôte>:8090
|
ajean web # http://<hôte>:8090
|
||||||
```
|
```
|
||||||
|
|
||||||
## Commandes
|
## Commandes
|
||||||
@@ -78,7 +78,7 @@ Service :
|
|||||||
start | stop | restart gérer le service
|
start | stop | restart gérer le service
|
||||||
status | logs état / logs en direct
|
status | logs état / logs en direct
|
||||||
enable | disable démarrage au boot
|
enable | disable démarrage au boot
|
||||||
edit éditer $JEAN_HOME/config.env
|
edit éditer $AJEAN_HOME/config.env
|
||||||
test | bench [N] vérifier que le modèle répond / mesurer les tok/s
|
test | bench [N] vérifier que le modèle répond / mesurer les tok/s
|
||||||
vram | gpu [index…] VRAM / choix des GPU (gpu all = tous)
|
vram | gpu [index…] VRAM / choix des GPU (gpu all = tous)
|
||||||
set-api-key [clé] protéger le moteur d'inférence (Bearer)
|
set-api-key [clé] protéger le moteur d'inférence (Bearer)
|
||||||
@@ -107,7 +107,7 @@ Installation :
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Tout vit sous **`$JEAN_HOME`** (`/etc/jean` sous Linux/macOS, `%ProgramData%\jean` sous Windows). Le service lit `config.env` :
|
Tout vit sous **`$AJEAN_HOME`** (`/etc/ajean` sous Linux/macOS, `%ProgramData%\ajean` sous Windows). Le service lit `config.env` :
|
||||||
|
|
||||||
| Clé | Signification | Défaut |
|
| Clé | Signification | Défaut |
|
||||||
|-----|---------------|--------|
|
|-----|---------------|--------|
|
||||||
@@ -119,7 +119,7 @@ Tout vit sous **`$JEAN_HOME`** (`/etc/jean` sous Linux/macOS, `%ProgramData%\jea
|
|||||||
| `BATCH` / `UBATCH` | batch / micro-batch | `2048` / `512` |
|
| `BATCH` / `UBATCH` | batch / micro-batch | `2048` / `512` |
|
||||||
| `THREADS` / `THREADS_BATCH` | threads CPU | `0` (auto) |
|
| `THREADS` / `THREADS_BATCH` | threads CPU | `0` (auto) |
|
||||||
| `KV_TYPE` (`_K` / `_V`) | quantization du cache KV | — |
|
| `KV_TYPE` (`_K` / `_V`) | quantization du cache KV | — |
|
||||||
| `CUDA_VISIBLE_DEVICES` | GPU utilisés (réglé par `jean gpu`) | tous |
|
| `CUDA_VISIBLE_DEVICES` | GPU utilisés (réglé par `ajean gpu`) | tous |
|
||||||
| `REASONING` | passthrough du mode raisonnement | — |
|
| `REASONING` | passthrough du mode raisonnement | — |
|
||||||
| `REASONING_BUDGET` | plafond de tokens de réflexion ; `-1` = illimité | `-1` |
|
| `REASONING_BUDGET` | plafond de tokens de réflexion ; `-1` = illimité | `-1` |
|
||||||
| `COMPACT` | compactage automatique du contexte (`off` pour couper) | activé |
|
| `COMPACT` | compactage automatique du contexte (`off` pour couper) | activé |
|
||||||
@@ -127,31 +127,31 @@ Tout vit sous **`$JEAN_HOME`** (`/etc/jean` sous Linux/macOS, `%ProgramData%\jea
|
|||||||
| `CRAWL4AI_URL` / `CRAWL4AI_KEY` | serveur d'accès internet | — |
|
| `CRAWL4AI_URL` / `CRAWL4AI_KEY` | serveur d'accès internet | — |
|
||||||
| `EXTRA_ARGS` | ajouté tel quel à la ligne de commande du moteur | — |
|
| `EXTRA_ARGS` | ajouté tel quel à la ligne de commande du moteur | — |
|
||||||
|
|
||||||
La clé API (`jean set-api-key`) vit dans `$JEAN_HOME/.api_key`, à part, afin de survivre aux changements de preset.
|
La clé API (`ajean set-api-key`) vit dans `$AJEAN_HOME/.api_key`, à part, afin de survivre aux changements de preset.
|
||||||
|
|
||||||
**Modèles sur un autre disque.** Les `.gguf` n'ont pas à résider dans `$JEAN_HOME` : dans l'éditeur de preset de l'interface, section *Modèle → Dossiers de modèles*, ajoutez le dossier voulu. Ses modèles apparaissent dans la liste, groupés par dossier. La liste est enregistrée dans `model_dirs.json`, donc conservée d'un preset à l'autre.
|
**Modèles sur un autre disque.** Les `.gguf` n'ont pas à résider dans `$AJEAN_HOME` : dans l'éditeur de preset de l'interface, section *Modèle → Dossiers de modèles*, ajoutez le dossier voulu. Ses modèles apparaissent dans la liste, groupés par dossier. La liste est enregistrée dans `model_dirs.json`, donc conservée d'un preset à l'autre.
|
||||||
|
|
||||||
### Variables d'environnement
|
### Variables d'environnement
|
||||||
|
|
||||||
| Variable | Rôle | Défaut |
|
| Variable | Rôle | Défaut |
|
||||||
|----------|------|--------|
|
|----------|------|--------|
|
||||||
| `JEAN_HOME` | racine des données | `/etc/jean`, `%ProgramData%\jean` |
|
| `AJEAN_HOME` | racine des données | `/etc/ajean`, `%ProgramData%\ajean` |
|
||||||
| `JEAN_MODEL_DIRS` | dossiers de modèles (séparés par `:`, `;` sous Windows) | — |
|
| `AJEAN_MODEL_DIRS` | dossiers de modèles (séparés par `:`, `;` sous Windows) | — |
|
||||||
| `JEAN_SERVICE` | nom de l'unité systemd | `jean` |
|
| `JEAN_SERVICE` | nom de l'unité systemd | `ajean` |
|
||||||
| `HF_TOKEN` | token Hugging Face pour les modèles privés | — |
|
| `HF_TOKEN` | token Hugging Face pour les modèles privés | — |
|
||||||
| `JEAN_DL_CONNS` | connexions parallèles au téléchargement | — |
|
| `AJEAN_DL_CONNS` | connexions parallèles au téléchargement | — |
|
||||||
| `EDITOR` | éditeur pour `jean edit` | `nano` / `notepad` |
|
| `EDITOR` | éditeur pour `ajean edit` | `nano` / `notepad` |
|
||||||
|
|
||||||
## Les capacités de l'IA
|
## Les capacités de l'IA
|
||||||
|
|
||||||
### Mémoire
|
### Mémoire
|
||||||
|
|
||||||
L'IA tient des pages Markdown sous `$JEAN_HOME/MEMORY/`, relues et mises à jour entre les sessions. Trois modes, indépendants du mode agent :
|
L'IA tient des pages Markdown sous `$AJEAN_HOME/MEMORY/`, relues et mises à jour entre les sessions. Trois modes, indépendants du mode agent :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
jean memory always # (défaut) elle cherche avant de répondre et enregistre d'elle-même
|
ajean memory always # (défaut) elle cherche avant de répondre et enregistre d'elle-même
|
||||||
jean memory ondemand # outils disponibles, mais utilisés seulement sur demande
|
ajean memory ondemand # outils disponibles, mais utilisés seulement sur demande
|
||||||
jean memory off # mémoire coupée
|
ajean memory off # mémoire coupée
|
||||||
```
|
```
|
||||||
|
|
||||||
### Accès internet
|
### Accès internet
|
||||||
@@ -160,9 +160,9 @@ Par défaut, l'IA n'a pas accès au web. En branchant un serveur [Crawl4AI](http
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d -p 11235:11235 --shm-size=1g unclecode/crawl4ai:latest
|
docker run -d -p 11235:11235 --shm-size=1g unclecode/crawl4ai:latest
|
||||||
jean internet url http://localhost:11235
|
ajean internet url http://localhost:11235
|
||||||
jean internet on
|
ajean internet on
|
||||||
jean internet status
|
ajean internet status
|
||||||
```
|
```
|
||||||
|
|
||||||
Les outils web ne sont proposés au modèle que si le mode agent est actif, l'accès internet activé **et** le serveur joignable. Sinon ils n'existent pas, et le modèle ne peut donc pas les inventer.
|
Les outils web ne sont proposés au modèle que si le mode agent est actif, l'accès internet activé **et** le serveur joignable. Sinon ils n'existent pas, et le modèle ne peut donc pas les inventer.
|
||||||
@@ -171,7 +171,7 @@ Les outils web ne sont proposés au modèle que si le mode agent est actif, l'ac
|
|||||||
|
|
||||||
AJEAN parle le [Model Context Protocol](https://modelcontextprotocol.io) : on y branche des serveurs tiers (fichiers, bases de données, API…) et leurs outils s'ajoutent à ceux de l'IA, nommés `mcp__<serveur>__<outil>`.
|
AJEAN parle le [Model Context Protocol](https://modelcontextprotocol.io) : on y branche des serveurs tiers (fichiers, bases de données, API…) et leurs outils s'ajoutent à ceux de l'IA, nommés `mcp__<serveur>__<outil>`.
|
||||||
|
|
||||||
La configuration se fait depuis l'interface web (section *Serveurs MCP*) ou en éditant `$JEAN_HOME/mcp.json`, au **même format que Claude Desktop**, si bien qu'un fichier existant se réutilise tel quel :
|
La configuration se fait depuis l'interface web (section *Serveurs MCP*) ou en éditant `$AJEAN_HOME/mcp.json`, au **même format que Claude Desktop**, si bien qu'un fichier existant se réutilise tel quel :
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -186,38 +186,38 @@ Les transports **stdio** et **HTTP** sont pris en charge. Comme le terminal, un
|
|||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
|
|
||||||
- **Pas de systemd** : `jean start` lance le service en arrière-plan (suivi par fichier PID) ; `stop`, `restart`, `status` et `logs` agissent dessus, sans droits administrateur. `enable` / `disable` ne sont pas gérés, il faut passer par une tâche planifiée.
|
- **Pas de systemd** : `ajean start` lance le service en arrière-plan (suivi par fichier PID) ; `stop`, `restart`, `status` et `logs` agissent dessus, sans droits administrateur. `enable` / `disable` ne sont pas gérés, il faut passer par une tâche planifiée.
|
||||||
- `JEAN_HOME` vaut `%ProgramData%\jean` (repli `%LOCALAPPDATA%\jean`).
|
- `AJEAN_HOME` vaut `%ProgramData%\ajean` (repli `%LOCALAPPDATA%\ajean`).
|
||||||
- `jean install` crée seulement le dossier de données et un `config.env` de départ.
|
- `ajean install` crée seulement le dossier de données et un `config.env` de départ.
|
||||||
- Le terminal de l'IA passe par `cmd.exe`. Elle le sait, et écrit ses fichiers par l'outil dédié plutôt que par le shell, ce qui lui permet de produire des scripts contenant des guillemets.
|
- Le terminal de l'IA passe par `cmd.exe`. Elle le sait, et écrit ses fichiers par l'outil dédié plutôt que par le shell, ce qui lui permet de produire des scripts contenant des guillemets.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
jean install
|
ajean install
|
||||||
jean edit # BIN=...\llama-server.exe et MODEL=...\modele.gguf
|
ajean edit # BIN=...\llama-server.exe et MODEL=...\modele.gguf
|
||||||
jean start
|
ajean start
|
||||||
jean status
|
ajean status
|
||||||
```
|
```
|
||||||
|
|
||||||
`jean llamacpp install` compile également sous Windows si `git` et `cmake` sont présents ; sinon, récupérer un `llama-server.exe` pré-compilé et pointer `BIN` dessus.
|
`ajean llamacpp install` compile également sous Windows si `git` et `cmake` sont présents ; sinon, récupérer un `llama-server.exe` pré-compilé et pointer `BIN` dessus.
|
||||||
|
|
||||||
## macOS
|
## macOS
|
||||||
|
|
||||||
La page [Releases](../../releases) publie `Jean-macos-arm64.zip` / `Jean-macos-amd64.zip`, un bundle **`Jean.app`**. Dézipper, glisser dans *Applications*, ouvrir : l'interface démarre sur `http://localhost:8090`, s'ouvre dans le navigateur, et l'icône se pose dans la **barre de menus**. Pas de fenêtre de Terminal, pas d'icône dans le Dock.
|
La page [Releases](../../releases) publie `AJEAN-macos-arm64.zip` / `AJEAN-macos-amd64.zip`, un bundle **`AJEAN.app`**. Dézipper, glisser dans *Applications*, ouvrir : l'interface démarre sur `http://localhost:8090`, s'ouvre dans le navigateur, et l'icône se pose dans la **barre de menus**. Pas de fenêtre de Terminal, pas d'icône dans le Dock.
|
||||||
|
|
||||||
L'application n'est signée qu'en ad-hoc : au premier lancement, faire **clic droit → Ouvrir**.
|
L'application n'est signée qu'en ad-hoc : au premier lancement, faire **clic droit → Ouvrir**.
|
||||||
|
|
||||||
Pour un usage en ligne de commande, prendre le binaire nu `jean-darwin-arm64` : hors bundle, il conserve son comportement CLI. Les services passent par **launchd**.
|
Pour un usage en ligne de commande, prendre le binaire nu `ajean-darwin-arm64` : hors bundle, il conserve son comportement CLI. Les services passent par **launchd**.
|
||||||
|
|
||||||
## Accès distant via ajean.link
|
## Accès distant via ajean.link
|
||||||
|
|
||||||
`jean link` ouvre une connexion **sortante** vers le relais : le serveur reste injoignable depuis l'extérieur, mais reste accessible de partout.
|
`ajean link` ouvre une connexion **sortante** vers le relais : le serveur reste injoignable depuis l'extérieur, mais reste accessible de partout.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
jean link <token> # token fourni sur ajean.link
|
ajean link <token> # token fourni sur ajean.link
|
||||||
jean link code # code d'appairage à saisir dans le portail
|
ajean link code # code d'appairage à saisir dans le portail
|
||||||
```
|
```
|
||||||
|
|
||||||
Le lien tourne comme un service : la commande rend la main, la connexion continue en tâche de fond. Inutile de lancer `jean web`, l'interface est servie dans le tunnel. Le portail donne accès à l'interface du serveur avec un chat chiffré, à la gestion de plusieurs machines, et en option à un endpoint compatible OpenAI.
|
Le lien tourne comme un service : la commande rend la main, la connexion continue en tâche de fond. Inutile de lancer `ajean web`, l'interface est servie dans le tunnel. Le portail donne accès à l'interface du serveur avec un chat chiffré, à la gestion de plusieurs machines, et en option à un endpoint compatible OpenAI.
|
||||||
|
|
||||||
Il s'agit d'un service optionnel et payant ; tout le reste d'AJEAN est et restera open source et gratuit.
|
Il s'agit d'un service optionnel et payant ; tout le reste d'AJEAN est et restera open source et gratuit.
|
||||||
|
|
||||||
@@ -226,8 +226,8 @@ Il s'agit d'un service optionnel et payant ; tout le reste d'AJEAN est et rester
|
|||||||
Le relais est conçu comme un **tube aveugle** : il transporte les données sans pouvoir les lire.
|
Le relais est conçu comme un **tube aveugle** : il transporte les données sans pouvoir les lire.
|
||||||
|
|
||||||
- **Chat chiffré de bout en bout** (X25519 + AES-GCM). La clé est dérivée du mot de passe via **OPAQUE** et ne quitte jamais le navigateur.
|
- **Chat chiffré de bout en bout** (X25519 + AES-GCM). La clé est dérivée du mot de passe via **OPAQUE** et ne quitte jamais le navigateur.
|
||||||
- **Empreinte vérifiée.** `jean link` affiche l'empreinte de la clé de la machine, à confirmer une fois dans le portail, ce qui défait toute tentative d'interception par le relais.
|
- **Empreinte vérifiée.** `ajean link` affiche l'empreinte de la clé de la machine, à confirmer une fois dans le portail, ce qui défait toute tentative d'interception par le relais.
|
||||||
- **Appairage authentifié.** Un code à usage unique (`jean link code`) garantit qu'un seul navigateur autorisé pilote le serveur ; même compromis, le relais ne peut pas forger de commande.
|
- **Appairage authentifié.** Un code à usage unique (`ajean link code`) garantit qu'un seul navigateur autorisé pilote le serveur ; même compromis, le relais ne peut pas forger de commande.
|
||||||
- **Code servi hors du relais.** Le portail provient d'une origine indépendante (GitHub Pages) : le relais ne peut pas injecter de code pour dérober la clé.
|
- **Code servi hors du relais.** Le portail provient d'une origine indépendante (GitHub Pages) : le relais ne peut pas injecter de code pour dérober la clé.
|
||||||
|
|
||||||
Reste visible du relais : des métadonnées techniques (machine en ligne, modèle chargé, VRAM), jamais le contenu des conversations.
|
Reste visible du relais : des métadonnées techniques (machine en ligne, modèle chargé, VRAM), jamais le contenu des conversations.
|
||||||
@@ -240,10 +240,10 @@ Le VPS effectue un simple **passthrough SNI** : le TLS est terminé sur la machi
|
|||||||
|
|
||||||
## API de pilotage
|
## API de pilotage
|
||||||
|
|
||||||
`jean web` expose une API HTTP pour piloter AJEAN à distance. À protéger avant toute exposition :
|
`ajean web` expose une API HTTP pour piloter AJEAN à distance. À protéger avant toute exposition :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
jean set-web-key # génère une clé
|
ajean set-web-key # génère une clé
|
||||||
```
|
```
|
||||||
|
|
||||||
Chaque appel `/api/*` présente alors `Authorization: Bearer <clé>` :
|
Chaque appel `/api/*` présente alors `Authorization: Bearer <clé>` :
|
||||||
@@ -257,7 +257,7 @@ Chaque appel `/api/*` présente alors `Authorization: Bearer <clé>` :
|
|||||||
| POST | `/api/start` · `/api/stop` · `/api/restart` | piloter le service |
|
| POST | `/api/start` · `/api/stop` · `/api/restart` | piloter le service |
|
||||||
| POST | `/api/chat` `{"messages":[…]}` | chat (flux SSE) |
|
| POST | `/api/chat` `{"messages":[…]}` | chat (flux SSE) |
|
||||||
|
|
||||||
> ⚠️ La clé voyage en clair en HTTP. Pour une exposition publique, placer un reverse-proxy HTTPS devant, ou utiliser `jean link`.
|
> ⚠️ La clé voyage en clair en HTTP. Pour une exposition publique, placer un reverse-proxy HTTPS devant, ou utiliser `ajean link`.
|
||||||
|
|
||||||
## Compiler depuis les sources
|
## Compiler depuis les sources
|
||||||
|
|
||||||
@@ -265,17 +265,17 @@ Go 1.25+. AJEAN est écrit à 100 % en Go, l'interface est embarquée via `go:em
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/nathaninline/ajean.git
|
git clone https://github.com/nathaninline/ajean.git
|
||||||
cd jean
|
cd ajean
|
||||||
CGO_ENABLED=0 go build -o jean ./cmd/jean
|
CGO_ENABLED=0 go build -o ajean ./cmd/ajean
|
||||||
```
|
```
|
||||||
|
|
||||||
> Compiler **AJEAN** ne demande que Go. Compiler le **moteur llama.cpp** demande `git`, `cmake` et le toolkit de l'accélérateur.
|
> Compiler **AJEAN** ne demande que Go. Compiler le **moteur llama.cpp** demande `git`, `cmake` et le toolkit de l'accélérateur.
|
||||||
|
|
||||||
## Arborescence
|
## Arborescence
|
||||||
|
|
||||||
- `cmd/jean/` : point d'entrée + ressources Windows (icône, versioninfo).
|
- `cmd/ajean/` : point d'entrée + ressources Windows (icône, versioninfo).
|
||||||
- `internal/jean/` : tout le code, fichiers préfixés par domaine (`web_*`, `chat_*`, `llm_*`, `backend_*`, `relay_*`, `sys_*`, `mcp_*`) ; carte dans `doc.go`.
|
- `internal/ajean/` : tout le code, fichiers préfixés par domaine (`web_*`, `chat_*`, `llm_*`, `backend_*`, `relay_*`, `sys_*`, `mcp_*`) ; carte dans `doc.go`.
|
||||||
- `internal/jean/ui/` : interface web embarquée. **`index.html` est généré** : les sources vivent dans `ui/src/`. Pour modifier l'interface, éditer `ui/src/` puis lancer `go generate ./internal/jean`.
|
- `internal/ajean/ui/` : interface web embarquée. **`index.html` est généré** : les sources vivent dans `ui/src/`. Pour modifier l'interface, éditer `ui/src/` puis lancer `go generate ./internal/ajean`.
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ Si vous étiez bloqué dans cet état, il n'y a rien à réparer : téléchargez
|
|||||||
## Mise à jour
|
## Mise à jour
|
||||||
|
|
||||||
```
|
```
|
||||||
jean update
|
ajean update
|
||||||
```
|
```
|
||||||
|
|
||||||
Sous Windows, télécharger le fichier depuis la page des releases et le lancer fait le même travail.
|
Sous Windows, télécharger le fichier depuis la page des releases et le lancer fait le même travail.
|
||||||
|
|||||||
@@ -21,9 +21,9 @@
|
|||||||
"StringFileInfo": {
|
"StringFileInfo": {
|
||||||
"CompanyName": "AJEAN contributors",
|
"CompanyName": "AJEAN contributors",
|
||||||
"FileDescription": "AJEAN — gestionnaire mono-binaire de serveurs llama.cpp auto-hébergés",
|
"FileDescription": "AJEAN — gestionnaire mono-binaire de serveurs llama.cpp auto-hébergés",
|
||||||
"InternalName": "jean",
|
"InternalName": "ajean",
|
||||||
"LegalCopyright": "Copyright (c) 2026 AJEAN contributors. MIT License.",
|
"LegalCopyright": "Copyright (c) 2026 AJEAN contributors. MIT License.",
|
||||||
"OriginalFilename": "jean.exe",
|
"OriginalFilename": "ajean.exe",
|
||||||
"ProductName": "AJEAN",
|
"ProductName": "AJEAN",
|
||||||
"ProductVersion": "0.6.15",
|
"ProductVersion": "0.6.15",
|
||||||
"Comments": "https://github.com/nathaninline/ajean — projet open source (MIT)"
|
"Comments": "https://github.com/nathaninline/ajean — projet open source (MIT)"
|
||||||
|
|||||||
@@ -228,15 +228,15 @@ func hintMissingBuildDep(p buildPlan, cfgLog string) {
|
|||||||
fmt.Printf(" Relance l'installeur du CUDA Toolkit (installation personnalisée) en cochant « CUDA → Visual Studio Integration »\n")
|
fmt.Printf(" Relance l'installeur du CUDA Toolkit (installation personnalisée) en cochant « CUDA → Visual Studio Integration »\n")
|
||||||
fmt.Printf(" (Visual Studio avec le workload C++ doit déjà être installé), ou copie les fichiers de\n")
|
fmt.Printf(" (Visual Studio avec le workload C++ doit déjà être installé), ou copie les fichiers de\n")
|
||||||
fmt.Printf(" <toolkit>\\extras\\visual_studio_integration\\MSBuildExtensions vers\n")
|
fmt.Printf(" <toolkit>\\extras\\visual_studio_integration\\MSBuildExtensions vers\n")
|
||||||
fmt.Printf(" <VS>\\MSBuild\\Microsoft\\VC\\<version>\\BuildCustomizations, puis relance %s.\n", bold("jean llamacpp install"))
|
fmt.Printf(" <VS>\\MSBuild\\Microsoft\\VC\\<version>\\BuildCustomizations, puis relance %s.\n", bold("ajean llamacpp install"))
|
||||||
}
|
}
|
||||||
if p.backend == "vulkan" && strings.Contains(log, "SPIRV-Headers") {
|
if p.backend == "vulkan" && strings.Contains(log, "SPIRV-Headers") {
|
||||||
fmt.Printf("\n%s dépendance manquante pour le backend %s : les en-têtes SPIR-V (paquet « SPIRV-Headers ») sont introuvables.\n",
|
fmt.Printf("\n%s dépendance manquante pour le backend %s : les en-têtes SPIR-V (paquet « SPIRV-Headers ») sont introuvables.\n",
|
||||||
yellow("[dépendance]"), green("Vulkan"))
|
yellow("[dépendance]"), green("Vulkan"))
|
||||||
if cmd := pkgInstallHint("spirv-headers"); cmd != "" {
|
if cmd := pkgInstallHint("spirv-headers"); cmd != "" {
|
||||||
fmt.Printf(" installe-les puis relance %s : %s\n", bold("jean llamacpp install"), bold(cmd))
|
fmt.Printf(" installe-les puis relance %s : %s\n", bold("ajean llamacpp install"), bold(cmd))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" installe le paquet de développement « SPIRV-Headers » de ta distribution, puis relance %s.\n", bold("jean llamacpp install"))
|
fmt.Printf(" installe le paquet de développement « SPIRV-Headers » de ta distribution, puis relance %s.\n", bold("ajean llamacpp install"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ func maskAPIKey(k string) string {
|
|||||||
// exposed on the internet, llama-server requires "Authorization: Bearer <clé>"
|
// exposed on the internet, llama-server requires "Authorization: Bearer <clé>"
|
||||||
// for every call.
|
// for every call.
|
||||||
//
|
//
|
||||||
// jean set-api-key <clé> définit la clé
|
// ajean set-api-key <clé> définit la clé
|
||||||
// jean set-api-key génère une clé aléatoire
|
// ajean set-api-key génère une clé aléatoire
|
||||||
// jean set-api-key "" supprime la protection
|
// ajean set-api-key "" supprime la protection
|
||||||
func cmdSetAPIKey(args []string) error {
|
func cmdSetAPIKey(args []string) error {
|
||||||
var key string
|
var key string
|
||||||
switch {
|
switch {
|
||||||
@@ -97,7 +97,7 @@ func cmdSetAPIKey(args []string) error {
|
|||||||
fmt.Print(dim("[info] redémarrer le service pour appliquer ? [Y/n] "))
|
fmt.Print(dim("[info] redémarrer le service pour appliquer ? [Y/n] "))
|
||||||
sc := bufio.NewScanner(os.Stdin)
|
sc := bufio.NewScanner(os.Stdin)
|
||||||
if sc.Scan() && strings.HasPrefix(strings.ToLower(strings.TrimSpace(sc.Text())), "n") {
|
if sc.Scan() && strings.HasPrefix(strings.ToLower(strings.TrimSpace(sc.Text())), "n") {
|
||||||
fmt.Println(dim("[info] pense à lancer 'jean restart'"))
|
fmt.Println(dim("[info] pense à lancer 'ajean restart'"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return serviceAction("restart")
|
return serviceAction("restart")
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import (
|
|||||||
|
|
||||||
// backend_gpu.go — sélection du/des GPU utilisés par llama-server.
|
// backend_gpu.go — sélection du/des GPU utilisés par llama-server.
|
||||||
//
|
//
|
||||||
// jean gpu liste les GPU et montre la sélection courante
|
// ajean gpu liste les GPU et montre la sélection courante
|
||||||
// jean gpu 1 n'utilise que le GPU d'index 1
|
// ajean gpu 1 n'utilise que le GPU d'index 1
|
||||||
// jean gpu 0 1 utilise les GPU 0 et 1
|
// ajean gpu 0 1 utilise les GPU 0 et 1
|
||||||
// jean gpu all réinitialise (tous les GPU visibles)
|
// ajean gpu all réinitialise (tous les GPU visibles)
|
||||||
//
|
//
|
||||||
// La sélection est stockée dans config.env sous CUDA_VISIBLE_DEVICES ; backend_serve.go
|
// La sélection est stockée dans config.env sous CUDA_VISIBLE_DEVICES ; backend_serve.go
|
||||||
// l'exporte (avec CUDA_DEVICE_ORDER=PCI_BUS_ID pour que les index correspondent
|
// l'exporte (avec CUDA_DEVICE_ORDER=PCI_BUS_ID pour que les index correspondent
|
||||||
@@ -53,7 +53,7 @@ func cmdGPU(args []string) error {
|
|||||||
return fmt.Errorf("index GPU invalide: %q (attendu un nombre)", tok)
|
return fmt.Errorf("index GPU invalide: %q (attendu un nombre)", tok)
|
||||||
}
|
}
|
||||||
if n < 0 || n >= len(gpus) {
|
if n < 0 || n >= len(gpus) {
|
||||||
return fmt.Errorf("index GPU %d hors limites (0..%d) — voir « jean gpu »", n, len(gpus)-1)
|
return fmt.Errorf("index GPU %d hors limites (0..%d) — voir « ajean gpu »", n, len(gpus)-1)
|
||||||
}
|
}
|
||||||
idx = append(idx, strconv.Itoa(n))
|
idx = append(idx, strconv.Itoa(n))
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,7 @@ func gpuList() error {
|
|||||||
} else {
|
} else {
|
||||||
fmt.Printf(" Sélection : %s (CUDA_VISIBLE_DEVICES=%s)\n", bold(sel), sel)
|
fmt.Printf(" Sélection : %s (CUDA_VISIBLE_DEVICES=%s)\n", bold(sel), sel)
|
||||||
}
|
}
|
||||||
fmt.Printf(" %s jean gpu <index…> pour choisir, jean gpu all pour réinitialiser\n", dim("→"))
|
fmt.Printf(" %s ajean gpu <index…> pour choisir, ajean gpu all pour réinitialiser\n", dim("→"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ func gpuSet(value string) error {
|
|||||||
fmt.Print(dim("[info] redémarrer le service pour appliquer ? [Y/n] "))
|
fmt.Print(dim("[info] redémarrer le service pour appliquer ? [Y/n] "))
|
||||||
sc := bufio.NewScanner(os.Stdin)
|
sc := bufio.NewScanner(os.Stdin)
|
||||||
if sc.Scan() && strings.HasPrefix(strings.ToLower(strings.TrimSpace(sc.Text())), "n") {
|
if sc.Scan() && strings.HasPrefix(strings.ToLower(strings.TrimSpace(sc.Text())), "n") {
|
||||||
fmt.Println(dim("[info] pense à lancer 'jean restart'"))
|
fmt.Println(dim("[info] pense à lancer 'ajean restart'"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return serviceAction("restart")
|
return serviceAction("restart")
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import (
|
|||||||
|
|
||||||
// backend_llamacpp.go — gestion du backend llama.cpp (clone, build, mise à jour).
|
// backend_llamacpp.go — gestion du backend llama.cpp (clone, build, mise à jour).
|
||||||
//
|
//
|
||||||
// `jean llamacpp install` installe un build neuf, détecte automatiquement
|
// `ajean llamacpp install` installe un build neuf, détecte automatiquement
|
||||||
// l'accélérateur (CUDA / ROCm / Metal / CPU) et la
|
// l'accélérateur (CUDA / ROCm / Metal / CPU) et la
|
||||||
// compute capability du GPU, puis pointe BIN dessus.
|
// compute capability du GPU, puis pointe BIN dessus.
|
||||||
// `jean llamacpp update` met à jour le dépôt existant (git pull) et recompile
|
// `ajean llamacpp update` met à jour le dépôt existant (git pull) et recompile
|
||||||
// avec la bonne config, sans intervention.
|
// avec la bonne config, sans intervention.
|
||||||
// `jean llamacpp status` montre le commit courant, le backend détecté et le
|
// `ajean llamacpp status` montre le commit courant, le backend détecté et le
|
||||||
// retard éventuel sur origin.
|
// retard éventuel sur origin.
|
||||||
|
|
||||||
const llamacppRepoURL = "https://github.com/ggml-org/llama.cpp.git"
|
const llamacppRepoURL = "https://github.com/ggml-org/llama.cpp.git"
|
||||||
@@ -55,7 +55,7 @@ func cmdLlamacpp(args []string) error {
|
|||||||
if err := SetConfigKey("BIN", bin); err != nil {
|
if err := SetConfigKey("BIN", bin); err != nil {
|
||||||
return fmt.Errorf("binaires installés mais échec écriture BIN dans config.env: %w", err)
|
return fmt.Errorf("binaires installés mais échec écriture BIN dans config.env: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s BIN mis à jour dans %s — %s pour appliquer\n", green("✓"), confPath(), bold("jean restart"))
|
fmt.Printf("%s BIN mis à jour dans %s — %s pour appliquer\n", green("✓"), confPath(), bold("ajean restart"))
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("sous-commande inconnue: %s (install | update | prebuilt | status)", sub)
|
return fmt.Errorf("sous-commande inconnue: %s (install | update | prebuilt | status)", sub)
|
||||||
@@ -161,7 +161,7 @@ func llamacppInstall(args []string) error {
|
|||||||
if isDir(filepath.Join(repo, ".git")) {
|
if isDir(filepath.Join(repo, ".git")) {
|
||||||
if !force {
|
if !force {
|
||||||
fmt.Printf("%s dépôt déjà présent dans %s\n", yellow("[info]"), repo)
|
fmt.Printf("%s dépôt déjà présent dans %s\n", yellow("[info]"), repo)
|
||||||
fmt.Printf(" → %s pour le mettre à jour, ou --force pour repartir de zéro\n", bold("jean llamacpp update"))
|
fmt.Printf(" → %s pour le mettre à jour, ou --force pour repartir de zéro\n", bold("ajean llamacpp update"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fmt.Printf("%s --force : suppression de %s\n", yellow("[info]"), repo)
|
fmt.Printf("%s --force : suppression de %s\n", yellow("[info]"), repo)
|
||||||
@@ -208,7 +208,7 @@ func llamacppInstall(args []string) error {
|
|||||||
}
|
}
|
||||||
fmt.Printf("%s BIN mis à jour dans %s\n", green("✓"), confPath())
|
fmt.Printf("%s BIN mis à jour dans %s\n", green("✓"), confPath())
|
||||||
fmt.Printf("\nProchaines étapes :\n 1. renseigne MODEL : %s\n 2. démarre : %s\n",
|
fmt.Printf("\nProchaines étapes :\n 1. renseigne MODEL : %s\n 2. démarre : %s\n",
|
||||||
bold("jean edit"), bold("jean restart"))
|
bold("ajean edit"), bold("ajean restart"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +246,7 @@ func llamacppUpdate(args []string) error {
|
|||||||
|
|
||||||
repo := llamacppRepoDir()
|
repo := llamacppRepoDir()
|
||||||
if !isDir(filepath.Join(repo, ".git")) {
|
if !isDir(filepath.Join(repo, ".git")) {
|
||||||
return fmt.Errorf("aucun dépôt llama.cpp trouvé (%s).\n → lance d'abord %s", repo, bold("jean llamacpp install"))
|
return fmt.Errorf("aucun dépôt llama.cpp trouvé (%s).\n → lance d'abord %s", repo, bold("ajean llamacpp install"))
|
||||||
}
|
}
|
||||||
fmt.Printf("%s dépôt : %s\n", cyan("▶"), repo)
|
fmt.Printf("%s dépôt : %s\n", cyan("▶"), repo)
|
||||||
|
|
||||||
@@ -317,14 +317,14 @@ func llamacppUpdate(args []string) error {
|
|||||||
fmt.Printf("\n%s mis à jour : %s → %s\n", green("✓"), oldCommit, newCommit)
|
fmt.Printf("\n%s mis à jour : %s → %s\n", green("✓"), oldCommit, newCommit)
|
||||||
|
|
||||||
if noRestart {
|
if noRestart {
|
||||||
fmt.Printf("%s --no-restart : pense à lancer %s\n", dim("[info]"), bold("jean restart"))
|
fmt.Printf("%s --no-restart : pense à lancer %s\n", dim("[info]"), bold("ajean restart"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if svcWasUp {
|
if svcWasUp {
|
||||||
fmt.Printf("%s redémarrage du service…\n", cyan("▶"))
|
fmt.Printf("%s redémarrage du service…\n", cyan("▶"))
|
||||||
return serviceAction("start")
|
return serviceAction("start")
|
||||||
}
|
}
|
||||||
fmt.Printf("%s service non démarré auparavant — lance %s quand tu veux\n", dim("[info]"), bold("jean start"))
|
fmt.Printf("%s service non démarré auparavant — lance %s quand tu veux\n", dim("[info]"), bold("ajean start"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,7 +337,7 @@ func llamacppStatus(args []string) error {
|
|||||||
fmt.Printf("%s\n", bold("llama.cpp"))
|
fmt.Printf("%s\n", bold("llama.cpp"))
|
||||||
fmt.Printf(" dépôt : %s\n", repo)
|
fmt.Printf(" dépôt : %s\n", repo)
|
||||||
if !isDir(filepath.Join(repo, ".git")) {
|
if !isDir(filepath.Join(repo, ".git")) {
|
||||||
fmt.Printf(" %s pas encore installé — %s\n", yellow("état"), bold("jean llamacpp install"))
|
fmt.Printf(" %s pas encore installé — %s\n", yellow("état"), bold("ajean llamacpp install"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
commit := gitOutput(repo, "log", "-1", "--format=%h %ci %s")
|
commit := gitOutput(repo, "log", "-1", "--format=%h %ci %s")
|
||||||
@@ -354,7 +354,7 @@ func llamacppStatus(args []string) error {
|
|||||||
// Retard sur origin (best-effort, sans fetch réseau).
|
// Retard sur origin (best-effort, sans fetch réseau).
|
||||||
if branch != "" && branch != "HEAD" {
|
if branch != "" && branch != "HEAD" {
|
||||||
if behind := gitOutput(repo, "rev-list", "--count", "HEAD..origin/"+branch); behind != "" && behind != "0" {
|
if behind := gitOutput(repo, "rev-list", "--count", "HEAD..origin/"+branch); behind != "" && behind != "0" {
|
||||||
fmt.Printf(" maj : %s commit(s) de retard sur origin/%s — %s\n", yellow(behind), branch, bold("jean llamacpp update"))
|
fmt.Printf(" maj : %s commit(s) de retard sur origin/%s — %s\n", yellow(behind), branch, bold("ajean llamacpp update"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func extraModelDirs() []string {
|
|||||||
raw = append(raw, dirs...)
|
raw = append(raw, dirs...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if v := strings.TrimSpace(os.Getenv("JEAN_MODEL_DIRS")); v != "" {
|
if v := envAny("AJEAN_MODEL_DIRS", "JEAN_MODEL_DIRS"); v != "" {
|
||||||
raw = append(raw, strings.Split(v, modelDirsListSep())...)
|
raw = append(raw, strings.Split(v, modelDirsListSep())...)
|
||||||
}
|
}
|
||||||
return dedupDirs(raw, AjeanHome())
|
return dedupDirs(raw, AjeanHome())
|
||||||
@@ -216,5 +216,5 @@ func handleModelDirs(w http.ResponseWriter, r *http.Request) {
|
|||||||
n := modelDirCount(d)
|
n := modelDirCount(d)
|
||||||
out = append(out, map[string]any{"path": d, "home": false, "count": n, "exists": n >= 0})
|
out = append(out, map[string]any{"path": d, "home": false, "count": n, "exists": n >= 0})
|
||||||
}
|
}
|
||||||
sendJSON(w, 200, map[string]any{"ok": true, "dirs": out, "env": os.Getenv("JEAN_MODEL_DIRS") != ""})
|
sendJSON(w, 200, map[string]any{"ok": true, "dirs": out, "env": envAny("AJEAN_MODEL_DIRS", "JEAN_MODEL_DIRS") != ""})
|
||||||
}
|
}
|
||||||
@@ -197,7 +197,7 @@ var dlClient = &http.Client{
|
|||||||
// Overridable with JEAN_DL_CONNS (1 disables parallelism).
|
// Overridable with JEAN_DL_CONNS (1 disables parallelism).
|
||||||
func dlConns() int {
|
func dlConns() int {
|
||||||
n := 8
|
n := 8
|
||||||
if v := strings.TrimSpace(os.Getenv("JEAN_DL_CONNS")); v != "" {
|
if v := envAny("AJEAN_DL_CONNS", "JEAN_DL_CONNS"); v != "" {
|
||||||
if p, err := strconv.Atoi(v); err == nil && p > 0 {
|
if p, err := strconv.Atoi(v); err == nil && p > 0 {
|
||||||
n = p
|
n = p
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func cmdServe(args []string) error {
|
|||||||
// Linux, PATH on Windows — handled inside execServer.
|
// Linux, PATH on Windows — handled inside execServer.
|
||||||
setLibraryPath(filepath.Dir(bin))
|
setLibraryPath(filepath.Dir(bin))
|
||||||
|
|
||||||
// Sélection GPU (jean gpu) : on filtre les devices visibles par llama-server.
|
// Sélection GPU (ajean gpu) : on filtre les devices visibles par llama-server.
|
||||||
// CUDA_DEVICE_ORDER=PCI_BUS_ID garantit que les index correspondent à ceux
|
// CUDA_DEVICE_ORDER=PCI_BUS_ID garantit que les index correspondent à ceux
|
||||||
// affichés par nvidia-smi (sinon CUDA réordonne par "device le plus rapide").
|
// affichés par nvidia-smi (sinon CUDA réordonne par "device le plus rapide").
|
||||||
if v := cfg["CUDA_VISIBLE_DEVICES"]; v != "" {
|
if v := cfg["CUDA_VISIBLE_DEVICES"]; v != "" {
|
||||||
@@ -104,7 +104,7 @@ func cmdServe(args []string) error {
|
|||||||
// mmproj-F16.gguf) still resolve.
|
// mmproj-F16.gguf) still resolve.
|
||||||
_ = os.Chdir(AjeanHome())
|
_ = os.Chdir(AjeanHome())
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "[jean serve] %s model=%s port=%s\n",
|
fmt.Fprintf(os.Stderr, "[ajean serve] %s model=%s port=%s\n",
|
||||||
bin, filepath.Base(model), get("PORT", "8080"))
|
bin, filepath.Base(model), get("PORT", "8080"))
|
||||||
|
|
||||||
// Hand off to the llama-server process. On Unix this replaces the current
|
// Hand off to the llama-server process. On Unix this replaces the current
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ func cmdAgent(args []string) error {
|
|||||||
fmt.Printf(" %s %s\n", bold(p.Name), p.Title)
|
fmt.Printf(" %s %s\n", bold(p.Name), p.Title)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("usage: jean agent [on|off|status]")
|
return fmt.Errorf("usage: ajean agent [on|off|status]")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -12,14 +12,14 @@ import (
|
|||||||
// First positional arg (if any) becomes the system prompt.
|
// First positional arg (if any) becomes the system prompt.
|
||||||
func cmdChat(args []string) error {
|
func cmdChat(args []string) error {
|
||||||
if !healthCheck() {
|
if !healthCheck() {
|
||||||
return fmt.Errorf("serveur injoignable sur :%d — jean start d'abord", LLMPort())
|
return fmt.Errorf("serveur injoignable sur :%d — ajean start d'abord", LLMPort())
|
||||||
}
|
}
|
||||||
sysPrompt := strings.Join(args, " ")
|
sysPrompt := strings.Join(args, " ")
|
||||||
msgs := []Message{}
|
msgs := []Message{}
|
||||||
if sysPrompt != "" {
|
if sysPrompt != "" {
|
||||||
msgs = append(msgs, Message{Role: "system", Content: sysPrompt})
|
msgs = append(msgs, Message{Role: "system", Content: sysPrompt})
|
||||||
}
|
}
|
||||||
fmt.Printf("\n%s — /reset pour vider, /sys <prompt> pour changer le system, /quit ou Ctrl-D pour sortir\n", cyan("jean chat"))
|
fmt.Printf("\n%s — /reset pour vider, /sys <prompt> pour changer le system, /quit ou Ctrl-D pour sortir\n", cyan("ajean chat"))
|
||||||
if sysPrompt != "" {
|
if sysPrompt != "" {
|
||||||
fmt.Println(dim("system: " + sysPrompt))
|
fmt.Println(dim("system: " + sysPrompt))
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ func cmdChat(args []string) error {
|
|||||||
inReason := false
|
inReason := false
|
||||||
var stats *StatsEvent
|
var stats *StatsEvent
|
||||||
// Print the assistant prefix once; reasoning is shown inline with a tag.
|
// Print the assistant prefix once; reasoning is shown inline with a tag.
|
||||||
fmt.Print(cyan("jean") + " > ")
|
fmt.Print(cyan("ajean") + " > ")
|
||||||
caps := globalCaps()
|
caps := globalCaps()
|
||||||
// Compaction proactive (façon Hermes) : on résume les vieux tours quand
|
// Compaction proactive (façon Hermes) : on résume les vieux tours quand
|
||||||
// l'historique dépasse le seuil, au lieu d'imposer un /reset.
|
// l'historique dépasse le seuil, au lieu d'imposer un /reset.
|
||||||
@@ -120,7 +120,7 @@ func cmdChat(args []string) error {
|
|||||||
fmt.Print("\n")
|
fmt.Print("\n")
|
||||||
inReason = false
|
inReason = false
|
||||||
}
|
}
|
||||||
fmt.Printf("\n%s %s : %s\n%s ", dim(icon+" "+verb), "", magenta(ev.ToolUsed.Label), cyan("jean")+" >")
|
fmt.Printf("\n%s %s : %s\n%s ", dim(icon+" "+verb), "", magenta(ev.ToolUsed.Label), cyan("ajean")+" >")
|
||||||
case ev.Reasoning != "":
|
case ev.Reasoning != "":
|
||||||
if !inReason {
|
if !inReason {
|
||||||
fmt.Print(magenta("[reasoning] ") + dim(""))
|
fmt.Print(magenta("[reasoning] ") + dim(""))
|
||||||
@@ -129,7 +129,7 @@ func cmdChat(args []string) error {
|
|||||||
fmt.Print(dim(ev.Reasoning))
|
fmt.Print(dim(ev.Reasoning))
|
||||||
case ev.Content != "":
|
case ev.Content != "":
|
||||||
if inReason {
|
if inReason {
|
||||||
fmt.Print("\n" + cyan("jean") + " > ")
|
fmt.Print("\n" + cyan("ajean") + " > ")
|
||||||
inReason = false
|
inReason = false
|
||||||
}
|
}
|
||||||
full.WriteString(ev.Content)
|
full.WriteString(ev.Content)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// chat_internet_tools.go — les 4 outils web exposés au modèle (web_search,
|
// chat_internet_tools.go — les 4 outils web exposés au modèle (web_search,
|
||||||
// web_open, web_read, web_grep) + la sous-commande `jean internet`.
|
// web_open, web_read, web_grep) + la sous-commande `ajean internet`.
|
||||||
package ajean
|
package ajean
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -251,7 +251,7 @@ func toolWebGrep(args map[string]any) string {
|
|||||||
entry.url, len(matchIdx), pattern, capped, strings.Join(blocks, "\n\n---\n\n"))
|
entry.url, len(matchIdx), pattern, capped, strings.Join(blocks, "\n\n---\n\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── CLI : jean internet [on|off|status|url <url>] ──────────────────────────
|
// ─── CLI : ajean internet [on|off|status|url <url>] ──────────────────────────
|
||||||
|
|
||||||
func cmdInternet(args []string) error {
|
func cmdInternet(args []string) error {
|
||||||
sub := ""
|
sub := ""
|
||||||
@@ -261,7 +261,7 @@ func cmdInternet(args []string) error {
|
|||||||
switch sub {
|
switch sub {
|
||||||
case "on":
|
case "on":
|
||||||
if crawl4aiURL() == "" {
|
if crawl4aiURL() == "" {
|
||||||
return fmt.Errorf("configure d'abord l'URL : jean internet url <url>")
|
return fmt.Errorf("configure d'abord l'URL : ajean internet url <url>")
|
||||||
}
|
}
|
||||||
if err := setInternetEnabled(true); err != nil {
|
if err := setInternetEnabled(true); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -274,7 +274,7 @@ func cmdInternet(args []string) error {
|
|||||||
fmt.Println(green("[ok]") + " accès internet désactivé")
|
fmt.Println(green("[ok]") + " accès internet désactivé")
|
||||||
case "url":
|
case "url":
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
return fmt.Errorf("usage: jean internet url <url> (ex: http://localhost:11235)")
|
return fmt.Errorf("usage: ajean internet url <url> (ex: http://localhost:11235)")
|
||||||
}
|
}
|
||||||
u := strings.TrimRight(strings.TrimSpace(args[1]), "/")
|
u := strings.TrimRight(strings.TrimSpace(args[1]), "/")
|
||||||
if err := SetConfigKey("CRAWL4AI_URL", u); err != nil {
|
if err := SetConfigKey("CRAWL4AI_URL", u); err != nil {
|
||||||
@@ -286,7 +286,7 @@ func cmdInternet(args []string) error {
|
|||||||
fmt.Printf("%s serveur Crawl4AI : %s\n", green("[ok]"), bold(u))
|
fmt.Printf("%s serveur Crawl4AI : %s\n", green("[ok]"), bold(u))
|
||||||
case "key":
|
case "key":
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
return fmt.Errorf("usage: jean internet key <clé> (vide pour l'enlever : jean internet key \"\")")
|
return fmt.Errorf("usage: ajean internet key <clé> (vide pour l'enlever : ajean internet key \"\")")
|
||||||
}
|
}
|
||||||
k := strings.TrimSpace(args[1])
|
k := strings.TrimSpace(args[1])
|
||||||
if err := writeCrawlKey(k); err != nil {
|
if err := writeCrawlKey(k); err != nil {
|
||||||
@@ -308,7 +308,7 @@ func cmdInternet(args []string) error {
|
|||||||
fmt.Printf("%s état: %s\n", cyan("Accès internet"), state)
|
fmt.Printf("%s état: %s\n", cyan("Accès internet"), state)
|
||||||
u := crawl4aiURL()
|
u := crawl4aiURL()
|
||||||
if u == "" {
|
if u == "" {
|
||||||
fmt.Printf(" serveur : %s — configure : jean internet url <url>\n", dim("(non configuré)"))
|
fmt.Printf(" serveur : %s — configure : ajean internet url <url>\n", dim("(non configuré)"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
reach := red("injoignable")
|
reach := red("injoignable")
|
||||||
@@ -318,7 +318,7 @@ func cmdInternet(args []string) error {
|
|||||||
fmt.Printf(" serveur : %s (%s)\n", bold(u), reach)
|
fmt.Printf(" serveur : %s (%s)\n", bold(u), reach)
|
||||||
fmt.Printf(" outils : web_search, web_open, web_read, web_grep\n")
|
fmt.Printf(" outils : web_search, web_open, web_read, web_grep\n")
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("usage: jean internet [on|off|status|url <url>]")
|
return fmt.Errorf("usage: ajean internet [on|off|status|url <url>]")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -372,7 +372,7 @@ func setMemMode(m MemMode) error {
|
|||||||
return SetConfigKey("MEM_MODE", string(m))
|
return SetConfigKey("MEM_MODE", string(m))
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmdMemory : jean memory [off|ondemand|always|status]
|
// cmdMemory : ajean memory [off|ondemand|always|status]
|
||||||
func cmdMemory(args []string) error {
|
func cmdMemory(args []string) error {
|
||||||
sub := ""
|
sub := ""
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
@@ -403,7 +403,7 @@ func cmdMemory(args []string) error {
|
|||||||
fmt.Printf(" %d page(s) sous %s\n", len(pages), memoryDir())
|
fmt.Printf(" %d page(s) sous %s\n", len(pages), memoryDir())
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("usage: jean memory [off|ondemand|always|status]")
|
return fmt.Errorf("usage: ajean memory [off|ondemand|always|status]")
|
||||||
}
|
}
|
||||||
m := memMode()
|
m := memMode()
|
||||||
fmt.Printf("%s mémoire : %s — %s\n", green("[ok]"), bold(string(m)), label[m])
|
fmt.Printf("%s mémoire : %s — %s\n", green("[ok]"), bold(string(m)), label[m])
|
||||||
|
|||||||
@@ -62,9 +62,12 @@ func workspaceCandidates() []string {
|
|||||||
}
|
}
|
||||||
c = append(c, filepath.Join(AjeanHome(), "workspace"))
|
c = append(c, filepath.Join(AjeanHome(), "workspace"))
|
||||||
if home, err := os.UserHomeDir(); err == nil && home != "" {
|
if home, err := os.UserHomeDir(); err == nil && home != "" {
|
||||||
|
// Le nouveau nom d'abord, l'ancien ensuite : un dossier ~/jean/workspace
|
||||||
|
// déjà rempli par l'agent reste utilisable au lieu de repartir de zéro.
|
||||||
|
c = append(c, filepath.Join(home, "ajean", "workspace"))
|
||||||
c = append(c, filepath.Join(home, "jean", "workspace"))
|
c = append(c, filepath.Join(home, "jean", "workspace"))
|
||||||
}
|
}
|
||||||
c = append(c, filepath.Join(os.TempDir(), "jean-workspace"))
|
c = append(c, filepath.Join(os.TempDir(), "ajean-workspace"))
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func setOAIPublic(on bool) error {
|
|||||||
func oaiTLSConfig() *tls.Config {
|
func oaiTLSConfig() *tls.Config {
|
||||||
certmagic.Default.Storage = &certmagic.FileStorage{Path: filepath.Join(AjeanHome(), "certs")}
|
certmagic.Default.Storage = &certmagic.FileStorage{Path: filepath.Join(AjeanHome(), "certs")}
|
||||||
certmagic.DefaultACME.Agreed = true
|
certmagic.DefaultACME.Agreed = true
|
||||||
certmagic.DefaultACME.Email = strings.TrimSpace(os.Getenv("JEAN_ACME_EMAIL"))
|
certmagic.DefaultACME.Email = envAny("AJEAN_ACME_EMAIL", "JEAN_ACME_EMAIL")
|
||||||
certmagic.DefaultACME.DisableHTTPChallenge = true // pas de :80 accessible (CGNAT) → TLS-ALPN uniquement
|
certmagic.DefaultACME.DisableHTTPChallenge = true // pas de :80 accessible (CGNAT) → TLS-ALPN uniquement
|
||||||
magic := certmagic.NewDefault()
|
magic := certmagic.NewDefault()
|
||||||
magic.OnDemand = &certmagic.OnDemandConfig{
|
magic.OnDemand = &certmagic.OnDemandConfig{
|
||||||
@@ -221,7 +221,7 @@ func selfSignedTLSConfig(host string) (*tls.Config, error) {
|
|||||||
|
|
||||||
// cmdOAI pilote l'accès OpenAI public côté agent.
|
// cmdOAI pilote l'accès OpenAI public côté agent.
|
||||||
//
|
//
|
||||||
// jean oai serve [port] [host] (test local) termine le TLS sur :port avec un
|
// ajean oai serve [port] [host] (test local) termine le TLS sur :port avec un
|
||||||
// cert auto-signé et proxifie vers llama /v1.
|
// cert auto-signé et proxifie vers llama /v1.
|
||||||
func cmdOAI(args []string) error {
|
func cmdOAI(args []string) error {
|
||||||
sub := ""
|
sub := ""
|
||||||
@@ -251,11 +251,11 @@ func cmdOAI(args []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf("[jean oai] front TLS (test, auto-signé) https://%s:%d/v1 → llama :%d\n", host, port, LLMPort())
|
fmt.Printf("[ajean oai] front TLS (test, auto-signé) https://%s:%d/v1 → llama :%d\n", host, port, LLMPort())
|
||||||
return runOAIFront(ln, tlsCfg)
|
return runOAIFront(ln, tlsCfg)
|
||||||
default:
|
default:
|
||||||
fmt.Println("usage: jean oai serve [port] [host] (front TLS de test → llama /v1)")
|
fmt.Println("usage: ajean oai serve [port] [host] (front TLS de test → llama /v1)")
|
||||||
fmt.Println(" en prod, le front TLS est servi automatiquement dans le tunnel (jean link)")
|
fmt.Println(" en prod, le front TLS est servi automatiquement dans le tunnel (ajean link)")
|
||||||
fmt.Println(" quand JEAN_LINK_ALLOW_OAI=1 ; cert Let's Encrypt via TLS-ALPN-01.")
|
fmt.Println(" quand JEAN_LINK_ALLOW_OAI=1 ; cert Let's Encrypt via TLS-ALPN-01.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func cmdTest(args []string) error {
|
|||||||
fmt.Printf("→ GET http://localhost:%d/health … ", port)
|
fmt.Printf("→ GET http://localhost:%d/health … ", port)
|
||||||
if !healthCheck() {
|
if !healthCheck() {
|
||||||
fmt.Println(red("ko"))
|
fmt.Println(red("ko"))
|
||||||
return fmt.Errorf("/health ne répond pas — jean start d'abord")
|
return fmt.Errorf("/health ne répond pas — ajean start d'abord")
|
||||||
}
|
}
|
||||||
fmt.Println(green("ok"))
|
fmt.Println(green("ok"))
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ package ajean
|
|||||||
//
|
//
|
||||||
// Modèle : cet agent a une paire X25519 long-terme (clé privée locale, jamais
|
// Modèle : cet agent a une paire X25519 long-terme (clé privée locale, jamais
|
||||||
// transmise). Sa clé publique est publiée au relais et son EMPREINTE est affichée
|
// transmise). Sa clé publique est publiée au relais et son EMPREINTE est affichée
|
||||||
// au `jean link` — l'utilisateur la confirme dans le portail, ce qui défait tout
|
// au `ajean link` — l'utilisateur la confirme dans le portail, ce qui défait tout
|
||||||
// MITM du relais. Le navigateur dérive une racine R de son mot de passe (exportKey
|
// MITM du relais. Le navigateur dérive une racine R de son mot de passe (exportKey
|
||||||
// OPAQUE), la SCELLE vers la clé publique de l'agent (le relais ne peut pas
|
// OPAQUE), la SCELLE vers la clé publique de l'agent (le relais ne peut pas
|
||||||
// l'ouvrir), puis chiffre/déchiffre le chat avec une clé dérivée de R.
|
// l'ouvrir), puis chiffre/déchiffre le chat avec une clé dérivée de R.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ package ajean
|
|||||||
// Modèle : l'utilisateur a une identité X25519 dérivée de façon déterministe de son
|
// Modèle : l'utilisateur a une identité X25519 dérivée de façon déterministe de son
|
||||||
// mot de passe (exportKey OPAQUE → racine R → uPriv) — le relais ne connaît pas R,
|
// mot de passe (exportKey OPAQUE → racine R → uPriv) — le relais ne connaît pas R,
|
||||||
// donc ne peut PAS reproduire cette identité. La clé publique uPub est APPAIRÉE une
|
// donc ne peut PAS reproduire cette identité. La clé publique uPub est APPAIRÉE une
|
||||||
// fois à l'agent via un code affiché par « jean link » (canal hors-bande : le log du
|
// fois à l'agent via un code affiché par « ajean link » (canal hors-bande : le log du
|
||||||
// serveur, que le relais ne voit pas ; le code voyage scellé vers l'agent). Ensuite,
|
// serveur, que le relais ne voit pas ; le code voyage scellé vers l'agent). Ensuite,
|
||||||
// chaque requête est chiffrée avec K = SHA256(ECDH(uPriv, agentPriv) || "authchan"),
|
// chaque requête est chiffrée avec K = SHA256(ECDH(uPriv, agentPriv) || "authchan"),
|
||||||
// liée à uPub+horodatage (AAD) et protégée contre le rejeu. Seul le vrai utilisateur
|
// liée à uPub+horodatage (AAD) et protégée contre le rejeu. Seul le vrai utilisateur
|
||||||
@@ -100,8 +100,8 @@ func authorizeUser(uPubHex string) error {
|
|||||||
|
|
||||||
// ---- Codes d'appairage : à la demande, usage unique, TTL 10 min --------------
|
// ---- Codes d'appairage : à la demande, usage unique, TTL 10 min --------------
|
||||||
//
|
//
|
||||||
// Générés par « jean link code » (ou affichés par « jean link »), ils sont
|
// Générés par « ajean link code » (ou affichés par « ajean link »), ils sont
|
||||||
// partagés avec le worker « jean link --foreground » (systemd, autre process) via
|
// partagés avec le worker « ajean link --foreground » (systemd, autre process) via
|
||||||
// un fichier AjeanHome/.pair_codes — sinon les deux process ne s'accorderaient pas.
|
// un fichier AjeanHome/.pair_codes — sinon les deux process ne s'accorderaient pas.
|
||||||
// Stockés HACHÉS (SHA-256) : le fichier ne révèle aucun code. Chaque code expire
|
// Stockés HACHÉS (SHA-256) : le fichier ne révèle aucun code. Chaque code expire
|
||||||
// au bout de 10 min et est consommé (retiré) au premier appairage réussi.
|
// au bout de 10 min et est consommé (retiré) au premier appairage réussi.
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
package ajean
|
package ajean
|
||||||
|
|
||||||
// relay_link.go — `jean link <token>` : connecte ce serveur Jean au relais public
|
// relay_link.go — `ajean link <token>` : connecte ce serveur Jean au relais public
|
||||||
// (ajean.link) par une connexion SORTANTE persistante, pour qu'un utilisateur
|
// (ajean.link) par une connexion SORTANTE persistante, pour qu'un utilisateur
|
||||||
// y accède depuis n'importe où sans ouvrir de port (CGNAT, box, etc.).
|
// y accède depuis n'importe où sans ouvrir de port (CGNAT, box, etc.).
|
||||||
//
|
//
|
||||||
// Principe : l'agent ouvre un WebSocket vers le relais, l'authentifie avec le
|
// Principe : l'agent ouvre un WebSocket vers le relais, l'authentifie avec le
|
||||||
// token d'abonnement, puis multiplexe (yamux) ce lien unique en un stream par
|
// token d'abonnement, puis multiplexe (yamux) ce lien unique en un stream par
|
||||||
// requête navigateur. Chaque stream est reverse-proxyfié vers le `jean web`
|
// requête navigateur. Chaque stream est reverse-proxyfié vers le `ajean web`
|
||||||
// local. Keepalive + reconnexion automatique avec backoff.
|
// local. Keepalive + reconnexion automatique avec backoff.
|
||||||
//
|
//
|
||||||
// Le token est fourni par la boutique à l'achat. Il est mémorisé dans
|
// Le token est fourni par la boutique à l'achat. Il est mémorisé dans
|
||||||
// $JEAN_HOME/.link_token pour que `jean link` (sans argument) reprenne la
|
// $JEAN_HOME/.link_token pour que `ajean link` (sans argument) reprenne la
|
||||||
// connexion.
|
// connexion.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -87,7 +87,7 @@ func removeLinkToken() error {
|
|||||||
|
|
||||||
// relayURL resolves the relay WebSocket endpoint (env override → default).
|
// relayURL resolves the relay WebSocket endpoint (env override → default).
|
||||||
func relayURL() string {
|
func relayURL() string {
|
||||||
if u := strings.TrimSpace(os.Getenv("JEAN_LINK_URL")); u != "" {
|
if u := envAny("AJEAN_LINK_URL", "JEAN_LINK_URL"); u != "" {
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
return defaultRelayURL
|
return defaultRelayURL
|
||||||
@@ -111,12 +111,12 @@ func cmdLink(args []string) error {
|
|||||||
}
|
}
|
||||||
switch sub {
|
switch sub {
|
||||||
case "":
|
case "":
|
||||||
// `jean link` seul : afficher l'aide des sous-commandes (NE démarre pas —
|
// `ajean link` seul : afficher l'aide des sous-commandes (NE démarre pas —
|
||||||
// éviter de prendre un mot pour un token et d'écraser le vrai).
|
// éviter de prendre un mot pour un token et d'écraser le vrai).
|
||||||
printLinkHelp()
|
printLinkHelp()
|
||||||
return nil
|
return nil
|
||||||
case "serve", "--foreground", "fg":
|
case "serve", "--foreground", "fg":
|
||||||
// Le worker réel (boucle de connexion au relais), pendant de `jean serve`.
|
// Le worker réel (boucle de connexion au relais), pendant de `ajean serve`.
|
||||||
// C'est ce que lance l'unité systemd ; il NE doit PAS rendre la main.
|
// C'est ce que lance l'unité systemd ; il NE doit PAS rendre la main.
|
||||||
return runLinkForeground()
|
return runLinkForeground()
|
||||||
case "start":
|
case "start":
|
||||||
@@ -124,14 +124,14 @@ func cmdLink(args []string) error {
|
|||||||
case "status":
|
case "status":
|
||||||
tok := readLinkToken()
|
tok := readLinkToken()
|
||||||
if tok == "" {
|
if tok == "" {
|
||||||
fmt.Println(yellow("[info]") + " aucun token enregistré — lance: jean link <token>")
|
fmt.Println(yellow("[info]") + " aucun token enregistré — lance: ajean link <token>")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fmt.Printf("%s token enregistré (%s…), relais: %s\n", green("[ok]"), tok[:min(8, len(tok))], relayURL())
|
fmt.Printf("%s token enregistré (%s…), relais: %s\n", green("[ok]"), tok[:min(8, len(tok))], relayURL())
|
||||||
if linkServiceActive() {
|
if linkServiceActive() {
|
||||||
fmt.Printf("%s service %s: actif\n", green("[ok]"), linkServiceName())
|
fmt.Printf("%s service %s: actif\n", green("[ok]"), linkServiceName())
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s service %s: arrêté (jean link pour démarrer)\n", yellow("[info]"), linkServiceName())
|
fmt.Printf("%s service %s: arrêté (ajean link pour démarrer)\n", yellow("[info]"), linkServiceName())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
case "logout":
|
case "logout":
|
||||||
@@ -174,7 +174,7 @@ func cmdLink(args []string) error {
|
|||||||
// on le signale au lieu d'un faux « démarré ».
|
// on le signale au lieu d'un faux « démarré ».
|
||||||
func startLink(force bool) error {
|
func startLink(force bool) error {
|
||||||
if readLinkToken() == "" {
|
if readLinkToken() == "" {
|
||||||
return fmt.Errorf("aucun token. Usage: jean link <token> (token fourni à l'achat sur la boutique)")
|
return fmt.Errorf("aucun token. Usage: ajean link <token> (token fourni à l'achat sur la boutique)")
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case force:
|
case force:
|
||||||
@@ -182,7 +182,7 @@ func startLink(force bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case linkServiceActive():
|
case linkServiceActive():
|
||||||
fmt.Printf("%s service %s déjà en cours — « jean link restart » pour le relancer\n", yellow("[info]"), linkServiceName())
|
fmt.Printf("%s service %s déjà en cours — « ajean link restart » pour le relancer\n", yellow("[info]"), linkServiceName())
|
||||||
default:
|
default:
|
||||||
if err := linkServiceCtl("start"); err != nil {
|
if err := linkServiceCtl("start"); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -191,20 +191,20 @@ func startLink(force bool) error {
|
|||||||
return linkPrintIdentity()
|
return linkPrintIdentity()
|
||||||
}
|
}
|
||||||
|
|
||||||
// printLinkHelp liste les sous-commandes de `jean link`.
|
// printLinkHelp liste les sous-commandes de `ajean link`.
|
||||||
func printLinkHelp() {
|
func printLinkHelp() {
|
||||||
fmt.Print(`jean link — accès distant via le relais ajean.link
|
fmt.Print(`ajean link — accès distant via le relais ajean.link
|
||||||
|
|
||||||
Usage :
|
Usage :
|
||||||
jean link <token> enregistre le token (1re fois / pour le changer) et démarre le lien
|
ajean link <token> enregistre le token (1re fois / pour le changer) et démarre le lien
|
||||||
jean link start démarre le lien en arrière-plan (service)
|
ajean link start démarre le lien en arrière-plan (service)
|
||||||
jean link restart redémarre le service de lien
|
ajean link restart redémarre le service de lien
|
||||||
jean link stop arrête le service de lien
|
ajean link stop arrête le service de lien
|
||||||
jean link status état du token et du service
|
ajean link status état du token et du service
|
||||||
jean link code génère un code d'appairage (valable 10 min, à usage unique)
|
ajean link code génère un code d'appairage (valable 10 min, à usage unique)
|
||||||
jean link logout oublie le token enregistré
|
ajean link logout oublie le token enregistré
|
||||||
|
|
||||||
Le token est fourni sur ajean.link. « jean link » seul affiche cette aide.
|
Le token est fourni sur ajean.link. « ajean link » seul affiche cette aide.
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ func linkPrintIdentity() error {
|
|||||||
code, err := newPairCode()
|
code, err := newPairCode()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%s code d'appairage indisponible (droits sur %s ?): %v\n", yellow("[link]"), AjeanHome(), err)
|
fmt.Printf("%s code d'appairage indisponible (droits sur %s ?): %v\n", yellow("[link]"), AjeanHome(), err)
|
||||||
fmt.Printf(" Réessaie : sudo jean link code\n")
|
fmt.Printf(" Réessaie : sudo ajean link code\n")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fmt.Printf(" Code d'appairage (valable 10 min, usage unique) : %s\n\n", bold(code))
|
fmt.Printf(" Code d'appairage (valable 10 min, usage unique) : %s\n\n", bold(code))
|
||||||
@@ -230,17 +230,17 @@ func linkPrintIdentity() error {
|
|||||||
func runLinkForeground() error {
|
func runLinkForeground() error {
|
||||||
token := readLinkToken()
|
token := readLinkToken()
|
||||||
if token == "" {
|
if token == "" {
|
||||||
return fmt.Errorf("aucun token. Usage: jean link <token>")
|
return fmt.Errorf("aucun token. Usage: ajean link <token>")
|
||||||
}
|
}
|
||||||
fmt.Printf("%s connexion au relais %s …\n", cyan("[link]"), relayURL())
|
fmt.Printf("%s connexion au relais %s …\n", cyan("[link]"), relayURL())
|
||||||
fmt.Printf(" (UI AJEAN + endpoint OpenAI servis dans le tunnel — pas besoin de 'jean web')\n")
|
fmt.Printf(" (UI AJEAN + endpoint OpenAI servis dans le tunnel — pas besoin de 'ajean web')\n")
|
||||||
if fp := e2eFingerprint(); fp != "" {
|
if fp := e2eFingerprint(); fp != "" {
|
||||||
fmt.Printf("%s empreinte E2E : %s (code d'appairage : jean link code)\n", green("[e2e]"), bold(fp))
|
fmt.Printf("%s empreinte E2E : %s (code d'appairage : ajean link code)\n", green("[e2e]"), bold(fp))
|
||||||
}
|
}
|
||||||
|
|
||||||
// UNE seule instance du mux web → UNE seule conversation en mémoire, PARTAGÉE
|
// UNE seule instance du mux web → UNE seule conversation en mémoire, PARTAGÉE
|
||||||
// entre l'UI locale (:8090, jean.n27.fr) et le tunnel (app.ajean.link). Avant,
|
// entre l'UI locale (:8090, jean.n27.fr) et le tunnel (app.ajean.link). Avant,
|
||||||
// `jean web` et `jean link` étaient deux process distincts avec chacun leur
|
// `ajean web` et `ajean link` étaient deux process distincts avec chacun leur
|
||||||
// historique → confusion. Désormais le process jean-link est l'unique
|
// historique → confusion. Désormais le process jean-link est l'unique
|
||||||
// propriétaire de la conversation et sert les DEUX surfaces.
|
// propriétaire de la conversation et sert les DEUX surfaces.
|
||||||
webMux := newWebMux()
|
webMux := newWebMux()
|
||||||
@@ -369,12 +369,12 @@ func killForeignLinkWorker() {
|
|||||||
// serveLocalWebMux sert l'UI web locale sur :8090 avec le mux fourni — le MÊME
|
// serveLocalWebMux sert l'UI web locale sur :8090 avec le mux fourni — le MÊME
|
||||||
// que celui exposé dans le tunnel. Le process jean-link devient ainsi l'unique
|
// que celui exposé dans le tunnel. Le process jean-link devient ainsi l'unique
|
||||||
// propriétaire de la conversation, partagée entre l'accès local et l'accès
|
// propriétaire de la conversation, partagée entre l'accès local et l'accès
|
||||||
// distant. Remplace le `jean web` autonome (à ne plus lancer séparément).
|
// distant. Remplace le `ajean web` autonome (à ne plus lancer séparément).
|
||||||
func serveLocalWebMux(mux *http.ServeMux) {
|
func serveLocalWebMux(mux *http.ServeMux) {
|
||||||
addr := "0.0.0.0:8090"
|
addr := "0.0.0.0:8090"
|
||||||
ln, err := net.Listen("tcp", addr)
|
ln, err := net.Listen("tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%s UI locale non démarrée (%s occupé ? tue le vieux « jean web ») : %v\n", yellow("[web]"), addr, err)
|
fmt.Printf("%s UI locale non démarrée (%s occupé ? tue le vieux « ajean web ») : %v\n", yellow("[web]"), addr, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("%s UI locale sur http://%s (même conversation que le tunnel)\n", green("[web]"), addr)
|
fmt.Printf("%s UI locale sur http://%s (même conversation que le tunnel)\n", green("[web]"), addr)
|
||||||
@@ -410,7 +410,7 @@ func adoptLegacyLinkFile(ext string) string {
|
|||||||
// linkServiceCtl pilote le worker de lien (start/stop/restart). Sous Linux c'est
|
// linkServiceCtl pilote le worker de lien (start/stop/restart). Sous Linux c'est
|
||||||
// l'unité systemd jean-link (avec sudo non interactif si on n'est pas root) ;
|
// l'unité systemd jean-link (avec sudo non interactif si on n'est pas root) ;
|
||||||
// ailleurs — macOS et Windows, où il n'y a ni systemd ni droits root — on lance
|
// ailleurs — macOS et Windows, où il n'y a ni systemd ni droits root — on lance
|
||||||
// « jean link serve » en processus détaché suivi par un fichier PID, comme le
|
// « ajean link serve » en processus détaché suivi par un fichier PID, comme le
|
||||||
// fait déjà le service principal. Sans ça, l'accès distant restait définitivement
|
// fait déjà le service principal. Sans ça, l'accès distant restait définitivement
|
||||||
// « arrêté » sur un Mac ou un PC : le token était enregistré mais rien ne
|
// « arrêté » sur un Mac ou un PC : le token était enregistré mais rien ne
|
||||||
// composait jamais le tunnel.
|
// composait jamais le tunnel.
|
||||||
@@ -527,7 +527,7 @@ func linkUserSvcCtl(action string) error {
|
|||||||
cmd := spawnDetached(self, "link", "serve")
|
cmd := spawnDetached(self, "link", "serve")
|
||||||
cmd.Stdout, cmd.Stderr = logf, logf
|
cmd.Stdout, cmd.Stderr = logf, logf
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return fmt.Errorf("démarrage de « jean link serve »: %w", err)
|
return fmt.Errorf("démarrage de « ajean link serve »: %w", err)
|
||||||
}
|
}
|
||||||
pid := cmd.Process.Pid
|
pid := cmd.Process.Pid
|
||||||
// PID + binaire d'origine : la 2e ligne dit QUELLE copie de l'app a lancé ce
|
// PID + binaire d'origine : la 2e ligne dit QUELLE copie de l'app a lancé ce
|
||||||
|
|||||||
+17
-15
@@ -1,5 +1,5 @@
|
|||||||
// jean — single-binary LLM server manager + web UI for llama.cpp deployments.
|
// ajean — single-binary LLM server manager + web UI for llama.cpp deployments.
|
||||||
// Point d'entrée réel : Main(), appelé par cmd/jean (les métadonnées Windows
|
// Point d'entrée réel : Main(), appelé par cmd/ajean (les métadonnées Windows
|
||||||
// .syso/go:generate vivent là-bas, dans le dossier du package main).
|
// .syso/go:generate vivent là-bas, dans le dossier du package main).
|
||||||
package ajean
|
package ajean
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
const Version = "0.6.15"
|
const Version = "0.6.15"
|
||||||
|
|
||||||
// Main est le vrai main() du binaire (cmd/jean ne fait que l'appeler).
|
// Main est le vrai main() du binaire (cmd/ajean ne fait que l'appeler).
|
||||||
func Main() {
|
func Main() {
|
||||||
// Rattache la console du terminal parent si on est lancé depuis un shell
|
// Rattache la console du terminal parent si on est lancé depuis un shell
|
||||||
// (Windows : binaire GUI). Retourne false au double-clic (aucune console) →
|
// (Windows : binaire GUI). Retourne false au double-clic (aucune console) →
|
||||||
@@ -37,7 +37,7 @@ func Main() {
|
|||||||
// lance l'expérience « application » (UI web + navigateur + icône tray) plutôt
|
// lance l'expérience « application » (UI web + navigateur + icône tray) plutôt
|
||||||
// que d'afficher l'aide. Le binaire étant compilé en sous-système GUI, il n'y a
|
// que d'afficher l'aide. Le binaire étant compilé en sous-système GUI, il n'y a
|
||||||
// AUCUNE console à ce stade (donc plus de fenêtre noire). Lancé depuis un shell,
|
// AUCUNE console à ce stade (donc plus de fenêtre noire). Lancé depuis un shell,
|
||||||
// `jean` sans argument garde son comportement d'aide.
|
// `ajean` sans argument garde son comportement d'aide.
|
||||||
if noArgs && !haveConsole {
|
if noArgs && !haveConsole {
|
||||||
mustExit(cmdApp(args))
|
mustExit(cmdApp(args))
|
||||||
return
|
return
|
||||||
@@ -96,7 +96,7 @@ func Main() {
|
|||||||
case "uninstall":
|
case "uninstall":
|
||||||
mustExit(cmdUninstall(args))
|
mustExit(cmdUninstall(args))
|
||||||
case "version", "-v", "--version":
|
case "version", "-v", "--version":
|
||||||
fmt.Println("jean", Version)
|
fmt.Println("ajean", Version)
|
||||||
case "help", "-h", "--help", "":
|
case "help", "-h", "--help", "":
|
||||||
printHelp()
|
printHelp()
|
||||||
default:
|
default:
|
||||||
@@ -107,9 +107,9 @@ func Main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printHelp() {
|
func printHelp() {
|
||||||
fmt.Printf(`jean %s — manager llama.cpp + UI web (single binary)
|
fmt.Printf(`ajean %s — manager llama.cpp + UI web (single binary)
|
||||||
|
|
||||||
Usage: jean <commande> [args]
|
Usage: ajean <commande> [args]
|
||||||
|
|
||||||
Application:
|
Application:
|
||||||
app lance l'UI web + ouvre le navigateur (auto au double-clic du binaire)
|
app lance l'UI web + ouvre le navigateur (auto au double-clic du binaire)
|
||||||
@@ -118,9 +118,9 @@ Service:
|
|||||||
start | stop | restart gérer le service (systemd sous Linux, processus en arrière-plan sous Windows)
|
start | stop | restart gérer le service (systemd sous Linux, processus en arrière-plan sous Windows)
|
||||||
status | logs état / logs en direct
|
status | logs état / logs en direct
|
||||||
enable | disable auto-démarrage au boot
|
enable | disable auto-démarrage au boot
|
||||||
edit éditer $JEAN_HOME/config.env
|
edit éditer $AJEAN_HOME/config.env
|
||||||
set-api-key [clé] protéger llama-server (clé Bearer); vide = générer, "" = retirer
|
set-api-key [clé] protéger llama-server (clé Bearer); vide = générer, "" = retirer
|
||||||
set-web-key [clé] protéger l'API de pilotage 'jean web'; vide = générer, "" = retirer
|
set-web-key [clé] protéger l'API de pilotage 'ajean web'; vide = générer, "" = retirer
|
||||||
vram utilisation GPU/VRAM (nvidia-smi)
|
vram utilisation GPU/VRAM (nvidia-smi)
|
||||||
gpu [index…] liste les GPU / choisit le(s)quel(s) utiliser (gpu all = tous)
|
gpu [index…] liste les GPU / choisit le(s)quel(s) utiliser (gpu all = tous)
|
||||||
test vérifie que l'IA répond (health + completion)
|
test vérifie que l'IA répond (health + completion)
|
||||||
@@ -141,7 +141,7 @@ Accès distant (ajean.link) :
|
|||||||
link code génère un code d'appairage (valable 10 min, à usage unique) pour le portail
|
link code génère un code d'appairage (valable 10 min, à usage unique) pour le portail
|
||||||
link status | logout état du lien / oublier le token
|
link status | logout état du lien / oublier le token
|
||||||
link (sans argument) affiche l'aide des sous-commandes link
|
link (sans argument) affiche l'aide des sous-commandes link
|
||||||
link serve exécute le worker au premier plan (utilisé par jean-link.service ; pendant de 'jean serve')
|
link serve exécute le worker au premier plan (utilisé par jean-link.service ; pendant de 'ajean serve')
|
||||||
|
|
||||||
Mode agent:
|
Mode agent:
|
||||||
agent [on|off|status] active TOUS les outils de l'IA (shell complet + mémoire) — un seul interrupteur
|
agent [on|off|status] active TOUS les outils de l'IA (shell complet + mémoire) — un seul interrupteur
|
||||||
@@ -151,20 +151,21 @@ Backend llama.cpp :
|
|||||||
llamacpp update git pull + recompile le backend existant (arrête/redémarre le service)
|
llamacpp update git pull + recompile le backend existant (arrête/redémarre le service)
|
||||||
llamacpp status commit courant, backend détecté, retard sur origin
|
llamacpp status commit courant, backend détecté, retard sur origin
|
||||||
|
|
||||||
Entrypoint (utilisé par jean.service) :
|
Entrypoint (utilisé par ajean.service) :
|
||||||
serve lit config.env et exec le binaire llama-server
|
serve lit config.env et exec le binaire llama-server
|
||||||
|
|
||||||
Installation:
|
Installation:
|
||||||
where affiche où sont le binaire, la config, la mémoire et le dossier de travail de l'agent
|
where affiche où sont le binaire, la config, la mémoire et le dossier de travail de l'agent
|
||||||
install installer (Linux: unité systemd, sudoers, dossiers ; Windows: dossiers + config)
|
install installer (Linux: unité systemd, sudoers, dossiers ; Windows: dossiers + config)
|
||||||
uninstall désinstaller
|
uninstall désinstaller
|
||||||
update [--check] mettre à jour jean depuis les releases GitHub (--check = signale sans installer)
|
update [--check] mettre à jour AJEAN depuis les releases GitHub (--check = signale sans installer)
|
||||||
|
|
||||||
Env:
|
Env:
|
||||||
JEAN_HOME racine (défaut: /etc/jean sur Linux/macOS, %%ProgramData%%\jean sur Windows)
|
AJEAN_HOME racine (défaut: /etc/ajean sur Linux/macOS, %%ProgramData%%\ajean sur Windows)
|
||||||
EDITOR éditeur pour 'jean edit' (défaut: nano sur Unix, notepad sur Windows)
|
JEAN_HOME reste accepté (héritage)
|
||||||
|
EDITOR éditeur pour 'ajean edit' (défaut: nano sur Unix, notepad sur Windows)
|
||||||
|
|
||||||
Config: $JEAN_HOME/config.env
|
Config: $AJEAN_HOME/config.env
|
||||||
`, Version)
|
`, Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,6 +235,7 @@ func legacySkillsFlag() string { return filepath.Join(skillsDir(), ".enabled") }
|
|||||||
func legacyToolsFlag() string { return filepath.Join(AjeanHome(), ".tools_enabled") }
|
func legacyToolsFlag() string { return filepath.Join(AjeanHome(), ".tools_enabled") }
|
||||||
func apiKeyPath() string { return filepath.Join(AjeanHome(), ".api_key") }
|
func apiKeyPath() string { return filepath.Join(AjeanHome(), ".api_key") }
|
||||||
func crawlKeyPath() string { return filepath.Join(AjeanHome(), ".crawl4ai_key") }
|
func crawlKeyPath() string { return filepath.Join(AjeanHome(), ".crawl4ai_key") }
|
||||||
|
|
||||||
// serviceName est le nom de l'unité qui exécute llama-server. AJEAN_SERVICE et
|
// serviceName est le nom de l'unité qui exécute llama-server. AJEAN_SERVICE et
|
||||||
// JEAN_SERVICE sont tous deux honorés : le second est posé à la main sur des
|
// JEAN_SERVICE sont tous deux honorés : le second est posé à la main sur des
|
||||||
// machines qu'on ne voit pas, le renommage ne doit pas les casser.
|
// machines qu'on ne voit pas, le renommage ne doit pas les casser.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ package ajean
|
|||||||
// qui s'ouvre et se ferme. En contrepartie, un binaire GUI lancé depuis un
|
// qui s'ouvre et se ferme. En contrepartie, un binaire GUI lancé depuis un
|
||||||
// terminal (cmd/PowerShell) n'écrit nulle part par défaut : on se rattache alors
|
// terminal (cmd/PowerShell) n'écrit nulle part par défaut : on se rattache alors
|
||||||
// explicitement à la console du parent et on réouvre stdout/stderr/stdin dessus,
|
// explicitement à la console du parent et on réouvre stdout/stderr/stdin dessus,
|
||||||
// pour que l'usage CLI (`jean web`, `jean status`, …) reste lisible.
|
// pour que l'usage CLI (`ajean web`, `ajean status`, …) reste lisible.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package ajean
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// envAny lit la première variable d'environnement renseignée parmi names.
|
||||||
|
//
|
||||||
|
// Sert au renommage jean → AJEAN : chaque réglage s'appelle désormais AJEAN_*,
|
||||||
|
// mais l'ancien nom JEAN_* reste lu en second. Ces variables sont posées à la
|
||||||
|
// main — dans un shell, un fichier d'unité, un docker-compose, un script de
|
||||||
|
// déploiement — et rien ne nous permet de les retrouver pour les réécrire. Les
|
||||||
|
// cesser de les lire, c'est casser silencieusement des installations qui
|
||||||
|
// marchaient, avec un réglage qui redevient sa valeur par défaut sans le dire.
|
||||||
|
func envAny(names ...string) string {
|
||||||
|
for _, n := range names {
|
||||||
|
if v := strings.TrimSpace(os.Getenv(n)); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ package ajean
|
|||||||
|
|
||||||
import "os"
|
import "os"
|
||||||
|
|
||||||
// appFirstRun : hors Windows, `jean app` est lancé depuis un terminal par
|
// appFirstRun : hors Windows, `ajean app` est lancé depuis un terminal par
|
||||||
// quelqu'un qui a déjà suivi l'installation — pas de double-clic à désambiguïser,
|
// quelqu'un qui a déjà suivi l'installation — pas de double-clic à désambiguïser,
|
||||||
// donc pas de boîte de dialogue. On se contente de provisionner les données au
|
// donc pas de boîte de dialogue. On se contente de provisionner les données au
|
||||||
// premier lancement, comme avant. Ne relance jamais (renvoie toujours false).
|
// premier lancement, comme avant. Ne relance jamais (renvoie toujours false).
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ func runAsInstaller(target string) bool {
|
|||||||
return launch(target) // l'instance en cours reprend la main (port occupé)
|
return launch(target) // l'instance en cours reprend la main (port occupé)
|
||||||
}
|
}
|
||||||
stopProcesses(running)
|
stopProcesses(running)
|
||||||
|
target = migrateThenResolveTarget(target)
|
||||||
if err := replaceInstalled(target); err != nil {
|
if err := replaceInstalled(target); err != nil {
|
||||||
messageBox("La mise à jour a échoué :\n\n"+err.Error()+"\n\nAJEAN va redémarrer dans sa version actuelle.",
|
messageBox("La mise à jour a échoué :\n\n"+err.Error()+"\n\nAJEAN va redémarrer dans sa version actuelle.",
|
||||||
"AJEAN", mbIconInfo)
|
"AJEAN", mbIconInfo)
|
||||||
@@ -205,6 +206,7 @@ func runAsInstaller(target string) bool {
|
|||||||
case cmp > 0:
|
case cmp > 0:
|
||||||
// Mise à jour, application arrêtée : rien à décider, on remplace et on
|
// Mise à jour, application arrêtée : rien à décider, on remplace et on
|
||||||
// démarre. C'est le cas courant et il doit rester muet.
|
// démarre. C'est le cas courant et il doit rester muet.
|
||||||
|
target = migrateThenResolveTarget(target)
|
||||||
_ = replaceInstalled(target)
|
_ = replaceInstalled(target)
|
||||||
ensureShortcuts(target)
|
ensureShortcuts(target)
|
||||||
return launch(target)
|
return launch(target)
|
||||||
@@ -225,7 +227,22 @@ func verLabel(v string) string {
|
|||||||
|
|
||||||
// replaceInstalled écrase le binaire installé par celui qu'on exécute. Le
|
// replaceInstalled écrase le binaire installé par celui qu'on exécute. Le
|
||||||
// renommage préalable en .old permet de remplacer un exécutable encore ouvert
|
// renommage préalable en .old permet de remplacer un exécutable encore ouvert
|
||||||
// (même mécanique que replaceBinary pour `jean update`).
|
// (même mécanique que replaceBinary pour `ajean update`).
|
||||||
|
// migrateThenResolveTarget tente la migration du dossier de données maintenant
|
||||||
|
// qu'aucune instance ne tient plus de handle, puis renvoie le chemin du binaire
|
||||||
|
// installé DANS le dossier résultant.
|
||||||
|
//
|
||||||
|
// Recalculer le chemin est indispensable et pas cosmétique : `target` a été
|
||||||
|
// calculé avant la migration, il désigne donc encore …\jean\bin\ajean.exe. Sans
|
||||||
|
// ce recalcul, on réinstallerait le binaire dans l'ancien dossier qu'on vient
|
||||||
|
// tout juste de déplacer, en recréant au passage une arborescence fantôme.
|
||||||
|
func migrateThenResolveTarget(target string) string {
|
||||||
|
if !retryHomeMigration() {
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
return installedExePath()
|
||||||
|
}
|
||||||
|
|
||||||
func replaceInstalled(target string) error {
|
func replaceInstalled(target string) error {
|
||||||
old := target + ".old"
|
old := target + ".old"
|
||||||
_ = os.Remove(old)
|
_ = os.Remove(old)
|
||||||
@@ -321,7 +338,7 @@ func portBusy(port int) bool {
|
|||||||
// posées par goversioninfo, cf cmd/jean/versioninfo.json), comme le fait
|
// posées par goversioninfo, cf cmd/jean/versioninfo.json), comme le fait
|
||||||
// l'onglet « Détails » des propriétés Windows.
|
// l'onglet « Détails » des propriétés Windows.
|
||||||
//
|
//
|
||||||
// ⚠️ NE PAS remplacer par un `jean version` exécuté : le binaire est compilé en
|
// ⚠️ NE PAS remplacer par un `ajean version` exécuté : le binaire est compilé en
|
||||||
// sous-système GUI, il s'attache à la console du parent et n'écrit RIEN dans un
|
// sous-système GUI, il s'attache à la console du parent et n'écrit RIEN dans un
|
||||||
// tuyau. La sortie capturée est vide, donc la comparaison de versions échouerait
|
// tuyau. La sortie capturée est vide, donc la comparaison de versions échouerait
|
||||||
// toujours en silence et la mise à jour ne se ferait jamais (vérifié).
|
// toujours en silence et la mise à jour ne se ferait jamais (vérifié).
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
// binaryVersion pilote la mise à jour automatique du binaire installé quand on
|
// binaryVersion pilote la mise à jour automatique du binaire installé quand on
|
||||||
// lance le fichier téléchargé : si elle renvoie du vide, plus aucune mise à jour
|
// lance le fichier téléchargé : si elle renvoie du vide, plus aucune mise à jour
|
||||||
// n'a lieu, et en silence. C'est exactement ce qui arrivait avec la première
|
// n'a lieu, et en silence. C'est exactement ce qui arrivait avec la première
|
||||||
// implémentation (exécuter `jean version`, dont la sortie est vide pour un
|
// implémentation (exécuter `ajean version`, dont la sortie est vide pour un
|
||||||
// binaire en sous-système GUI). D'où ces tests sur la plomberie syscall.
|
// binaire en sous-système GUI). D'où ces tests sur la plomberie syscall.
|
||||||
func TestBinaryVersionReadsResource(t *testing.T) {
|
func TestBinaryVersionReadsResource(t *testing.T) {
|
||||||
// kernel32.dll porte toujours une ressource de version.
|
// kernel32.dll porte toujours une ressource de version.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
// sur « systemctl: command not found » / « /etc/default/jean », cf. issue #4).
|
// sur « systemctl: command not found » / « /etc/default/jean », cf. issue #4).
|
||||||
// Implémentation prudente ; à valider sur une machine Apple.
|
// Implémentation prudente ; à valider sur une machine Apple.
|
||||||
|
|
||||||
const configTemplate = `# Configuration JEAN — édite-moi puis: jean restart
|
const configTemplate = `# Configuration JEAN — édite-moi puis: ajean restart
|
||||||
# Le service launchd lit ce fichier et lance ton binaire llama.cpp.
|
# Le service launchd lit ce fichier et lance ton binaire llama.cpp.
|
||||||
|
|
||||||
# Chemins
|
# Chemins
|
||||||
@@ -38,9 +38,15 @@ NGL="999"
|
|||||||
EXTRA_ARGS=""
|
EXTRA_ARGS=""
|
||||||
`
|
`
|
||||||
|
|
||||||
// launchdPlistTemplate — champs formatés : Label, UserName, WorkingDirectory,
|
// launchdPlistTemplate — champs formatés : Label, chemin du binaire, UserName,
|
||||||
// JEAN_HOME, StandardOutPath, StandardErrorPath. KeepAlive/SuccessfulExit=false
|
// WorkingDirectory, AJEAN_HOME, JEAN_HOME, StandardOutPath, StandardErrorPath.
|
||||||
// ≈ Restart=on-failure ; RunAtLoad relance au boot une fois chargé avec `-w`.
|
// KeepAlive/SuccessfulExit=false ≈ Restart=on-failure ; RunAtLoad relance au
|
||||||
|
// boot une fois chargé avec `-w`.
|
||||||
|
//
|
||||||
|
// Le chemin du binaire est injecté et non codé en dur : il a changé avec le
|
||||||
|
// renommage, et un plist figé sur l'ancien chemin donnerait un daemon qui refuse
|
||||||
|
// de démarrer. Les deux variables d'environnement sont posées car le code lit
|
||||||
|
// encore JEAN_HOME en héritage.
|
||||||
const launchdPlistTemplate = `<?xml version="1.0" encoding="UTF-8"?>
|
const launchdPlistTemplate = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
@@ -48,13 +54,13 @@ const launchdPlistTemplate = `<?xml version="1.0" encoding="UTF-8"?>
|
|||||||
<key>Label</key><string>%s</string>
|
<key>Label</key><string>%s</string>
|
||||||
<key>ProgramArguments</key>
|
<key>ProgramArguments</key>
|
||||||
<array>
|
<array>
|
||||||
<string>/usr/local/bin/jean</string>
|
<string>%s</string>
|
||||||
<string>serve</string>
|
<string>serve</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UserName</key><string>%s</string>
|
<key>UserName</key><string>%s</string>
|
||||||
<key>WorkingDirectory</key><string>%s</string>
|
<key>WorkingDirectory</key><string>%s</string>
|
||||||
<key>EnvironmentVariables</key>
|
<key>EnvironmentVariables</key>
|
||||||
<dict><key>JEAN_HOME</key><string>%s</string></dict>
|
<dict><key>AJEAN_HOME</key><string>%s</string><key>JEAN_HOME</key><string>%s</string></dict>
|
||||||
<key>RunAtLoad</key><true/>
|
<key>RunAtLoad</key><true/>
|
||||||
<key>KeepAlive</key>
|
<key>KeepAlive</key>
|
||||||
<dict><key>SuccessfulExit</key><false/></dict>
|
<dict><key>SuccessfulExit</key><false/></dict>
|
||||||
@@ -66,7 +72,7 @@ const launchdPlistTemplate = `<?xml version="1.0" encoding="UTF-8"?>
|
|||||||
|
|
||||||
func cmdInstall(args []string) error {
|
func cmdInstall(args []string) error {
|
||||||
if os.Geteuid() != 0 {
|
if os.Geteuid() != 0 {
|
||||||
return fmt.Errorf("jean install doit être exécuté en root (sudo jean install)")
|
return fmt.Errorf("ajean install doit être exécuté en root (sudo ajean install)")
|
||||||
}
|
}
|
||||||
targetUser := os.Getenv("SUDO_USER")
|
targetUser := os.Getenv("SUDO_USER")
|
||||||
if targetUser == "" {
|
if targetUser == "" {
|
||||||
@@ -108,7 +114,7 @@ func cmdInstall(args []string) error {
|
|||||||
fmt.Printf(" %s écrit %s\n", green("✓"), conf)
|
fmt.Printf(" %s écrit %s\n", green("✓"), conf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Symlink idempotent vers /usr/local/bin/jean — même garde-fou que Linux
|
// 3. Symlink idempotent vers /usr/local/bin/ajean — même garde-fou que Linux
|
||||||
// (issue #5 : ne pas se lier sur soi-même si on tourne déjà depuis la cible).
|
// (issue #5 : ne pas se lier sur soi-même si on tourne déjà depuis la cible).
|
||||||
self, err := os.Executable()
|
self, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -117,7 +123,7 @@ func cmdInstall(args []string) error {
|
|||||||
if rp, e := filepath.EvalSymlinks(self); e == nil {
|
if rp, e := filepath.EvalSymlinks(self); e == nil {
|
||||||
self = rp
|
self = rp
|
||||||
}
|
}
|
||||||
target := "/usr/local/bin/jean"
|
target := installedExePath()
|
||||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -137,6 +143,18 @@ func cmdInstall(args []string) error {
|
|||||||
fmt.Printf(" %s %s -> %s\n", green("✓"), target, self)
|
fmt.Printf(" %s %s -> %s\n", green("✓"), target, self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3 bis. Alias hérité : `jean` doit rester tapable (voir sys_paths.go).
|
||||||
|
if alias := legacyExePath(); alias != target {
|
||||||
|
if rp, e := filepath.EvalSymlinks(alias); e != nil || rp != self {
|
||||||
|
_ = os.Remove(alias)
|
||||||
|
if err := os.Symlink(target, alias); err != nil {
|
||||||
|
fmt.Printf(" %s alias %s non créé (%v) — « ajean » reste disponible\n", yellow("[info]"), alias, err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" %s %s -> %s (alias hérité)\n", green("✓"), alias, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 4. /etc/default/ajean pour que les invocations CLI résolvent AJEAN_HOME.
|
// 4. /etc/default/ajean pour que les invocations CLI résolvent AJEAN_HOME.
|
||||||
// Les deux clés sont écrites : des scripts d'utilisateurs sourcent ce fichier
|
// Les deux clés sont écrites : des scripts d'utilisateurs sourcent ce fichier
|
||||||
// et lisent encore $JEAN_HOME.
|
// et lisent encore $JEAN_HOME.
|
||||||
@@ -149,7 +167,7 @@ func cmdInstall(args []string) error {
|
|||||||
|
|
||||||
// 5. LaunchDaemon plist (le log va sous JEAN_HOME, accessible au user cible)
|
// 5. LaunchDaemon plist (le log va sous JEAN_HOME, accessible au user cible)
|
||||||
logPath := filepath.Join(ajeanHome, svc+".log")
|
logPath := filepath.Join(ajeanHome, svc+".log")
|
||||||
plist := fmt.Sprintf(launchdPlistTemplate, launchdLabel(svc), targetUser, ajeanHome, ajeanHome, logPath, logPath)
|
plist := fmt.Sprintf(launchdPlistTemplate, launchdLabel(svc), target, targetUser, ajeanHome, ajeanHome, ajeanHome, logPath, logPath)
|
||||||
plistPath := launchdPlistPath(svc)
|
plistPath := launchdPlistPath(svc)
|
||||||
if err := os.WriteFile(plistPath, []byte(plist), 0o644); err != nil {
|
if err := os.WriteFile(plistPath, []byte(plist), 0o644); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -162,16 +180,16 @@ func cmdInstall(args []string) error {
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s installation terminée.\n", green("[ok]"))
|
fmt.Printf("%s installation terminée.\n", green("[ok]"))
|
||||||
fmt.Printf("\nProchaines étapes :\n")
|
fmt.Printf("\nProchaines étapes :\n")
|
||||||
fmt.Printf(" 1. édite la config : %s\n", bold("sudo -u "+targetUser+" jean edit"))
|
fmt.Printf(" 1. édite la config : %s\n", bold("sudo -u "+targetUser+" ajean edit"))
|
||||||
fmt.Printf(" (renseigne BIN, MODEL, etc.)\n")
|
fmt.Printf(" (renseigne BIN, MODEL, etc.)\n")
|
||||||
fmt.Printf(" 2. démarre le service: %s\n", bold("sudo jean start"))
|
fmt.Printf(" 2. démarre le service: %s\n", bold("sudo ajean start"))
|
||||||
fmt.Printf(" 3. UI web : %s\n", bold("sudo -u "+targetUser+" jean web"))
|
fmt.Printf(" 3. UI web : %s\n", bold("sudo -u "+targetUser+" ajean web"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmdUninstall(args []string) error {
|
func cmdUninstall(args []string) error {
|
||||||
if os.Geteuid() != 0 {
|
if os.Geteuid() != 0 {
|
||||||
return fmt.Errorf("jean uninstall doit être exécuté en root")
|
return fmt.Errorf("ajean uninstall doit être exécuté en root")
|
||||||
}
|
}
|
||||||
svc := serviceName()
|
svc := serviceName()
|
||||||
_ = exec.Command("launchctl", "unload", "-w", launchdPlistPath(svc)).Run()
|
_ = exec.Command("launchctl", "unload", "-w", launchdPlistPath(svc)).Run()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const configTemplate = `# Configuration JEAN — édite-moi puis: jean restart
|
const configTemplate = `# Configuration JEAN — édite-moi puis: ajean restart
|
||||||
# Le service systemd lit ce fichier et lance ton binaire llama.cpp.
|
# Le service systemd lit ce fichier et lance ton binaire llama.cpp.
|
||||||
|
|
||||||
# Chemins
|
# Chemins
|
||||||
@@ -61,7 +61,7 @@ WantedBy=multi-user.target
|
|||||||
|
|
||||||
func cmdInstall(args []string) error {
|
func cmdInstall(args []string) error {
|
||||||
if os.Geteuid() != 0 {
|
if os.Geteuid() != 0 {
|
||||||
return fmt.Errorf("jean install doit être exécuté en root (sudo jean install)")
|
return fmt.Errorf("ajean install doit être exécuté en root (sudo ajean install)")
|
||||||
}
|
}
|
||||||
targetUser := os.Getenv("SUDO_USER")
|
targetUser := os.Getenv("SUDO_USER")
|
||||||
if targetUser == "" {
|
if targetUser == "" {
|
||||||
@@ -113,7 +113,7 @@ func cmdInstall(args []string) error {
|
|||||||
}
|
}
|
||||||
target := installedExePath()
|
target := installedExePath()
|
||||||
// Garde-fou (issue #5) : si on tourne DÉJÀ depuis la cible (l'utilisateur a
|
// Garde-fou (issue #5) : si on tourne DÉJÀ depuis la cible (l'utilisateur a
|
||||||
// posé le binaire dans /usr/local/bin/jean puis lancé `sudo jean install`),
|
// posé le binaire dans /usr/local/bin/jean puis lancé `sudo ajean install`),
|
||||||
// ne surtout pas Remove+Symlink sur soi-même — ça effacerait le vrai binaire
|
// ne surtout pas Remove+Symlink sur soi-même — ça effacerait le vrai binaire
|
||||||
// et créerait /usr/local/bin/jean -> /usr/local/bin/jean (« Too many levels
|
// et créerait /usr/local/bin/jean -> /usr/local/bin/jean (« Too many levels
|
||||||
// of symbolic links »). On compare les chemins réels (résolus).
|
// of symbolic links »). On compare les chemins réels (résolus).
|
||||||
@@ -134,7 +134,7 @@ func cmdInstall(args []string) error {
|
|||||||
fmt.Printf(" %s %s -> %s\n", green("✓"), target, self)
|
fmt.Printf(" %s %s -> %s\n", green("✓"), target, self)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3 bis. Alias hérité : `jean` doit rester tapable. Des utilisateurs ont des
|
// 3 bis. Alias hérité : `ajean` doit rester tapable. Des utilisateurs ont des
|
||||||
// alias shell, des cron, des scripts de déploiement et des tutos qui appellent
|
// alias shell, des cron, des scripts de déploiement et des tutos qui appellent
|
||||||
// « jean » — on ne contrôle rien de tout ça, et un binaire renommé qui casse
|
// « jean » — on ne contrôle rien de tout ça, et un binaire renommé qui casse
|
||||||
// la commande d'hier est une régression pour eux. Le lien pointe sur le
|
// la commande d'hier est une régression pour eux. Le lien pointe sur le
|
||||||
@@ -185,16 +185,16 @@ func cmdInstall(args []string) error {
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s installation terminée.\n", green("[ok]"))
|
fmt.Printf("%s installation terminée.\n", green("[ok]"))
|
||||||
fmt.Printf("\nProchaines étapes :\n")
|
fmt.Printf("\nProchaines étapes :\n")
|
||||||
fmt.Printf(" 1. édite la config : %s\n", bold("sudo -u "+targetUser+" jean edit"))
|
fmt.Printf(" 1. édite la config : %s\n", bold("sudo -u "+targetUser+" ajean edit"))
|
||||||
fmt.Printf(" (renseigne BIN, MODEL, etc.)\n")
|
fmt.Printf(" (renseigne BIN, MODEL, etc.)\n")
|
||||||
fmt.Printf(" 2. démarre le service: %s\n", bold("sudo -u "+targetUser+" jean start"))
|
fmt.Printf(" 2. démarre le service: %s\n", bold("sudo -u "+targetUser+" ajean start"))
|
||||||
fmt.Printf(" 3. UI web : %s\n", bold("sudo -u "+targetUser+" jean web"))
|
fmt.Printf(" 3. UI web : %s\n", bold("sudo -u "+targetUser+" ajean web"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmdUninstall(args []string) error {
|
func cmdUninstall(args []string) error {
|
||||||
if os.Geteuid() != 0 {
|
if os.Geteuid() != 0 {
|
||||||
return fmt.Errorf("jean uninstall doit être exécuté en root")
|
return fmt.Errorf("ajean uninstall doit être exécuté en root")
|
||||||
}
|
}
|
||||||
svc := serviceName()
|
svc := serviceName()
|
||||||
keepData := false
|
keepData := false
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// On Windows there's no systemd unit, sudoers, or /usr/local/bin to populate.
|
// On Windows there's no systemd unit, sudoers, or /usr/local/bin to populate.
|
||||||
// `jean install` simply provisions the data directory and a starter config; the
|
// `ajean install` simply provisions the data directory and a starter config; the
|
||||||
// service itself is managed by the PID-file supervisor in sys_service_windows.go
|
// service itself is managed by the PID-file supervisor in sys_service_windows.go
|
||||||
// (jean start / stop / status), which needs no admin rights.
|
// (ajean start / stop / status), which needs no admin rights.
|
||||||
|
|
||||||
const configTemplate = `# Configuration JEAN — édite-moi puis: jean restart
|
const configTemplate = `# Configuration JEAN — édite-moi puis: ajean restart
|
||||||
# jean serve lit ce fichier et lance ton binaire llama.cpp.
|
# ajean serve lit ce fichier et lance ton binaire llama.cpp.
|
||||||
|
|
||||||
# Chemins (utilise des chemins Windows ; les antislashs ou slashs marchent)
|
# Chemins (utilise des chemins Windows ; les antislashs ou slashs marchent)
|
||||||
BIN="C:/llama.cpp/build/bin/Release/llama-server.exe"
|
BIN="C:/llama.cpp/build/bin/Release/llama-server.exe"
|
||||||
@@ -59,7 +59,7 @@ func cmdInstall(args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Install the binary into JEAN_HOME\bin and put that dir on the user PATH,
|
// 3. Install the binary into JEAN_HOME\bin and put that dir on the user PATH,
|
||||||
// so `jean` is callable from any shell (this is the Windows analogue of the
|
// so `ajean` is callable from any shell (this is the Windows analogue of the
|
||||||
// /usr/local/bin symlink the Unix installer creates).
|
// /usr/local/bin symlink the Unix installer creates).
|
||||||
binDir := filepath.Join(jeanHome, "bin")
|
binDir := filepath.Join(jeanHome, "bin")
|
||||||
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
||||||
@@ -86,9 +86,9 @@ func cmdInstall(args []string) error {
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s installation terminée.\n", green("[ok]"))
|
fmt.Printf("%s installation terminée.\n", green("[ok]"))
|
||||||
fmt.Printf("\nProchaines étapes :\n")
|
fmt.Printf("\nProchaines étapes :\n")
|
||||||
fmt.Printf(" 1. édite la config : %s (renseigne BIN, MODEL)\n", bold("jean edit"))
|
fmt.Printf(" 1. édite la config : %s (renseigne BIN, MODEL)\n", bold("ajean edit"))
|
||||||
fmt.Printf(" 2. démarre le service: %s\n", bold("jean start"))
|
fmt.Printf(" 2. démarre le service: %s\n", bold("ajean start"))
|
||||||
fmt.Printf(" 3. UI web : %s\n", bold("jean web"))
|
fmt.Printf(" 3. UI web : %s\n", bold("ajean web"))
|
||||||
if onPath {
|
if onPath {
|
||||||
fmt.Printf("\n%s ouvre un NOUVEAU terminal pour que 'jean' soit reconnu (le PATH n'est lu qu'au démarrage du shell).\n", dim("[info]"))
|
fmt.Printf("\n%s ouvre un NOUVEAU terminal pour que 'jean' soit reconnu (le PATH n'est lu qu'au démarrage du shell).\n", dim("[info]"))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -50,6 +50,26 @@ func resolveDefaultHome() string {
|
|||||||
return migrateHome(defaultAjeanHome(), legacyDefaultHome())
|
return migrateHome(defaultAjeanHome(), legacyDefaultHome())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// retryHomeMigration relance la résolution — donc la migration — après avoir
|
||||||
|
// arrêté ce qui tournait. Renvoie true si le dossier a effectivement changé.
|
||||||
|
//
|
||||||
|
// Raison d'être : sous Windows, un rename de dossier échoue tant qu'un process
|
||||||
|
// y tient un handle. Or le service AJEAN détaché en tient en permanence, si bien
|
||||||
|
// que la migration tentée au démarrage échouerait indéfiniment et que la machine
|
||||||
|
// resterait sur l'ancien chemin. L'installateur, lui, a une fenêtre où tout est
|
||||||
|
// arrêté : c'est là qu'on retente.
|
||||||
|
//
|
||||||
|
// À N'APPELER QUE dans cette fenêtre, et avant que quoi que ce soit d'autre
|
||||||
|
// n'ait ouvert de fichier de données : réinitialiser le cache n'est ni atomique
|
||||||
|
// ni sûr vis-à-vis des goroutines, et surtout les chemins déjà calculés par
|
||||||
|
// l'appelant deviennent obsolètes (voir migrateThenResolveTarget).
|
||||||
|
func retryHomeMigration() bool {
|
||||||
|
before := migratedDefaultHome()
|
||||||
|
homeOnce = sync.Once{}
|
||||||
|
homePath = ""
|
||||||
|
return migratedDefaultHome() != before
|
||||||
|
}
|
||||||
|
|
||||||
// migrateHome contient toute la logique de migration, isolée des chemins réels
|
// migrateHome contient toute la logique de migration, isolée des chemins réels
|
||||||
// de la plateforme pour être testable telle quelle. Renvoie le dossier à
|
// de la plateforme pour être testable telle quelle. Renvoie le dossier à
|
||||||
// utiliser — le nouveau si la migration a réussi ou n'était pas nécessaire,
|
// utiliser — le nouveau si la migration a réussi ou n'était pas nécessaire,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
// emplacements invisibles : on ne sait plus si le .exe qu'on a double-cliqué EST
|
// emplacements invisibles : on ne sait plus si le .exe qu'on a double-cliqué EST
|
||||||
// l'application ou seulement un installateur, ni où atterrissent la config, les
|
// l'application ou seulement un installateur, ni où atterrissent la config, les
|
||||||
// modèles et les fichiers que l'agent écrit. On centralise donc la réponse ici,
|
// modèles et les fichiers que l'agent écrit. On centralise donc la réponse ici,
|
||||||
// et on l'expose à la fois en ligne de commande (`jean where`) et dans l'UI.
|
// et on l'expose à la fois en ligne de commande (`ajean where`) et dans l'UI.
|
||||||
|
|
||||||
type ajeanPaths struct {
|
type ajeanPaths struct {
|
||||||
Home string `json:"home"` // JEAN_HOME : racine des données
|
Home string `json:"home"` // JEAN_HOME : racine des données
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func isWritableDir(dir string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultEditor is used by `jean edit` when $EDITOR is unset.
|
// defaultEditor is used by `ajean edit` when $EDITOR is unset.
|
||||||
func defaultEditor() string { return "nano" }
|
func defaultEditor() string { return "nano" }
|
||||||
|
|
||||||
// hideCmd : no-op sur Unix (pas de fenêtre de console à masquer).
|
// hideCmd : no-op sur Unix (pas de fenêtre de console à masquer).
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func defaultAjeanHome() string { return filepath.Join(homeRoot(), "ajean") }
|
|||||||
// legacyDefaultHome is the pre-rename data root, migrated on first run.
|
// legacyDefaultHome is the pre-rename data root, migrated on first run.
|
||||||
func legacyDefaultHome() string { return filepath.Join(homeRoot(), "jean") }
|
func legacyDefaultHome() string { return filepath.Join(homeRoot(), "jean") }
|
||||||
|
|
||||||
// defaultEditor is used by `jean edit` when $EDITOR is unset.
|
// defaultEditor is used by `ajean edit` when $EDITOR is unset.
|
||||||
func defaultEditor() string { return "notepad" }
|
func defaultEditor() string { return "notepad" }
|
||||||
|
|
||||||
// openBrowser ouvre l'URL dans le navigateur par défaut. rundll32 évite les
|
// openBrowser ouvre l'URL dans le navigateur par défaut. rundll32 évite les
|
||||||
@@ -130,7 +130,7 @@ func setLibraryPath(dir string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// execServer runs llama-server as a child process and waits for it. Windows has
|
// execServer runs llama-server as a child process and waits for it. Windows has
|
||||||
// no exec() that replaces the current image, so `jean serve` stays alive as the
|
// no exec() that replaces the current image, so `ajean serve` stays alive as the
|
||||||
// parent (this is the detached process the service supervisor tracks).
|
// parent (this is the detached process the service supervisor tracks).
|
||||||
// args[0] is the binary path; the rest are its arguments.
|
// args[0] is the binary path; the rest are its arguments.
|
||||||
func execServer(bin string, args []string) error {
|
func execServer(bin string, args []string) error {
|
||||||
@@ -232,7 +232,7 @@ func msvcGenerator() string {
|
|||||||
|
|
||||||
// ensureCompiler makes sure an MSVC C++ toolchain is available, installing the
|
// ensureCompiler makes sure an MSVC C++ toolchain is available, installing the
|
||||||
// Visual Studio 2022 Build Tools (VCTools workload) via winget if not. This is a
|
// Visual Studio 2022 Build Tools (VCTools workload) via winget if not. This is a
|
||||||
// large download but keeps `jean llamacpp install` fully unattended on a bare
|
// large download but keeps `ajean llamacpp install` fully unattended on a bare
|
||||||
// Windows box.
|
// Windows box.
|
||||||
func ensureCompiler() error {
|
func ensureCompiler() error {
|
||||||
if msvcInstallVersion() != "" {
|
if msvcInstallVersion() != "" {
|
||||||
@@ -354,7 +354,7 @@ Sans elle, CMake échoue sur « No CUDA toolset found ». Pour corriger, au choi
|
|||||||
1. relance l'installeur du CUDA Toolkit (installation personnalisée) et coche « CUDA → Visual Studio Integration » — Visual Studio doit déjà être installé à ce moment-là ;
|
1. relance l'installeur du CUDA Toolkit (installation personnalisée) et coche « CUDA → Visual Studio Integration » — Visual Studio doit déjà être installé à ce moment-là ;
|
||||||
2. ou copie (en admin) les fichiers de
|
2. ou copie (en admin) les fichiers de
|
||||||
%s
|
%s
|
||||||
vers le dossier BuildCustomizations ci-dessus, puis relance jean llamacpp install`, installPath, src)
|
vers le dossier BuildCustomizations ci-dessus, puis relance ajean llamacpp install`, installPath, src)
|
||||||
}
|
}
|
||||||
|
|
||||||
// cudaPathEnv returns the CUDA toolkit env vars the MSBuild CUDA integration
|
// cudaPathEnv returns the CUDA toolkit env vars the MSBuild CUDA integration
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func editConfig() error {
|
|||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Println(dim("[info] jean restart pour appliquer"))
|
fmt.Println(dim("[info] ajean restart pour appliquer"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ func serviceAction(action string) error {
|
|||||||
svc := serviceName()
|
svc := serviceName()
|
||||||
plist := launchdPlistPath(svc)
|
plist := launchdPlistPath(svc)
|
||||||
// Pas de LaunchDaemon installé — cas normal d'un Mac de bureau, où Jean tourne
|
// Pas de LaunchDaemon installé — cas normal d'un Mac de bureau, où Jean tourne
|
||||||
// via Jean.app sans jamais passer par `sudo jean install` : on gère alors le
|
// via Jean.app sans jamais passer par `sudo ajean install` : on gère alors le
|
||||||
// service comme sous Windows, avec un `jean serve` détaché suivi par un fichier
|
// service comme sous Windows, avec un `ajean serve` détaché suivi par un fichier
|
||||||
// PID. Aucun droit root requis. Sans ça, `start` échouait sur un plist absent
|
// PID. Aucun droit root requis. Sans ça, `start` échouait sur un plist absent
|
||||||
// et l'UI affichait éternellement « service arrêté ».
|
// et l'UI affichait éternellement « service arrêté ».
|
||||||
if _, err := os.Stat(plist); err != nil {
|
if _, err := os.Stat(plist); err != nil {
|
||||||
@@ -117,7 +117,7 @@ func checkStarted(svc string) error {
|
|||||||
fmt.Println(strings.Join(lines, "\n"))
|
fmt.Println(strings.Join(lines, "\n"))
|
||||||
}
|
}
|
||||||
fmt.Println("------------------------------------------------")
|
fmt.Println("------------------------------------------------")
|
||||||
fmt.Printf("→ jean logs pour plus de détails\n→ jean edit pour corriger config.env\n")
|
fmt.Printf("→ ajean logs pour plus de détails\n→ ajean edit pour corriger config.env\n")
|
||||||
return fmt.Errorf("service %s non démarré", svc)
|
return fmt.Errorf("service %s non démarré", svc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ func serviceIsActive() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Mode utilisateur (sans launchd) — même approche que Windows : `jean serve` est
|
// Mode utilisateur (sans launchd) — même approche que Windows : `ajean serve` est
|
||||||
// lancé détaché, son PID va dans JEAN_HOME/<svc>.pid et sa sortie dans
|
// lancé détaché, son PID va dans JEAN_HOME/<svc>.pid et sa sortie dans
|
||||||
// JEAN_HOME/<svc>.log. Setsid le détache de notre session pour qu'il survive à
|
// JEAN_HOME/<svc>.log. Setsid le détache de notre session pour qu'il survive à
|
||||||
// la fermeture de Jean.app, et permet de tuer tout le groupe (llama-server
|
// la fermeture de Jean.app, et permet de tuer tout le groupe (llama-server
|
||||||
@@ -175,7 +175,7 @@ func userSvcAction(action string) error {
|
|||||||
return nil
|
return nil
|
||||||
case "enable", "disable":
|
case "enable", "disable":
|
||||||
fmt.Printf("%s '%s' réclame un service système : lance %s pour installer le LaunchDaemon.\n",
|
fmt.Printf("%s '%s' réclame un service système : lance %s pour installer le LaunchDaemon.\n",
|
||||||
yellow("[info]"), action, bold("sudo jean install"))
|
yellow("[info]"), action, bold("sudo ajean install"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("action inconnue: %s", action)
|
return fmt.Errorf("action inconnue: %s", action)
|
||||||
@@ -207,7 +207,7 @@ func userSvcStart() error {
|
|||||||
cmd.Dir = AjeanHome()
|
cmd.Dir = AjeanHome()
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return fmt.Errorf("démarrage de 'jean serve': %w", err)
|
return fmt.Errorf("démarrage de 'ajean serve': %w", err)
|
||||||
}
|
}
|
||||||
pid := cmd.Process.Pid
|
pid := cmd.Process.Pid
|
||||||
if err := os.WriteFile(pidFilePath(), []byte(strconv.Itoa(pid)), 0o644); err != nil {
|
if err := os.WriteFile(pidFilePath(), []byte(strconv.Itoa(pid)), 0o644); err != nil {
|
||||||
@@ -221,14 +221,14 @@ func userCheckStarted(pid int) error {
|
|||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
if processAlive(pid) {
|
if processAlive(pid) {
|
||||||
fmt.Printf("%s %s: démarré (PID %d)\n", green("[ok]"), serviceName(), pid)
|
fmt.Printf("%s %s: démarré (PID %d)\n", green("[ok]"), serviceName(), pid)
|
||||||
fmt.Printf(" logs: %s (jean logs pour suivre)\n", dim(logFilePath()))
|
fmt.Printf(" logs: %s (ajean logs pour suivre)\n", dim(logFilePath()))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s: le processus s'est arrêté — derniers logs :\n", red("[ERREUR]"), serviceName())
|
fmt.Printf("%s %s: le processus s'est arrêté — derniers logs :\n", red("[ERREUR]"), serviceName())
|
||||||
fmt.Println("------------------------------------------------")
|
fmt.Println("------------------------------------------------")
|
||||||
fmt.Print(tailFile(logFilePath(), 20))
|
fmt.Print(tailFile(logFilePath(), 20))
|
||||||
fmt.Println("------------------------------------------------")
|
fmt.Println("------------------------------------------------")
|
||||||
fmt.Printf("→ jean logs pour plus de détails\n→ jean edit pour corriger config.env\n")
|
fmt.Printf("→ ajean logs pour plus de détails\n→ ajean edit pour corriger config.env\n")
|
||||||
_ = os.Remove(pidFilePath())
|
_ = os.Remove(pidFilePath())
|
||||||
return fmt.Errorf("service %s non démarré", serviceName())
|
return fmt.Errorf("service %s non démarré", serviceName())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ func checkStarted(svc string) error {
|
|||||||
logs, _ := exec.Command("journalctl", "-u", svc, "-n", "20", "--no-pager").Output()
|
logs, _ := exec.Command("journalctl", "-u", svc, "-n", "20", "--no-pager").Output()
|
||||||
fmt.Print(string(logs))
|
fmt.Print(string(logs))
|
||||||
fmt.Println("------------------------------------------------")
|
fmt.Println("------------------------------------------------")
|
||||||
fmt.Printf("→ jean logs pour plus de détails\n→ jean edit pour corriger config.env\n")
|
fmt.Printf("→ ajean logs pour plus de détails\n→ ajean edit pour corriger config.env\n")
|
||||||
return fmt.Errorf("service %s non démarré", svc)
|
return fmt.Errorf("service %s non démarré", svc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// On Windows there is no systemd, so "the service" is a background copy of
|
// On Windows there is no systemd, so "the service" is a background copy of
|
||||||
// `jean serve` that this process launches detached. We track it with a PID file
|
// `ajean serve` that this process launches detached. We track it with a PID file
|
||||||
// and stream its output to a log file under JEAN_HOME. This needs no admin
|
// and stream its output to a log file under JEAN_HOME. This needs no admin
|
||||||
// rights and no external tools beyond the always-present tasklist/taskkill.
|
// rights and no external tools beyond the always-present tasklist/taskkill.
|
||||||
|
|
||||||
@@ -72,14 +72,14 @@ func svcStart() error {
|
|||||||
cmd.Stdout = logf
|
cmd.Stdout = logf
|
||||||
cmd.Stderr = logf
|
cmd.Stderr = logf
|
||||||
cmd.Dir = AjeanHome() // les chemins relatifs de config.env se résolvent depuis JEAN_HOME
|
cmd.Dir = AjeanHome() // les chemins relatifs de config.env se résolvent depuis JEAN_HOME
|
||||||
// createNoWindow + HideWindow : le service enfant (`jean serve`) ne doit JAMAIS
|
// createNoWindow + HideWindow : le service enfant (`ajean serve`) ne doit JAMAIS
|
||||||
// faire clignoter de console noire quand Jean est lancé en mode app (double-clic).
|
// faire clignoter de console noire quand Jean est lancé en mode app (double-clic).
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
HideWindow: true,
|
HideWindow: true,
|
||||||
CreationFlags: createNewProcessGroup | detachedProcess | createNoWindow,
|
CreationFlags: createNewProcessGroup | detachedProcess | createNoWindow,
|
||||||
}
|
}
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return fmt.Errorf("démarrage de 'jean serve': %w", err)
|
return fmt.Errorf("démarrage de 'ajean serve': %w", err)
|
||||||
}
|
}
|
||||||
pid := cmd.Process.Pid
|
pid := cmd.Process.Pid
|
||||||
if err := os.WriteFile(pidFilePath(), []byte(strconv.Itoa(pid)), 0o644); err != nil {
|
if err := os.WriteFile(pidFilePath(), []byte(strconv.Itoa(pid)), 0o644); err != nil {
|
||||||
@@ -94,14 +94,14 @@ func checkStarted(pid int) error {
|
|||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
if processAlive(pid) {
|
if processAlive(pid) {
|
||||||
fmt.Printf("%s %s: démarré (PID %d)\n", green("[ok]"), serviceName(), pid)
|
fmt.Printf("%s %s: démarré (PID %d)\n", green("[ok]"), serviceName(), pid)
|
||||||
fmt.Printf(" logs: %s (jean logs pour suivre)\n", dim(logFilePath()))
|
fmt.Printf(" logs: %s (ajean logs pour suivre)\n", dim(logFilePath()))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s: le processus s'est arrêté — derniers logs :\n", red("[ERREUR]"), serviceName())
|
fmt.Printf("%s %s: le processus s'est arrêté — derniers logs :\n", red("[ERREUR]"), serviceName())
|
||||||
fmt.Println("------------------------------------------------")
|
fmt.Println("------------------------------------------------")
|
||||||
fmt.Print(tailFile(logFilePath(), 20))
|
fmt.Print(tailFile(logFilePath(), 20))
|
||||||
fmt.Println("------------------------------------------------")
|
fmt.Println("------------------------------------------------")
|
||||||
fmt.Printf("→ jean logs pour plus de détails\n→ jean edit pour corriger config.env\n")
|
fmt.Printf("→ ajean logs pour plus de détails\n→ ajean edit pour corriger config.env\n")
|
||||||
_ = os.Remove(pidFilePath())
|
_ = os.Remove(pidFilePath())
|
||||||
return fmt.Errorf("service %s non démarré", serviceName())
|
return fmt.Errorf("service %s non démarré", serviceName())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
package ajean
|
package ajean
|
||||||
|
|
||||||
// sys_tray_windows.go — icône de Jean dans la zone de notification Windows (system
|
// sys_tray_windows.go — icône de Jean dans la zone de notification Windows (system
|
||||||
// tray). Quand on lance l'app (double-clic ou `jean app`), l'UI web s'ouvre dans
|
// tray). Quand on lance l'app (double-clic ou `ajean app`), l'UI web s'ouvre dans
|
||||||
// le navigateur ET une petite icône apparaît près de l'horloge : elle montre que
|
// le navigateur ET une petite icône apparaît près de l'horloge : elle montre que
|
||||||
// Jean tourne et offre un menu « Ouvrir Jean » / « Quitter ».
|
// Jean tourne et offre un menu « Ouvrir Jean » / « Quitter ».
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"golang.org/x/mod/semver"
|
"golang.org/x/mod/semver"
|
||||||
)
|
)
|
||||||
|
|
||||||
// jean update — met à jour le binaire depuis les releases GitHub du projet.
|
// ajean update — met à jour le binaire depuis les releases GitHub du projet.
|
||||||
// Périmètre minimal : compare la version, télécharge l'asset correspondant à
|
// Périmètre minimal : compare la version, télécharge l'asset correspondant à
|
||||||
// l'OS/arch courant, remplace le binaire en place, puis affiche quoi redémarrer.
|
// l'OS/arch courant, remplace le binaire en place, puis affiche quoi redémarrer.
|
||||||
// AUCUN redémarrage de service automatique (choix volontaire, plus sûr).
|
// AUCUN redémarrage de service automatique (choix volontaire, plus sûr).
|
||||||
@@ -109,7 +109,7 @@ type updateInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// checkForUpdate interroge GitHub et compare à la version courante. Réutilisé
|
// checkForUpdate interroge GitHub et compare à la version courante. Réutilisé
|
||||||
// par `jean update` (CLI) et par l'endpoint web /api/update.
|
// par `ajean update` (CLI) et par l'endpoint web /api/update.
|
||||||
func checkForUpdate() (updateInfo, error) {
|
func checkForUpdate() (updateInfo, error) {
|
||||||
info := updateInfo{Current: Version}
|
info := updateInfo{Current: Version}
|
||||||
rel, err := fetchLatestRelease()
|
rel, err := fetchLatestRelease()
|
||||||
@@ -158,7 +158,7 @@ func applyUpdate() (string, error) {
|
|||||||
// Contrôle des droits AVANT de télécharger : sans ça, l'échec survient sur le
|
// Contrôle des droits AVANT de télécharger : sans ça, l'échec survient sur le
|
||||||
// os.Create du fichier temporaire et l'utilisateur reçoit un « open
|
// os.Create du fichier temporaire et l'utilisateur reçoit un « open
|
||||||
// /usr/local/bin/.ajean-update.tmp: permission denied » incompréhensible,
|
// /usr/local/bin/.ajean-update.tmp: permission denied » incompréhensible,
|
||||||
// notamment depuis le bouton de l'UI web (jean web lancé sans sudo).
|
// notamment depuis le bouton de l'UI web (ajean web lancé sans sudo).
|
||||||
if err := checkUpdateWritable(exe); err != nil {
|
if err := checkUpdateWritable(exe); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -203,7 +203,7 @@ func updatePermissionError(exe string) error {
|
|||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
return fmt.Errorf("droits insuffisants pour remplacer %s — relance AJEAN en administrateur puis réessaie", exe)
|
return fmt.Errorf("droits insuffisants pour remplacer %s — relance AJEAN en administrateur puis réessaie", exe)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("droits insuffisants pour remplacer %s (le binaire appartient à root) — lance la mise à jour en ligne de commande : sudo jean update", exe)
|
return fmt.Errorf("droits insuffisants pour remplacer %s (le binaire appartient à root) — lance la mise à jour en ligne de commande : sudo ajean update", exe)
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkUpdateWritable vérifie qu'on peut écrire dans le dossier du binaire, en
|
// checkUpdateWritable vérifie qu'on peut écrire dans le dossier du binaire, en
|
||||||
@@ -243,7 +243,7 @@ func cmdUpdate(args []string) error {
|
|||||||
fmt.Printf("nouvelle version disponible : %s (actuelle : %s)\n", info.Latest, Version)
|
fmt.Printf("nouvelle version disponible : %s (actuelle : %s)\n", info.Latest, Version)
|
||||||
fmt.Printf(" %s\n", info.URL)
|
fmt.Printf(" %s\n", info.URL)
|
||||||
if checkOnly {
|
if checkOnly {
|
||||||
fmt.Println("lance 'jean update' pour l'installer.")
|
fmt.Println("lance 'ajean update' pour l'installer.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fmt.Printf("téléchargement de %s…\n", updateAssetName())
|
fmt.Printf("téléchargement de %s…\n", updateAssetName())
|
||||||
|
|||||||
@@ -1256,7 +1256,7 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
|||||||
<input id="m-dir-path" class="pe-inp" placeholder="/media/disque-externe/models">
|
<input id="m-dir-path" class="pe-inp" placeholder="/media/disque-externe/models">
|
||||||
<button id="m-dir-add" onclick="addModelDir()" title="Ajouter ce dossier">+</button>
|
<button id="m-dir-add" onclick="addModelDir()" title="Ajouter ce dossier">+</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="pe-note">Les téléchargements, eux, vont toujours dans le dossier jean.</div>
|
<div class="pe-note">Les téléchargements, eux, vont toujours dans le dossier AJEAN.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -428,7 +428,7 @@
|
|||||||
<input id="m-dir-path" class="pe-inp" placeholder="/media/disque-externe/models">
|
<input id="m-dir-path" class="pe-inp" placeholder="/media/disque-externe/models">
|
||||||
<button id="m-dir-add" onclick="addModelDir()" title="Ajouter ce dossier">+</button>
|
<button id="m-dir-add" onclick="addModelDir()" title="Ajouter ce dossier">+</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="pe-note">Les téléchargements, eux, vont toujours dans le dossier jean.</div>
|
<div class="pe-note">Les téléchargements, eux, vont toujours dans le dossier AJEAN.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// web_auth.go protège l'API de pilotage (jean web) quand elle est exposée sur
|
// web_auth.go protège l'API de pilotage (ajean web) quand elle est exposée sur
|
||||||
// internet — c.-à-d. l'API que tout client (navigateur, app mobile, script…)
|
// internet — c.-à-d. l'API que tout client (navigateur, app mobile, script…)
|
||||||
// utilise pour switcher de preset, redémarrer le service, lire le status, etc.
|
// utilise pour switcher de preset, redémarrer le service, lire le status, etc.
|
||||||
//
|
//
|
||||||
@@ -70,9 +70,9 @@ func checkBearer(r *http.Request, key string) bool {
|
|||||||
|
|
||||||
// cmdSetWebKey sets (or clears) the control-API key in $JEAN_HOME/.web_key.
|
// cmdSetWebKey sets (or clears) the control-API key in $JEAN_HOME/.web_key.
|
||||||
//
|
//
|
||||||
// jean set-web-key <clé> définit la clé
|
// ajean set-web-key <clé> définit la clé
|
||||||
// jean set-web-key génère une clé aléatoire
|
// ajean set-web-key génère une clé aléatoire
|
||||||
// jean set-web-key "" supprime la protection (API ouverte)
|
// ajean set-web-key "" supprime la protection (API ouverte)
|
||||||
//
|
//
|
||||||
// Contrairement à set-api-key, aucun redémarrage n'est nécessaire : le serveur
|
// Contrairement à set-api-key, aucun redémarrage n'est nécessaire : le serveur
|
||||||
// web relit la clé à chaque requête.
|
// web relit la clé à chaque requête.
|
||||||
@@ -106,6 +106,6 @@ func cmdSetWebKey(args []string) error {
|
|||||||
}
|
}
|
||||||
fmt.Printf("%s clé de pilotage enregistrée dans %s\n", green("[ok]"), webKeyPath())
|
fmt.Printf("%s clé de pilotage enregistrée dans %s\n", green("[ok]"), webKeyPath())
|
||||||
fmt.Printf(" les clients doivent envoyer : %s\n", dim("Authorization: Bearer "+key))
|
fmt.Printf(" les clients doivent envoyer : %s\n", dim("Authorization: Bearer "+key))
|
||||||
fmt.Printf(" (relance 'jean web' si le serveur web tourne déjà — non requis, lu à chaud)\n")
|
fmt.Printf(" (relance 'ajean web' si le serveur web tourne déjà — non requis, lu à chaud)\n")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ package ajean
|
|||||||
// web_link_api.go — API locale pilotant l'accès distant (ajean.link) depuis l'UI
|
// web_link_api.go — API locale pilotant l'accès distant (ajean.link) depuis l'UI
|
||||||
// web de Jean, pour éviter le terminal. Le panneau « Accès distant » ouvre une
|
// web de Jean, pour éviter le terminal. Le panneau « Accès distant » ouvre une
|
||||||
// popup app.ajean.link/connect.html qui gère compte + paiement puis renvoie une
|
// popup app.ajean.link/connect.html qui gère compte + paiement puis renvoie une
|
||||||
// clé de liaison (jl_…) ; l'UI la POSTe ici, et Jean fait le `jean link` tout
|
// clé de liaison (jl_…) ; l'UI la POSTe ici, et Jean fait le `ajean link` tout
|
||||||
// seul (écrit le token + (re)démarre le service).
|
// seul (écrit le token + (re)démarre le service).
|
||||||
//
|
//
|
||||||
// Ces routes ne sont accessibles qu'en LOCAL : à travers le tunnel du relais,
|
// Ces routes ne sont accessibles qu'en LOCAL : à travers le tunnel du relais,
|
||||||
@@ -79,7 +79,7 @@ func handleLinkStart(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleLinkDisconnect (POST /api/link/disconnect) : arrête le service et oublie
|
// handleLinkDisconnect (POST /api/link/disconnect) : arrête le service et oublie
|
||||||
// la clé (équivalent `jean link stop` + `jean link logout`).
|
// la clé (équivalent `ajean link stop` + `ajean link logout`).
|
||||||
func handleLinkDisconnect(w http.ResponseWriter, r *http.Request) {
|
func handleLinkDisconnect(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = linkServiceCtl("stop")
|
_ = linkServiceCtl("stop")
|
||||||
_ = removeLinkToken()
|
_ = removeLinkToken()
|
||||||
@@ -89,7 +89,7 @@ func handleLinkDisconnect(w http.ResponseWriter, r *http.Request) {
|
|||||||
// handleLinkPairCode (POST /api/link/paircode) : génère un code d'appairage frais
|
// handleLinkPairCode (POST /api/link/paircode) : génère un code d'appairage frais
|
||||||
// (usage unique, TTL 10 min) + l'empreinte E2E, à saisir UNE fois dans le portail
|
// (usage unique, TTL 10 min) + l'empreinte E2E, à saisir UNE fois dans le portail
|
||||||
// distant lors de la première connexion (confirmation de la boîte noire). Évite le
|
// distant lors de la première connexion (confirmation de la boîte noire). Évite le
|
||||||
// détour par `jean link code` en terminal.
|
// détour par `ajean link code` en terminal.
|
||||||
func handleLinkPairCode(w http.ResponseWriter, r *http.Request) {
|
func handleLinkPairCode(w http.ResponseWriter, r *http.Request) {
|
||||||
code, err := newPairCode()
|
code, err := newPairCode()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ func cmdWeb(args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Printf("[jean web] http://%s (Ctrl-C pour arrêter)\n", addr)
|
fmt.Printf("[ajean web] http://%s (Ctrl-C pour arrêter)\n", addr)
|
||||||
if readWebKey() == "" {
|
if readWebKey() == "" {
|
||||||
fmt.Printf("%s API de pilotage NON protégée (aucune clé). Avant de l'exposer sur internet :\n", yellow("[!]"))
|
fmt.Printf("%s API de pilotage NON protégée (aucune clé). Avant de l'exposer sur internet :\n", yellow("[!]"))
|
||||||
fmt.Printf(" %s\n", bold("jean set-web-key"))
|
fmt.Printf(" %s\n", bold("ajean set-web-key"))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s API protégée par clé (Authorization: Bearer …)\n", green("[ok]"))
|
fmt.Printf("%s API protégée par clé (Authorization: Bearer …)\n", green("[ok]"))
|
||||||
}
|
}
|
||||||
@@ -56,13 +56,13 @@ func cmdWeb(args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newWebMux construit le routeur HTTP de l'UI web. Extrait de cmdWeb pour être
|
// newWebMux construit le routeur HTTP de l'UI web. Extrait de cmdWeb pour être
|
||||||
// réutilisé par `jean link`, qui sert ce même mux à travers le tunnel sans
|
// réutilisé par `ajean link`, qui sert ce même mux à travers le tunnel sans
|
||||||
// repasser par un écouteur TCP local.
|
// repasser par un écouteur TCP local.
|
||||||
var convLoadOnce sync.Once
|
var convLoadOnce sync.Once
|
||||||
|
|
||||||
func newWebMux() *http.ServeMux {
|
func newWebMux() *http.ServeMux {
|
||||||
// Charge l'état de conversation persisté (une fois par process : jean web ET
|
// Charge l'état de conversation persisté (une fois par process : ajean web ET
|
||||||
// jean link serve appellent newWebMux).
|
// ajean link serve appellent newWebMux).
|
||||||
convLoadOnce.Do(LoadConversation)
|
convLoadOnce.Do(LoadConversation)
|
||||||
// Pré-chauffe les serveurs MCP en tâche de fond : sinon le handshake (plusieurs
|
// Pré-chauffe les serveurs MCP en tâche de fond : sinon le handshake (plusieurs
|
||||||
// secondes pour un serveur lancé via npx) est payé par le premier message.
|
// secondes pour un serveur lancé via npx) est payé par le premier message.
|
||||||
@@ -120,7 +120,7 @@ func newWebMux() *http.ServeMux {
|
|||||||
api("/api/apikey", handleAPIKey)
|
api("/api/apikey", handleAPIKey)
|
||||||
api("/api/oai/public", handleOAIPublic)
|
api("/api/oai/public", handleOAIPublic)
|
||||||
api("/api/link/status", handleLinkStatus) // état de l'accès distant (ajean.link)
|
api("/api/link/status", handleLinkStatus) // état de l'accès distant (ajean.link)
|
||||||
api("/api/link/connect", handleLinkConnect) // clé de liaison remise par connect.html → jean link
|
api("/api/link/connect", handleLinkConnect) // clé de liaison remise par connect.html → ajean link
|
||||||
api("/api/link/start", handleLinkStart) // (re)démarre le tunnel avec la clé déjà enregistrée
|
api("/api/link/start", handleLinkStart) // (re)démarre le tunnel avec la clé déjà enregistrée
|
||||||
api("/api/link/disconnect", handleLinkDisconnect) // arrête le lien + oublie la clé
|
api("/api/link/disconnect", handleLinkDisconnect) // arrête le lien + oublie la clé
|
||||||
api("/api/link/paircode", handleLinkPairCode) // code d'appairage + empreinte pour la 1re connexion
|
api("/api/link/paircode", handleLinkPairCode) // code d'appairage + empreinte pour la 1re connexion
|
||||||
|
|||||||
Reference in new issue
Block a user