Spaces:
Sleeping
Sleeping
File size: 3,583 Bytes
5b5da0e fd0a8b6 5b5da0e fd0a8b6 5b5da0e bd25310 5b5da0e bd25310 5b5da0e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | 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();
}
|