opencode-manager 0.3.1 → 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/PROJECT-SUMMARY.md +104 -24
- package/README.md +403 -14
- package/bun.lock +22 -1
- package/manage_opencode_projects.py +71 -66
- package/package.json +7 -3
- package/src/bin/opencode-manager.ts +133 -3
- package/src/cli/backup.ts +324 -0
- package/src/cli/commands/chat.ts +336 -0
- package/src/cli/commands/projects.ts +238 -0
- package/src/cli/commands/sessions.ts +520 -0
- package/src/cli/commands/tokens.ts +180 -0
- package/src/cli/commands/tui.ts +36 -0
- package/src/cli/errors.ts +259 -0
- package/src/cli/formatters/json.ts +184 -0
- package/src/cli/formatters/ndjson.ts +71 -0
- package/src/cli/formatters/table.ts +837 -0
- package/src/cli/index.ts +209 -0
- package/src/cli/output.ts +661 -0
- package/src/cli/resolvers.ts +274 -0
- package/src/lib/clipboard.ts +37 -0
- package/src/lib/opencode-data-provider.ts +685 -0
- package/src/lib/opencode-data-sqlite.ts +1973 -0
- package/src/lib/opencode-data.ts +380 -1
- package/src/lib/search.ts +170 -0
- package/src/{opencode-tui.tsx → tui/app.tsx} +739 -105
- package/src/tui/args.ts +92 -0
- package/src/tui/index.tsx +46 -0
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI resolver helpers for converting user-provided IDs to records.
|
|
3
|
+
*
|
|
4
|
+
* These helpers provide consistent ID resolution across all CLI commands,
|
|
5
|
+
* supporting both exact matches and flexible matching patterns.
|
|
6
|
+
*
|
|
7
|
+
* Resolvers can optionally accept a DataProvider to support both JSONL and
|
|
8
|
+
* SQLite backends. When no provider is given, they fall back to direct JSONL
|
|
9
|
+
* loading for backward compatibility.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
loadProjectRecords,
|
|
14
|
+
loadSessionRecords,
|
|
15
|
+
type LoadOptions,
|
|
16
|
+
type SessionLoadOptions,
|
|
17
|
+
type ProjectRecord,
|
|
18
|
+
type SessionRecord,
|
|
19
|
+
} from "../lib/opencode-data"
|
|
20
|
+
import { type DataProvider } from "../lib/opencode-data-provider"
|
|
21
|
+
import { NotFoundError, projectNotFound, sessionNotFound } from "./errors"
|
|
22
|
+
|
|
23
|
+
// ========================
|
|
24
|
+
// Session Resolution
|
|
25
|
+
// ========================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Options for resolving session IDs.
|
|
29
|
+
*/
|
|
30
|
+
export interface ResolveSessionOptions extends SessionLoadOptions {
|
|
31
|
+
/**
|
|
32
|
+
* If true, allow partial prefix matching when exact match fails.
|
|
33
|
+
* Requires the prefix to match exactly one session uniquely.
|
|
34
|
+
* Defaults to false.
|
|
35
|
+
*/
|
|
36
|
+
allowPrefix?: boolean
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Optional data provider for backend-agnostic data loading.
|
|
40
|
+
* When provided, uses the provider's loadSessionRecords method.
|
|
41
|
+
* When omitted, falls back to direct JSONL loading for backward compatibility.
|
|
42
|
+
*/
|
|
43
|
+
provider?: DataProvider
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Result of a session resolution attempt.
|
|
48
|
+
*/
|
|
49
|
+
export interface ResolveSessionResult {
|
|
50
|
+
session: SessionRecord
|
|
51
|
+
/** How the session was matched */
|
|
52
|
+
matchType: "exact" | "prefix"
|
|
53
|
+
/** All sessions that were loaded (for reuse) */
|
|
54
|
+
allSessions: SessionRecord[]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Find a session by exact ID from a pre-loaded list of sessions.
|
|
59
|
+
*
|
|
60
|
+
* @param sessions - Pre-loaded session records
|
|
61
|
+
* @param sessionId - Session ID to find
|
|
62
|
+
* @returns The matching session
|
|
63
|
+
* @throws NotFoundError if session doesn't exist
|
|
64
|
+
*/
|
|
65
|
+
export function findSessionById(
|
|
66
|
+
sessions: SessionRecord[],
|
|
67
|
+
sessionId: string
|
|
68
|
+
): SessionRecord {
|
|
69
|
+
const session = sessions.find((s) => s.sessionId === sessionId)
|
|
70
|
+
if (!session) {
|
|
71
|
+
sessionNotFound(sessionId)
|
|
72
|
+
}
|
|
73
|
+
return session
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Find sessions matching a prefix from a pre-loaded list.
|
|
78
|
+
*
|
|
79
|
+
* @param sessions - Pre-loaded session records
|
|
80
|
+
* @param prefix - Session ID prefix to match
|
|
81
|
+
* @returns Array of matching sessions
|
|
82
|
+
*/
|
|
83
|
+
export function findSessionsByPrefix(
|
|
84
|
+
sessions: SessionRecord[],
|
|
85
|
+
prefix: string
|
|
86
|
+
): SessionRecord[] {
|
|
87
|
+
return sessions.filter((s) => s.sessionId.startsWith(prefix))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve a session ID to a session record, loading data as needed.
|
|
92
|
+
*
|
|
93
|
+
* Supports exact matching and optional prefix matching.
|
|
94
|
+
*
|
|
95
|
+
* @param sessionId - Session ID or prefix to resolve
|
|
96
|
+
* @param options - Resolution options including root, projectId filters, and optional provider
|
|
97
|
+
* @returns Resolution result with session and metadata
|
|
98
|
+
* @throws NotFoundError if no session matches
|
|
99
|
+
* @throws NotFoundError if prefix matches multiple sessions (ambiguous)
|
|
100
|
+
*/
|
|
101
|
+
export async function resolveSessionId(
|
|
102
|
+
sessionId: string,
|
|
103
|
+
options: ResolveSessionOptions = {}
|
|
104
|
+
): Promise<ResolveSessionResult> {
|
|
105
|
+
// Use provider if available, otherwise fall back to direct JSONL loading
|
|
106
|
+
const sessions = options.provider
|
|
107
|
+
? await options.provider.loadSessionRecords({ projectId: options.projectId })
|
|
108
|
+
: await loadSessionRecords({
|
|
109
|
+
root: options.root,
|
|
110
|
+
projectId: options.projectId,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// Try exact match first
|
|
114
|
+
const exactMatch = sessions.find((s) => s.sessionId === sessionId)
|
|
115
|
+
if (exactMatch) {
|
|
116
|
+
return {
|
|
117
|
+
session: exactMatch,
|
|
118
|
+
matchType: "exact",
|
|
119
|
+
allSessions: sessions,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Try prefix match if allowed
|
|
124
|
+
if (options.allowPrefix) {
|
|
125
|
+
const prefixMatches = findSessionsByPrefix(sessions, sessionId)
|
|
126
|
+
|
|
127
|
+
if (prefixMatches.length === 1) {
|
|
128
|
+
return {
|
|
129
|
+
session: prefixMatches[0],
|
|
130
|
+
matchType: "prefix",
|
|
131
|
+
allSessions: sessions,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (prefixMatches.length > 1) {
|
|
136
|
+
throw new NotFoundError(
|
|
137
|
+
`Ambiguous session ID prefix "${sessionId}" matches ${prefixMatches.length} sessions: ${prefixMatches
|
|
138
|
+
.slice(0, 3)
|
|
139
|
+
.map((s) => s.sessionId)
|
|
140
|
+
.join(", ")}${prefixMatches.length > 3 ? "..." : ""}`,
|
|
141
|
+
"session"
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// No match found
|
|
147
|
+
sessionNotFound(sessionId)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ========================
|
|
151
|
+
// Project Resolution
|
|
152
|
+
// ========================
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Options for resolving project IDs.
|
|
156
|
+
*/
|
|
157
|
+
export interface ResolveProjectOptions extends LoadOptions {
|
|
158
|
+
/**
|
|
159
|
+
* If true, allow partial prefix matching when exact match fails.
|
|
160
|
+
* Requires the prefix to match exactly one project uniquely.
|
|
161
|
+
* Defaults to false.
|
|
162
|
+
*/
|
|
163
|
+
allowPrefix?: boolean
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Optional data provider for backend-agnostic data loading.
|
|
167
|
+
* When provided, uses the provider's loadProjectRecords method.
|
|
168
|
+
* When omitted, falls back to direct JSONL loading for backward compatibility.
|
|
169
|
+
*/
|
|
170
|
+
provider?: DataProvider
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Result of a project resolution attempt.
|
|
175
|
+
*/
|
|
176
|
+
export interface ResolveProjectResult {
|
|
177
|
+
project: ProjectRecord
|
|
178
|
+
/** How the project was matched */
|
|
179
|
+
matchType: "exact" | "prefix"
|
|
180
|
+
/** All projects that were loaded (for reuse) */
|
|
181
|
+
allProjects: ProjectRecord[]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Find a project by exact ID from a pre-loaded list of projects.
|
|
186
|
+
*
|
|
187
|
+
* @param projects - Pre-loaded project records
|
|
188
|
+
* @param projectId - Project ID to find
|
|
189
|
+
* @returns The matching project
|
|
190
|
+
* @throws NotFoundError if project doesn't exist
|
|
191
|
+
*/
|
|
192
|
+
export function findProjectById(
|
|
193
|
+
projects: ProjectRecord[],
|
|
194
|
+
projectId: string
|
|
195
|
+
): ProjectRecord {
|
|
196
|
+
const project = projects.find((p) => p.projectId === projectId)
|
|
197
|
+
if (!project) {
|
|
198
|
+
projectNotFound(projectId)
|
|
199
|
+
}
|
|
200
|
+
return project
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Find projects matching a prefix from a pre-loaded list.
|
|
205
|
+
*
|
|
206
|
+
* @param projects - Pre-loaded project records
|
|
207
|
+
* @param prefix - Project ID prefix to match
|
|
208
|
+
* @returns Array of matching projects
|
|
209
|
+
*/
|
|
210
|
+
export function findProjectsByPrefix(
|
|
211
|
+
projects: ProjectRecord[],
|
|
212
|
+
prefix: string
|
|
213
|
+
): ProjectRecord[] {
|
|
214
|
+
return projects.filter((p) => p.projectId.startsWith(prefix))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Resolve a project ID to a project record, loading data as needed.
|
|
219
|
+
*
|
|
220
|
+
* Supports exact matching and optional prefix matching.
|
|
221
|
+
*
|
|
222
|
+
* @param projectId - Project ID or prefix to resolve
|
|
223
|
+
* @param options - Resolution options including root and optional provider
|
|
224
|
+
* @returns Resolution result with project and metadata
|
|
225
|
+
* @throws NotFoundError if no project matches
|
|
226
|
+
* @throws NotFoundError if prefix matches multiple projects (ambiguous)
|
|
227
|
+
*/
|
|
228
|
+
export async function resolveProjectId(
|
|
229
|
+
projectId: string,
|
|
230
|
+
options: ResolveProjectOptions = {}
|
|
231
|
+
): Promise<ResolveProjectResult> {
|
|
232
|
+
// Use provider if available, otherwise fall back to direct JSONL loading
|
|
233
|
+
const projects = options.provider
|
|
234
|
+
? await options.provider.loadProjectRecords()
|
|
235
|
+
: await loadProjectRecords({
|
|
236
|
+
root: options.root,
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
// Try exact match first
|
|
240
|
+
const exactMatch = projects.find((p) => p.projectId === projectId)
|
|
241
|
+
if (exactMatch) {
|
|
242
|
+
return {
|
|
243
|
+
project: exactMatch,
|
|
244
|
+
matchType: "exact",
|
|
245
|
+
allProjects: projects,
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Try prefix match if allowed
|
|
250
|
+
if (options.allowPrefix) {
|
|
251
|
+
const prefixMatches = findProjectsByPrefix(projects, projectId)
|
|
252
|
+
|
|
253
|
+
if (prefixMatches.length === 1) {
|
|
254
|
+
return {
|
|
255
|
+
project: prefixMatches[0],
|
|
256
|
+
matchType: "prefix",
|
|
257
|
+
allProjects: projects,
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (prefixMatches.length > 1) {
|
|
262
|
+
throw new NotFoundError(
|
|
263
|
+
`Ambiguous project ID prefix "${projectId}" matches ${prefixMatches.length} projects: ${prefixMatches
|
|
264
|
+
.slice(0, 3)
|
|
265
|
+
.map((p) => p.projectId)
|
|
266
|
+
.join(", ")}${prefixMatches.length > 3 ? "..." : ""}`,
|
|
267
|
+
"project"
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// No match found
|
|
273
|
+
projectNotFound(projectId)
|
|
274
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { exec } from "node:child_process"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Copy text to the system clipboard.
|
|
5
|
+
* Uses pbcopy on macOS and xclip on Linux.
|
|
6
|
+
*
|
|
7
|
+
* @param text The text to copy to clipboard
|
|
8
|
+
* @returns Promise that resolves when copy is complete, rejects on error
|
|
9
|
+
*/
|
|
10
|
+
export function copyToClipboard(text: string): Promise<void> {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const cmd =
|
|
13
|
+
process.platform === "darwin" ? "pbcopy" : "xclip -selection clipboard"
|
|
14
|
+
const proc = exec(cmd, (error) => {
|
|
15
|
+
if (error) {
|
|
16
|
+
reject(error)
|
|
17
|
+
} else {
|
|
18
|
+
resolve()
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
proc.stdin?.write(text)
|
|
22
|
+
proc.stdin?.end()
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Copy text to clipboard, logging errors to console.
|
|
28
|
+
* This is a fire-and-forget version for use in contexts where
|
|
29
|
+
* error handling is not critical.
|
|
30
|
+
*
|
|
31
|
+
* @param text The text to copy to clipboard
|
|
32
|
+
*/
|
|
33
|
+
export function copyToClipboardSync(text: string): void {
|
|
34
|
+
copyToClipboard(text).catch((error) => {
|
|
35
|
+
console.error("Failed to copy to clipboard:", error)
|
|
36
|
+
})
|
|
37
|
+
}
|