release: git-arboretum 3.3.0 (« Démarrer le projet » : lancement multi-terminaux), vscode 0.4.0, desktop 0.1.3
CI / Build & test (Node 22) (push) Successful in 10m17s
CI / Build & test (Node 24) (push) Successful in 10m13s
CI / No em/en dashes (push) Successful in 4s
Deploy site (production) / build-and-deploy (push) Successful in 24s
Release / Publish to Gitea npm registry (push) Successful in 10m17s
VSCode Release / Package VSIX (push) Successful in 9m39s
Desktop Release / Build Linux (AppImage + deb) (push) Successful in 15m41s
CI / Pack & boot smoke (Node 22) (push) Successful in 10m3s

« Démarrer le projet » : un repo définit une fois ses commandes de démarrage
(serveur de dev, API, base de données), un clic ouvre un terminal PTY par
commande dans le dock IDE.

Serveur (additif, PROTOCOL_VERSION inchangé) :
- LaunchCommand[] persistées sur repos.launch_commands (migration 13) ; champ additif SessionSummary.launchRunId.
- POST /repos/:id/launch : résolution du worktree côté serveur, cwd de commande borné (anti-traversal), commandIds outrepasse enabled.
- GET /repos/:id/launch/detect : détection package.json / Procfile / docker-compose.
- Shell de login interactif ($SHELL -l -i, charge le PATH nvm/asdf) + auto-type de la commande ; le shell survit à la commande (échec visible).

Web : LaunchProjectModal + actions (ProjectTreeNode, SessionsPanel, CommandPalette), stores sessions/worktrees, i18n EN/FR.

Alignement du reste du projet :
- Extension VS Code 0.4.0 : commande Start Project (repo/worktree), Stop Launch, badge « launch » dans l'arbre, méthode REST startLaunch.
- Site vitrine : 16e feature card (Rocket) + section showcase « Start the project » (mockup fidèle au modal), i18n EN/FR.
- Documentation : README (EN + FR), help-content (EN + FR), CHANGELOGs server + vscode.

