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