feat(lanes): per-lane database, Redis, and .env isolation (A1+A2)

Gives each lane its own slot-derived runtime (ports, detached process
lifecycle, profile-driven hooks) and its own database/Redis logical
index/.env file, so two lanes running the same repo's stack at once no
longer share state. Machine-level DB/Redis credentials live at
~/.ccam/secrets.env (mode 0600, never returned by any route); a hook's
output is redacted of that password (raw and URL-encoded forms) before
it reaches a log file or the lane_hook_output websocket broadcast.
Wired into provision/up/reset/remove; reset accepts --keep-db to skip
the drop/recreate/migrate/reseed block entirely.
This commit is contained in:
2026-08-04 10:03:40 +07:00
parent d71086f677
commit 9d145865dd
19 changed files with 3265 additions and 12 deletions
+11
View File
@@ -10,6 +10,8 @@ const fs = require("node:fs");
const { db } = require("../db");
const wt = require("./worktree");
const lanesLib = require("./lanes");
const { resolveProfile } = require("./lane-profile");
const { dbName } = require("./lane-slots");
/**
* Preflight for reset, remove, or purge. Returns an object describing what will happen:
@@ -74,6 +76,14 @@ async function preflight(lane, action) {
}
}
// The database name this action would drop (reset unless --keep-db,
// remove always) — echoed the same way `head`/`dirty` are, so the
// confirmation dialog names the destructive fact rather than leaving it a
// surprise. Null when the lane has no slot yet or the profile declares no
// DB_PREFIX — nothing has been derived to drop.
const profile = resolveProfile(lane);
const database = profile && lane.slot ? dbName(profile, lane.slot) : null;
return {
action,
lane: lane.id,
@@ -83,6 +93,7 @@ async function preflight(lane, action) {
untracked,
unpushed,
head: head || null,
database,
blocked,
warnings,
};