UNPKG

@lenne.tech/cli

Version:

lenne.Tech CLI: lt

167 lines (166 loc) 9.33 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const fs_1 = require("fs"); const dev_identity_1 = require("../../lib/dev-identity"); const dev_process_1 = require("../../lib/dev-process"); const dev_project_1 = require("../../lib/dev-project"); const dev_state_1 = require("../../lib/dev-state"); const dev_ticket_1 = require("../../lib/dev-ticket"); /** * `lt ticket stop [<id>]` — tear a ticket env down + remove its worktree. * * 1. `lt dev down` inside the worktree (stops the ticket stack + any test * stacks, removes the Caddy block — residue-free), * 2. `git worktree remove` (the BRANCH is kept, so nothing is lost), * 3. `--drop-db` also drops the ticket's empty dev + test databases. * * Run with NO id from INSIDE a ticket worktree to clean up THIS environment * (the current folder is removed; the process steps out to the main repo first). */ const StopCommand = { alias: ['rm'], description: 'Stop a ticket env + remove its worktree (branch kept); no id = the current worktree', name: 'stop', run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () { var _a, _b, _c, _d, _e; const { filesystem, parameters, print: { colors, error, info, success, warning }, } = toolbox; const layout = (0, dev_project_1.resolveLayout)(filesystem.cwd(), filesystem); let mainRepoRoot; try { mainRepoRoot = (0, dev_ticket_1.gitMainRepoRoot)(layout.root); } catch (_f) { error('Not inside a git repository.'); if (!parameters.options.fromGluegunMenu) process.exit(1); return 'ticket stop: not a git repo'; } // Id from the argument — or, when invoked with NO id from INSIDE a ticket // worktree, the current worktree's own ticket (so a bare `lt ticket stop` // cleans up "this" environment and removes this very folder). const fromMarker = parameters.first == null ? (0, dev_ticket_1.readTicketMarker)(layout.root) : null; const id = String((_b = (_a = parameters.first) !== null && _a !== void 0 ? _a : fromMarker) !== null && _b !== void 0 ? _b : '').trim(); if (!id) { error('Usage: lt ticket stop <id> [--drop-db] [--force] — or run with no id from INSIDE a ticket worktree.'); if (!parameters.options.fromGluegunMenu) process.exit(1); return 'ticket stop: missing id'; } const wt = (0, dev_ticket_1.listWorktrees)(mainRepoRoot).find((w) => w.ticket === id); if (!wt) { error(`No ticket worktree "${id}" found. See \`lt ticket list\`.`); if (!parameters.options.fromGluegunMenu) process.exit(1); return 'ticket stop: not found'; } // SAFETY: never silently delete unsaved work. Warn + REFUSE (unless --force) // when the worktree has uncommitted changes OR unpushed commits, so the user // commits + pushes first. (`--force` removes anyway; the branch is kept, so // committed history survives regardless.) const safety = (0, dev_ticket_1.worktreeSafetyReport)(wt.path); if ((safety.dirtySource.length > 0 || safety.unpushed > 0) && parameters.options.force !== true) { warning(''); warning(`Refusing to remove ticket "${id}" — work is not fully committed + pushed:`); if (safety.dirtySource.length > 0) { warning(` • ${safety.dirtySource.length} uncommitted change(s) — would be LOST on removal:`); safety.dirtySource.slice(0, 12).forEach((l) => info(colors.dim(` ${l}`))); if (safety.dirtySource.length > 12) info(colors.dim(` … and ${safety.dirtySource.length - 12} more`)); } if (safety.unpushed > 0) { warning(` • ${safety.unpushed} commit(s) on "${(_c = wt.branch) !== null && _c !== void 0 ? _c : '-'}" not pushed to any remote`); } info(''); info(colors.dim(' Commit + push first (the branch is kept), or re-run with --force to remove anyway.')); if (!parameters.options.fromGluegunMenu) process.exit(1); return 'ticket stop: unsaved work (use --force)'; } // If we are removing the worktree we are standing in, step the process out // to the main repo first so git can remove the folder cleanly. const removingCwd = fromMarker !== null || samePath(wt.path, layout.root); if (removingCwd) { try { process.chdir(mainRepoRoot); } catch (_g) { /* best-effort */ } } info(''); info(colors.bold(`Stopping ticket "${id}"`)); // 1. Tear the isolated stack down from inside the worktree (marker-aware). info(colors.dim(' lt dev down …')); yield (0, dev_process_1.runChildInherit)(process.execPath, [process.argv[1], 'dev', 'down'], { cwd: wt.path, env: process.env }); // 2. Optionally drop the ticket databases (they are otherwise just left empty). if (parameters.options.dropDb === true || parameters.options['drop-db'] === true) { const base = (0, dev_identity_1.buildIdentity)(mainRepoRoot); const entry = (0, dev_state_1.loadRegistry)().projects[`${base.slug}-${id}`]; const mainLayout = (0, dev_project_1.resolveLayout)(mainRepoRoot, filesystem); const devDb = (_d = entry === null || entry === void 0 ? void 0 : entry.dbName) !== null && _d !== void 0 ? _d : (0, dev_project_1.deriveTicketDbName)((0, dev_project_1.deriveDbName)(mainLayout.apiDir, base.slug), id); const testDb = (0, dev_project_1.deriveTestDbName)(devDb); for (const db of [devDb, testDb]) { if ((0, dev_ticket_1.dropDatabase)(db)) info(colors.dim(` dropped db ${db}`)); else warning(` could not drop db ${db} (mongosh missing or DB not reachable) — drop it manually if needed.`); } } // 3. Remove the worktree (branch is kept). Auto-force when the ONLY dirty // files are framework-generated (e.g. `nuxt dev` rewrites the tracked // `.nuxtrc` on boot), which would otherwise block the remove — but NEVER // discard real source edits (those keep the non-forced remove, which // errors with a hint so unsaved work is never lost). const force = parameters.options.force === true || (0, dev_ticket_1.worktreeDirtyOnlyGenerated)(wt.path); if (force && parameters.options.force !== true) { info(colors.dim(' (worktree had only generated files dirty, e.g. .nuxtrc — removing)')); } try { (0, dev_ticket_1.worktreeRemove)(mainRepoRoot, wt.path, force); } catch (e) { error(`git worktree remove failed: ${e.message}`); info(colors.dim(' The worktree has uncommitted SOURCE changes — commit/stash them, or pass --force to discard.')); if (!parameters.options.fromGluegunMenu) process.exit(1); return 'ticket stop: worktree remove failed'; } // The whole env is gone now — drop the ticket's registry entry so its slug + // reserved ports are reclaimed (`lt dev down` only ends the session, keeping // the entry for a restart; `lt ticket stop` removes the env entirely). { const reg = (0, dev_state_1.loadRegistry)(); const ticketSlug = `${(0, dev_identity_1.buildIdentity)(mainRepoRoot).slug}-${id}`; if (reg.projects[ticketSlug]) { delete reg.projects[ticketSlug]; (0, dev_state_1.saveRegistry)(reg); } } info(''); success(`Ticket "${id}" stopped — worktree removed, branch "${(_e = wt.branch) !== null && _e !== void 0 ? _e : '-'}" kept.`); if (removingCwd) info(colors.dim(` This folder is gone — your shell is still in it. Run: cd ${mainRepoRoot}`)); if (!parameters.options.fromGluegunMenu) process.exit(); return `ticket stop: ${id}`; }), }; /** True if two paths point at the same location (resolving symlinks, e.g. /tmp → /private/tmp). */ function samePath(a, b) { try { return (0, fs_1.realpathSync)(a) === (0, fs_1.realpathSync)(b); } catch (_a) { return a === b; } } module.exports = StopCommand;