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
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:
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user