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
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:
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
@@ -41,14 +41,21 @@ jobs:
|
|||||||
cache: npm
|
cache: npm
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
# @arboretum/shared est une dépendance runtime non publiée : on packe les
|
# @arboretum/shared (dépendance runtime non publiée) est embarquée dans le tarball
|
||||||
# deux tarballs et on les installe ensemble dans un projet vierge.
|
# via bundleDependencies (matérialisée au prepack). On packe donc UN SEUL tarball
|
||||||
- name: Pack tarballs
|
# et on l'installe seul, comme un vrai consommateur depuis le registre.
|
||||||
|
- name: Pack tarball
|
||||||
run: |
|
run: |
|
||||||
mkdir -p /tmp/tarballs
|
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
|
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: |
|
run: |
|
||||||
mkdir /tmp/smoke
|
mkdir /tmp/smoke
|
||||||
cd /tmp/smoke
|
cd /tmp/smoke
|
||||||
|
|||||||
42
.github/workflows/release.yml
vendored
Normal file
42
.github/workflows/release.yml
vendored
Normal 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
4
.gitignore
vendored
@@ -7,6 +7,10 @@ packages/server/public/
|
|||||||
*.tgz
|
*.tgz
|
||||||
*.tsbuildinfo
|
*.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
|
# Test / coverage
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
|
|||||||
5
.npmrc
Normal file
5
.npmrc
Normal 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/
|
||||||
47
README.md
47
README.md
@@ -40,8 +40,17 @@ A single Node.js daemon you run on your dev machine (`npx git-arboretum`), servi
|
|||||||
|
|
||||||
### Run it (recommended)
|
### 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
|
```bash
|
||||||
npx git-arboretum
|
npx @johanleroy/git-arboretum
|
||||||
```
|
```
|
||||||
|
|
||||||
On first start, Arboretum prints a one-time **access token** and the URL to open:
|
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):
|
Then start Arboretum allowing your tailnet origin (the strict Origin check needs to know about it):
|
||||||
|
|
||||||
```bash
|
```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.
|
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.
|
> ⚠️ 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
|
## Configuration
|
||||||
|
|
||||||
All options are CLI flags:
|
All options are CLI flags:
|
||||||
@@ -141,7 +182,7 @@ Arboretum wraps the **interactive** Claude Code CLI in a PTY — the same thing
|
|||||||
|
|
||||||
## Development
|
## 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
|
```bash
|
||||||
npm run build # build shared → server → web (order matters)
|
npm run build # build shared → server → web (order matters)
|
||||||
|
|||||||
15
package-lock.json
generated
15
package-lock.json
generated
@@ -848,6 +848,10 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@johanleroy/git-arboretum": {
|
||||||
|
"resolved": "packages/server",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"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": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/git-arboretum": {
|
|
||||||
"resolved": "packages/server",
|
|
||||||
"link": true
|
|
||||||
},
|
|
||||||
"node_modules/github-from-package": {
|
"node_modules/github-from-package": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||||
@@ -4959,8 +4959,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "git-arboretum",
|
"name": "@johanleroy/git-arboretum",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
|
"bundleDependencies": [
|
||||||
|
"@arboretum/shared"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@arboretum/shared": "0.1.0",
|
"@arboretum/shared": "0.1.0",
|
||||||
|
|||||||
@@ -10,11 +10,11 @@
|
|||||||
"node": ">=22.16"
|
"node": ">=22.16"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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",
|
"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",
|
"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"
|
"dev:web": "npm run dev -w @arboretum/web"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,26 +1,55 @@
|
|||||||
{
|
{
|
||||||
"name": "git-arboretum",
|
"name": "@johanleroy/git-arboretum",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"description": "Self-hosted web dashboard for git worktrees and the Claude Code sessions running on them",
|
"description": "Self-hosted web dashboard for git worktrees and the Claude Code sessions running on them",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"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": {
|
"bin": {
|
||||||
"arboretum": "./dist/index.js"
|
"arboretum": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"main": "./dist/app.js",
|
"main": "./dist/app.js",
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist/**/*.js",
|
||||||
"public"
|
"public"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.16"
|
"node": ">=22.16"
|
||||||
},
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://git.lidge.fr/api/packages/johanleroy/npm/"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b",
|
"build": "tsc -b",
|
||||||
"dev": "tsc -b --watch & node --watch dist/index.js",
|
"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"
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
|
"bundleDependencies": [
|
||||||
|
"@arboretum/shared"
|
||||||
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@arboretum/shared": "0.1.0",
|
"@arboretum/shared": "0.1.0",
|
||||||
"@fastify/cookie": "^11.0.0",
|
"@fastify/cookie": "^11.0.0",
|
||||||
|
|||||||
33
packages/server/scripts/copy-meta.mjs
Normal file
33
packages/server/scripts/copy-meta.mjs
Normal 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);
|
||||||
|
}
|
||||||
43
packages/server/scripts/vendor-shared.mjs
Normal file
43
packages/server/scripts/vendor-shared.mjs
Normal 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}`);
|
||||||
Reference in New Issue
Block a user