@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.
- package/MIGRATION.md +57 -0
- package/README.md +75 -48
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1097 -614
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +8 -25
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/control.d.ts +10 -4
- package/dist/lib/control.d.ts.map +1 -1
- package/dist/lib/paths.d.ts +5 -12
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/replay-warmer.d.ts +95 -0
- package/dist/lib/replay-warmer.d.ts.map +1 -0
- package/dist/lib/state.d.ts +15 -114
- package/dist/lib/state.d.ts.map +1 -1
- package/dist/lib/tui/commands.d.ts +2 -0
- package/dist/lib/tui/commands.d.ts.map +1 -1
- package/dist/lib/tui/footer.d.ts +5 -5
- package/dist/lib/tui/footer.d.ts.map +1 -1
- package/dist/lib/tui/format.d.ts +8 -0
- package/dist/lib/tui/format.d.ts.map +1 -1
- package/dist/lib/types.d.ts +123 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/tui.d.ts +3 -0
- package/dist/tui.d.ts.map +1 -1
- package/lib/config.ts +106 -74
- package/lib/control.ts +69 -20
- package/lib/paths.ts +12 -14
- package/lib/replay-warmer.ts +1200 -0
- package/lib/state.ts +172 -175
- package/lib/tui/commands.tsx +76 -0
- package/lib/tui/footer.tsx +55 -130
- package/lib/tui/format.ts +40 -0
- package/lib/types.ts +130 -0
- package/package.json +3 -2
- package/tui.tsx +40 -0
- package/dist/lib/keepalive.d.ts +0 -85
- package/dist/lib/keepalive.d.ts.map +0 -1
- package/dist/lib/model.d.ts +0 -8
- package/dist/lib/model.d.ts.map +0 -1
- package/dist/lib/system.d.ts +0 -11
- package/dist/lib/system.d.ts.map +0 -1
- package/lib/keepalive.ts +0 -636
- package/lib/model.ts +0 -19
- package/lib/system.ts +0 -19
- 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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (typeof value.
|
|
19
|
-
|
|
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
|
|
39
|
+
return { version: VERSION, sessions: {} }
|
|
22
40
|
}
|
|
23
41
|
}
|
|
24
42
|
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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}.${
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
*
|
|
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)
|