release: git-arboretum 3.3.0 (« Démarrer le projet » : lancement multi-terminaux), vscode 0.4.0, desktop 0.1.3
All checks were successful
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
All checks were successful
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:
@@ -53,7 +53,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch, type Component } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Search, GitBranch, TerminalSquare, Boxes, Plus, FolderPlus } from '@lucide/vue';
|
||||
import { Search, GitBranch, TerminalSquare, Boxes, Plus, FolderPlus, Rocket } from '@lucide/vue';
|
||||
import type { WorktreeSummary } from '@arboretum/shared';
|
||||
import { useSessionsStore } from '../stores/sessions';
|
||||
import { useWorktreesStore } from '../stores/worktrees';
|
||||
@@ -63,6 +63,7 @@ import { useModalsStore } from '../stores/modals';
|
||||
import { useCommandPalette } from '../composables/useCommandPalette';
|
||||
import NewSessionModal from './ide/modals/NewSessionModal.vue';
|
||||
import GroupCreateModal from './ide/modals/GroupCreateModal.vue';
|
||||
import LaunchProjectModal from './ide/modals/LaunchProjectModal.vue';
|
||||
|
||||
interface PaletteItem {
|
||||
id: string;
|
||||
@@ -103,6 +104,14 @@ const items = computed<PaletteItem[]>(() => {
|
||||
const out: PaletteItem[] = [];
|
||||
for (const r of worktrees.repos) {
|
||||
out.push({ id: `repo-${r.id}`, type: 'repo', label: r.label, sublabel: r.path, icon: GitBranch as Component, keywords: `${r.label} ${r.path}`.toLowerCase(), run: () => revealRepo(r.id) });
|
||||
out.push({
|
||||
id: `launch-${r.id}`,
|
||||
type: 'action',
|
||||
label: t('launch.startNamed', { name: r.label }),
|
||||
icon: Rocket as Component,
|
||||
keywords: `${t('launch.startProject')} ${r.label}`.toLowerCase(),
|
||||
run: () => void modals.open(LaunchProjectModal, { repoId: r.id }),
|
||||
});
|
||||
}
|
||||
for (const w of worktrees.worktrees) {
|
||||
out.push({ id: `wt-${w.path}`, type: 'worktree', label: w.branch ?? basename(w.path), sublabel: w.path, icon: GitBranch as Component, keywords: `${w.branch ?? ''} ${w.path}`.toLowerCase(), run: () => revealWorktree(w) });
|
||||
|
||||
@@ -93,6 +93,7 @@ import {
|
||||
Scissors,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Rocket,
|
||||
} from '@lucide/vue';
|
||||
import type { RepoSummary, SessionSummary, WorktreeSummary } from '@arboretum/shared';
|
||||
import { ApiError } from '../../lib/api';
|
||||
@@ -107,6 +108,7 @@ import SessionStateBadge from '../SessionStateBadge.vue';
|
||||
import ConfirmModal from './modals/ConfirmModal.vue';
|
||||
import WorktreeCreateModal from './modals/WorktreeCreateModal.vue';
|
||||
import NewSessionModal from './modals/NewSessionModal.vue';
|
||||
import LaunchProjectModal from './modals/LaunchProjectModal.vue';
|
||||
|
||||
const props = defineProps<{ repo: RepoSummary }>();
|
||||
const { t } = useI18n();
|
||||
@@ -144,6 +146,7 @@ function onWtClick(wt: WorktreeSummary): void {
|
||||
// --- menu contextuel repo ---
|
||||
function openRepoMenu(e: MouseEvent): void {
|
||||
const items: ContextMenuItem[] = [
|
||||
{ label: t('launch.startProject'), icon: Rocket, onSelect: () => void modals.open(LaunchProjectModal, { repoId: props.repo.id }) },
|
||||
{ label: t('ide.menu.newWorktree'), icon: GitBranch, onSelect: () => void modals.open(WorktreeCreateModal, { repo: props.repo, initialMode: 'worktree' }) },
|
||||
{ label: t('ide.menu.workOnMain'), icon: Home, onSelect: () => void modals.open(WorktreeCreateModal, { repo: props.repo, initialMode: 'main' }) },
|
||||
{ label: t('ide.menu.prune'), icon: Scissors, onSelect: () => void onPrune() },
|
||||
@@ -190,6 +193,7 @@ function confirmRemoveRepo(): void {
|
||||
// --- menu contextuel worktree ---
|
||||
function openWtMenu(e: MouseEvent, wt: WorktreeSummary): void {
|
||||
const items: ContextMenuItem[] = [
|
||||
{ label: t('launch.startProject'), icon: Rocket, onSelect: () => void modals.open(LaunchProjectModal, { repoId: wt.repoId, worktreePath: wt.path, ...(wt.branch ? { targetLabel: wt.branch } : {}) }) },
|
||||
{ label: t('ide.menu.openTerminal'), icon: SquareTerminal, onSelect: () => void modals.open(NewSessionModal, { cwd: wt.path }) },
|
||||
{ label: t('ide.menu.commit'), icon: GitCompare, onSelect: () => openGitPanel(wt) },
|
||||
{ label: t('ide.menu.push'), icon: Upload, onSelect: () => void onPush(wt) },
|
||||
|
||||
@@ -62,6 +62,8 @@ import {
|
||||
ArchiveRestore,
|
||||
RefreshCw,
|
||||
FolderPlus,
|
||||
Rocket,
|
||||
Square,
|
||||
} from '@lucide/vue';
|
||||
import type { SessionSummary } from '@arboretum/shared';
|
||||
import { useIdeStore } from '../../stores/ide';
|
||||
@@ -77,6 +79,7 @@ import AttentionList from './AttentionList.vue';
|
||||
import ConfirmModal from './modals/ConfirmModal.vue';
|
||||
import NewSessionModal from './modals/NewSessionModal.vue';
|
||||
import NewProjectModal from '../NewProjectModal.vue';
|
||||
import LaunchProjectModal from './modals/LaunchProjectModal.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const ide = useIdeStore();
|
||||
@@ -120,6 +123,12 @@ function menuFor(s: SessionSummary): ContextMenuItem[] {
|
||||
else if (s.source === 'discovered') items.push({ label: t('sessions.hide'), icon: EyeOff, onSelect: () => void actions.hide(s.id) });
|
||||
if (s.archived) items.push({ label: t('sessions.unarchive'), icon: ArchiveRestore, onSelect: () => void actions.unarchive(s.id) });
|
||||
else if (s.source === 'managed' && !s.live) items.push({ label: t('sessions.archive'), icon: Archive, onSelect: () => void actions.archive(s.id) });
|
||||
// « Démarrer le projet » : arrêter d'un coup tous les terminaux vivants du même lancement.
|
||||
if (s.live && s.launchRunId) {
|
||||
const runId = s.launchRunId;
|
||||
const n = store.sessions.filter((x) => x.live && x.launchRunId === runId).length;
|
||||
if (n >= 2) items.push({ label: t('launch.stopAll', { n }), icon: Square, danger: true, onSelect: () => void store.stopLaunch(runId) });
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -129,10 +138,20 @@ function openRowMenu(e: MouseEvent, s: SessionSummary): void {
|
||||
}
|
||||
|
||||
function openAddMenu(e: MouseEvent): void {
|
||||
ctx.open(e.clientX, e.clientY, [
|
||||
const items: ContextMenuItem[] = [
|
||||
{ label: t('sessions.newSession'), icon: SquareTerminal, onSelect: () => void modals.open(NewSessionModal) },
|
||||
{ label: t('project.new'), icon: FolderPlus, onSelect: () => void modals.open(NewProjectModal) },
|
||||
]);
|
||||
];
|
||||
// Raccourci « Démarrer le projet » ciblant le worktree actif de l'IDE, quand il existe.
|
||||
const active = ide.activeContext;
|
||||
if (active?.repoId) {
|
||||
items.push({
|
||||
label: t('launch.startProject'),
|
||||
icon: Rocket,
|
||||
onSelect: () => void modals.open(LaunchProjectModal, { repoId: active.repoId, ...(active.wtPath ? { worktreePath: active.wtPath } : {}) }),
|
||||
});
|
||||
}
|
||||
ctx.open(e.clientX, e.clientY, items);
|
||||
}
|
||||
|
||||
function openMoreMenu(e: MouseEvent): void {
|
||||
|
||||
185
packages/web/src/components/ide/modals/LaunchProjectModal.vue
Normal file
185
packages/web/src/components/ide/modals/LaunchProjectModal.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" @click.self="emit('close')">
|
||||
<div class="flex max-h-[90vh] w-full max-w-2xl flex-col gap-3 overflow-y-auto rounded-lg border border-border bg-surface-1 p-4">
|
||||
<header class="flex items-center gap-2">
|
||||
<Rocket :size="16" class="text-accent" />
|
||||
<h2 class="font-semibold text-fg">{{ t('launch.title') }}</h2>
|
||||
<span v-if="targetLabel" class="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-[11px] text-fg-muted">{{ targetLabel }}</span>
|
||||
<button class="btn ml-auto text-xs" @click="emit('close')">{{ t('common.cancel') }}</button>
|
||||
</header>
|
||||
|
||||
<p class="text-xs text-fg-subtle">{{ t('launch.hint') }}</p>
|
||||
|
||||
<!-- lignes de commandes éditables -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div v-for="(c, i) in rows" :key="c.id" class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="c.enabled"
|
||||
type="checkbox"
|
||||
class="size-4 shrink-0 accent-[var(--color-accent-solid)]"
|
||||
:title="t('launch.enabledTitle')"
|
||||
/>
|
||||
<input v-model="c.label" class="input w-28 shrink-0 text-xs" :placeholder="t('launch.labelPlaceholder')" />
|
||||
<input v-model="c.run" class="input min-w-0 flex-1 font-mono text-xs" :placeholder="t('launch.runPlaceholder')" />
|
||||
<input v-model="c.cwd" class="input w-24 shrink-0 font-mono text-xs" :placeholder="t('launch.cwdPlaceholder')" />
|
||||
<button type="button" class="btn shrink-0 px-1.5" :title="t('launch.remove')" @click="removeRow(i)">
|
||||
<Trash2 :size="13" />
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="rows.length === 0" class="rounded border border-dashed border-border px-3 py-4 text-center text-xs text-fg-subtle">
|
||||
{{ t('launch.empty') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button type="button" class="btn inline-flex items-center gap-1 text-xs" @click="addRow">
|
||||
<Plus :size="13" /> {{ t('launch.add') }}
|
||||
</button>
|
||||
<button type="button" class="btn inline-flex items-center gap-1 text-xs" :disabled="detecting" @click="detect">
|
||||
<Sparkles :size="13" /> {{ detecting ? t('common.working') : t('launch.detect') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="errorMsg" class="text-xs text-danger">{{ errorMsg }}</p>
|
||||
|
||||
<footer class="mt-1 flex items-center gap-2 border-t border-border pt-3">
|
||||
<button type="button" class="btn text-xs" :disabled="saving || working" @click="onSave">
|
||||
{{ saving ? t('common.working') : t('launch.save') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-primary ml-auto inline-flex items-center gap-1.5"
|
||||
:disabled="working || saving || enabledCount === 0"
|
||||
@click="onStart"
|
||||
>
|
||||
<Play :size="14" />
|
||||
{{ working ? t('common.working') : t('launch.start', { n: enabledCount }) }}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// « Démarrer le projet » : édite les commandes de démarrage d'un repo (label / commande / sous-dossier
|
||||
// / activée), détecte des suggestions (package.json/Procfile/docker-compose), enregistre, et lance
|
||||
// un terminal par commande activée (ide.openTerminal en boucle). Le worktree cible est passé en prop.
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Play, Plus, Rocket, Sparkles, Trash2 } from '@lucide/vue';
|
||||
import type { LaunchCommand } from '@arboretum/shared';
|
||||
import { useWorktreesStore } from '../../../stores/worktrees';
|
||||
import { useSessionsStore } from '../../../stores/sessions';
|
||||
import { useIdeStore } from '../../../stores/ide';
|
||||
import { useToastsStore } from '../../../stores/toasts';
|
||||
|
||||
const props = defineProps<{ repoId: string; worktreePath?: string; targetLabel?: string }>();
|
||||
const emit = defineEmits<{ close: [] }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const worktrees = useWorktreesStore();
|
||||
const sessions = useSessionsStore();
|
||||
const ide = useIdeStore();
|
||||
const toasts = useToastsStore();
|
||||
|
||||
// Copie éditable locale des commandes du repo (jamais muter le store directement).
|
||||
const rows = ref<LaunchCommand[]>([]);
|
||||
const detecting = ref(false);
|
||||
const saving = ref(false);
|
||||
const working = ref(false);
|
||||
const errorMsg = ref<string | null>(null);
|
||||
|
||||
const enabledCount = computed(() => rows.value.filter((c) => c.enabled && c.run.trim() !== '').length);
|
||||
|
||||
function newId(): string {
|
||||
return globalThis.crypto?.randomUUID?.() ?? `lc-${Date.now()}-${rows.value.length}`;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const repo = worktrees.repos.find((r) => r.id === props.repoId);
|
||||
rows.value = (repo?.launchCommands ?? []).map((c) => ({ ...c }));
|
||||
// Auto-détection au premier ouvert si aucune commande n'est encore définie.
|
||||
if (rows.value.length === 0) await detect();
|
||||
});
|
||||
|
||||
function addRow(): void {
|
||||
rows.value = [...rows.value, { id: newId(), label: '', run: '', enabled: true }];
|
||||
}
|
||||
function removeRow(i: number): void {
|
||||
rows.value = rows.value.filter((_, idx) => idx !== i);
|
||||
}
|
||||
|
||||
async function detect(): Promise<void> {
|
||||
if (detecting.value) return;
|
||||
detecting.value = true;
|
||||
errorMsg.value = null;
|
||||
try {
|
||||
const suggestions = await worktrees.detectLaunch(props.repoId, props.worktreePath);
|
||||
// Fusion : n'ajoute que les commandes dont le `run` n'est pas déjà présent.
|
||||
const known = new Set(rows.value.map((c) => c.run.trim()));
|
||||
const fresh = suggestions.filter((s) => !known.has(s.run.trim()));
|
||||
if (fresh.length === 0 && suggestions.length > 0) toasts.info(t('launch.detectNone'));
|
||||
rows.value = [...rows.value, ...fresh.map((c) => ({ ...c, id: newId() }))];
|
||||
} catch (err) {
|
||||
errorMsg.value = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
detecting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Normalise avant persistance : trim, retire les lignes sans commande, enlève cwd vide. */
|
||||
function normalized(): LaunchCommand[] {
|
||||
return rows.value
|
||||
.map((c) => ({
|
||||
id: c.id,
|
||||
label: c.label.trim() || c.run.trim().slice(0, 24),
|
||||
run: c.run.trim(),
|
||||
enabled: c.enabled,
|
||||
...(c.cwd && c.cwd.trim() !== '' ? { cwd: c.cwd.trim() } : {}),
|
||||
}))
|
||||
.filter((c) => c.run !== '');
|
||||
}
|
||||
|
||||
async function persist(): Promise<void> {
|
||||
const cmds = normalized();
|
||||
await worktrees.updateRepoLaunchCommands(props.repoId, cmds);
|
||||
rows.value = cmds.map((c) => ({ ...c }));
|
||||
}
|
||||
|
||||
async function onSave(): Promise<void> {
|
||||
if (saving.value) return;
|
||||
saving.value = true;
|
||||
errorMsg.value = null;
|
||||
try {
|
||||
await persist();
|
||||
toasts.success(t('launch.saved'));
|
||||
} catch (err) {
|
||||
errorMsg.value = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onStart(): Promise<void> {
|
||||
if (working.value || enabledCount.value === 0) return;
|
||||
working.value = true;
|
||||
errorMsg.value = null;
|
||||
try {
|
||||
// On enregistre d'abord (les ids persistés doivent correspondre aux commandIds envoyés).
|
||||
await persist();
|
||||
const ids = normalized().filter((c) => c.enabled).map((c) => c.id);
|
||||
const res = await sessions.startLaunch(props.repoId, {
|
||||
commandIds: ids,
|
||||
...(props.worktreePath ? { worktreePath: props.worktreePath } : {}),
|
||||
});
|
||||
for (const s of res.sessions) ide.openTerminal(s.id);
|
||||
if (res.skipped.length > 0) toasts.info(t('launch.partial', { started: res.sessions.length, skipped: res.skipped.length }));
|
||||
else toasts.success(t('launch.started', { n: res.sessions.length }));
|
||||
emit('close');
|
||||
} catch (err) {
|
||||
errorMsg.value = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
working.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -206,6 +206,27 @@ export default {
|
||||
create: 'Create',
|
||||
creating: 'Creating…',
|
||||
},
|
||||
launch: {
|
||||
title: 'Start project',
|
||||
startProject: 'Start project',
|
||||
startNamed: 'Start project: {name}',
|
||||
hint: 'Define the commands that start this project. Each enabled command opens its own terminal (e.g. dev server, API, database).',
|
||||
labelPlaceholder: 'label',
|
||||
runPlaceholder: 'npm run dev',
|
||||
cwdPlaceholder: 'subdir',
|
||||
enabledTitle: 'Launch this command',
|
||||
remove: 'Remove',
|
||||
empty: 'No launch command yet. Add one or detect them from the project.',
|
||||
add: 'Add command',
|
||||
detect: 'Detect',
|
||||
detectNone: 'No new command detected.',
|
||||
save: 'Save',
|
||||
saved: 'Launch commands saved.',
|
||||
start: 'Start terminals ({n})',
|
||||
started: 'Started {n} terminals.',
|
||||
partial: 'Started {started}, {skipped} skipped.',
|
||||
stopAll: 'Stop launch ({n})',
|
||||
},
|
||||
workspace: {
|
||||
open: 'Open IDE',
|
||||
files: 'Files',
|
||||
|
||||
@@ -208,6 +208,27 @@ const fr: typeof en = {
|
||||
create: 'Créer',
|
||||
creating: 'Création…',
|
||||
},
|
||||
launch: {
|
||||
title: 'Démarrer le projet',
|
||||
startProject: 'Démarrer le projet',
|
||||
startNamed: 'Démarrer le projet : {name}',
|
||||
hint: 'Définissez les commandes qui démarrent ce projet. Chaque commande activée ouvre son propre terminal (ex. serveur de dev, API, base de données).',
|
||||
labelPlaceholder: 'libellé',
|
||||
runPlaceholder: 'npm run dev',
|
||||
cwdPlaceholder: 'sous-dossier',
|
||||
enabledTitle: 'Lancer cette commande',
|
||||
remove: 'Retirer',
|
||||
empty: 'Aucune commande de démarrage. Ajoutez-en une ou détectez-les depuis le projet.',
|
||||
add: 'Ajouter une commande',
|
||||
detect: 'Détecter',
|
||||
detectNone: 'Aucune nouvelle commande détectée.',
|
||||
save: 'Enregistrer',
|
||||
saved: 'Commandes de démarrage enregistrées.',
|
||||
start: 'Démarrer les terminaux ({n})',
|
||||
started: 'Terminaux démarrés : {n}.',
|
||||
partial: 'Démarrés : {started}, ignorés : {skipped}.',
|
||||
stopAll: 'Arrêter le lancement ({n})',
|
||||
},
|
||||
workspace: {
|
||||
open: 'Ouvrir l’IDE',
|
||||
files: 'Fichiers',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import type { CreateProjectRequest, CreateProjectResponse, CreateSessionRequest, HideDiscoveredResponse, SessionResponse, SessionSummary, SessionsListResponse } from '@arboretum/shared';
|
||||
import type { CreateProjectRequest, CreateProjectResponse, CreateSessionRequest, HideDiscoveredResponse, SessionResponse, SessionSummary, SessionsListResponse, StartLaunchRequest, StartLaunchResponse } from '@arboretum/shared';
|
||||
import { api } from '../lib/api';
|
||||
import { wsClient, type SessionEvent } from '../lib/ws-client';
|
||||
|
||||
@@ -152,6 +152,20 @@ export const useSessionsStore = defineStore('sessions', () => {
|
||||
await api.delete<{ ok: true }>(`/api/v1/sessions/${id}`);
|
||||
}
|
||||
|
||||
// « Démarrer le projet » : lance un terminal par commande activée (un seul appel serveur).
|
||||
// Les sessions renvoyées partagent un launchRunId ; le caller ouvre les onglets correspondants.
|
||||
async function startLaunch(repoId: string, opts: StartLaunchRequest = {}): Promise<StartLaunchResponse> {
|
||||
const res = await api.post<StartLaunchResponse>(`/api/v1/repos/${repoId}/launch`, opts);
|
||||
for (const s of res.sessions) upsert(s);
|
||||
return res;
|
||||
}
|
||||
|
||||
// « Tout arrêter » : coupe tous les terminaux vivants d'un même lancement de projet.
|
||||
async function stopLaunch(launchRunId: string): Promise<void> {
|
||||
const targets = sessions.value.filter((s) => s.launchRunId === launchRunId && s.live);
|
||||
await Promise.all(targets.map((s) => killSession(s.id)));
|
||||
}
|
||||
|
||||
// Reprise d'une session morte (nouveau PTY managé dans son cwd d'origine, côté serveur).
|
||||
async function resumeSession(id: string): Promise<SessionSummary> {
|
||||
const res = await api.post<SessionResponse>(`/api/v1/sessions/${id}/resume`);
|
||||
@@ -180,6 +194,8 @@ export const useSessionsStore = defineStore('sessions', () => {
|
||||
createSession,
|
||||
createProject,
|
||||
killSession,
|
||||
startLaunch,
|
||||
stopLaunch,
|
||||
resumeSession,
|
||||
forkSession,
|
||||
hideSession,
|
||||
|
||||
@@ -3,7 +3,9 @@ import { ref } from 'vue';
|
||||
import type {
|
||||
CreateWorktreeRequest,
|
||||
CreateWorktreeResponse,
|
||||
DetectLaunchResponse,
|
||||
DiscoverReposResponse,
|
||||
LaunchCommand,
|
||||
RepoBranchesResponse,
|
||||
RepoResponse,
|
||||
ReposListResponse,
|
||||
@@ -104,6 +106,20 @@ export const useWorktreesStore = defineStore('worktrees', () => {
|
||||
upsertRepo(res.repo);
|
||||
}
|
||||
|
||||
/** Enregistre les commandes de démarrage d'un projet (« Démarrer le projet »). */
|
||||
async function updateRepoLaunchCommands(id: string, launchCommands: LaunchCommand[]): Promise<RepoSummary> {
|
||||
const res = await api.patch<RepoResponse>(`/api/v1/repos/${id}`, { launchCommands });
|
||||
upsertRepo(res.repo);
|
||||
return res.repo;
|
||||
}
|
||||
|
||||
/** Détecte des commandes candidates (package.json/Procfile/docker-compose) dans le worktree cible. */
|
||||
async function detectLaunch(id: string, worktreePath?: string): Promise<LaunchCommand[]> {
|
||||
const qs = worktreePath ? `?worktreePath=${encodeURIComponent(worktreePath)}` : '';
|
||||
const res = await api.get<DetectLaunchResponse>(`/api/v1/repos/${id}/launch/detect${qs}`);
|
||||
return res.suggestions;
|
||||
}
|
||||
|
||||
/** Lance un scan de découverte ; les nouveaux repos arrivent par WS, on resynchronise par sûreté. */
|
||||
async function discover(): Promise<DiscoverReposResponse> {
|
||||
const res = await api.post<DiscoverReposResponse>('/api/v1/repos/discover');
|
||||
@@ -183,6 +199,8 @@ export const useWorktreesStore = defineStore('worktrees', () => {
|
||||
addRepo,
|
||||
removeRepo,
|
||||
setHidden,
|
||||
updateRepoLaunchCommands,
|
||||
detectLaunch,
|
||||
discover,
|
||||
refreshRepoWorktrees,
|
||||
createWorktree,
|
||||
|
||||
@@ -55,6 +55,10 @@ const en: HelpSection[] = [
|
||||
title: 'Delete & prune',
|
||||
body: 'Delete a worktree from its card (force-delete if dirty or locked), or prune all orphaned worktrees at the repo level.',
|
||||
},
|
||||
{
|
||||
title: 'Start the project',
|
||||
body: 'From a repo or worktree menu (or the command palette), choose “Start project”. Define the commands that boot your project (dev server, API, database), auto-detected from package.json / Procfile / docker-compose, then launch them all at once: one terminal per command, opened in the dock. Each runs in your login shell so npm/docker are on PATH, and stays live after the command exits so failures stay visible. Stop the whole set from any of its session rows.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -235,6 +239,10 @@ const fr: HelpSection[] = [
|
||||
title: 'Supprimer & élaguer',
|
||||
body: 'Supprimez un worktree depuis sa carte (suppression forcée s’il est sale ou verrouillé), ou élaguez tous les worktrees orphelins au niveau du dépôt.',
|
||||
},
|
||||
{
|
||||
title: 'Démarrer le projet',
|
||||
body: 'Depuis le menu d’un dépôt ou d’un worktree (ou la palette de commandes), choisissez « Démarrer le projet ». Définissez les commandes qui démarrent votre projet (serveur de dev, API, base de données), auto-détectées depuis package.json / Procfile / docker-compose, puis lancez-les toutes d’un coup : un terminal par commande, ouvert dans le dock. Chacune tourne dans votre shell de login (npm/docker sont donc dans le PATH) et reste vivante après la fin de la commande, pour que les échecs restent visibles. Arrêtez tout le lot depuis n’importe quelle ligne de session.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user