feat(lanes): Add Lane repo/worktree mode toggle, manual branch, folder browse
- Repo mode adopts a directory as-is via /lanes/ensure (no worktree, no branch fields) - the right choice for a main repo you want stage detection on. Worktree mode (default) keeps the existing provisioning flow but now requires a manually-typed branch name instead of deriving one from the title. - POST /lanes/worktree accepts an optional `branch`, validated via `git check-ref-format --branch`; omitting it preserves the CLI's existing auto-derived-branch behavior. - New GET /lanes/browse lists a directory's immediate subdirectories, backing a small folder-browse modal on both path fields - browsers cannot expose an absolute path from a native picker, so this is server-backed instead, consistent with the tool's local-first model.
This commit is contained in:
+55
-2
@@ -10,6 +10,7 @@
|
||||
|
||||
const { Router } = require("express");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { db } = require("../db");
|
||||
const lanesLib = require("../lib/lanes");
|
||||
@@ -25,6 +26,7 @@ const {
|
||||
addWorktree,
|
||||
gitFacts,
|
||||
isGitRepo,
|
||||
isValidBranchName,
|
||||
listBranches,
|
||||
removeWorktree,
|
||||
resetWorktree,
|
||||
@@ -193,6 +195,47 @@ router.post("/gc", sameOriginGuard, (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Directory listing for the Add Lane modal's folder browser — browsers cannot
|
||||
* expose absolute filesystem paths from a native picker, so path selection is
|
||||
* done by browsing server-side instead. Read-only; registered ahead of
|
||||
* "/:id" so the literal "browse" segment is never captured as a lane id.
|
||||
*/
|
||||
router.get("/browse", (req, res) => {
|
||||
const raw =
|
||||
typeof req.query.path === "string" && req.query.path.trim() ? req.query.path : os.homedir();
|
||||
const resolved = path.resolve(raw);
|
||||
|
||||
let stat;
|
||||
try {
|
||||
stat = fs.statSync(resolved);
|
||||
} catch {
|
||||
return res.status(400).json({ error: { code: "ENOTFOUND", message: "path does not exist" } });
|
||||
}
|
||||
if (!stat.isDirectory()) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: { code: "ENOTADIR", message: "path is not a directory" } });
|
||||
}
|
||||
|
||||
let entries = [];
|
||||
try {
|
||||
entries = fs
|
||||
.readdirSync(resolved, { withFileTypes: true })
|
||||
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
||||
.map((e) => {
|
||||
const full = path.join(resolved, e.name);
|
||||
return { name: e.name, path: full, isGitRepo: fs.existsSync(path.join(full, ".git")) };
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
} catch {
|
||||
// An unreadable entry mid-listing is skipped, not a request failure.
|
||||
}
|
||||
|
||||
const parent = path.dirname(resolved) === resolved ? null : path.dirname(resolved);
|
||||
res.json({ path: resolved, parent, entries });
|
||||
});
|
||||
|
||||
router.get("/:id", (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
@@ -503,8 +546,18 @@ router.post("/worktree", sameOriginGuard, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const branchPrefix = process.env.LANE_BRANCH_PREFIX || "feat/";
|
||||
const branch = `${branchPrefix}${slug}`;
|
||||
let branch;
|
||||
if (body.branch !== undefined) {
|
||||
if (!(await isValidBranchName(resolvedSourceRepo, body.branch))) {
|
||||
return res.status(400).json({
|
||||
error: { code: "EBADBRANCH", message: "branch is not a valid git branch name" },
|
||||
});
|
||||
}
|
||||
branch = body.branch;
|
||||
} else {
|
||||
const branchPrefix = process.env.LANE_BRANCH_PREFIX || "feat/";
|
||||
branch = `${branchPrefix}${slug}`;
|
||||
}
|
||||
let lane;
|
||||
try {
|
||||
lane = lanesLib.createLane({
|
||||
|
||||
Reference in New Issue
Block a user