Vérifié : 430 tests, typecheck, build (web + site + vscode), acceptance-p13 ALL GREEN, VSIX packagé, garde anti-tirets, vérif visuelle du site (thèmes clair et sombre).
This commit is contained in:
2026-07-21 13:54:16 +02:00
parent 7327407193
commit a7e04278fd
39 changed files with 1226 additions and 25 deletions
+48 -4
View File
@@ -40,8 +40,29 @@ type HistoricalRow = {
added_dirs: string | null;
group_id: string | null;
archived_at: string | null;
launch_run_id: string | null;
};
/** Longueur max d'une commande auto-tapée (garde-fou ; une ligne shell raisonnable). */
const MAX_INITIAL_INPUT_LEN = 4096;
/**
* Assainit une commande de lancement avant de l'écrire dans le PTY : trim, borne de longueur,
* retrait de tous les caractères de contrôle (dont retours chariot/ligne : le `\r` de soumission
* est ajouté par l'appelant). Empêche l'injection de plusieurs lignes / séquences de contrôle par
* le champ de commande ; le modèle de menace reste inchangé (terminal = RCE par conception).
*/
function sanitizeInitialInput(raw: string): string {
let out = '';
for (const ch of raw.slice(0, MAX_INITIAL_INPUT_LEN)) {
const code = ch.codePointAt(0) ?? 0;
// saute les caractères de contrôle C0 (0x00-0x1F) et DEL (0x7F) : ni multi-lignes ni séquences ANSI.
if (code < 0x20 || code === 0x7f) continue;
out += ch;
}
return out.trim();
}
/** Parse la colonne `added_dirs` (JSON array de chemins) de façon défensive ; [] si NULL/invalide. */
function parseAddedDirs(raw: string | null): string[] {
if (!raw) return [];
@@ -85,6 +106,8 @@ interface ManagedSession {
addedDirs: string[];
/** groupe propriétaire d'une session de groupe multi-repo ; null sinon (P6). */
groupId: string | null;
/** identifiant partagé par les terminaux d'un même « Démarrer le projet » ; null sinon. */
launchRunId: string | null;
/** détection d'état fin (busy/waiting/idle + dialogue) ; null pour bash (P3-B). */
tracker: SessionActivityTracker | null;
/** dernière activité notifiée (détection du front montant vers `waiting` pour le push P4-B). */
@@ -117,6 +140,14 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
addDirs?: string[];
/** groupe propriétaire (session de groupe, P6). */
groupId?: string;
/** shell de login interactif (charge le PATH utilisateur) : lancement de projet uniquement. */
login?: boolean;
/** commande auto-tapée dans le PTY juste après le spawn (« Démarrer le projet »). */
initialInput?: string;
/** titre initial de la session (libellé de l'onglet ; ex. label de commande de lancement). */
title?: string;
/** identifiant partagé par tous les terminaux d'un même lancement de projet. */
launchRunId?: string;
}): SessionSummary {
const cwd = opts.cwd;
if (!existsSync(cwd) || !statSync(cwd).isDirectory()) {
@@ -138,6 +169,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
...(claudeBinPath ? { claudeBinPath } : {}),
...(opts.resume ? { resume: opts.resume } : {}),
...(addedDirs.length ? { addDirs: addedDirs } : {}),
...(opts.login ? { login: true } : {}),
});
const id = randomUUID();
const proc = pty.spawn(spec.file, spec.args, {
@@ -151,7 +183,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
id,
cwd,
command,
title: null,
title: opts.title ?? null,
createdAt: new Date().toISOString(),
proc,
ring: new RingBuffer(RING_CAPACITY),
@@ -162,6 +194,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
claudeSessionId: null,
addedDirs,
groupId: opts.groupId ?? null,
launchRunId: opts.launchRunId ?? null,
tracker: null,
prevActivity: null,
notifyTimer: null,
@@ -177,19 +210,28 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
}
this.live.set(id, session);
this.db
.prepare('INSERT INTO sessions (id, cwd, command, created_at, resumed_from, added_dirs, group_id) VALUES (?, ?, ?, ?, ?, ?, ?)')
.prepare('INSERT INTO sessions (id, cwd, command, title, created_at, resumed_from, added_dirs, group_id, launch_run_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
.run(
id,
cwd,
command,
session.title,
session.createdAt,
opts.resume?.claudeSessionId ?? null,
addedDirs.length ? JSON.stringify(addedDirs) : null,
session.groupId,
session.launchRunId,
);
proc.onData((data) => this.handleOutput(session, Buffer.from(data, 'utf8')));
proc.onExit(({ exitCode, signal }) => this.handleExit(session, exitCode, signal ?? null));
// Auto-type de la commande de lancement APRÈS onData : la commande et sa sortie entrent dans le
// ring et sont rejouées à l'attach. Les octets sont mis en file par le tty tant que le shell
// n'a pas commencé à lire → pas de course. Un seul `\r` final (aligné sur answer()).
if (opts.initialInput) {
const line = sanitizeInitialInput(opts.initialInput);
if (line) proc.write(`${line}\r`);
}
if (command === 'claude') this.captureClaudeSessionId(session);
const summary = this.summarize(session);
@@ -269,7 +311,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
const liveSummaries = [...this.live.values()].map((s) => this.summarize(s));
const liveIds = new Set(this.live.keys());
const rows = this.db
.prepare('SELECT id, cwd, command, title, created_at, ended_at, exit_code, claude_session_id, added_dirs, group_id, archived_at FROM sessions ORDER BY created_at DESC LIMIT 100')
.prepare('SELECT id, cwd, command, title, created_at, ended_at, exit_code, claude_session_id, added_dirs, group_id, archived_at, launch_run_id FROM sessions ORDER BY created_at DESC LIMIT 100')
.all() as HistoricalRow[];
const historical: SessionSummary[] = rows
.filter((r) => !liveIds.has(r.id))
@@ -302,6 +344,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
registryStatus: null,
...(addedDirs.length ? { addedDirs } : {}),
groupId: r.group_id,
launchRunId: r.launch_run_id,
archived: r.archived_at != null,
};
}
@@ -314,7 +357,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
emitHistoricalUpdate(id: string): void {
if (this.live.has(id)) return;
const r = this.db
.prepare('SELECT id, cwd, command, title, created_at, ended_at, exit_code, claude_session_id, added_dirs, group_id, archived_at FROM sessions WHERE id = ?')
.prepare('SELECT id, cwd, command, title, created_at, ended_at, exit_code, claude_session_id, added_dirs, group_id, archived_at, launch_run_id FROM sessions WHERE id = ?')
.get(id) as HistoricalRow | undefined;
if (!r) return;
this.emit('session_update', this.historicalSummary(r));
@@ -558,6 +601,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
dialog: act?.dialog ?? null,
...(s.addedDirs.length ? { addedDirs: s.addedDirs } : {}),
groupId: s.groupId,
launchRunId: s.launchRunId,
};
}
}