release: @johanleroy/git-arboretum 1.0.0 — publication sur le registre Gitea
Some checks failed
Release / Publish to Gitea npm registry (push) Has been cancelled

- renomme le paquet en @johanleroy/git-arboretum (scope routé vers le registre npm Gitea privé)
- embarque @arboretum/shared via bundleDependencies (scripts/vendor-shared.mjs au prepack)
- joint README/LICENSE au tarball (scripts/copy-meta.mjs) + metadata, keywords, publishConfig
- CI pack-smoke en mono-tarball avec assertion bundleDep ; nouveau workflow release.yml (publish sur tag v*)
- version 1.0.0 ; README mis à jour (install scopé + service systemd)
This commit is contained in:
2026-06-17 13:53:30 +02:00
parent 3acf27d48f
commit 08166c108b
10 changed files with 228 additions and 21 deletions

View File

@@ -41,14 +41,21 @@ jobs:
cache: npm
- run: npm ci
- run: npm run build
# @arboretum/shared est une dépendance runtime non publiée : on packe les
# deux tarballs et on les installe ensemble dans un projet vierge.
- name: Pack tarballs
# @arboretum/shared (dépendance runtime non publiée) est embarquée dans le tarball
# via bundleDependencies (matérialisée au prepack). On packe donc UN SEUL tarball
# et on l'installe seul, comme un vrai consommateur depuis le registre.
- name: Pack tarball
run: |
mkdir -p /tmp/tarballs
npm pack -w @arboretum/shared -w git-arboretum --pack-destination /tmp/tarballs
npm pack -w @johanleroy/git-arboretum --pack-destination /tmp/tarballs
ls -l /tmp/tarballs
- name: Install tarballs in an empty project
- name: Assert @arboretum/shared is bundled in the tarball
run: |
tgz=$(ls /tmp/tarballs/*.tgz)
tar -tzf "$tgz" | grep -q 'node_modules/@arboretum/shared/dist/index.js' \
|| { echo "ERREUR: @arboretum/shared non embarqué dans $tgz"; exit 1; }
echo "OK: bundleDependency @arboretum/shared présente dans $tgz"
- name: Install tarball in an empty project
run: |
mkdir /tmp/smoke
cd /tmp/smoke

42
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
# Publication du paquet @johanleroy/git-arboretum sur le registre npm du Gitea,
# déclenchée UNIQUEMENT par un tag vX.Y.Z (jamais sur un push de branche).
name: Release
on:
push:
tags: ['v*']
permissions:
contents: read
packages: write
jobs:
publish:
name: Publish to Gitea npm registry
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: 'https://git.lidge.fr/api/packages/johanleroy/npm/'
scope: '@johanleroy'
cache: npm
- run: npm ci
- run: npm run build
# Garde-fou : le tag (sans le "v") doit correspondre à la version du paquet.
# GITHUB_REF_NAME est une variable d'env du runner (pas d'interpolation ${{ }} dans le shell).
- name: Verify tag matches package version
run: |
pkg=$(node -p "require('./packages/server/package.json').version")
tag="${GITHUB_REF_NAME#v}"
if [ "$pkg" != "$tag" ]; then
echo "ERREUR: tag '$tag' != version paquet '$pkg'"
exit 1
fi
echo "OK: tag $tag == version $pkg"
- run: npm publish -w @johanleroy/git-arboretum
env:
# Mapper le secret du registre (PAT Gitea write:package, ou le token auto
# GITEA_TOKEN s'il a ce scope) sur NODE_AUTH_TOKEN lu par le .npmrc de setup-node.
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

4
.gitignore vendored
View File

@@ -7,6 +7,10 @@ packages/server/public/
*.tgz
*.tsbuildinfo
# README/LICENSE copiés dans le paquet au prepack (générés depuis la racine)
packages/server/README.md
packages/server/LICENSE
# Test / coverage
coverage/

5
.npmrc Normal file
View File

@@ -0,0 +1,5 @@
# Route le scope @johanleroy vers le registre npm intégré du Gitea self-hosted.
# Les dépendances publiques (fastify, node-pty…) restent résolues depuis npmjs.
# Le token d'auth (_authToken) ne se commit JAMAIS : il vit dans ~/.npmrc (local)
# ou dans un secret CI (NODE_AUTH_TOKEN écrit par setup-node).
@johanleroy:registry=https://git.lidge.fr/api/packages/johanleroy/npm/

View File

@@ -40,8 +40,17 @@ A single Node.js daemon you run on your dev machine (`npx git-arboretum`), servi
### Run it (recommended)
Arboretum is published to a private Gitea npm registry. Point the `@johanleroy` scope at it once per machine — add to `~/.npmrc`:
```
@johanleroy:registry=https://git.lidge.fr/api/packages/johanleroy/npm/
//git.lidge.fr/api/packages/johanleroy/npm/:_authToken=<your-gitea-token>
```
(The `_authToken` line is only needed if the package is private.) Then run it from anywhere:
```bash
npx git-arboretum
npx @johanleroy/git-arboretum
```
On first start, Arboretum prints a one-time **access token** and the URL to open:
@@ -89,13 +98,45 @@ tailscale serve --bg 7317
Then start Arboretum allowing your tailnet origin (the strict Origin check needs to know about it):
```bash
npx git-arboretum --allow-origin https://<machine>.<tailnet>.ts.net
npx @johanleroy/git-arboretum --allow-origin https://<machine>.<tailnet>.ts.net
```
Open `https://<machine>.<tailnet>.ts.net` from any device on your tailnet. **Web Push requires HTTPS**, so Tailscale Serve (or another HTTPS front) is also what enables mobile notifications. On **iOS**, install the app to your home screen first, then allow notifications.
> ⚠️ A web terminal is remote code execution **by design**. Never expose Arboretum directly to the public internet.
## Running it as a background service
For a daemon that survives logout and restarts on boot, run it under a **systemd user service**. Install a pinned version globally (`npm i -g @johanleroy/git-arboretum`), then create `~/.config/systemd/user/arboretum.service`:
```ini
[Unit]
Description=Arboretum — git worktree & Claude Code dashboard
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=%h/.local/bin/arboretum --port 7317 --allow-origin https://MACHINE.TAILNET.ts.net
Restart=on-failure
RestartSec=5
KillSignal=SIGTERM
TimeoutStopSec=10
Environment=NODE_ENV=production
[Install]
WantedBy=default.target
```
```bash
which arboretum # adjust ExecStart to the real path if needed
systemctl --user daemon-reload
systemctl --user enable --now arboretum
loginctl enable-linger "$USER" # start the service at boot, without an open session
journalctl --user -u arboretum -f # logs
```
> The one-time **access token is printed only on the very first run** (empty database). When running as a service, capture it from `journalctl`, or do one manual run before enabling the service. The token is hashed and never shown again.
## Configuration
All options are CLI flags:
@@ -141,7 +182,7 @@ Arboretum wraps the **interactive** Claude Code CLI in a PTY — the same thing
## Development
Arboretum is an npm-workspaces monorepo: `@arboretum/shared` (WS/REST protocol, source of truth), `git-arboretum` (the Fastify daemon, the published package), and `@arboretum/web` (the Vue 3 SPA).
Arboretum is an npm-workspaces monorepo: `@arboretum/shared` (WS/REST protocol, source of truth), `@johanleroy/git-arboretum` (the Fastify daemon, the published package), and `@arboretum/web` (the Vue 3 SPA).
```bash
npm run build # build shared → server → web (order matters)

15
package-lock.json generated
View File

@@ -848,6 +848,10 @@
"node": ">=18"
}
},
"node_modules/@johanleroy/git-arboretum": {
"resolved": "packages/server",
"link": true
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -3003,10 +3007,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/git-arboretum": {
"resolved": "packages/server",
"link": true
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
@@ -4959,8 +4959,11 @@
}
},
"packages/server": {
"name": "git-arboretum",
"version": "0.1.0",
"name": "@johanleroy/git-arboretum",
"version": "1.0.0",
"bundleDependencies": [
"@arboretum/shared"
],
"license": "MIT",
"dependencies": {
"@arboretum/shared": "0.1.0",

View File

@@ -10,11 +10,11 @@
"node": ">=22.16"
},
"scripts": {
"build": "npm run build -w @arboretum/shared -w git-arboretum -w @arboretum/web",
"build": "npm run build -w @arboretum/shared -w @johanleroy/git-arboretum -w @arboretum/web",
"typecheck": "tsc -b packages/shared packages/server",
"pack": "npm run build && npm pack -w git-arboretum",
"pack": "npm run build && npm pack -w @johanleroy/git-arboretum",
"test": "vitest run",
"dev:server": "npm run dev -w git-arboretum",
"dev:server": "npm run dev -w @johanleroy/git-arboretum",
"dev:web": "npm run dev -w @arboretum/web"
},
"devDependencies": {

View File

@@ -1,26 +1,55 @@
{
"name": "git-arboretum",
"version": "0.1.0",
"name": "@johanleroy/git-arboretum",
"version": "1.0.0",
"description": "Self-hosted web dashboard for git worktrees and the Claude Code sessions running on them",
"license": "MIT",
"type": "module",
"author": "Johan LEROY <contact@johanleroy.fr>",
"homepage": "https://git.lidge.fr/johanleroy/arboretum#readme",
"repository": {
"type": "git",
"url": "git+https://git.lidge.fr/johanleroy/arboretum.git",
"directory": "packages/server"
},
"bugs": {
"url": "https://git.lidge.fr/johanleroy/arboretum/issues"
},
"keywords": [
"git",
"worktree",
"claude",
"claude-code",
"dashboard",
"self-hosted",
"pty",
"terminal",
"cli",
"daemon",
"pwa"
],
"bin": {
"arboretum": "./dist/index.js"
},
"main": "./dist/app.js",
"files": [
"dist",
"dist/**/*.js",
"public"
],
"engines": {
"node": ">=22.16"
},
"publishConfig": {
"registry": "https://git.lidge.fr/api/packages/johanleroy/npm/"
},
"scripts": {
"build": "tsc -b",
"dev": "tsc -b --watch & node --watch dist/index.js",
"prepack": "node scripts/copy-web.mjs",
"prepack": "node scripts/copy-web.mjs && node scripts/vendor-shared.mjs && node scripts/copy-meta.mjs",
"test": "vitest run"
},
"bundleDependencies": [
"@arboretum/shared"
],
"dependencies": {
"@arboretum/shared": "0.1.0",
"@fastify/cookie": "^11.0.0",

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env node
// Copie le README et la LICENSE racine dans packages/server/ avant le pack, pour que
// la page du paquet (registre Gitea) ne soit pas nue et que la licence soit jointe.
// npm inclut automatiquement README* et LICENSE* dans le tarball s'ils sont présents.
// Branché sur le hook "prepack". Ces copies sont gitignorées (générées, non versionnées).
import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const serverDir = join(dirname(fileURLToPath(import.meta.url)), '..');
const rootDir = join(serverDir, '..', '..');
// README : réécrit les chemins d'images relatifs (brand/…) en URL absolue Gitea raw,
// sinon le logo est cassé hors du repo (sur la page du paquet).
const readmeSrc = join(rootDir, 'README.md');
if (existsSync(readmeSrc)) {
const rawBase = 'https://git.lidge.fr/johanleroy/arboretum/raw/branch/main/';
const readme = readFileSync(readmeSrc, 'utf8').replaceAll('src="brand/', `src="${rawBase}brand/`);
writeFileSync(join(serverDir, 'README.md'), readme);
console.log('copy-meta: README.md copié (chemins images réécrits en absolu)');
} else {
console.error(`copy-meta: ${readmeSrc} introuvable`);
process.exit(1);
}
const licenseSrc = join(rootDir, 'LICENSE');
if (existsSync(licenseSrc)) {
copyFileSync(licenseSrc, join(serverDir, 'LICENSE'));
console.log('copy-meta: LICENSE copié');
} else {
console.error(`copy-meta: ${licenseSrc} introuvable`);
process.exit(1);
}

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env node
// Embarque @arboretum/shared (dépendance runtime non publiée) DANS le tarball npm.
// Branché sur le hook "prepack", aux côtés de bundleDependencies dans package.json.
//
// Pourquoi un vrai dossier et pas le symlink workspace : sous npm workspaces, shared
// est seulement symlinké dans le node_modules racine ; `npm pack` n'embarque une
// bundleDependency que si elle existe comme VRAI dossier dans le node_modules du
// paquet packé (packages/server/node_modules/@arboretum/shared). On le matérialise ici.
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const serverDir = join(dirname(fileURLToPath(import.meta.url)), '..');
const sharedDir = join(serverDir, '..', 'shared');
const sharedDist = join(sharedDir, 'dist');
const target = join(serverDir, 'node_modules', '@arboretum', 'shared');
if (!existsSync(sharedDist)) {
console.error(
`vendor-shared: ${sharedDist} introuvable — lance "npm run build" (shared doit être compilé) avant le pack.`,
);
process.exit(1);
}
// package.json minimal et déterministe : on reprend les champs de résolution réels de
// shared (version incluse, pour rester synchro), sans scripts ni files inutiles au runtime.
const shared = JSON.parse(readFileSync(join(sharedDir, 'package.json'), 'utf8'));
const minimal = {
name: shared.name,
version: shared.version,
type: shared.type,
main: shared.main,
types: shared.types,
exports: shared.exports,
};
// Idempotent : on repart d'un dossier propre à chaque pack.
rmSync(target, { recursive: true, force: true });
mkdirSync(target, { recursive: true });
writeFileSync(join(target, 'package.json'), JSON.stringify(minimal, null, 2) + '\n');
cpSync(sharedDist, join(target, 'dist'), { recursive: true });
console.log(`vendor-shared: embarqué ${shared.name}@${shared.version} -> ${target}`);