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.
@@ -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
+ }