release: git-arboretum 3.4.0 (visibilité temps réel, historisation), desktop 0.2.0 (Windows, logo), vscode 0.4.1, site 0.4.0
CI / Build & test (Node 22) (push) Successful in 11m12s
CI / Build & test (Node 24) (push) Successful in 10m14s
CI / No em/en dashes (push) Successful in 3s
Deploy site (production) / build-and-deploy (push) Successful in 19s
CI / Pack & boot smoke (Node 22) (push) Has been cancelled
Tout est additif : PROTOCOL_VERSION inchangé, aucune rupture d'API. Temps réel réellement armé - `pinSession` n'était appelé nulle part : une session vivante épingle désormais le watcher FS de son worktree (`WorktreeManager.syncSessionPin` + `resolveWorktreeForCwd`), donc un worktree où un agent écrit se rafraîchit même si personne ne le regarde (mesuré ~350 ms). - Les abonnements `watch` sortent de `GitPanel`, démonté dès qu'on quitte son onglet, ce qui coupait le seul abonnement de toute l'app : `composables/useWatchedWorktrees.ts` (monté dans App.vue) suit le worktree actif et les dépôts dépliés, borné à 40. - Une coupure WS ne laisse plus l'UI sur des listes périmées : rechargement complet au retour. - `worktree_changes` alimente `worktrees.changeVersion`, consommé par l'arbre de fichiers, le diff (son `:version` était câblé à 0) et l'éditeur, qui recharge un tampon propre ou lève la bannière de conflit avant la sauvegarde au lieu d'attendre le 409. Corrélation session ↔ worktree par contenance (`@arboretum/shared/path-match.ts`) - Un terminal lancé dans un sous-répertoire (« Démarrer le projet ») ou une session de groupe reliée par `--add-dir` apparaissent enfin sous leur worktree ; le worktree le plus spécifique gagne. - Règle unique partagée par le daemon, le web et l'extension. Historisation - `commitLog` / `commitDiff` purs, `GET /repos/:id/worktrees/log` et `diff?commit=` (hash strictement validé, mêmes bornes que les diffs de fichiers). - `CommitHistory.vue` sous le panneau Git : commits, marquage des non poussés, diff déplié sur place. Visibilité - Compteurs git complets sur chaque worktree de l'arbre et du panneau Groupes (ils n'existaient qu'en barre de statut, pour le seul worktree actif), avec upstream et dernier commit en infobulle ; `locked`, `prunable` et un dépôt invalide sont désormais visibles. - Le panneau Groupes montre sa composition réelle (dépôts, worktrees, sessions) et teinte l'explorateur. Polish visuel - Les toasts d'erreur, persistants, s'empilaient derrière les modals : téléportés au-dessus. - Sur mobile, ouvrir un terminal ou changer d'activité n'avait aucun effet visible. - Tailles de panneaux clampées sur la fenêtre, barres d'onglets sans scrollbar parasite, états de chargement et d'erreur dans les trois panneaux, accessibilité des 11 modals centralisée dans ModalHost, splitters au clavier, numéros de diff collants, `window.confirm` remplacé. Windows (daemon et packaging) - `where.exe`, PowerShell comme shell de lancement, askpass `.cmd` (clone/push HTTPS par PAT), `taskkill /T`, `%APPDATA%`, `arboretum install` via tâche planifiée. - Scripts de build exécutables sur un hôte Windows (`npm.cmd`, extraction sans `unzip` ni `bash`). - Job CI `windows-latest` conditionné par ENABLE_WINDOWS_BUILD ; procédure runner dans docs/CI_RUNNERS.md. Logo Debian : cause racine - Une icône unique de 895×895 atterrissait dans `hicolor/895x895`, répertoire absent d'`index.theme` donc ignoré par la spécification freedesktop ; et `executableName` dérivait du nom scopé du paquet (`@arboretumdesktop`). Jeu d'icônes standard généré + `executableName: arboretum`, plus `deb.synopsis` (description courte vide dans apt) et `Section: devel`. - Runtime Node embarqué élagué : 205 → 118 Mo. - Auto-update réparé : la release flottante `desktop-latest` que les binaires interrogent n'existait pas. Doc et vitrine - README/README.fr : installation par plateforme, mode serveur web (nginx, LAN), dépannage, variables d'environnement, flags manquants. - Doc in-app réécrite (elle renvoyait aux pages Worktrees et Sessions supprimées). - Section « Accès distant » dans les Réglages ; le 403 BAD_ORIGIN nomme le flag à ajouter. - Site : prérequis et registre npm privé (le `npx` affiché renvoyait un 404), téléchargements réels par plateforme, section « trois façons de l'utiliser », navigation complétée, 16 clés i18n mortes purgées. Vérifications : 483 tests unitaires, 14 acceptances E2E vertes (dont p14/p15 nouvelles), captures de rendu sans erreur console (nouveau `verify-ui.mjs`), .deb reconstruit et contrôlé (icônes aux tailles standard, entrée .desktop valide).
@@ -0,0 +1,55 @@
|
||||
# Changelog
|
||||
|
||||
Notable changes to the Arboretum desktop app (`packages/desktop`). The daemon and the VS Code
|
||||
extension keep their own changelogs in `packages/server/CHANGELOG.md` and
|
||||
`packages/vscode/CHANGELOG.md`.
|
||||
|
||||
## 0.2.0
|
||||
|
||||
Distribution release: the Linux launcher icon finally shows up, Windows becomes a first-class target,
|
||||
and the embedded runtime loses a third of its weight.
|
||||
|
||||
- **Launcher icon fixed (Linux).** Earlier packages installed a single 895×895 icon. That size is not
|
||||
declared in `hicolor/index.theme`, so by the freedesktop spec every desktop environment ignored it and
|
||||
the launcher fell back to a generic icon. The build now generates the standard set (16 → 512) plus a
|
||||
proper `.ico` for Windows, and forces `executableName: arboretum` (the scoped package name was
|
||||
producing `@arboretumdesktop` as binary, `.desktop` file and icon name).
|
||||
- **Package metadata.** A non-empty short description in `apt show` (`deb.synopsis` was missing),
|
||||
`Section: devel` instead of `default`, a single-line `Comment` in the desktop entry (it was multi-line,
|
||||
hence invalid), plus `GenericName` and `Keywords` for search.
|
||||
- **Windows.** Build scripts run on a Windows host again (`npm`/`npx` are `.cmd` shims that
|
||||
`execFileSync` cannot resolve; the Node extraction used `unzip` and `bash -c cp/rm`, none of which
|
||||
exist there). The daemon side gained what it needed to actually work: `where.exe` to find the Claude
|
||||
CLI, PowerShell as the launch shell, a `.cmd` askpass so HTTPS clone/push with a token works, and
|
||||
`taskkill /T` so stopping a terminal takes its whole process tree down. CI has a `windows-latest` job,
|
||||
enabled by the `ENABLE_WINDOWS_BUILD` repository variable, see `docs/CI_RUNNERS.md`.
|
||||
- **Auto-update repaired.** Shipped binaries point at a `desktop-latest` release that never existed, so
|
||||
no client could ever see an update. The release workflow now recreates that floating release on every
|
||||
version and attaches the `latest*.yml` files and installers to it, with `.blockmap`s for differential
|
||||
updates and `SHA256SUMS`.
|
||||
- **Smaller download.** The bundled Node runtime is pruned to the binary and its licence (no headers, no
|
||||
docs, no `npm`/`corepack`): ~205 MB → ~118 MB. Nothing at runtime used them, the daemon's dependencies
|
||||
being installed at build time.
|
||||
- **macOS integration.** An application menu (without it ⌘C / ⌘V / ⌘A were not bound anywhere in the
|
||||
app), `app.on('activate')` so the Dock icon brings back a hidden window, and a monochrome template tray
|
||||
icon that follows the menu-bar theme.
|
||||
- **PATH enrichment on Windows.** `%LOCALAPPDATA%\Programs` and `%APPDATA%\npm` are added to the daemon's
|
||||
PATH, where the Claude CLI and global npm binaries live (this was POSIX-only).
|
||||
|
||||
## 0.1.3
|
||||
|
||||
Ships the 3.3.0 daemon ("Start the project": launch commands and multi-terminal boot).
|
||||
|
||||
## 0.1.2
|
||||
|
||||
Ships the 3.2.0 daemon (Emerald visual overhaul, light and dark themes).
|
||||
|
||||
## 0.1.1
|
||||
|
||||
- Fixed the missing window/launcher logo under Debian and Wayland by pinning the runtime app id
|
||||
(`app.setName('Arboretum')`) to the `StartupWMClass` written in the desktop entry.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
First desktop release: an Electron shell that runs the daemon as a child process and opens its UI
|
||||
already authenticated, with a bundled Node runtime, a tray icon, launch-at-login and auto-update.
|
||||
@@ -15,7 +15,8 @@ not by the main `npm run build`.
|
||||
2. The daemon mints a fresh token and writes `{token, url}` on file descriptor 3 (private stdio pipe).
|
||||
3. The shell posts that token to `/api/v1/auth/login` from the window's session (server to server),
|
||||
which drops the `arb_session` cookie into the session jar, then loads the SPA on `127.0.0.1`.
|
||||
4. On quit, the daemon child gets `SIGTERM` (then `SIGKILL` after a grace delay).
|
||||
4. On quit, the daemon child is asked to stop (`SIGTERM` on POSIX, `taskkill /T` on Windows, which
|
||||
Windows requires to take the whole process tree down rather than leaving PTY grandchildren behind).
|
||||
|
||||
A standalone Node runtime (pinned, >= 22.16) is bundled instead of reusing Electron's Node, so
|
||||
`node:sqlite` works without a flag and the `node-pty` prebuild keeps the `node.` ABI prefix.
|
||||
@@ -56,24 +57,61 @@ Fully supported. `dist:linux` runs on a Linux host or the Gitea CI runner.
|
||||
|
||||
### Windows
|
||||
|
||||
Build on a Windows host (recommended): the `node-pty` win32 native binary and the installer
|
||||
(`makensis`) are most reliable there. Cross-building from Linux via Wine is a best-effort fallback.
|
||||
The app uses ConPTY (Windows 10 1809+). The installer is not code-signed yet, so SmartScreen shows
|
||||
**Must be built on a Windows host.** Cross-building from Linux (including via Wine) does not work, and
|
||||
the option has been removed from this document to stop people losing time on it:
|
||||
|
||||
- `node-pty`'s `check-prebuild.js` exits successfully as soon as the *host* binary exists, so
|
||||
`prebuild-install` never runs and no win32 binary is fetched (its published tarball only ships
|
||||
`prebuilds/linux-*`);
|
||||
- its `post-install.js` copies `conpty.dll` and `OpenConsole.exe` **only when the build platform is
|
||||
win32**. Without them there is no ConPTY, hence no terminal at all.
|
||||
|
||||
In CI this is a dedicated job on a `windows-latest` runner, enabled by the `ENABLE_WINDOWS_BUILD`
|
||||
repository variable. Full procedure to register such a runner: [`docs/CI_RUNNERS.md`](../../docs/CI_RUNNERS.md).
|
||||
|
||||
The app requires Windows 10 1809+ (ConPTY). The installer is not code-signed, so SmartScreen shows
|
||||
"unknown publisher": choose "More info" then "Run anyway".
|
||||
|
||||
### macOS (best-effort)
|
||||
|
||||
Build on a Mac (`dmg`/`zip` cannot be produced elsewhere). The app is **not** signed or notarized,
|
||||
so Gatekeeper blocks the first launch: right-click the app then "Open", or run
|
||||
`xattr -dr com.apple.quarantine /Applications/Arboretum.app`.
|
||||
Build on a Mac (`dmg`/`zip` cannot be produced elsewhere); there is no macOS runner, so it is a manual
|
||||
step. The app is **not** signed or notarized, so Gatekeeper blocks the first launch: right-click the app
|
||||
then "Open", or run `xattr -dr com.apple.quarantine /Applications/Arboretum.app`.
|
||||
|
||||
## What the shell adds beyond the window
|
||||
|
||||
- **Tray icon** (`src/main/tray.ts`): open the window, toggle launch-at-login, quit. On macOS it uses a
|
||||
monochrome *template* image so it follows the menu-bar theme.
|
||||
- **Application menu** (`src/main/app-menu.ts`): required on macOS, where without it ⌘C / ⌘V / ⌘A are not
|
||||
bound anywhere in the app. Closing the window hides it; `app.on('activate')` brings it back from the Dock.
|
||||
- **Launch at login** (`src/main/autostart.ts`): a `.desktop` file under `~/.config/autostart` on Linux,
|
||||
`app.setLoginItemSettings` on Windows/macOS.
|
||||
- **Auto-update** (`src/main/updater.ts`): see below.
|
||||
- **PATH enrichment** (`src/main/env.ts`): a GUI app starts with a minimal PATH. On POSIX we add
|
||||
`/usr/local/bin`, `/opt/homebrew/bin`, `~/.local/bin`; on Windows `%LOCALAPPDATA%\Programs` and
|
||||
`%APPDATA%\npm`, where the Claude CLI and global npm binaries actually live.
|
||||
|
||||
## Auto-update
|
||||
|
||||
electron-builder emits `latest*.yml` next to the artifacts; `electron-updater` (wired in a later
|
||||
change) points at the Gitea release assets. Auto-update works for Windows (NSIS) and Linux
|
||||
(AppImage); macOS updates are manual while the app is unsigned.
|
||||
electron-builder emits `latest*.yml` next to the artifacts and `electron-updater` reads them from a
|
||||
**floating `desktop-latest` release** on Gitea, which the release workflow recreates on every version
|
||||
(that URL is baked into shipped binaries, so it must always exist). Auto-update covers Windows (NSIS)
|
||||
and Linux (AppImage); macOS updates are manual while the app is unsigned.
|
||||
|
||||
## Icon
|
||||
## Bundled Node runtime
|
||||
|
||||
`resources/icon.png` (square, >= 512px) is the single source; electron-builder derives every
|
||||
platform icon from it.
|
||||
`scripts/fetch-node.mjs` downloads a pinned Node (SHA256 verified) and **prunes it** to the binary and
|
||||
its licence: headers, docs and `npm`/`corepack` are removed, since the daemon's dependencies are
|
||||
installed at build time, never at runtime. That takes the embedded runtime from ~205 MB to ~118 MB.
|
||||
|
||||
## Icons
|
||||
|
||||
Generated by `python3 brand/build-assets.py` from the source logo, into `resources/`:
|
||||
|
||||
- `icons/{16,24,32,48,64,128,256,512}x*.png` : the Linux set, at **standard hicolor sizes**. This is not
|
||||
cosmetic: with a single non-standard size (the old 895×895), the directory is not declared in
|
||||
`hicolor/index.theme` and the freedesktop spec makes desktops ignore it, so the launcher showed no
|
||||
icon at all.
|
||||
- `icon.png` (1024) : macOS source and generic fallback.
|
||||
- `icon.ico` : Windows (NSIS installer and window).
|
||||
- `trayTemplate.png` (+`@2x`) : monochrome macOS menu-bar icon.
|
||||
|
||||
@@ -21,27 +21,59 @@ extraResources:
|
||||
to: node
|
||||
- from: resources/icon.png
|
||||
to: icon.png
|
||||
- from: resources/trayTemplate.png
|
||||
to: trayTemplate.png
|
||||
- from: resources/trayTemplate@2x.png
|
||||
to: trayTemplate@2x.png
|
||||
|
||||
# Icône : electron-builder dérive toutes les tailles/formats par OS depuis resources/icon.png
|
||||
# (buildResources), pas besoin de .ico/.icns séparés.
|
||||
# Icônes : générées par `python3 brand/build-assets.py` depuis le logo source.
|
||||
# - `resources/icons/` : jeu Linux aux TAILLES STANDARD hicolor (16→512). Indispensable : sans lui,
|
||||
# electron-builder installe l'unique taille du PNG source (895x895), or `hicolor/index.theme` ne
|
||||
# déclare pas ce répertoire, donc la spécification freedesktop l'ignore et AUCUN logo n'apparaît
|
||||
# au lanceur (c'était le bug du .deb 0.1.x).
|
||||
# - `resources/icon.png` (1024) : source macOS et dérivations.
|
||||
# - `resources/icon.ico` : Windows (installeur NSIS + fenêtre).
|
||||
linux:
|
||||
target: [AppImage, deb]
|
||||
category: Development
|
||||
icon: resources/icons
|
||||
# `executableName` explicite : sinon electron-builder le dérive du `name` SCOPÉ du package
|
||||
# (@arboretum/desktop → « @arboretumdesktop »), qui se retrouvait dans /usr/bin, le .desktop et son
|
||||
# `Icon=` · un nom d'icône commençant par « @ » n'est pas résoluble.
|
||||
executableName: arboretum
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
synopsis: Self-hosted multi-project AI IDE for git worktrees
|
||||
# Entrée .desktop (forme plate, mergée telle quelle par electron-builder 25). StartupWMClass DOIT
|
||||
# correspondre à l'app_id runtime (posé par app.setName('Arboretum') dans src/main/main.ts) pour
|
||||
# que GNOME/Wayland associe la fenêtre au lanceur et affiche le logo. Redondant avec le défaut
|
||||
# (productName) mais explicite et robuste à un futur changement de productName.
|
||||
desktop:
|
||||
StartupWMClass: Arboretum
|
||||
# Note : le .deb installe l'icône et rafraîchit le cache (postinst electron-builder). L'AppImage,
|
||||
# lui, n'installe aucun .desktop sans intégration (appimaged) : sur Debian, préférer le .deb.
|
||||
GenericName: AI IDE for git worktrees
|
||||
Keywords: git;worktree;claude;ide;terminal;
|
||||
# Pas de `Comment` ici : electron-builder l'écrase systématiquement après la surcharge
|
||||
# (LinuxTargetHelper : desktopMeta.Comment = deb.description || package.json description). C'est
|
||||
# donc la description du package.json qui fait foi, et elle DOIT rester sur une seule ligne : un
|
||||
# texte multi-lignes produirait une entrée .desktop invalide (lignes suivantes lues comme clés).
|
||||
# Note : avec des tailles standard, GTK/KDE résolvent l'icône même sans cache d'icônes rafraîchi
|
||||
# (le postinst d'electron-builder n'appelle pas gtk-update-icon-cache). L'AppImage, lui, n'installe
|
||||
# aucun .desktop sans intégration (appimaged) : sur Debian, préférer le .deb.
|
||||
|
||||
deb:
|
||||
# git est requis pour les operations de worktree ; claude n'est pas dans les depots (documente).
|
||||
depends: [git]
|
||||
# Mainteneur .deb explicite (electron-builder l'exige ; sinon derive de author.email du package.json).
|
||||
maintainer: Johan LEROY <contact@johanleroy.fr>
|
||||
# `synopsis` alimente la description COURTE du paquet : sans lui, `apt show` affichait une ligne
|
||||
# vide (electron-builder concatène `synopsis || ''` puis la description longue).
|
||||
# `synopsis` = description COURTE du paquet : sans elle, `apt show` affichait une ligne vide
|
||||
# (electron-builder concatène `synopsis || ''` puis la description longue). La description longue
|
||||
# reste celle du package.json, volontairement sur une seule ligne (cf. note sur Comment ci-dessus).
|
||||
synopsis: Self-hosted multi-project AI IDE for git worktrees
|
||||
# electron-builder nomme ce champ `packageCategory` (et non `section`) : il alimente le champ
|
||||
# Section: du paquet, qui valait « default » jusqu'ici.
|
||||
packageCategory: devel
|
||||
priority: optional
|
||||
|
||||
win:
|
||||
target:
|
||||
@@ -49,22 +81,33 @@ win:
|
||||
arch: [x64]
|
||||
- target: portable
|
||||
arch: [x64]
|
||||
icon: resources/icon.ico
|
||||
# Affiché par SmartScreen et dans les métadonnées de l'exécutable. Le binaire n'est PAS signé :
|
||||
# SmartScreen montrera « éditeur inconnu » (documenté dans le README).
|
||||
publisherName: Johan LEROY
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
|
||||
nsis:
|
||||
oneClick: false
|
||||
perMachine: false
|
||||
allowToChangeInstallationDirectory: true
|
||||
shortcutName: Arboretum
|
||||
uninstallDisplayName: Arboretum ${version}
|
||||
createDesktopShortcut: true
|
||||
license: ../../LICENSE
|
||||
|
||||
mac:
|
||||
target: [dmg, zip]
|
||||
icon: resources/icon.png
|
||||
category: public.app-category.developer-tools
|
||||
# macOS best-effort : non signe (documente : clic droit -> Ouvrir, ou xattr -dr com.apple.quarantine)
|
||||
identity: null
|
||||
hardenedRuntime: false
|
||||
|
||||
# Auto-update (electron-updater, cable en C5) : provider generic pointant sur les assets de release
|
||||
# Gitea. Genere latest*.yml a cote des artefacts.
|
||||
# Auto-update (electron-updater) : provider generic pointant sur un tag FLOTTANT `desktop-latest`,
|
||||
# que la CI recrée à chaque release en y attachant les installeurs et les `latest*.yml`. Ce tag doit
|
||||
# exister, sinon l'updater reçoit un 404 (c'était le cas jusqu'en 0.1.3) : voir
|
||||
# .gitea/workflows/desktop-release.yml, étape « Publish floating desktop-latest release ».
|
||||
publish:
|
||||
provider: generic
|
||||
url: https://git.lidge.fr/johanleroy/arboretum/releases/download/desktop-latest
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@arboretum/desktop",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@arboretum/desktop",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.0",
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
{
|
||||
"name": "@arboretum/desktop",
|
||||
"private": true,
|
||||
"version": "0.1.3",
|
||||
"description": "Arboretum desktop app: Electron shell that runs the daemon and shows its web UI",
|
||||
"version": "0.2.0",
|
||||
"description": "Self-hosted multi-project AI IDE for git worktrees and Claude Code sessions",
|
||||
"homepage": "https://git-arboretum.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://git.lidge.fr/johanleroy/arboretum.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://git.lidge.fr/johanleroy/arboretum/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"arboretum",
|
||||
"git",
|
||||
"worktree",
|
||||
"claude",
|
||||
"ide",
|
||||
"electron",
|
||||
"desktop"
|
||||
],
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Johan LEROY",
|
||||
|
||||
|
After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 650 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 433 B |
|
After Width: | Height: | Size: 1.1 KiB |
@@ -2,7 +2,7 @@
|
||||
// SHA256. Le daemon tourne SUR ce Node (pas celui d'Electron) pour garantir node:sqlite sans flag
|
||||
// et l'ABI node-pty attendue (prefixe `node.`). Options : --platform / --arch (défaut : hôte).
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -41,9 +41,45 @@ if (expected !== actual) throw new Error(`SHA256 mismatch pour ${name}.${ext}`);
|
||||
|
||||
const archive = join(BUILD, `${name}.${ext}`);
|
||||
writeFileSync(archive, tarball);
|
||||
if (ext === 'zip') execFileSync('unzip', ['-q', archive, '-d', BUILD], { stdio: 'inherit' });
|
||||
else execFileSync('tar', ['-xJf', archive, '-C', BUILD], { stdio: 'inherit' });
|
||||
// aplatir node-vX-os-arch/ -> build/node/
|
||||
execFileSync('bash', ['-c', `cp -R "${join(BUILD, name)}/." "${NODE_DIR}/" && rm -rf "${join(BUILD, name)}" "${archive}"`], { stdio: 'inherit' });
|
||||
// `tar` de Windows 10+ (bsdtar) lit aussi les .zip : une seule commande pour les trois plateformes,
|
||||
// là où `unzip` n'existe pas sur un Windows standard.
|
||||
execFileSync('tar', [ext === 'zip' ? '-xf' : '-xJf', archive, '-C', BUILD], { stdio: 'inherit' });
|
||||
|
||||
console.log(`fetch-node: Node ${NODE_VERSION} (${OS}-${arch}) -> build/node`);
|
||||
// Aplatir node-vX-os-arch/ -> build/node/ avec l'API Node (l'ancien `bash -c 'cp -R … && rm -rf …'`
|
||||
// rendait ce script inexécutable sur Windows, où il n'y a ni bash, ni cp, ni rm).
|
||||
const extracted = join(BUILD, name);
|
||||
cpSync(extracted, NODE_DIR, { recursive: true });
|
||||
rmSync(extracted, { recursive: true, force: true });
|
||||
rmSync(archive, { force: true });
|
||||
|
||||
// --- élagage ---------------------------------------------------------------------------------
|
||||
// On n'embarque QUE de quoi exécuter le daemon. La distribution complète pèse ~205 Mo, dont l'essentiel
|
||||
// est inutile ici : en-têtes de compilation, docs, et surtout npm/corepack (le `npm install --omit=dev`
|
||||
// du daemon a lieu au BUILD, jamais au runtime).
|
||||
const PRUNE = ['include', 'share', 'lib', 'CHANGELOG.md', 'README.md'];
|
||||
for (const rel of PRUNE) rmSync(join(NODE_DIR, rel), { recursive: true, force: true });
|
||||
// les shims npm/npx/corepack (POSIX : bin/, Windows : racine)
|
||||
for (const shim of ['npm', 'npx', 'corepack', 'npm.cmd', 'npx.cmd', 'corepack.cmd', 'npm.ps1', 'npx.ps1', 'corepack.ps1']) {
|
||||
rmSync(join(NODE_DIR, 'bin', shim), { force: true });
|
||||
rmSync(join(NODE_DIR, shim), { force: true });
|
||||
}
|
||||
|
||||
// Garde-fou : le binaire doit avoir survécu à l'élagage.
|
||||
const nodeBin = platform === 'win32' ? join(NODE_DIR, 'node.exe') : join(NODE_DIR, 'bin', 'node');
|
||||
if (!existsSync(nodeBin)) throw new Error(`binaire Node introuvable apres extraction: ${nodeBin}`);
|
||||
|
||||
console.log(`fetch-node: Node ${NODE_VERSION} (${OS}-${arch}) -> build/node (${duMb(NODE_DIR)} Mo)`);
|
||||
|
||||
/** Taille approximative d'un dossier, en Mo (diagnostic de l'élagage). */
|
||||
function duMb(dir) {
|
||||
let total = 0;
|
||||
const walk = (d) => {
|
||||
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
||||
const p = join(d, entry.name);
|
||||
if (entry.isDirectory()) walk(p);
|
||||
else if (entry.isFile()) total += statSync(p).size;
|
||||
}
|
||||
};
|
||||
walk(dir);
|
||||
return Math.round(total / 1024 / 1024);
|
||||
}
|
||||
|
||||
@@ -20,8 +20,12 @@ const arg = (name) => args.find((a) => a.startsWith(`--${name}=`))?.split('=')[1
|
||||
const platform = arg('platform');
|
||||
const arch = arg('arch');
|
||||
|
||||
// Sur Windows, `npm`/`npx` sont des shims `.cmd` : `execFileSync` ne les résout pas (ENOENT), il faut
|
||||
// leur nom complet. Sans ça, tout le chemin de build documenté échouait sur un hôte Windows.
|
||||
const winShim = (cmd) => (process.platform === 'win32' && (cmd === 'npm' || cmd === 'npx') ? `${cmd}.cmd` : cmd);
|
||||
|
||||
const run = (cmd, cmdArgs, cwd, env) =>
|
||||
execFileSync(cmd, cmdArgs, { cwd, stdio: 'inherit', env: { ...process.env, ...env } });
|
||||
execFileSync(winShim(cmd), cmdArgs, { cwd, stdio: 'inherit', env: { ...process.env, ...env } });
|
||||
|
||||
rmSync(SERVER_DIR, { recursive: true, force: true });
|
||||
mkdirSync(SERVER_DIR, { recursive: true });
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { app, Menu, shell, type MenuItemConstructorOptions } from 'electron';
|
||||
|
||||
/**
|
||||
* Menu applicatif. Sur macOS il n'est PAS optionnel : sans lui, aucun raccourci d'édition n'est
|
||||
* enregistré et ⌘C / ⌘V / ⌘A / ⌘Z ne fonctionnent nulle part dans l'app (y compris dans les terminaux
|
||||
* et l'éditeur). Sur Linux/Windows on garde un menu minimal, masqué par défaut (`setMenuBarVisibility`
|
||||
* côté fenêtre) mais qui enregistre quand même les accélérateurs standard.
|
||||
*/
|
||||
export function installAppMenu(opts: { url: string; onQuit: () => void }): void {
|
||||
const isMac = process.platform === 'darwin';
|
||||
|
||||
const macAppMenu: MenuItemConstructorOptions[] = isMac
|
||||
? [
|
||||
{
|
||||
label: app.name,
|
||||
submenu: [
|
||||
{ role: 'about' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide' },
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Quit Arboretum', accelerator: 'Command+Q', click: opts.onQuit },
|
||||
],
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const template: MenuItemConstructorOptions[] = [
|
||||
...macAppMenu,
|
||||
{
|
||||
label: 'File',
|
||||
submenu: isMac ? [{ role: 'close' }] : [{ label: 'Quit', accelerator: 'Ctrl+Q', click: opts.onQuit }],
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{ role: 'undo' },
|
||||
{ role: 'redo' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'cut' },
|
||||
{ role: 'copy' },
|
||||
{ role: 'paste' },
|
||||
{ role: 'selectAll' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{ role: 'reload' },
|
||||
{ role: 'forceReload' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetZoom' },
|
||||
{ role: 'zoomIn' },
|
||||
{ role: 'zoomOut' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen' },
|
||||
{ role: 'toggleDevTools' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Window',
|
||||
submenu: isMac ? [{ role: 'minimize' }, { role: 'zoom' }, { type: 'separator' }, { role: 'front' }] : [{ role: 'minimize' }],
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{ label: 'Open in browser', click: () => void shell.openExternal(opts.url) },
|
||||
{ label: 'Website', click: () => void shell.openExternal('https://git-arboretum.com') },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
||||
|
||||
app.setAboutPanelOptions({
|
||||
applicationName: 'Arboretum',
|
||||
applicationVersion: app.getVersion(),
|
||||
copyright: 'Copyright © 2026 Johan Leroy',
|
||||
website: 'https://git-arboretum.com',
|
||||
});
|
||||
}
|
||||
@@ -6,10 +6,30 @@ import { homedir } from 'node:os';
|
||||
// `claude`. Le réglage `claude_bin_path` (UI) reste le filet de secours.
|
||||
export function buildChildEnv(extra: Record<string, string>): NodeJS.ProcessEnv {
|
||||
const env: NodeJS.ProcessEnv = { ...process.env, ...extra };
|
||||
if (process.platform !== 'win32') {
|
||||
const extras = ['/usr/local/bin', '/opt/homebrew/bin', join(homedir(), '.local', 'bin'), '/usr/bin', '/bin'];
|
||||
const extras = pathExtras(process.platform, env);
|
||||
if (extras.length > 0) {
|
||||
const current = env.PATH ? env.PATH.split(delimiter) : [];
|
||||
env.PATH = [...new Set([...extras, ...current])].join(delimiter);
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Répertoires à ajouter au PATH du daemon, par plateforme. Windows était entièrement ignoré : or
|
||||
* l'installeur natif de Claude Code se pose dans %LOCALAPPDATA%\Programs et npm global dans
|
||||
* %APPDATA%\npm, deux emplacements absents du PATH d'une app lancée depuis le menu Démarrer.
|
||||
*/
|
||||
export function pathExtras(platform: NodeJS.Platform, env: NodeJS.ProcessEnv = process.env): string[] {
|
||||
const home = env.USERPROFILE ?? homedir();
|
||||
if (platform === 'win32') {
|
||||
const local = env.LOCALAPPDATA ?? join(home, 'AppData', 'Local');
|
||||
const roaming = env.APPDATA ?? join(home, 'AppData', 'Roaming');
|
||||
return [
|
||||
join(local, 'Programs'),
|
||||
join(local, 'Programs', 'claude'),
|
||||
join(roaming, 'npm'),
|
||||
join(home, '.local', 'bin'),
|
||||
];
|
||||
}
|
||||
return ['/usr/local/bin', '/opt/homebrew/bin', join(home, '.local', 'bin'), '/usr/bin', '/bin'];
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { startDaemon, type DaemonHandle } from './daemon';
|
||||
import { seedSessionCookie } from './auth';
|
||||
import { loadWindowState, saveWindowState } from './window-state';
|
||||
import { createTray } from './tray';
|
||||
import { installAppMenu } from './app-menu';
|
||||
import { initUpdater } from './updater';
|
||||
import { resolveIconPath } from './paths';
|
||||
|
||||
@@ -39,10 +40,15 @@ async function bootstrap(): Promise<void> {
|
||||
daemon = await startDaemon({ dataDir, port: PORT, onLog: (l) => process.stdout.write(l) });
|
||||
await seedSessionCookie(PARTITION, daemon.url, daemon.token);
|
||||
createWindow(daemon.url);
|
||||
installAppMenu({ url: daemon.url, onQuit: quitApp });
|
||||
tray = createTray({ show: showWindow, quit: quitApp });
|
||||
initUpdater();
|
||||
}
|
||||
|
||||
// macOS : la fenêtre est cachée (pas détruite) à la fermeture. Sans ce handler, cliquer l'icône du
|
||||
// Dock ne la ramenait jamais et l'app paraissait bloquée en arrière-plan.
|
||||
app.on('activate', showWindow);
|
||||
|
||||
function showWindow(): void {
|
||||
if (!win) return;
|
||||
if (win.isMinimized()) win.restore();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { app } from 'electron';
|
||||
import { join } from 'node:path';
|
||||
import { existsSync } from 'node:fs';
|
||||
|
||||
// Résolution des chemins runtime : dev (depuis le repo) vs packagé (extraResources).
|
||||
// __dirname pointe sur dist/ (bundle esbuild) une fois construit.
|
||||
@@ -23,6 +24,19 @@ export function resolveIconPath(): string {
|
||||
: join(__dirname, '..', 'resources', 'icon.png');
|
||||
}
|
||||
|
||||
/**
|
||||
* Icône de barre système. macOS exige une image « template » (monochrome) dans la barre de menus ;
|
||||
* ailleurs on retombe sur le logo couleur. `null` si l'asset n'est pas présent (build sans
|
||||
* régénération des icônes) : l'appelant utilise alors resolveIconPath().
|
||||
*/
|
||||
export function resolveTrayIconPath(): string | null {
|
||||
if (process.platform !== 'darwin') return null;
|
||||
const path = app.isPackaged
|
||||
? join(process.resourcesPath, 'trayTemplate.png')
|
||||
: join(__dirname, '..', 'resources', 'trayTemplate.png');
|
||||
return existsSync(path) ? path : null;
|
||||
}
|
||||
|
||||
/** Binaire Node qui exécute le daemon (>= 22.16 : node:sqlite + ABI node-pty maîtrisé). */
|
||||
export function resolveNodeBin(): string {
|
||||
if (app.isPackaged) {
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { Menu, Tray, nativeImage } from 'electron';
|
||||
import { isAutoStartEnabled, setAutoStart } from './autostart';
|
||||
import { resolveIconPath } from './paths';
|
||||
import { resolveIconPath, resolveTrayIconPath } from './paths';
|
||||
|
||||
/** Icône de barre système : ouvrir la fenêtre, basculer le lancement au login, quitter. */
|
||||
export function createTray(opts: { show: () => void; quit: () => void }): Tray {
|
||||
const image = nativeImage.createFromPath(resolveIconPath());
|
||||
const tray = new Tray(image.isEmpty() ? nativeImage.createEmpty() : image.resize({ width: 18, height: 18 }));
|
||||
// macOS exige une image TEMPLATE (monochrome + alpha) dans la barre de menus : elle s'inverse
|
||||
// automatiquement selon le thème système. Une icône couleur y est illisible. Windows attend 16px.
|
||||
const trayPath = resolveTrayIconPath() ?? resolveIconPath();
|
||||
const raw = nativeImage.createFromPath(trayPath);
|
||||
const size = process.platform === 'darwin' ? 16 : process.platform === 'win32' ? 16 : 18;
|
||||
const image = raw.isEmpty() ? nativeImage.createEmpty() : raw.resize({ width: size, height: size });
|
||||
if (process.platform === 'darwin' && !image.isEmpty()) image.setTemplateImage(true);
|
||||
const tray = new Tray(image);
|
||||
tray.setToolTip('Arboretum');
|
||||
|
||||
const buildMenu = (): void => {
|
||||
|
||||