opencode-manager 0.4.0 → 0.4.1
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/README.md +70 -9
- package/bun.lock +5 -0
- package/package.json +2 -1
- package/src/cli/commands/chat.ts +37 -23
- package/src/cli/commands/projects.ts +25 -9
- package/src/cli/commands/sessions.ts +52 -27
- package/src/cli/commands/tokens.ts +28 -16
- package/src/cli/index.ts +41 -1
- package/src/cli/resolvers.ts +34 -9
- package/src/lib/opencode-data-provider.ts +685 -0
- package/src/lib/opencode-data-sqlite.ts +1973 -0
- package/tsconfig.json +1 -1
package/README.md
CHANGED
|
@@ -8,15 +8,27 @@ Terminal UI for inspecting, filtering, and pruning OpenCode metadata stored on d
|
|
|
8
8
|
## Screenshots
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
11
|
-
<img src="
|
|
11
|
+
<img src="oc-manager.png" alt="OpenCode Metadata Manager home screen showing projects and sessions" width="85%" />
|
|
12
12
|
<br />
|
|
13
13
|
<em>Main workspace with Projects (left) and Sessions (right) panels.</em>
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
|
-
<img src="
|
|
17
|
+
<img src="oc-manager-home.png" alt="OpenCode Metadata Manager home screen alternate view" width="85%" />
|
|
18
18
|
<br />
|
|
19
|
-
<em>
|
|
19
|
+
<em>Alternate home view with project/session context.</em>
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
<p align="center">
|
|
23
|
+
<img src="oc-manager-search.png" alt="OpenCode Metadata Manager search view" width="85%" />
|
|
24
|
+
<br />
|
|
25
|
+
<em>Fuzzy search across sessions with ranked results.</em>
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
<img src="oc-manager-cli.png" alt="OpenCode Metadata Manager CLI output" width="85%" />
|
|
30
|
+
<br />
|
|
31
|
+
<em>Scriptable CLI output for listing projects and sessions.</em>
|
|
20
32
|
</p>
|
|
21
33
|
|
|
22
34
|
## Features
|
|
@@ -32,6 +44,7 @@ Terminal UI for inspecting, filtering, and pruning OpenCode metadata stored on d
|
|
|
32
44
|
- Rich help overlay with live key hints (`?` or `H`).
|
|
33
45
|
- Zero-install via `bunx` so even CI shells can run it without cloning.
|
|
34
46
|
- **Token counting**: View token usage per session, per project, and globally.
|
|
47
|
+
- **Experimental SQLite backend**: Faster queries for large stores via `--experimental-sqlite`.
|
|
35
48
|
|
|
36
49
|
## Token Counting
|
|
37
50
|
|
|
@@ -117,6 +130,43 @@ The CLI provides scriptable access to all management operations. Use subcommands
|
|
|
117
130
|
| `-q, --quiet` | `false` | Suppress non-essential output |
|
|
118
131
|
| `-c, --clipboard` | `false` | Copy output to clipboard |
|
|
119
132
|
| `--backup-dir <path>` | — | Directory for backup copies before deletion |
|
|
133
|
+
| `--experimental-sqlite` | `false` | Use SQLite database instead of JSONL files (experimental) |
|
|
134
|
+
| `--db <path>` | `~/.local/share/opencode/opencode.db` | Path to SQLite database (implies `--experimental-sqlite`) |
|
|
135
|
+
| `--sqlite-strict` | `false` | Fail on any SQLite warning or malformed data |
|
|
136
|
+
| `--force-write` | `false` | Wait for SQLite write locks to clear before failing |
|
|
137
|
+
|
|
138
|
+
#### Experimental SQLite Support
|
|
139
|
+
|
|
140
|
+
OpenCode can store metadata in SQLite databases. The CLI supports this mode with `--experimental-sqlite` or by pointing directly at a database with `--db <path>`.
|
|
141
|
+
|
|
142
|
+
Key behaviors:
|
|
143
|
+
- Read operations open SQLite in readonly mode by default.
|
|
144
|
+
- Write operations may fail if OpenCode has the database locked. Use `--force-write` to wait for the lock to clear.
|
|
145
|
+
- When malformed rows are encountered, the CLI logs a warning and returns partial results. Use `--sqlite-strict` to fail fast.
|
|
146
|
+
|
|
147
|
+
When to use SQLite vs JSONL:
|
|
148
|
+
- Use SQLite when your OpenCode installation no longer writes JSONL files or when you want faster list/search operations on large stores.
|
|
149
|
+
- Use JSONL when you need maximal compatibility with older OpenCode versions or when you want to avoid SQLite locking.
|
|
150
|
+
|
|
151
|
+
Known limitations and differences:
|
|
152
|
+
- Schema changes in OpenCode may break compatibility. The CLI validates required tables/columns and warns if the schema is incomplete.
|
|
153
|
+
- SQLite records use virtual file paths (e.g., `sqlite:project:proj_123`) instead of JSON file paths.
|
|
154
|
+
- Results may differ slightly from JSONL when extra SQLite-only rows exist.
|
|
155
|
+
|
|
156
|
+
Examples:
|
|
157
|
+
```bash
|
|
158
|
+
# List projects using the default SQLite database
|
|
159
|
+
opencode-manager projects list --experimental-sqlite
|
|
160
|
+
|
|
161
|
+
# List sessions using an explicit SQLite database path
|
|
162
|
+
opencode-manager sessions list --db ~/.local/share/opencode/opencode.db
|
|
163
|
+
|
|
164
|
+
# Fail fast on malformed SQLite data
|
|
165
|
+
opencode-manager projects list --db ~/.local/share/opencode/opencode.db --sqlite-strict
|
|
166
|
+
|
|
167
|
+
# Wait for locks before destructive operations
|
|
168
|
+
opencode-manager projects delete --id proj_missing --db ~/.local/share/opencode/opencode.db --yes --force-write
|
|
169
|
+
```
|
|
120
170
|
|
|
121
171
|
#### Commands Overview
|
|
122
172
|
|
|
@@ -424,13 +474,24 @@ Delete commands (`projects delete`, `sessions delete`) remove **metadata files o
|
|
|
424
474
|
### Project Structure
|
|
425
475
|
```
|
|
426
476
|
src/
|
|
427
|
-
bin/opencode-manager.ts
|
|
477
|
+
bin/opencode-manager.ts # Bun-native CLI shim exposed as the bin entry
|
|
478
|
+
cli/
|
|
479
|
+
index.ts # Commander program with global options
|
|
480
|
+
commands/ # Subcommand implementations (projects, sessions, chat, tokens)
|
|
481
|
+
resolvers.ts # ID prefix resolution helpers
|
|
482
|
+
lib/
|
|
483
|
+
opencode-data.ts # JSONL file-based data access
|
|
484
|
+
opencode-data-sqlite.ts # SQLite backend (experimental)
|
|
485
|
+
opencode-data-provider.ts # Unified DataProvider abstraction
|
|
428
486
|
tui/
|
|
429
|
-
app.tsx
|
|
430
|
-
index.tsx
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
487
|
+
app.tsx # Main TUI implementation (panels, search, help)
|
|
488
|
+
index.tsx # TUI entrypoint with launchTUI(), parseArgs(), bootstrap()
|
|
489
|
+
tests/
|
|
490
|
+
fixtures/ # Test data (JSONL and SQLite fixtures)
|
|
491
|
+
lib/ # Unit tests for data modules
|
|
492
|
+
cli/ # CLI integration tests
|
|
493
|
+
manage_opencode_projects.py # Legacy Python launcher for backwards compatibility
|
|
494
|
+
PROJECT-SUMMARY.md # Extended design notes & roadmap
|
|
434
495
|
```
|
|
435
496
|
|
|
436
497
|
## Packaging & Publish
|
package/bun.lock
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"react": "^19.0.0",
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
+
"@types/bun": "^1.3.6",
|
|
15
16
|
"@types/node": "^22.8.5",
|
|
16
17
|
"@types/react": "^19.0.0",
|
|
17
18
|
"typescript": "^5.6.3",
|
|
@@ -95,6 +96,8 @@
|
|
|
95
96
|
|
|
96
97
|
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
|
97
98
|
|
|
99
|
+
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
|
100
|
+
|
|
98
101
|
"@types/node": ["@types/node@22.19.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA=="],
|
|
99
102
|
|
|
100
103
|
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
|
@@ -115,6 +118,8 @@
|
|
|
115
118
|
|
|
116
119
|
"bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
|
|
117
120
|
|
|
121
|
+
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
|
122
|
+
|
|
118
123
|
"bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
|
|
119
124
|
|
|
120
125
|
"bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eDgLN9teKTfmvrCqgwwmWNsNszxYs7IZdCqk0S1DCarvMhr4wcajoSBlA/nQA0/owwLduPTS8xxCnQp4/N/gDg=="],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-manager",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Terminal UI for inspecting OpenCode metadata stores.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"react": "^19.0.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
+
"@types/bun": "^1.3.6",
|
|
54
55
|
"@types/node": "^22.8.5",
|
|
55
56
|
"@types/react": "^19.0.0",
|
|
56
57
|
"typescript": "^5.6.3"
|
package/src/cli/commands/chat.ts
CHANGED
|
@@ -7,13 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { Command, type OptionValues } from "commander"
|
|
9
9
|
import { parseGlobalOptions, type GlobalOptions } from "../index"
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
loadSessionRecords,
|
|
13
|
-
hydrateChatMessageParts,
|
|
14
|
-
searchSessionsChat,
|
|
15
|
-
type ChatMessage,
|
|
16
|
-
} from "../../lib/opencode-data"
|
|
10
|
+
import { type ChatMessage, type ChatSearchResult } from "../../lib/opencode-data"
|
|
11
|
+
import { createProviderFromGlobalOptions } from "../../lib/opencode-data-provider"
|
|
17
12
|
import { copyToClipboard } from "../../lib/clipboard"
|
|
18
13
|
import { resolveSessionId } from "../resolvers"
|
|
19
14
|
import { withErrorHandling, UsageError, NotFoundError } from "../errors"
|
|
@@ -141,6 +136,16 @@ export function registerChatCommands(parent: Command): void {
|
|
|
141
136
|
searchOpts
|
|
142
137
|
)
|
|
143
138
|
})
|
|
139
|
+
|
|
140
|
+
chat.addHelpText(
|
|
141
|
+
"after",
|
|
142
|
+
[
|
|
143
|
+
"",
|
|
144
|
+
"Examples:",
|
|
145
|
+
" opencode-manager chat list --session <id> --experimental-sqlite",
|
|
146
|
+
" opencode-manager chat list --session <id> --db ~/.local/share/opencode/opencode.db",
|
|
147
|
+
].join("\n")
|
|
148
|
+
)
|
|
144
149
|
}
|
|
145
150
|
|
|
146
151
|
/**
|
|
@@ -153,19 +158,23 @@ async function handleChatList(
|
|
|
153
158
|
globalOpts: GlobalOptions,
|
|
154
159
|
listOpts: ChatListOptions
|
|
155
160
|
): Promise<void> {
|
|
156
|
-
//
|
|
161
|
+
// Create provider from global options (JSONL or SQLite)
|
|
162
|
+
const provider = createProviderFromGlobalOptions(globalOpts)
|
|
163
|
+
|
|
164
|
+
// Resolve session ID (with prefix matching) using the provider
|
|
157
165
|
const { session } = await resolveSessionId(listOpts.session, {
|
|
158
166
|
root: globalOpts.root,
|
|
159
167
|
allowPrefix: true,
|
|
168
|
+
provider,
|
|
160
169
|
})
|
|
161
170
|
|
|
162
171
|
// Load message index for the session
|
|
163
|
-
let messages = await loadSessionChatIndex(session.sessionId
|
|
172
|
+
let messages = await provider.loadSessionChatIndex(session.sessionId)
|
|
164
173
|
|
|
165
174
|
// Hydrate parts if requested
|
|
166
175
|
if (listOpts.includeParts) {
|
|
167
176
|
messages = await Promise.all(
|
|
168
|
-
messages.map((msg) => hydrateChatMessageParts(msg
|
|
177
|
+
messages.map((msg: ChatMessage) => provider.hydrateChatMessageParts(msg))
|
|
169
178
|
)
|
|
170
179
|
}
|
|
171
180
|
|
|
@@ -175,7 +184,7 @@ async function handleChatList(
|
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
// Add 1-based index for display
|
|
178
|
-
const indexedMessages: IndexedChatMessage[] = messages.map((msg, i) => ({
|
|
187
|
+
const indexedMessages: IndexedChatMessage[] = messages.map((msg: ChatMessage, i: number) => ({
|
|
179
188
|
...msg,
|
|
180
189
|
index: i + 1,
|
|
181
190
|
}))
|
|
@@ -207,14 +216,18 @@ async function handleChatShow(
|
|
|
207
216
|
)
|
|
208
217
|
}
|
|
209
218
|
|
|
210
|
-
//
|
|
219
|
+
// Create provider from global options (JSONL or SQLite)
|
|
220
|
+
const provider = createProviderFromGlobalOptions(globalOpts)
|
|
221
|
+
|
|
222
|
+
// Resolve session ID (with prefix matching) using the provider
|
|
211
223
|
const { session } = await resolveSessionId(showOpts.session, {
|
|
212
224
|
root: globalOpts.root,
|
|
213
225
|
allowPrefix: true,
|
|
226
|
+
provider,
|
|
214
227
|
})
|
|
215
228
|
|
|
216
229
|
// Load all messages for the session
|
|
217
|
-
const messages = await loadSessionChatIndex(session.sessionId
|
|
230
|
+
const messages = await provider.loadSessionChatIndex(session.sessionId)
|
|
218
231
|
|
|
219
232
|
if (messages.length === 0) {
|
|
220
233
|
throw new NotFoundError(
|
|
@@ -228,17 +241,17 @@ async function handleChatShow(
|
|
|
228
241
|
if (showOpts.message) {
|
|
229
242
|
// Find by message ID (exact or prefix match)
|
|
230
243
|
const messageId = showOpts.message
|
|
231
|
-
message = messages.find((m) => m.messageId === messageId)
|
|
244
|
+
message = messages.find((m: ChatMessage) => m.messageId === messageId)
|
|
232
245
|
if (!message) {
|
|
233
246
|
// Try prefix matching
|
|
234
|
-
const prefixMatches = messages.filter((m) =>
|
|
247
|
+
const prefixMatches = messages.filter((m: ChatMessage) =>
|
|
235
248
|
m.messageId.startsWith(messageId)
|
|
236
249
|
)
|
|
237
250
|
if (prefixMatches.length === 1) {
|
|
238
251
|
message = prefixMatches[0]
|
|
239
252
|
} else if (prefixMatches.length > 1) {
|
|
240
253
|
throw new NotFoundError(
|
|
241
|
-
`Ambiguous message ID prefix "${messageId}" matches ${prefixMatches.length} messages: ${prefixMatches.map((m) => m.messageId).join(", ")}`,
|
|
254
|
+
`Ambiguous message ID prefix "${messageId}" matches ${prefixMatches.length} messages: ${prefixMatches.map((m: ChatMessage) => m.messageId).join(", ")}`,
|
|
242
255
|
"message"
|
|
243
256
|
)
|
|
244
257
|
} else {
|
|
@@ -261,12 +274,12 @@ async function handleChatShow(
|
|
|
261
274
|
}
|
|
262
275
|
|
|
263
276
|
// Hydrate message parts to get full content
|
|
264
|
-
const hydratedMessage = await hydrateChatMessageParts(message
|
|
277
|
+
const hydratedMessage = await provider.hydrateChatMessageParts(message)
|
|
265
278
|
|
|
266
279
|
// Copy to clipboard if requested
|
|
267
280
|
if (showOpts.clipboard) {
|
|
268
281
|
const content = hydratedMessage.parts
|
|
269
|
-
?.map((p) => p.text)
|
|
282
|
+
?.map((p: { text: string }) => p.text)
|
|
270
283
|
.join("\n\n") ?? hydratedMessage.previewText
|
|
271
284
|
try {
|
|
272
285
|
await copyToClipboard(content)
|
|
@@ -296,22 +309,23 @@ async function handleChatSearch(
|
|
|
296
309
|
globalOpts: GlobalOptions,
|
|
297
310
|
searchOpts: ChatSearchOptions
|
|
298
311
|
): Promise<void> {
|
|
312
|
+
// Create provider from global options (JSONL or SQLite)
|
|
313
|
+
const provider = createProviderFromGlobalOptions(globalOpts)
|
|
314
|
+
|
|
299
315
|
// Load sessions to search
|
|
300
|
-
const sessions = await loadSessionRecords({
|
|
301
|
-
root: globalOpts.root,
|
|
316
|
+
const sessions = await provider.loadSessionRecords({
|
|
302
317
|
projectId: searchOpts.project,
|
|
303
318
|
})
|
|
304
319
|
|
|
305
320
|
// Search across sessions using the limit from global options
|
|
306
|
-
const results = await searchSessionsChat(
|
|
321
|
+
const results = await provider.searchSessionsChat(
|
|
307
322
|
sessions,
|
|
308
323
|
searchOpts.query,
|
|
309
|
-
globalOpts.root,
|
|
310
324
|
{ maxResults: globalOpts.limit }
|
|
311
325
|
)
|
|
312
326
|
|
|
313
327
|
// Add 1-based index for display
|
|
314
|
-
const indexedResults: IndexedChatSearchResult[] = results.map((result, i) => ({
|
|
328
|
+
const indexedResults: IndexedChatSearchResult[] = results.map((result: ChatSearchResult, i: number) => ({
|
|
315
329
|
...result,
|
|
316
330
|
index: i + 1,
|
|
317
331
|
}))
|
|
@@ -7,11 +7,10 @@
|
|
|
7
7
|
import { Command, type OptionValues } from "commander"
|
|
8
8
|
import { parseGlobalOptions, type GlobalOptions } from "../index"
|
|
9
9
|
import {
|
|
10
|
-
loadProjectRecords,
|
|
11
10
|
filterProjectsByState,
|
|
12
|
-
deleteProjectMetadata,
|
|
13
11
|
type ProjectRecord,
|
|
14
12
|
} from "../../lib/opencode-data"
|
|
13
|
+
import { createProviderFromGlobalOptions } from "../../lib/opencode-data-provider"
|
|
15
14
|
import {
|
|
16
15
|
getOutputOptions,
|
|
17
16
|
printProjectsOutput,
|
|
@@ -107,6 +106,16 @@ export function registerProjectsCommands(parent: Command): void {
|
|
|
107
106
|
deleteOpts
|
|
108
107
|
)
|
|
109
108
|
})
|
|
109
|
+
|
|
110
|
+
projects.addHelpText(
|
|
111
|
+
"after",
|
|
112
|
+
[
|
|
113
|
+
"",
|
|
114
|
+
"Examples:",
|
|
115
|
+
" opencode-manager projects list --experimental-sqlite",
|
|
116
|
+
" opencode-manager projects list --db ~/.local/share/opencode/opencode.db",
|
|
117
|
+
].join("\n")
|
|
118
|
+
)
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
/**
|
|
@@ -116,8 +125,11 @@ async function handleProjectsList(
|
|
|
116
125
|
globalOpts: GlobalOptions,
|
|
117
126
|
listOpts: ProjectsListOptions
|
|
118
127
|
): Promise<void> {
|
|
128
|
+
// Create data provider based on global options (JSONL or SQLite backend)
|
|
129
|
+
const provider = createProviderFromGlobalOptions(globalOpts)
|
|
130
|
+
|
|
119
131
|
// Load project records from the data layer
|
|
120
|
-
let projects = await loadProjectRecords(
|
|
132
|
+
let projects = await provider.loadProjectRecords()
|
|
121
133
|
|
|
122
134
|
// Apply missing-only filter if requested
|
|
123
135
|
if (listOpts.missingOnly) {
|
|
@@ -160,10 +172,14 @@ async function handleProjectsDelete(
|
|
|
160
172
|
): Promise<void> {
|
|
161
173
|
const outputOpts = getOutputOptions(globalOpts)
|
|
162
174
|
|
|
163
|
-
//
|
|
175
|
+
// Create data provider based on global options (JSONL or SQLite backend)
|
|
176
|
+
const provider = createProviderFromGlobalOptions(globalOpts)
|
|
177
|
+
|
|
178
|
+
// Resolve project ID to a project record (use provider for backend-agnostic resolution)
|
|
164
179
|
const { project } = await resolveProjectId(deleteOpts.id, {
|
|
165
180
|
root: globalOpts.root,
|
|
166
181
|
allowPrefix: true,
|
|
182
|
+
provider,
|
|
167
183
|
})
|
|
168
184
|
|
|
169
185
|
const pathsToDelete = [project.filePath]
|
|
@@ -178,8 +194,8 @@ async function handleProjectsDelete(
|
|
|
178
194
|
// Require confirmation for destructive operation
|
|
179
195
|
requireConfirmation(deleteOpts.yes, "Project deletion")
|
|
180
196
|
|
|
181
|
-
// Backup files if requested
|
|
182
|
-
if (deleteOpts.backupDir) {
|
|
197
|
+
// Backup files if requested (only applies to JSONL backend - SQLite has no files to backup)
|
|
198
|
+
if (deleteOpts.backupDir && provider.backend === "jsonl") {
|
|
183
199
|
const backupResult = await copyToBackupDir(pathsToDelete, {
|
|
184
200
|
backupDir: deleteOpts.backupDir,
|
|
185
201
|
prefix: "project",
|
|
@@ -201,13 +217,13 @@ async function handleProjectsDelete(
|
|
|
201
217
|
}
|
|
202
218
|
}
|
|
203
219
|
|
|
204
|
-
// Perform the deletion
|
|
205
|
-
const deleteResult = await deleteProjectMetadata([project], { dryRun: false })
|
|
220
|
+
// Perform the deletion using the provider (handles both JSONL and SQLite)
|
|
221
|
+
const deleteResult = await provider.deleteProjectMetadata([project], { dryRun: false })
|
|
206
222
|
|
|
207
223
|
if (deleteResult.failed.length > 0) {
|
|
208
224
|
throw new FileOperationError(
|
|
209
225
|
`Failed to delete ${deleteResult.failed.length} file(s): ${deleteResult.failed
|
|
210
|
-
.map((f) => `${f.path}: ${f.error}`)
|
|
226
|
+
.map((f: { path: string; error?: string }) => `${f.path}: ${f.error || "unknown error"}`)
|
|
211
227
|
.join(", ")}`,
|
|
212
228
|
"delete"
|
|
213
229
|
)
|
|
@@ -8,14 +8,10 @@
|
|
|
8
8
|
import { Command, type OptionValues } from "commander"
|
|
9
9
|
import { parseGlobalOptions, type GlobalOptions } from "../index"
|
|
10
10
|
import {
|
|
11
|
-
loadSessionRecords,
|
|
12
|
-
loadProjectRecords,
|
|
13
|
-
deleteSessionMetadata,
|
|
14
|
-
updateSessionTitle,
|
|
15
|
-
moveSession,
|
|
16
11
|
copySession,
|
|
17
12
|
type SessionRecord,
|
|
18
13
|
} from "../../lib/opencode-data"
|
|
14
|
+
import { createProviderFromGlobalOptions } from "../../lib/opencode-data-provider"
|
|
19
15
|
import {
|
|
20
16
|
getOutputOptions,
|
|
21
17
|
printSessionsOutput,
|
|
@@ -195,6 +191,16 @@ export function registerSessionsCommands(parent: Command): void {
|
|
|
195
191
|
copyOpts
|
|
196
192
|
)
|
|
197
193
|
})
|
|
194
|
+
|
|
195
|
+
sessions.addHelpText(
|
|
196
|
+
"after",
|
|
197
|
+
[
|
|
198
|
+
"",
|
|
199
|
+
"Examples:",
|
|
200
|
+
" opencode-manager sessions list --experimental-sqlite",
|
|
201
|
+
" opencode-manager sessions list --db ~/.local/share/opencode/opencode.db",
|
|
202
|
+
].join("\n")
|
|
203
|
+
)
|
|
198
204
|
}
|
|
199
205
|
|
|
200
206
|
/**
|
|
@@ -217,10 +223,12 @@ async function handleSessionsList(
|
|
|
217
223
|
globalOpts: GlobalOptions,
|
|
218
224
|
listOpts: SessionsListOptions
|
|
219
225
|
): Promise<void> {
|
|
226
|
+
// Create data provider based on global options (JSONL or SQLite backend)
|
|
227
|
+
const provider = createProviderFromGlobalOptions(globalOpts)
|
|
228
|
+
|
|
220
229
|
// Load session records from the data layer
|
|
221
230
|
// If a project filter is provided, pass it to the loader
|
|
222
|
-
let sessions = await loadSessionRecords({
|
|
223
|
-
root: globalOpts.root,
|
|
231
|
+
let sessions = await provider.loadSessionRecords({
|
|
224
232
|
projectId: listOpts.project,
|
|
225
233
|
})
|
|
226
234
|
|
|
@@ -277,7 +285,7 @@ async function handleSessionsList(
|
|
|
277
285
|
* Handle the sessions delete command.
|
|
278
286
|
*
|
|
279
287
|
* This command deletes a session's metadata file from the OpenCode storage.
|
|
280
|
-
*
|
|
288
|
+
* For SQLite backend, it deletes session, messages, and parts in a transaction.
|
|
281
289
|
*
|
|
282
290
|
* Exit codes:
|
|
283
291
|
* - 0: Success (or dry-run completed)
|
|
@@ -291,10 +299,14 @@ async function handleSessionsDelete(
|
|
|
291
299
|
): Promise<void> {
|
|
292
300
|
const outputOpts = getOutputOptions(globalOpts)
|
|
293
301
|
|
|
294
|
-
//
|
|
302
|
+
// Create data provider based on global options (JSONL or SQLite backend)
|
|
303
|
+
const provider = createProviderFromGlobalOptions(globalOpts)
|
|
304
|
+
|
|
305
|
+
// Resolve session ID to a session record (use provider for backend-agnostic resolution)
|
|
295
306
|
const { session } = await resolveSessionId(deleteOpts.session, {
|
|
296
307
|
root: globalOpts.root,
|
|
297
308
|
allowPrefix: true,
|
|
309
|
+
provider,
|
|
298
310
|
})
|
|
299
311
|
|
|
300
312
|
const pathsToDelete = [session.filePath]
|
|
@@ -309,8 +321,8 @@ async function handleSessionsDelete(
|
|
|
309
321
|
// Require confirmation for destructive operation
|
|
310
322
|
requireConfirmation(deleteOpts.yes, "Session deletion")
|
|
311
323
|
|
|
312
|
-
// Backup files if requested
|
|
313
|
-
if (deleteOpts.backupDir) {
|
|
324
|
+
// Backup files if requested (only applies to JSONL backend - SQLite has no files to backup)
|
|
325
|
+
if (deleteOpts.backupDir && provider.backend === "jsonl") {
|
|
314
326
|
const backupResult = await copyToBackupDir(pathsToDelete, {
|
|
315
327
|
backupDir: deleteOpts.backupDir,
|
|
316
328
|
prefix: "session",
|
|
@@ -332,13 +344,13 @@ async function handleSessionsDelete(
|
|
|
332
344
|
}
|
|
333
345
|
}
|
|
334
346
|
|
|
335
|
-
// Perform the deletion
|
|
336
|
-
const deleteResult = await deleteSessionMetadata([session], { dryRun: false })
|
|
347
|
+
// Perform the deletion using the provider (handles both JSONL and SQLite)
|
|
348
|
+
const deleteResult = await provider.deleteSessionMetadata([session], { dryRun: false })
|
|
337
349
|
|
|
338
350
|
if (deleteResult.failed.length > 0) {
|
|
339
351
|
throw new FileOperationError(
|
|
340
352
|
`Failed to delete ${deleteResult.failed.length} file(s): ${deleteResult.failed
|
|
341
|
-
.map((f) => `${f.path}: ${f.error}`)
|
|
353
|
+
.map((f) => `${f.path}: ${f.error || "unknown error"}`)
|
|
342
354
|
.join(", ")}`,
|
|
343
355
|
"delete"
|
|
344
356
|
)
|
|
@@ -356,6 +368,7 @@ async function handleSessionsDelete(
|
|
|
356
368
|
* Handle the sessions rename command.
|
|
357
369
|
*
|
|
358
370
|
* This command updates a session's title in its metadata file.
|
|
371
|
+
* For SQLite backend, it updates the title in the database.
|
|
359
372
|
*
|
|
360
373
|
* Exit codes:
|
|
361
374
|
* - 0: Success
|
|
@@ -374,14 +387,18 @@ async function handleSessionsRename(
|
|
|
374
387
|
throw new UsageError("Title cannot be empty")
|
|
375
388
|
}
|
|
376
389
|
|
|
377
|
-
//
|
|
390
|
+
// Create data provider based on global options (JSONL or SQLite backend)
|
|
391
|
+
const provider = createProviderFromGlobalOptions(globalOpts)
|
|
392
|
+
|
|
393
|
+
// Resolve session ID to a session record (use provider for backend-agnostic resolution)
|
|
378
394
|
const { session } = await resolveSessionId(renameOpts.session, {
|
|
379
395
|
root: globalOpts.root,
|
|
380
396
|
allowPrefix: true,
|
|
397
|
+
provider,
|
|
381
398
|
})
|
|
382
399
|
|
|
383
|
-
// Update the session title
|
|
384
|
-
await updateSessionTitle(session
|
|
400
|
+
// Update the session title using the provider
|
|
401
|
+
await provider.updateSessionTitle(session, newTitle)
|
|
385
402
|
|
|
386
403
|
// Output success
|
|
387
404
|
printSuccessOutput(
|
|
@@ -395,7 +412,8 @@ async function handleSessionsRename(
|
|
|
395
412
|
* Handle the sessions move command.
|
|
396
413
|
*
|
|
397
414
|
* This command moves a session to a different project.
|
|
398
|
-
*
|
|
415
|
+
* For JSONL backend, the session file is moved to the target project's session directory.
|
|
416
|
+
* For SQLite backend, the project_id column is updated in the database.
|
|
399
417
|
*
|
|
400
418
|
* Exit codes:
|
|
401
419
|
* - 0: Success
|
|
@@ -408,39 +426,46 @@ async function handleSessionsMove(
|
|
|
408
426
|
): Promise<void> {
|
|
409
427
|
const outputOpts = getOutputOptions(globalOpts)
|
|
410
428
|
|
|
411
|
-
//
|
|
429
|
+
// Create data provider based on global options (JSONL or SQLite backend)
|
|
430
|
+
const provider = createProviderFromGlobalOptions(globalOpts)
|
|
431
|
+
|
|
432
|
+
// Resolve session ID to a session record (use provider for backend-agnostic resolution)
|
|
412
433
|
const { session } = await resolveSessionId(moveOpts.session, {
|
|
413
434
|
root: globalOpts.root,
|
|
414
435
|
allowPrefix: true,
|
|
436
|
+
provider,
|
|
415
437
|
})
|
|
416
438
|
|
|
417
439
|
// Validate target project exists
|
|
418
440
|
// Use prefix matching for convenience, but require exactly one match
|
|
419
|
-
|
|
441
|
+
// For SQLite, we don't enforce target project existence (consistent with JSONL behavior)
|
|
442
|
+
// but we still try to resolve it for prefix matching
|
|
443
|
+
const { project: targetProject } = await resolveProjectId(moveOpts.to, {
|
|
420
444
|
root: globalOpts.root,
|
|
421
445
|
allowPrefix: true,
|
|
446
|
+
provider,
|
|
422
447
|
})
|
|
423
448
|
|
|
424
449
|
// Check if session is already in the target project
|
|
425
|
-
if (session.projectId ===
|
|
450
|
+
if (session.projectId === targetProject.projectId) {
|
|
426
451
|
printSuccessOutput(
|
|
427
|
-
`Session ${session.sessionId} is already in project ${
|
|
428
|
-
{ sessionId: session.sessionId, projectId:
|
|
452
|
+
`Session ${session.sessionId} is already in project ${targetProject.projectId}`,
|
|
453
|
+
{ sessionId: session.sessionId, projectId: targetProject.projectId, moved: false },
|
|
429
454
|
outputOpts.format
|
|
430
455
|
)
|
|
431
456
|
return
|
|
432
457
|
}
|
|
433
458
|
|
|
434
|
-
// Move the session
|
|
435
|
-
const newRecord = await moveSession(session,
|
|
459
|
+
// Move the session using the provider
|
|
460
|
+
const newRecord = await provider.moveSession(session, targetProject.projectId)
|
|
436
461
|
|
|
437
462
|
// Output success
|
|
438
463
|
printSuccessOutput(
|
|
439
|
-
`Moved session ${session.sessionId} to project ${
|
|
464
|
+
`Moved session ${session.sessionId} to project ${targetProject.projectId}`,
|
|
440
465
|
{
|
|
441
466
|
sessionId: session.sessionId,
|
|
442
467
|
fromProject: session.projectId,
|
|
443
|
-
toProject:
|
|
468
|
+
toProject: targetProject.projectId,
|
|
444
469
|
newPath: newRecord.filePath,
|
|
445
470
|
},
|
|
446
471
|
outputOpts.format
|