Spaces:
Running
Running
| import fs from 'node:fs'; | |
| import path from 'node:path'; | |
| import crypto from 'node:crypto'; | |
| import { DATA_DIR } from './config.js'; | |
| const GROUPS_FILE = path.join(DATA_DIR, 'groups.json'); | |
| let groups = []; | |
| function persist() { | |
| const tmp = `${GROUPS_FILE}.tmp`; | |
| fs.writeFileSync(tmp, JSON.stringify(groups, null, 2)); | |
| fs.renameSync(tmp, GROUPS_FILE); | |
| } | |
| // Groups are purely VISUAL: sidebar organization + tiling. They own no folder — | |
| // each session records its own `path`. (Old groups.json entries may still carry | |
| // a `folder` key; it's kept for the one-time path migration in index.js and | |
| // otherwise ignored.) | |
| export function init() { | |
| try { | |
| groups = JSON.parse(fs.readFileSync(GROUPS_FILE, 'utf8')); | |
| if (!Array.isArray(groups)) groups = []; | |
| } catch { | |
| groups = []; | |
| } | |
| } | |
| export function list() { | |
| return groups.slice(); | |
| } | |
| export function get(id) { | |
| return groups.find((g) => g.id === id) || null; | |
| } | |
| export function create(name) { | |
| const clean = (name || '').trim() || 'Group'; | |
| const g = { | |
| id: `g-${crypto.randomBytes(3).toString('hex')}`, | |
| name: clean, | |
| sessionIds: [], | |
| createdAt: new Date().toISOString(), | |
| }; | |
| groups.push(g); | |
| persist(); | |
| return g; | |
| } | |
| export function update(id, { name, sessionIds, layout }) { | |
| const g = get(id); | |
| if (!g) return null; | |
| if (typeof name === 'string' && name.trim()) g.name = name.trim(); | |
| // Tile layout: {cols, rows} each 1..3; anything else (incl. null) = auto. | |
| if (layout !== undefined) { | |
| const ok = layout && Number.isInteger(layout.cols) && Number.isInteger(layout.rows) | |
| && layout.cols >= 1 && layout.cols <= 3 && layout.rows >= 1 && layout.rows <= 3; | |
| if (ok) g.layout = { cols: layout.cols, rows: layout.rows }; | |
| else delete g.layout; | |
| } | |
| if (Array.isArray(sessionIds)) { | |
| // Enforce single-group membership: drop these ids from every other group. | |
| const set = new Set(sessionIds); | |
| for (const other of groups) { | |
| if (other.id !== id) other.sessionIds = other.sessionIds.filter((s) => !set.has(s)); | |
| } | |
| g.sessionIds = [...new Set(sessionIds)]; | |
| } | |
| persist(); | |
| return g; | |
| } | |
| export function remove(id) { | |
| groups = groups.filter((g) => g.id !== id); | |
| persist(); | |
| } | |
| /** Attach a session to a group at an optional index, removing it from any other group. */ | |
| export function attach(groupId, sessionId, index) { | |
| const g = get(groupId); | |
| if (!g) return null; | |
| for (const other of groups) other.sessionIds = other.sessionIds.filter((s) => s !== sessionId); | |
| const at = Number.isInteger(index) && index >= 0 && index <= g.sessionIds.length ? index : g.sessionIds.length; | |
| g.sessionIds.splice(at, 0, sessionId); | |
| persist(); | |
| return g; | |
| } | |
| /** Which group (if any) currently contains this session. */ | |
| export function groupOf(sessionId) { | |
| return groups.find((g) => g.sessionIds.includes(sessionId)) || null; | |
| } | |
| /** Remove a session from every group (used on session delete). */ | |
| export function detachSession(sessionId) { | |
| let changed = false; | |
| for (const g of groups) { | |
| const next = g.sessionIds.filter((s) => s !== sessionId); | |
| if (next.length !== g.sessionIds.length) { | |
| g.sessionIds = next; | |
| changed = true; | |
| } | |
| } | |
| if (changed) persist(); | |
| } | |
| /** Drop ids that no longer correspond to a live session. */ | |
| export function prune(validIds) { | |
| const valid = new Set(validIds); | |
| let changed = false; | |
| for (const g of groups) { | |
| const next = g.sessionIds.filter((s) => valid.has(s)); | |
| if (next.length !== g.sessionIds.length) { | |
| g.sessionIds = next; | |
| changed = true; | |
| } | |
| } | |
| if (changed) persist(); | |
| } | |