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,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects CLI subcommands.
|
|
3
|
+
*
|
|
4
|
+
* Provides commands for listing and deleting OpenCode projects.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Command, type OptionValues } from "commander"
|
|
8
|
+
import { parseGlobalOptions, type GlobalOptions } from "../index"
|
|
9
|
+
import {
|
|
10
|
+
filterProjectsByState,
|
|
11
|
+
type ProjectRecord,
|
|
12
|
+
} from "../../lib/opencode-data"
|
|
13
|
+
import { createProviderFromGlobalOptions } from "../../lib/opencode-data-provider"
|
|
14
|
+
import {
|
|
15
|
+
getOutputOptions,
|
|
16
|
+
printProjectsOutput,
|
|
17
|
+
printDryRunOutput,
|
|
18
|
+
createDryRunResult,
|
|
19
|
+
printSuccessOutput,
|
|
20
|
+
} from "../output"
|
|
21
|
+
import { tokenizedSearch } from "../../lib/search"
|
|
22
|
+
import { resolveProjectId } from "../resolvers"
|
|
23
|
+
import { requireConfirmation, withErrorHandling, FileOperationError } from "../errors"
|
|
24
|
+
import { copyToBackupDir, formatBackupResult } from "../backup"
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Collect all options from a command and its ancestors.
|
|
28
|
+
* Commander stores global options on the root program, not on subcommands.
|
|
29
|
+
*/
|
|
30
|
+
function collectOptions(cmd: Command): OptionValues {
|
|
31
|
+
const opts: OptionValues = {}
|
|
32
|
+
let current: Command | null = cmd
|
|
33
|
+
while (current) {
|
|
34
|
+
Object.assign(opts, current.opts())
|
|
35
|
+
current = current.parent
|
|
36
|
+
}
|
|
37
|
+
return opts
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Options specific to the projects list command.
|
|
42
|
+
*/
|
|
43
|
+
export interface ProjectsListOptions {
|
|
44
|
+
/** Only show projects with missing directories */
|
|
45
|
+
missingOnly: boolean
|
|
46
|
+
/** Search query to filter projects */
|
|
47
|
+
search?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Options specific to the projects delete command.
|
|
52
|
+
*/
|
|
53
|
+
export interface ProjectsDeleteOptions {
|
|
54
|
+
/** Project ID to delete */
|
|
55
|
+
id: string
|
|
56
|
+
/** Skip confirmation prompt */
|
|
57
|
+
yes: boolean
|
|
58
|
+
/** Preview changes without deleting */
|
|
59
|
+
dryRun: boolean
|
|
60
|
+
/** Directory to backup files before deletion */
|
|
61
|
+
backupDir?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Register projects subcommands on the given parent command.
|
|
66
|
+
*/
|
|
67
|
+
export function registerProjectsCommands(parent: Command): void {
|
|
68
|
+
const projects = parent
|
|
69
|
+
.command("projects")
|
|
70
|
+
.description("Manage OpenCode projects")
|
|
71
|
+
|
|
72
|
+
projects
|
|
73
|
+
.command("list")
|
|
74
|
+
.description("List projects")
|
|
75
|
+
.option("--missing-only", "Only show projects with missing directories", false)
|
|
76
|
+
.option("-s, --search <query>", "Search query to filter projects")
|
|
77
|
+
.action(function (this: Command) {
|
|
78
|
+
const globalOpts = parseGlobalOptions(collectOptions(this))
|
|
79
|
+
const cmdOpts = this.opts()
|
|
80
|
+
const listOpts: ProjectsListOptions = {
|
|
81
|
+
missingOnly: Boolean(cmdOpts.missingOnly),
|
|
82
|
+
search: cmdOpts.search as string | undefined,
|
|
83
|
+
}
|
|
84
|
+
handleProjectsList(globalOpts, listOpts)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
projects
|
|
88
|
+
.command("delete")
|
|
89
|
+
.description("Delete a project's metadata file")
|
|
90
|
+
.requiredOption("--id <projectId>", "Project ID to delete")
|
|
91
|
+
.option("--yes", "Skip confirmation prompt", false)
|
|
92
|
+
.option("--dry-run", "Preview changes without deleting", false)
|
|
93
|
+
.option("--backup-dir <dir>", "Directory to backup files before deletion")
|
|
94
|
+
.action(async function (this: Command) {
|
|
95
|
+
const allOpts = collectOptions(this)
|
|
96
|
+
const globalOpts = parseGlobalOptions(allOpts)
|
|
97
|
+
const cmdOpts = this.opts()
|
|
98
|
+
const deleteOpts: ProjectsDeleteOptions = {
|
|
99
|
+
id: String(cmdOpts.id),
|
|
100
|
+
yes: Boolean(allOpts.yes ?? cmdOpts.yes),
|
|
101
|
+
dryRun: Boolean(allOpts.dryRun ?? cmdOpts.dryRun),
|
|
102
|
+
backupDir: (allOpts.backupDir ?? cmdOpts.backupDir) as string | undefined,
|
|
103
|
+
}
|
|
104
|
+
await withErrorHandling(handleProjectsDelete, getOutputOptions(globalOpts).format)(
|
|
105
|
+
globalOpts,
|
|
106
|
+
deleteOpts
|
|
107
|
+
)
|
|
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
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Handle the projects list command.
|
|
123
|
+
*/
|
|
124
|
+
async function handleProjectsList(
|
|
125
|
+
globalOpts: GlobalOptions,
|
|
126
|
+
listOpts: ProjectsListOptions
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
// Create data provider based on global options (JSONL or SQLite backend)
|
|
129
|
+
const provider = createProviderFromGlobalOptions(globalOpts)
|
|
130
|
+
|
|
131
|
+
// Load project records from the data layer
|
|
132
|
+
let projects = await provider.loadProjectRecords()
|
|
133
|
+
|
|
134
|
+
// Apply missing-only filter if requested
|
|
135
|
+
if (listOpts.missingOnly) {
|
|
136
|
+
projects = filterProjectsByState(projects, "missing")
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Apply tokenized search if query provided (matches TUI semantics)
|
|
140
|
+
if (listOpts.search) {
|
|
141
|
+
projects = tokenizedSearch(
|
|
142
|
+
projects,
|
|
143
|
+
listOpts.search,
|
|
144
|
+
(p) => [p.projectId, p.worktree],
|
|
145
|
+
{ limit: globalOpts.limit }
|
|
146
|
+
)
|
|
147
|
+
} else {
|
|
148
|
+
// Apply limit cap even without search (default 200)
|
|
149
|
+
projects = projects.slice(0, globalOpts.limit)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Output the projects using the appropriate formatter
|
|
153
|
+
const outputOpts = getOutputOptions(globalOpts)
|
|
154
|
+
printProjectsOutput(projects, outputOpts)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Handle the projects delete command.
|
|
159
|
+
*
|
|
160
|
+
* This command deletes a project's metadata file from the OpenCode storage.
|
|
161
|
+
* It does NOT delete the actual project directory on disk.
|
|
162
|
+
*
|
|
163
|
+
* Exit codes:
|
|
164
|
+
* - 0: Success (or dry-run completed)
|
|
165
|
+
* - 2: Usage error (--yes not provided for destructive operation)
|
|
166
|
+
* - 3: Project not found
|
|
167
|
+
* - 4: File operation failure (backup or delete failed)
|
|
168
|
+
*/
|
|
169
|
+
async function handleProjectsDelete(
|
|
170
|
+
globalOpts: GlobalOptions,
|
|
171
|
+
deleteOpts: ProjectsDeleteOptions
|
|
172
|
+
): Promise<void> {
|
|
173
|
+
const outputOpts = getOutputOptions(globalOpts)
|
|
174
|
+
|
|
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)
|
|
179
|
+
const { project } = await resolveProjectId(deleteOpts.id, {
|
|
180
|
+
root: globalOpts.root,
|
|
181
|
+
allowPrefix: true,
|
|
182
|
+
provider,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const pathsToDelete = [project.filePath]
|
|
186
|
+
|
|
187
|
+
// Handle dry-run mode
|
|
188
|
+
if (deleteOpts.dryRun) {
|
|
189
|
+
const dryRunResult = createDryRunResult(pathsToDelete, "delete", "project")
|
|
190
|
+
printDryRunOutput(dryRunResult, outputOpts.format)
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Require confirmation for destructive operation
|
|
195
|
+
requireConfirmation(deleteOpts.yes, "Project deletion")
|
|
196
|
+
|
|
197
|
+
// Backup files if requested (only applies to JSONL backend - SQLite has no files to backup)
|
|
198
|
+
if (deleteOpts.backupDir && provider.backend === "jsonl") {
|
|
199
|
+
const backupResult = await copyToBackupDir(pathsToDelete, {
|
|
200
|
+
backupDir: deleteOpts.backupDir,
|
|
201
|
+
prefix: "project",
|
|
202
|
+
preserveStructure: true,
|
|
203
|
+
structureRoot: globalOpts.root,
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
if (backupResult.failed.length > 0) {
|
|
207
|
+
throw new FileOperationError(
|
|
208
|
+
`Backup failed for ${backupResult.failed.length} file(s): ${backupResult.failed
|
|
209
|
+
.map((f) => f.path)
|
|
210
|
+
.join(", ")}`,
|
|
211
|
+
"backup"
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!globalOpts.quiet) {
|
|
216
|
+
console.log(formatBackupResult(backupResult))
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Perform the deletion using the provider (handles both JSONL and SQLite)
|
|
221
|
+
const deleteResult = await provider.deleteProjectMetadata([project], { dryRun: false })
|
|
222
|
+
|
|
223
|
+
if (deleteResult.failed.length > 0) {
|
|
224
|
+
throw new FileOperationError(
|
|
225
|
+
`Failed to delete ${deleteResult.failed.length} file(s): ${deleteResult.failed
|
|
226
|
+
.map((f: { path: string; error?: string }) => `${f.path}: ${f.error || "unknown error"}`)
|
|
227
|
+
.join(", ")}`,
|
|
228
|
+
"delete"
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Output success
|
|
233
|
+
printSuccessOutput(
|
|
234
|
+
`Deleted project: ${project.projectId}`,
|
|
235
|
+
{ projectId: project.projectId, deleted: deleteResult.removed },
|
|
236
|
+
outputOpts.format
|
|
237
|
+
)
|
|
238
|
+
}
|