@vikrant82/opencode-cache-keepalive 0.1.5 → 0.2.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.
Files changed (46) hide show
  1. package/MIGRATION.md +57 -0
  2. package/README.md +75 -48
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +1097 -614
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/config.d.ts +8 -25
  7. package/dist/lib/config.d.ts.map +1 -1
  8. package/dist/lib/control.d.ts +10 -4
  9. package/dist/lib/control.d.ts.map +1 -1
  10. package/dist/lib/paths.d.ts +5 -12
  11. package/dist/lib/paths.d.ts.map +1 -1
  12. package/dist/lib/replay-warmer.d.ts +95 -0
  13. package/dist/lib/replay-warmer.d.ts.map +1 -0
  14. package/dist/lib/state.d.ts +15 -114
  15. package/dist/lib/state.d.ts.map +1 -1
  16. package/dist/lib/tui/commands.d.ts +2 -0
  17. package/dist/lib/tui/commands.d.ts.map +1 -1
  18. package/dist/lib/tui/footer.d.ts +5 -5
  19. package/dist/lib/tui/footer.d.ts.map +1 -1
  20. package/dist/lib/tui/format.d.ts +8 -0
  21. package/dist/lib/tui/format.d.ts.map +1 -1
  22. package/dist/lib/types.d.ts +123 -0
  23. package/dist/lib/types.d.ts.map +1 -0
  24. package/dist/tui.d.ts +3 -0
  25. package/dist/tui.d.ts.map +1 -1
  26. package/lib/config.ts +106 -74
  27. package/lib/control.ts +69 -20
  28. package/lib/paths.ts +12 -14
  29. package/lib/replay-warmer.ts +1200 -0
  30. package/lib/state.ts +172 -175
  31. package/lib/tui/commands.tsx +76 -0
  32. package/lib/tui/footer.tsx +55 -130
  33. package/lib/tui/format.ts +40 -0
  34. package/lib/types.ts +130 -0
  35. package/package.json +3 -2
  36. package/tui.tsx +40 -0
  37. package/dist/lib/keepalive.d.ts +0 -85
  38. package/dist/lib/keepalive.d.ts.map +0 -1
  39. package/dist/lib/model.d.ts +0 -8
  40. package/dist/lib/model.d.ts.map +0 -1
  41. package/dist/lib/system.d.ts +0 -11
  42. package/dist/lib/system.d.ts.map +0 -1
  43. package/lib/keepalive.ts +0 -636
  44. package/lib/model.ts +0 -19
  45. package/lib/system.ts +0 -19
  46. package/lib/tui/commands.ts +0 -59
package/lib/control.ts CHANGED
@@ -1,35 +1,84 @@
1
+ import { randomUUID } from "node:crypto"
1
2
  import { readFileSync } from "node:fs"
2
3
  import { mkdir, rename, writeFile } from "node:fs/promises"
3
4
  import { dirname } from "node:path"
4
5
  import { controlFilePath } from "./paths"
5
6
 
6
- export type KeepaliveControl = {
7
- version: 1
8
- enabled: boolean
9
- updatedAt: number
10
- }
7
+ const VERSION = 2
8
+ const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000
9
+
10
+ export type SessionControl = { enabled: boolean; updatedAt: number }
11
+ export type SessionControlFile = { version: 2; sessions: Record<string, SessionControl> }
11
12
 
