session-steward 0.10.3 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +17 -1
- package/bin/session-steward-cli.mjs +1 -0
- package/bin/session-steward-scheduler.mjs +5 -1
- package/dist/assets/index-BAo7Iw_z.js +9 -0
- package/dist/assets/index-DQxHKbAB.css +2 -0
- package/dist/index.html +2 -2
- package/lib/cleanup-schedules.mjs +38 -18
- package/lib/cli.mjs +81 -2
- package/lib/mcp.mjs +82 -13
- package/lib/providers/claude-code/store.mjs +1 -0
- package/lib/providers/codex/store.mjs +10 -3
- package/lib/server.mjs +90 -5
- package/lib/session-cleanup.mjs +230 -53
- package/lib/session-protections.mjs +467 -0
- package/package.json +1 -1
- package/dist/assets/index-BDiEQG6G.js +0 -9
- package/dist/assets/index-CXq8Tw8T.css +0 -2
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { getDefaultConfigDirectory } from "./settings.mjs";
|
|
6
|
+
|
|
7
|
+
export const SESSION_PROTECTION_BUSY = "SESSION_PROTECTION_BUSY";
|
|
8
|
+
|
|
9
|
+
const STATE_VERSION = 1;
|
|
10
|
+
const MAX_PROTECTIONS = 10_000;
|
|
11
|
+
const MAX_ID_LENGTH = 512;
|
|
12
|
+
const MAX_PATH_LENGTH = 4_096;
|
|
13
|
+
const LOCK_TIMEOUT_MS = 5_000;
|
|
14
|
+
const INCOMPLETE_LOCK_MS = 60_000;
|
|
15
|
+
const PROVIDERS = new Set(["codex", "claude-code"]);
|
|
16
|
+
|
|
17
|
+
function emptyState() {
|
|
18
|
+
return { revision: 0, sessions: [], version: STATE_VERSION, workspaces: [] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function pathsFor(platform) {
|
|
22
|
+
return platform === "win32" ? path.win32 : path.posix;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function comparisonPath(value, platform) {
|
|
26
|
+
const normalized = pathsFor(platform).normalize(value);
|
|
27
|
+
return platform === "win32" ? normalized.toLocaleLowerCase("en-US") : normalized;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeProvider(providerId) {
|
|
31
|
+
if (!PROVIDERS.has(providerId)) throw new Error("That session provider is not available.");
|
|
32
|
+
return providerId;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeSessionId(sessionId) {
|
|
36
|
+
if (
|
|
37
|
+
typeof sessionId !== "string"
|
|
38
|
+
|| sessionId.length === 0
|
|
39
|
+
|| sessionId.length > MAX_ID_LENGTH
|
|
40
|
+
|| sessionId.trim() !== sessionId
|
|
41
|
+
|| sessionId.includes("\0")
|
|
42
|
+
) {
|
|
43
|
+
throw new Error("Enter a valid session ID.");
|
|
44
|
+
}
|
|
45
|
+
return sessionId;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeAbsolutePath(value, label, platform) {
|
|
49
|
+
if (
|
|
50
|
+
typeof value !== "string"
|
|
51
|
+
|| value.length === 0
|
|
52
|
+
|| value.length > MAX_PATH_LENGTH
|
|
53
|
+
|| value.includes("\0")
|
|
54
|
+
) {
|
|
55
|
+
throw new Error(`Enter a valid ${label}.`);
|
|
56
|
+
}
|
|
57
|
+
const trimmed = value.trim();
|
|
58
|
+
const paths = pathsFor(platform);
|
|
59
|
+
if (!paths.isAbsolute(trimmed)) throw new Error(`Enter a full ${label}.`);
|
|
60
|
+
return paths.normalize(trimmed);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function normalizeWorkspacePath(value, { platform = process.platform } = {}) {
|
|
64
|
+
return normalizeAbsolutePath(value, "workspace path", platform);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function sessionProviderHomeKey(providerHome, { platform = process.platform } = {}) {
|
|
68
|
+
const normalized = normalizeAbsolutePath(providerHome, "provider folder path", platform);
|
|
69
|
+
return createHash("sha256")
|
|
70
|
+
.update(comparisonPath(normalized, platform))
|
|
71
|
+
.digest("hex");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createProtectionMatcher({
|
|
75
|
+
platform = process.platform,
|
|
76
|
+
providerHome,
|
|
77
|
+
providerId,
|
|
78
|
+
snapshot,
|
|
79
|
+
}) {
|
|
80
|
+
const normalizedProvider = normalizeProvider(providerId);
|
|
81
|
+
const homeKey = sessionProviderHomeKey(providerHome, { platform });
|
|
82
|
+
const sessionIds = new Set(snapshot.sessions
|
|
83
|
+
.filter((item) => item.providerId === normalizedProvider && item.providerHomeKey === homeKey)
|
|
84
|
+
.map((item) => item.sessionId));
|
|
85
|
+
const workspacePaths = new Map(snapshot.workspaces.map((item) => [
|
|
86
|
+
comparisonPath(item.path, platform),
|
|
87
|
+
item.path,
|
|
88
|
+
]));
|
|
89
|
+
const paths = pathsFor(platform);
|
|
90
|
+
|
|
91
|
+
return (record) => {
|
|
92
|
+
const sessionId = normalizeSessionId(record?.id);
|
|
93
|
+
const session = sessionIds.has(sessionId);
|
|
94
|
+
let workspacePath = null;
|
|
95
|
+
if (typeof record?.cwd === "string" && record.cwd.length > 0) {
|
|
96
|
+
try {
|
|
97
|
+
let candidate = normalizeWorkspacePath(record.cwd, { platform });
|
|
98
|
+
while (true) {
|
|
99
|
+
workspacePath = workspacePaths.get(comparisonPath(candidate, platform)) ?? null;
|
|
100
|
+
if (workspacePath) break;
|
|
101
|
+
const parent = paths.dirname(candidate);
|
|
102
|
+
if (parent === candidate) break;
|
|
103
|
+
candidate = parent;
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
workspacePath = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
kept: session || Boolean(workspacePath),
|
|
111
|
+
reasons: [session ? "session" : null, workspacePath ? "workspace" : null].filter(Boolean),
|
|
112
|
+
session,
|
|
113
|
+
workspace: Boolean(workspacePath),
|
|
114
|
+
workspacePath,
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function compileSessionProtectionMatcher(args) {
|
|
120
|
+
return createProtectionMatcher(args);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function validateState(parsed, platform) {
|
|
124
|
+
if (!parsed || typeof parsed !== "object" || parsed.version !== STATE_VERSION) {
|
|
125
|
+
throw new Error("Session Steward cannot use this Keep data version. Cleanup was paused.");
|
|
126
|
+
}
|
|
127
|
+
if (
|
|
128
|
+
!Number.isSafeInteger(parsed.revision)
|
|
129
|
+
|| parsed.revision < 0
|
|
130
|
+
|| !Array.isArray(parsed.sessions)
|
|
131
|
+
|| !Array.isArray(parsed.workspaces)
|
|
132
|
+
|| parsed.sessions.length + parsed.workspaces.length > MAX_PROTECTIONS
|
|
133
|
+
) {
|
|
134
|
+
throw new Error("Session Steward Keep data is invalid. Cleanup was paused.");
|
|
135
|
+
}
|
|
136
|
+
for (const item of parsed.sessions) {
|
|
137
|
+
if (
|
|
138
|
+
!item || typeof item !== "object"
|
|
139
|
+
|| !PROVIDERS.has(item.providerId)
|
|
140
|
+
|| typeof item.providerHomeKey !== "string"
|
|
141
|
+
|| !/^[a-f0-9]{64}$/u.test(item.providerHomeKey)
|
|
142
|
+
|| typeof item.sessionId !== "string"
|
|
143
|
+
|| item.sessionId.length === 0
|
|
144
|
+
|| item.sessionId.length > MAX_ID_LENGTH
|
|
145
|
+
|| item.sessionId.trim() !== item.sessionId
|
|
146
|
+
|| item.sessionId.includes("\0")
|
|
147
|
+
|| !Number.isFinite(item.createdAtMs)
|
|
148
|
+
) {
|
|
149
|
+
throw new Error("Session Steward Keep data is invalid. Cleanup was paused.");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const item of parsed.workspaces) {
|
|
153
|
+
if (
|
|
154
|
+
!item || typeof item !== "object"
|
|
155
|
+
|| typeof item.path !== "string"
|
|
156
|
+
|| item.path.length === 0
|
|
157
|
+
|| item.path.length > MAX_PATH_LENGTH
|
|
158
|
+
|| !Number.isFinite(item.createdAtMs)
|
|
159
|
+
) {
|
|
160
|
+
throw new Error("Session Steward Keep data is invalid. Cleanup was paused.");
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
if (normalizeWorkspacePath(item.path, { platform }) !== item.path) {
|
|
164
|
+
throw new Error("not normalized");
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
throw new Error("Session Steward Keep data is invalid. Cleanup was paused.");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return structuredClone(parsed);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function readState(statePath, platform) {
|
|
174
|
+
let source;
|
|
175
|
+
try {
|
|
176
|
+
source = await fs.readFile(statePath, "utf8");
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error?.code === "ENOENT") return emptyState();
|
|
179
|
+
throw new Error("Session Steward could not read Keep data. Cleanup was paused.", { cause: error });
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
return validateState(JSON.parse(source), platform);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error?.message?.includes("Cleanup was paused")) throw error;
|
|
185
|
+
throw new Error("Session Steward Keep data is invalid. Cleanup was paused.", { cause: error });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function writeState(statePath, state) {
|
|
190
|
+
const directory = path.dirname(statePath);
|
|
191
|
+
const temporaryPath = path.join(
|
|
192
|
+
directory,
|
|
193
|
+
`.session-protections-${process.pid}-${randomBytes(8).toString("hex")}.tmp`,
|
|
194
|
+
);
|
|
195
|
+
await fs.mkdir(directory, { mode: 0o700, recursive: true });
|
|
196
|
+
let handle;
|
|
197
|
+
try {
|
|
198
|
+
handle = await fs.open(temporaryPath, "wx", 0o600);
|
|
199
|
+
await handle.writeFile(`${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
200
|
+
await handle.sync();
|
|
201
|
+
await handle.close();
|
|
202
|
+
handle = null;
|
|
203
|
+
await fs.rename(temporaryPath, statePath);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
await handle?.close().catch(() => {});
|
|
206
|
+
await fs.rm(temporaryPath, { force: true }).catch(() => {});
|
|
207
|
+
throw new Error("Session Steward could not save Keep data.", { cause: error });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function processIsRunning(pid) {
|
|
212
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return null;
|
|
213
|
+
try {
|
|
214
|
+
process.kill(pid, 0);
|
|
215
|
+
return true;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error?.code === "ESRCH") return false;
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function lockIsStale(lockPath, now) {
|
|
223
|
+
let stats;
|
|
224
|
+
try {
|
|
225
|
+
stats = await fs.stat(lockPath);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error?.code === "ENOENT") return true;
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
const metadata = JSON.parse(await fs.readFile(lockPath, "utf8"));
|
|
232
|
+
const running = await processIsRunning(metadata?.pid);
|
|
233
|
+
return running === null ? now() - stats.mtimeMs >= INCOMPLETE_LOCK_MS : !running;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (error?.code === "ENOENT") return true;
|
|
236
|
+
return now() - stats.mtimeMs >= INCOMPLETE_LOCK_MS;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function delay(milliseconds) {
|
|
241
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function acquireLock(lockPath, now) {
|
|
245
|
+
const token = randomBytes(18).toString("base64url");
|
|
246
|
+
const startedAt = Date.now();
|
|
247
|
+
await fs.mkdir(path.dirname(lockPath), { mode: 0o700, recursive: true });
|
|
248
|
+
let handle;
|
|
249
|
+
while (Date.now() - startedAt < LOCK_TIMEOUT_MS) {
|
|
250
|
+
try {
|
|
251
|
+
handle = await fs.open(lockPath, "wx", 0o600);
|
|
252
|
+
break;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (error?.code !== "EEXIST") throw error;
|
|
255
|
+
if (await lockIsStale(lockPath, now)) {
|
|
256
|
+
await fs.rm(lockPath, { force: true });
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
await delay(25);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (!handle) {
|
|
263
|
+
const error = new Error("Keep settings are busy. Try again after the current cleanup finishes.");
|
|
264
|
+
error.code = SESSION_PROTECTION_BUSY;
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
await handle.writeFile(`${JSON.stringify({ createdAtMs: now(), pid: process.pid, token })}\n`);
|
|
269
|
+
await handle.sync();
|
|
270
|
+
} catch (error) {
|
|
271
|
+
await handle.close().catch(() => {});
|
|
272
|
+
await fs.rm(lockPath, { force: true }).catch(() => {});
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
let released = false;
|
|
276
|
+
return async () => {
|
|
277
|
+
if (released) return;
|
|
278
|
+
released = true;
|
|
279
|
+
await handle.close().catch(() => {});
|
|
280
|
+
try {
|
|
281
|
+
const metadata = JSON.parse(await fs.readFile(lockPath, "utf8"));
|
|
282
|
+
if (metadata?.token === token) await fs.rm(lockPath, { force: true });
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (error?.code !== "ENOENT" && !(error instanceof SyntaxError)) throw error;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function classifySessionProtection({
|
|
290
|
+
platform = process.platform,
|
|
291
|
+
providerHome,
|
|
292
|
+
providerId,
|
|
293
|
+
record,
|
|
294
|
+
snapshot,
|
|
295
|
+
}) {
|
|
296
|
+
return createProtectionMatcher({ platform, providerHome, providerId, snapshot })(record);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function decorateSessionProtection(args) {
|
|
300
|
+
const protection = classifySessionProtection(args);
|
|
301
|
+
return { ...args.record, keep: protection };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function createSessionProtectionStore({
|
|
305
|
+
configDirectory = getDefaultConfigDirectory(),
|
|
306
|
+
now = Date.now,
|
|
307
|
+
platform = process.platform,
|
|
308
|
+
} = {}) {
|
|
309
|
+
const statePath = path.join(configDirectory, "protections.json");
|
|
310
|
+
const lockPath = path.join(configDirectory, "protections.lock");
|
|
311
|
+
let matcherCache = null;
|
|
312
|
+
|
|
313
|
+
async function mutate(mutator) {
|
|
314
|
+
const release = await acquireLock(lockPath, now);
|
|
315
|
+
try {
|
|
316
|
+
const state = await readState(statePath, platform);
|
|
317
|
+
const { changed, value } = mutator(state);
|
|
318
|
+
if (changed) {
|
|
319
|
+
state.revision += 1;
|
|
320
|
+
await writeState(statePath, state);
|
|
321
|
+
}
|
|
322
|
+
return value;
|
|
323
|
+
} finally {
|
|
324
|
+
await release();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function keepSessions({ providerHome, providerId, sessionIds }) {
|
|
329
|
+
if (!Array.isArray(sessionIds) || sessionIds.length === 0) {
|
|
330
|
+
throw new Error("Choose at least one session to keep.");
|
|
331
|
+
}
|
|
332
|
+
const normalizedProvider = normalizeProvider(providerId);
|
|
333
|
+
const providerHomeKey = sessionProviderHomeKey(providerHome, { platform });
|
|
334
|
+
const normalizedIds = [...new Set(sessionIds.map(normalizeSessionId))];
|
|
335
|
+
return mutate((state) => {
|
|
336
|
+
const existingIds = new Set(state.sessions
|
|
337
|
+
.filter((item) => item.providerId === normalizedProvider && item.providerHomeKey === providerHomeKey)
|
|
338
|
+
.map((item) => item.sessionId));
|
|
339
|
+
const additions = normalizedIds
|
|
340
|
+
.filter((sessionId) => !existingIds.has(sessionId))
|
|
341
|
+
.map((sessionId) => ({
|
|
342
|
+
createdAtMs: now(),
|
|
343
|
+
providerHomeKey,
|
|
344
|
+
providerId: normalizedProvider,
|
|
345
|
+
sessionId,
|
|
346
|
+
}));
|
|
347
|
+
if (state.sessions.length + state.workspaces.length + additions.length > MAX_PROTECTIONS) {
|
|
348
|
+
throw new Error(`Session Steward supports up to ${MAX_PROTECTIONS.toLocaleString()} kept items.`);
|
|
349
|
+
}
|
|
350
|
+
state.sessions.push(...additions);
|
|
351
|
+
return {
|
|
352
|
+
changed: additions.length > 0,
|
|
353
|
+
value: normalizedIds.map((sessionId) => structuredClone(
|
|
354
|
+
state.sessions.find((item) =>
|
|
355
|
+
item.providerId === normalizedProvider
|
|
356
|
+
&& item.providerHomeKey === providerHomeKey
|
|
357
|
+
&& item.sessionId === sessionId),
|
|
358
|
+
)),
|
|
359
|
+
};
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function removeSessions({ providerHome, providerId, sessionIds }) {
|
|
364
|
+
if (!Array.isArray(sessionIds) || sessionIds.length === 0) {
|
|
365
|
+
throw new Error("Choose at least one session to stop keeping.");
|
|
366
|
+
}
|
|
367
|
+
const normalizedProvider = normalizeProvider(providerId);
|
|
368
|
+
const providerHomeKey = sessionProviderHomeKey(providerHome, { platform });
|
|
369
|
+
const normalizedIds = new Set(sessionIds.map(normalizeSessionId));
|
|
370
|
+
return mutate((state) => {
|
|
371
|
+
const before = state.sessions.length;
|
|
372
|
+
state.sessions = state.sessions.filter((item) => !(
|
|
373
|
+
item.providerId === normalizedProvider
|
|
374
|
+
&& item.providerHomeKey === providerHomeKey
|
|
375
|
+
&& normalizedIds.has(item.sessionId)
|
|
376
|
+
));
|
|
377
|
+
return { changed: state.sessions.length !== before, value: before - state.sessions.length };
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function matcher({ providerHome, providerId, snapshot }) {
|
|
382
|
+
const cacheKey = `${snapshot.revision}\0${providerId}\0${sessionProviderHomeKey(providerHome, { platform })}`;
|
|
383
|
+
if (matcherCache?.key !== cacheKey) {
|
|
384
|
+
matcherCache = {
|
|
385
|
+
key: cacheKey,
|
|
386
|
+
value: createProtectionMatcher({ platform, providerHome, providerId, snapshot }),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
return matcherCache.value;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
async acquireLease() {
|
|
394
|
+
return acquireLock(lockPath, now);
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
classify({ providerHome, providerId, record, snapshot }) {
|
|
398
|
+
return matcher({ providerHome, providerId, snapshot })(record);
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
matcher,
|
|
402
|
+
|
|
403
|
+
homeKey(providerHome) {
|
|
404
|
+
return sessionProviderHomeKey(providerHome, { platform });
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
async keepSession({ providerHome, providerId, sessionId }) {
|
|
408
|
+
return (await keepSessions({ providerHome, providerId, sessionIds: [sessionId] }))[0];
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
keepSessions,
|
|
412
|
+
|
|
413
|
+
removeSessions,
|
|
414
|
+
|
|
415
|
+
async keepWorkspace({ workspace }) {
|
|
416
|
+
const item = { createdAtMs: now(), path: normalizeWorkspacePath(workspace, { platform }) };
|
|
417
|
+
const key = comparisonPath(item.path, platform);
|
|
418
|
+
return mutate((state) => {
|
|
419
|
+
const existing = state.workspaces.find((candidate) => comparisonPath(candidate.path, platform) === key);
|
|
420
|
+
if (existing) return { changed: false, value: structuredClone(existing) };
|
|
421
|
+
if (state.sessions.length + state.workspaces.length >= MAX_PROTECTIONS) {
|
|
422
|
+
throw new Error(`Session Steward supports up to ${MAX_PROTECTIONS.toLocaleString()} kept items.`);
|
|
423
|
+
}
|
|
424
|
+
state.workspaces.push(item);
|
|
425
|
+
return { changed: true, value: structuredClone(item) };
|
|
426
|
+
});
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
async list() {
|
|
430
|
+
return readState(statePath, platform);
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
async listWorkspaceRules({ page = 1, pageSize = 25, search = "" } = {}) {
|
|
434
|
+
const state = await readState(statePath, platform);
|
|
435
|
+
const boundedPageSize = Math.min(100, Math.max(1, Math.trunc(pageSize) || 25));
|
|
436
|
+
const normalizedSearch = String(search ?? "").trim().toLocaleLowerCase("en-US");
|
|
437
|
+
const filtered = state.workspaces
|
|
438
|
+
.filter((item) => !normalizedSearch || item.path.toLocaleLowerCase("en-US").includes(normalizedSearch))
|
|
439
|
+
.sort((left, right) => right.createdAtMs - left.createdAtMs || left.path.localeCompare(right.path));
|
|
440
|
+
const pageCount = Math.max(1, Math.ceil(filtered.length / boundedPageSize));
|
|
441
|
+
const currentPage = Math.min(Math.max(1, Math.trunc(page) || 1), pageCount);
|
|
442
|
+
return {
|
|
443
|
+
page: currentPage,
|
|
444
|
+
pageCount,
|
|
445
|
+
pageSize: boundedPageSize,
|
|
446
|
+
records: filtered.slice((currentPage - 1) * boundedPageSize, currentPage * boundedPageSize),
|
|
447
|
+
revision: state.revision,
|
|
448
|
+
total: filtered.length,
|
|
449
|
+
};
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
async removeSession({ providerHome, providerId, sessionId }) {
|
|
453
|
+
return (await removeSessions({ providerHome, providerId, sessionIds: [sessionId] })) > 0;
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
async removeWorkspace({ workspace }) {
|
|
457
|
+
const normalized = normalizeWorkspacePath(workspace, { platform });
|
|
458
|
+
const key = comparisonPath(normalized, platform);
|
|
459
|
+
return mutate((state) => {
|
|
460
|
+
const next = state.workspaces.filter((item) => comparisonPath(item.path, platform) !== key);
|
|
461
|
+
const changed = next.length !== state.workspaces.length;
|
|
462
|
+
state.workspaces = next;
|
|
463
|
+
return { changed, value: changed };
|
|
464
|
+
});
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
}
|
package/package.json
CHANGED