12
- export function readControl(directory: string): KeepaliveControl | undefined {
13
+ /** Read current session overrides; old folder-level controls are intentionally ignored. */
14
+ export function readSessionControls(directory: string, now = Date.now()): SessionControlFile {
13
15
  try {
14
- const value = JSON.parse(
15
- readFileSync(controlFilePath(directory), "utf8"),
16
- ) as Partial<KeepaliveControl>
17
- if (value.version !== 1 || typeof value.enabled !== "boolean") return undefined
18
- if (typeof value.updatedAt !== "number") return undefined
19
- return value as KeepaliveControl
16
+ const value = JSON.parse(readFileSync(controlFilePath(directory), "utf8")) as Record<
17
+ string,
18
+ unknown
19
+ >
20
+ if (value.version !== VERSION || !value.sessions || typeof value.sessions !== "object")
21
+ return { version: VERSION, sessions: {} }
22
+ const sessions: Record<string, SessionControl> = {}
23
+ for (const [id, raw] of Object.entries(value.sessions)) {
24
+ if (
25
+ !raw ||
26
+ typeof raw !== "object" ||
27
+ typeof (raw as SessionControl).enabled !== "boolean" ||
28
+ typeof (raw as SessionControl).updatedAt !== "number" ||
29
+ (raw as SessionControl).updatedAt < now - MAX_AGE_MS
30
+ )
31
+ continue
32
+ sessions[id] = {
33
+ enabled: (raw as SessionControl).enabled,
34
+ updatedAt: (raw as SessionControl).updatedAt,
35
+ }
36
+ }
37
+ return { version: VERSION, sessions }
20
38
  } catch {
21
- return undefined
39
+ return { version: VERSION, sessions: {} }
22
40
  }
23
41
  }
24
42
 
25
- export async function writeControl(enabled: boolean, directory: string): Promise<KeepaliveControl> {
26
- const value: KeepaliveControl = {
27
- version: 1,
28
- enabled,
29
- updatedAt: Date.now(),
30
- }
43
+ export function readSessionControl(
44
+ directory: string,
45
+ sessionID: string,
46
+ ): SessionControl | undefined {
47
+ return readSessionControls(directory).sessions[sessionID]
48
+ }
49
+
50
+ /** Atomically update only this session key and prune expired overrides. */
51
+ export async function setSessionEnabled(
52
+ directory: string,
53
+ sessionID: string,
54
+ enabled: boolean,
55
+ now = Date.now(),
56
+ ): Promise<SessionControlFile> {
57
+ const run = updateQueue.then(() => writeSessionControl(directory, sessionID, enabled, now))
58
+ updateQueue = run.then(
59
+ () => undefined,
60
+ () => undefined,
61
+ )
62
+ return run
63
+ }
64
+
65
+ let updateQueue: Promise<void> = Promise.resolve()
66
+
67
+ async function writeSessionControl(
68
+ directory: string,
69
+ sessionID: string,
70
+ enabled: boolean,
71
+ now: number,
72
+ ): Promise<SessionControlFile> {
73
+ const current = readSessionControls(directory, now)
74
+ const cutoff = now - MAX_AGE_MS
75
+ const sessions = Object.fromEntries(
76
+ Object.entries(current.sessions).filter(([, value]) => value.updatedAt >= cutoff),
77
+ )
78
+ sessions[sessionID] = { enabled, updatedAt: now }
79
+ const value: SessionControlFile = { version: VERSION, sessions }
31
80
  const path = controlFilePath(directory)
32
- const tmp = `${path}.${process.pid}.${value.updatedAt}.tmp`
81
+ const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`
33
82
  await mkdir(dirname(path), { recursive: true })
34
83
  await writeFile(tmp, `${JSON.stringify(value)}\n`, "utf8")
35
84
  await rename(tmp, path)
package/lib/paths.ts CHANGED
@@ -2,17 +2,17 @@ import { createHash } from "node:crypto"
2
2
  import { homedir } from "node:os"
3
3
  import { join } from "node:path"
4
4
 
5
- /**
6
- * Absolute path to the JSON state file shared between the server plugin (writer)
7
- * and the TUI plugin (reader). Both processes run on the same machine, so a file
8
- * under opencode's plugin storage dir is the simplest cross-process channel.
9
- *
10
- * Mirrors the storage layout used by other opencode plugins:
11
- * <XDG_DATA_HOME | ~/.local/share>/opencode/storage/plugin/keepalive/state.json
12
- */
13
- export function stateFilePath(directory: string): string {
14
- const key = createHash("sha256").update(directory).digest("hex").slice(0, 16)
15
- return join(stateDirectoryPath(), `state-${key}.json`)
5
+ /** Stable short directory key shared by that directory's state-file readers/writers. */
6
+ export function stateDirectoryKey(directory: string): string {
7
+ return createHash("sha256").update(directory).digest("hex").slice(0, 16)
8
+ }
9
+
10
+ /** Filename for one process/plugin-instance state snapshot. */
11
+ export function instanceStateFilePath(directory: string, pid: number, instanceId: string): string {
12
+ return join(
13
+ stateDirectoryPath(),
14
+ `state-v2-${stateDirectoryKey(directory)}-${pid}-${instanceId}.json`,
15
+ )
16
16
  }
17
17
 
18
18
  export function stateDirectoryPath(): string {
@@ -26,9 +26,7 @@ export function stateDirectoryPath(): string {
26
26
  }
27
27
 
28
28
  /**
29
- * Runtime on/off control written by the TUI and read by the server plugin.
30
- * Scoped per directory (like the state file) so toggling keepalive in one
31
- * project does not affect every other open opencode session.
29
+ * Session runtime controls are stored by directory, with entries keyed by session ID.
32
30
  */
33
31
  export function controlFilePath(directory: string): string {
34
32
  const key = createHash("sha256").update(directory).digest("hex").slice(0, 16)