@wwkit/opm 1.0.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 +381 -0
- package/bin/index.js +9 -0
- package/docs/managers/apt.md +45 -0
- package/docs/managers/brew.md +92 -0
- package/docs/managers/bun.md +50 -0
- package/docs/managers/cft.md +57 -0
- package/docs/managers/composer.md +96 -0
- package/docs/managers/config.md +32 -0
- package/docs/managers/dnf.md +46 -0
- package/docs/managers/node.md +49 -0
- package/docs/managers/npm.md +100 -0
- package/docs/managers/nvm.md +34 -0
- package/docs/managers/php.md +48 -0
- package/docs/managers/phpenv.md +34 -0
- package/docs/managers/pip.md +98 -0
- package/docs/managers/python.md +53 -0
- package/docs/managers/uv.md +27 -0
- package/docs/opm.html +570 -0
- package/docs/packages/opencode.md +41 -0
- package/package.json +51 -0
- package/scripts/postinstall.js +7 -0
- package/src/binaries/base.js +508 -0
- package/src/binaries/cft/index.js +35 -0
- package/src/binaries/depchecker.js +172 -0
- package/src/binaries/exefinder.js +110 -0
- package/src/binaries/platform.js +17 -0
- package/src/cli/doc.js +335 -0
- package/src/cli/groups/config.js +78 -0
- package/src/cli/groups/ensure.js +96 -0
- package/src/cli/groups/homebrew.js +73 -0
- package/src/cli/groups/nvm.js +73 -0
- package/src/cli/groups/package.js +514 -0
- package/src/cli/groups/phpenv.js +89 -0
- package/src/cli/groups/uv.js +73 -0
- package/src/cli/groups/version-manager.js +186 -0
- package/src/cli/helpers/args.js +49 -0
- package/src/cli/index.js +129 -0
- package/src/config.js +133 -0
- package/src/config.json5 +158 -0
- package/src/formatter.js +13 -0
- package/src/harnesses/base.js +205 -0
- package/src/harnesses/opencode/clear.js +239 -0
- package/src/harnesses/opencode/db.js +39 -0
- package/src/harnesses/opencode/index.js +46 -0
- package/src/harnesses/opencode/sqlite.js +200 -0
- package/src/index.js +46 -0
- package/src/managers/apt.js +377 -0
- package/src/managers/base.js +167 -0
- package/src/managers/brew.js +374 -0
- package/src/managers/bun.js +342 -0
- package/src/managers/composer.js +280 -0
- package/src/managers/dnf.js +370 -0
- package/src/managers/http.js +432 -0
- package/src/managers/node.js +150 -0
- package/src/managers/npm.js +473 -0
- package/src/managers/php.js +157 -0
- package/src/managers/pip.js +555 -0
- package/src/managers/python.js +180 -0
- package/src/managers/runtime.js +309 -0
- package/src/managers/winget.js +268 -0
- package/src/presets.js +24 -0
- package/src/proxy-presets.js +12 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI harness 统一命令组基类
|
|
3
|
+
*
|
|
4
|
+
* 各 harness(opencode / claude / …)共享同一套命令:
|
|
5
|
+
* version / installed / install / uninstall / upgrade / config / clear / dir
|
|
6
|
+
*
|
|
7
|
+
* install/uninstall/upgrade 委托 NpmManager(global),自动使用 opm 配置的
|
|
8
|
+
* npm registry 镜像与 proxy,而非裸 npm。
|
|
9
|
+
*
|
|
10
|
+
* 新增 harness:在 src/harnesses/<name>/ 下提供 config 后在此注册即可。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs'
|
|
14
|
+
import path from 'node:path'
|
|
15
|
+
import { parseFlags } from '../cli/helpers/args.js'
|
|
16
|
+
import { NpmManager } from '../managers/npm.js'
|
|
17
|
+
import { getActiveProxy } from '../config.js'
|
|
18
|
+
import { output } from '../formatter.js'
|
|
19
|
+
|
|
20
|
+
const ACTIONS = {
|
|
21
|
+
version: { desc: 'Show installed version' },
|
|
22
|
+
installed: { desc: 'Check if installed' },
|
|
23
|
+
install: { desc: 'Install via npm -g (uses opm npm registry + proxy)' },
|
|
24
|
+
uninstall: { desc: 'Uninstall the npm global package' },
|
|
25
|
+
upgrade: { desc: 'Upgrade to latest via npm -g (uses opm npm registry + proxy)' },
|
|
26
|
+
config: { desc: 'Show config file content' },
|
|
27
|
+
clear: { desc: 'Clear sessions / data' },
|
|
28
|
+
dir: { desc: 'Show data / db / log / config paths' },
|
|
29
|
+
help: { desc: 'Show this help' },
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {Object} HarnessConfig
|
|
34
|
+
* @property {string} name - harness 名(命令名)
|
|
35
|
+
* @property {string} desc - 描述
|
|
36
|
+
* @property {string} npmPackage - npm 全局包名
|
|
37
|
+
* @property {() => string|null} getConfigFile - 配置文件路径
|
|
38
|
+
* @property {() => string} getDataDir - 数据目录
|
|
39
|
+
* @property {() => string} getDbPath - DB 路径
|
|
40
|
+
* @property {() => string} getLogPath - 日志路径
|
|
41
|
+
* @property {(argv: string[]) => Promise<void>} clear - 清理实现
|
|
42
|
+
* @property {string[][]} [clearOptions] - clear 子选项帮助 [flag, desc]
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
export class HarnessGroup {
|
|
46
|
+
/**
|
|
47
|
+
* @param {HarnessConfig} cfg
|
|
48
|
+
*/
|
|
49
|
+
constructor(cfg) {
|
|
50
|
+
this.name = cfg.name
|
|
51
|
+
this.desc = cfg.desc
|
|
52
|
+
this.cfg = cfg
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async run(argv) {
|
|
56
|
+
const [action, ...rest] = argv
|
|
57
|
+
|
|
58
|
+
if (!action || action === '-h' || action === '--help' || action === 'help') {
|
|
59
|
+
this.printHelp()
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
switch (action) {
|
|
64
|
+
case 'version':
|
|
65
|
+
return this._version()
|
|
66
|
+
case 'installed':
|
|
67
|
+
return this._installed()
|
|
68
|
+
case 'install':
|
|
69
|
+
return this._install(rest)
|
|
70
|
+
case 'uninstall':
|
|
71
|
+
return this._uninstall()
|
|
72
|
+
case 'upgrade':
|
|
73
|
+
return this._upgrade(rest)
|
|
74
|
+
case 'config':
|
|
75
|
+
return this._config()
|
|
76
|
+
case 'clear':
|
|
77
|
+
return this._clear(rest)
|
|
78
|
+
case 'dir':
|
|
79
|
+
return this._dir()
|
|
80
|
+
default:
|
|
81
|
+
console.error(`Unknown action: ${action}`)
|
|
82
|
+
this.printHelp()
|
|
83
|
+
process.exit(1)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 解析 proxy(-p/--proxy 优先,否则回退配置 active proxy)
|
|
89
|
+
* @param {string[]} argv
|
|
90
|
+
* @returns {string}
|
|
91
|
+
* @private
|
|
92
|
+
*/
|
|
93
|
+
_resolveProxy(argv) {
|
|
94
|
+
const parsed = parseFlags(argv)
|
|
95
|
+
let proxy = parsed.flags.proxy || parsed.flags.p || ''
|
|
96
|
+
if (proxy === 'true') proxy = ''
|
|
97
|
+
if (!proxy) proxy = getActiveProxy()
|
|
98
|
+
return proxy
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async _version() {
|
|
102
|
+
const npm = new NpmManager()
|
|
103
|
+
const result = await npm.isInstalled(this.cfg.npmPackage, { global: true })
|
|
104
|
+
output((result.version || '').replace(/^v/, ''))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async _installed() {
|
|
108
|
+
const npm = new NpmManager()
|
|
109
|
+
const result = await npm.isInstalled(this.cfg.npmPackage, { global: true })
|
|
110
|
+
output({
|
|
111
|
+
name: this.cfg.name,
|
|
112
|
+
installed: result.installed,
|
|
113
|
+
...(result.version ? { version: result.version } : {}),
|
|
114
|
+
...(result.installed ? {} : { hint: `Run \`opm ${this.cfg.name} install\` to install` }),
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async _install(rest) {
|
|
119
|
+
const proxy = this._resolveProxy(rest)
|
|
120
|
+
const npm = new NpmManager()
|
|
121
|
+
const result = await npm.install(this.cfg.npmPackage, null, { global: true, proxy })
|
|
122
|
+
output({ ...result, name: this.cfg.name, package: this.cfg.npmPackage })
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async _uninstall() {
|
|
126
|
+
const npm = new NpmManager()
|
|
127
|
+
const result = await npm.uninstall(this.cfg.npmPackage, { global: true })
|
|
128
|
+
output({ ...result, name: this.cfg.name, package: this.cfg.npmPackage })
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async _upgrade(rest) {
|
|
132
|
+
const proxy = this._resolveProxy(rest)
|
|
133
|
+
const npm = new NpmManager()
|
|
134
|
+
const result = await npm.upgrade(this.cfg.npmPackage, { global: true, proxy })
|
|
135
|
+
output({ ...result, name: this.cfg.name, package: this.cfg.npmPackage })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async _config() {
|
|
139
|
+
const file = this.cfg.getConfigFile()
|
|
140
|
+
if (!file) {
|
|
141
|
+
output({ error: `${this.cfg.name} config not found`, hint: `Expected ${this.cfg.name} config file` })
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
const content = fs.readFileSync(file, 'utf8')
|
|
145
|
+
output({ file, content })
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async _clear(rest) {
|
|
149
|
+
const parsed = parseFlags(rest)
|
|
150
|
+
if (parsed.flags.help === 'true') {
|
|
151
|
+
this.printHelp()
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
await this.cfg.clear(rest)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async _dir() {
|
|
158
|
+
const npm = new NpmManager()
|
|
159
|
+
const npmResult = await npm.isInstalled(this.cfg.npmPackage, { global: true })
|
|
160
|
+
const configFile = this.cfg.getConfigFile()
|
|
161
|
+
const configDir = configFile ? path.dirname(configFile) : ''
|
|
162
|
+
output({
|
|
163
|
+
name: this.cfg.name,
|
|
164
|
+
install: npmResult.installed ? npmResult.path || '' : '',
|
|
165
|
+
config: configDir,
|
|
166
|
+
dataDir: this.cfg.getDataDir(),
|
|
167
|
+
dbPath: this.cfg.getDbPath(),
|
|
168
|
+
logPath: this.cfg.getLogPath(),
|
|
169
|
+
configFile: configFile || '(not found)',
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
printHelp() {
|
|
174
|
+
const actionLines = Object.entries(ACTIONS)
|
|
175
|
+
.map(([sig, { desc }]) => ` ${sig.padEnd(12)} ${desc}`)
|
|
176
|
+
.join('\n')
|
|
177
|
+
|
|
178
|
+
let clearSection = ''
|
|
179
|
+
if (this.cfg.clearOptions && this.cfg.clearOptions.length > 0) {
|
|
180
|
+
const optLines = this.cfg.clearOptions
|
|
181
|
+
.map(([flag, desc]) => ` ${flag.padEnd(20)} ${desc}`)
|
|
182
|
+
.join('\n')
|
|
183
|
+
clearSection = `\nClear options:\n${optLines}\n`
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
console.log(`
|
|
187
|
+
Usage: opm ${this.cfg.name} <action> [options]
|
|
188
|
+
|
|
189
|
+
Actions:
|
|
190
|
+
${actionLines}
|
|
191
|
+
${clearSection}
|
|
192
|
+
Options:
|
|
193
|
+
-p, --proxy <url> Proxy for install/upgrade (overrides config proxy.active)
|
|
194
|
+
-h, --help Show this help
|
|
195
|
+
|
|
196
|
+
Examples:
|
|
197
|
+
opm ${this.cfg.name} version
|
|
198
|
+
opm ${this.cfg.name} install
|
|
199
|
+
opm ${this.cfg.name} upgrade
|
|
200
|
+
opm ${this.cfg.name} config
|
|
201
|
+
opm ${this.cfg.name} clear
|
|
202
|
+
opm ${this.cfg.name} dir
|
|
203
|
+
`)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opm opencode clear 实现
|
|
3
|
+
*
|
|
4
|
+
* 参考 docs/clean-opencode-db.sh,用 Node 实现,跨平台:
|
|
5
|
+
* 1. 预览待删除会话
|
|
6
|
+
* 2. 确认(--dry-run 仅预览,-y 跳过确认)
|
|
7
|
+
* 3. 关闭 opencode 进程(尽力而为)
|
|
8
|
+
* 4. 备份数据库
|
|
9
|
+
* 5. 删除过期会话(event_sequence + session)并 VACUUM
|
|
10
|
+
* 6. 清空日志、报告清理结果
|
|
11
|
+
*
|
|
12
|
+
* 保留策略参数(三选一,缺省 -d 1 即只保留今天):
|
|
13
|
+
* -d/--days <n> 保留最近 n 天(对齐本地零点,含今天)
|
|
14
|
+
* -h/--hours <n> 保留最近 n 小时(滚动窗口)
|
|
15
|
+
* -n/--num <n> 保留最近 n 个会话(按 time_created 倒序取最新 n 个)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'node:fs'
|
|
19
|
+
import os from 'node:os'
|
|
20
|
+
import path from 'node:path'
|
|
21
|
+
import readline from 'node:readline/promises'
|
|
22
|
+
import { spawnSync } from 'node:child_process'
|
|
23
|
+
import { parseFlags } from '../../cli/helpers/args.js'
|
|
24
|
+
import { openDb } from './sqlite.js'
|
|
25
|
+
import { getDbPath, getLogPath } from './db.js'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 某毫秒时间戳所在「天」的本地零点
|
|
29
|
+
* @param {number} ts - epoch ms
|
|
30
|
+
* @returns {number}
|
|
31
|
+
*/
|
|
32
|
+
export function startOfDayLocal(ts) {
|
|
33
|
+
const d = new Date(ts)
|
|
34
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 解析 -d/--days | -h/--hours | -n/--num 的计数参数
|
|
39
|
+
* @param {string|undefined} value
|
|
40
|
+
* @param {string} name - 参数名(days/hours/num),用于报错提示
|
|
41
|
+
* @returns {number|null} 未提供返回 null,否则返回正整数
|
|
42
|
+
*/
|
|
43
|
+
export function parseCount(value, name) {
|
|
44
|
+
if (value === undefined) return null
|
|
45
|
+
const n = Number(value)
|
|
46
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
47
|
+
throw new Error(`invalid --${name} value: "${value}" (expect a positive integer)`)
|
|
48
|
+
}
|
|
49
|
+
return n
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 计算时间清理边界(毫秒)
|
|
54
|
+
* days: 最近 n 天,对齐本地零点(含今天),边界 = 今天零点 - (n-1) 天
|
|
55
|
+
* hours: 最近 n 小时,滚动窗口,边界 = now - n 小时
|
|
56
|
+
* @param {{ days?: number|null, hours?: number|null }} opts
|
|
57
|
+
* @returns {number}
|
|
58
|
+
*/
|
|
59
|
+
export function computeBoundary({ days = null, hours = null } = {}) {
|
|
60
|
+
const now = Date.now()
|
|
61
|
+
if (days !== null) {
|
|
62
|
+
return startOfDayLocal(now) - (days - 1) * 86400000
|
|
63
|
+
}
|
|
64
|
+
if (hours !== null) {
|
|
65
|
+
return now - hours * 3600000
|
|
66
|
+
}
|
|
67
|
+
return startOfDayLocal(now)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 本地时间格式化 YYYY-MM-DD HH:mm:ss
|
|
72
|
+
* @param {number} ms
|
|
73
|
+
* @returns {string}
|
|
74
|
+
*/
|
|
75
|
+
export function formatLocal(ms) {
|
|
76
|
+
const d = new Date(ms)
|
|
77
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
78
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 人类可读文件大小
|
|
83
|
+
* @param {number} bytes
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
export function formatSize(bytes) {
|
|
87
|
+
if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(2)} MB`
|
|
88
|
+
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
89
|
+
return `${bytes} B`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function killOpencodeProcesses() {
|
|
93
|
+
try {
|
|
94
|
+
if (process.platform === 'win32') {
|
|
95
|
+
spawnSync('taskkill', ['/IM', 'opencode.exe', '/F'], { stdio: 'ignore' })
|
|
96
|
+
} else {
|
|
97
|
+
spawnSync('pkill', ['-f', 'bin/opencode'], { stdio: 'ignore' })
|
|
98
|
+
}
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 备份目录:$XDG_DATA_HOME/opm/opencode-backup(默认 ~/.local/share/opm/opencode-backup)
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
export function getBackupDir() {
|
|
107
|
+
const xdgData = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share')
|
|
108
|
+
return path.join(xdgData, 'opm', 'opencode-backup')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function backupDb(dbPath) {
|
|
112
|
+
const backupDir = getBackupDir()
|
|
113
|
+
fs.mkdirSync(backupDir, { recursive: true })
|
|
114
|
+
const backupPath = path.join(backupDir, `opencode.db.bak.${Date.now()}`)
|
|
115
|
+
fs.copyFileSync(dbPath, backupPath)
|
|
116
|
+
return backupPath
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function clearLog() {
|
|
120
|
+
const logPath = getLogPath()
|
|
121
|
+
if (fs.existsSync(logPath)) {
|
|
122
|
+
try {
|
|
123
|
+
fs.writeFileSync(logPath, '', 'utf8')
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function promptConfirm(message) {
|
|
129
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
130
|
+
try {
|
|
131
|
+
const ans = await rl.question(message)
|
|
132
|
+
return /^[yY]/.test(ans.trim())
|
|
133
|
+
} finally {
|
|
134
|
+
rl.close()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* opencode clear 主流程
|
|
140
|
+
* @param {string[]} argv - opencode clear 之后的参数
|
|
141
|
+
*/
|
|
142
|
+
export async function runClear(argv) {
|
|
143
|
+
const parsed = parseFlags(argv)
|
|
144
|
+
const flags = parsed.flags
|
|
145
|
+
|
|
146
|
+
const dryRun = flags['dry-run'] === 'true'
|
|
147
|
+
const yes = flags.y === 'true' || flags.yes === 'true'
|
|
148
|
+
const backup = flags.backup === 'true'
|
|
149
|
+
const dbPath = flags.db || getDbPath()
|
|
150
|
+
|
|
151
|
+
const days = parseCount(flags.days ?? flags.d, 'days')
|
|
152
|
+
const hours = parseCount(flags.hours ?? flags.h, 'hours')
|
|
153
|
+
const num = parseCount(flags.num ?? flags.n, 'num')
|
|
154
|
+
|
|
155
|
+
const given = ['days', 'hours', 'num'].filter((k) => ({ days, hours, num })[k] !== null)
|
|
156
|
+
if (given.length > 1) {
|
|
157
|
+
throw new Error('--days / --hours / --num are mutually exclusive')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const mode = given[0] || 'days'
|
|
161
|
+
const count = mode === 'days' ? (days ?? 1) : mode === 'hours' ? hours : num
|
|
162
|
+
|
|
163
|
+
if (!fs.existsSync(dbPath)) {
|
|
164
|
+
throw new Error(`opencode database not found: ${dbPath}`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const driver = await openDb(dbPath)
|
|
168
|
+
try {
|
|
169
|
+
let rows
|
|
170
|
+
let scopeDesc
|
|
171
|
+
let boundary = null
|
|
172
|
+
|
|
173
|
+
if (mode === 'num') {
|
|
174
|
+
const all = driver.listSessionsAsc()
|
|
175
|
+
const keep = Math.min(count, all.length)
|
|
176
|
+
rows = all.slice(0, all.length - keep)
|
|
177
|
+
scopeDesc = `仅保留最新 ${keep} 个会话`
|
|
178
|
+
} else {
|
|
179
|
+
boundary = computeBoundary({ days, hours })
|
|
180
|
+
rows = driver.listBefore(boundary)
|
|
181
|
+
scopeDesc = `删除 ${mode === 'days' ? `最近 ${count} 天` : `最近 ${count} 小时`}(${formatLocal(boundary)})之前的会话`
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(`==> DB: ${dbPath}`)
|
|
185
|
+
console.log(`==> 清理范围: ${scopeDesc}`)
|
|
186
|
+
console.log(`==> 待删除会话 (${rows.length} 个):`)
|
|
187
|
+
|
|
188
|
+
for (const row of rows) {
|
|
189
|
+
console.log(` - ${formatLocal(row.time_created)} ${row.title} (${row.id})`)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (rows.length === 0) {
|
|
193
|
+
console.log('==> 没有符合条件的会话,无需清理')
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (dryRun) {
|
|
198
|
+
console.log('==> DRY_RUN 模式:仅预览,未修改任何数据')
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!yes) {
|
|
203
|
+
const ok = await promptConfirm(`==> 确认删除以上 ${rows.length} 个会话?[y/N] `)
|
|
204
|
+
if (!ok) {
|
|
205
|
+
console.log('已取消')
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log('==> 关闭 opencode 进程')
|
|
211
|
+
killOpencodeProcesses()
|
|
212
|
+
|
|
213
|
+
let backupPath = null
|
|
214
|
+
if (backup) {
|
|
215
|
+
console.log('==> 备份数据库')
|
|
216
|
+
backupPath = backupDb(dbPath)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log('==> 删除过期会话并压缩数据库')
|
|
220
|
+
driver.checkpoint()
|
|
221
|
+
if (mode === 'num') {
|
|
222
|
+
driver.deleteSessions(rows.map((row) => row.id))
|
|
223
|
+
} else {
|
|
224
|
+
driver.deleteBefore(boundary)
|
|
225
|
+
}
|
|
226
|
+
clearLog()
|
|
227
|
+
|
|
228
|
+
console.log(`==> 清理后数据库大小: ${formatSize(fs.statSync(dbPath).size)}`)
|
|
229
|
+
if (backupPath) {
|
|
230
|
+
console.log(`==> 备份文件: ${backupPath}`)
|
|
231
|
+
}
|
|
232
|
+
console.log('==> 剩余会话:')
|
|
233
|
+
for (const row of driver.listAll()) {
|
|
234
|
+
console.log(` - ${row.title} (${row.id})`)
|
|
235
|
+
}
|
|
236
|
+
} finally {
|
|
237
|
+
driver.close()
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode 数据路径解析
|
|
3
|
+
*
|
|
4
|
+
* 跨平台定位 opencode 数据目录(与 opencode 实际行为一致):
|
|
5
|
+
* - macOS/Linux: $XDG_DATA_HOME/opencode (默认 ~/.local/share/opencode)
|
|
6
|
+
* - Windows: %APPDATA%/opencode
|
|
7
|
+
* 可通过环境变量 OPM_OPENCODE_DATA_DIR 覆盖。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import os from 'node:os'
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
import { existsSync } from 'node:fs'
|
|
13
|
+
|
|
14
|
+
export function getDataDir() {
|
|
15
|
+
const override = process.env.OPM_OPENCODE_DATA_DIR
|
|
16
|
+
if (override) return override
|
|
17
|
+
|
|
18
|
+
if (process.platform === 'win32') {
|
|
19
|
+
return process.env.APPDATA
|
|
20
|
+
? path.join(process.env.APPDATA, 'opencode')
|
|
21
|
+
: path.join(os.homedir(), 'AppData', 'Roaming', 'opencode')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (process.platform === 'darwin') {
|
|
25
|
+
const macDir = path.join(os.homedir(), 'Library', 'Application Support', 'opencode')
|
|
26
|
+
if (existsSync(macDir)) return macDir
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const xdgData = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share')
|
|
30
|
+
return path.join(xdgData, 'opencode')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getDbPath() {
|
|
34
|
+
return path.join(getDataDir(), 'opencode.db')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getLogPath() {
|
|
38
|
+
return path.join(getDataDir(), 'log', 'opencode.log')
|
|
39
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode harness 配置
|
|
3
|
+
*
|
|
4
|
+
* opencode 是 AI coding agent,通过 npm 全局包 opencode-ai 安装(binary: opencode)。
|
|
5
|
+
* 数据目录跨平台(db.js),会话清理复用 clear.js。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs'
|
|
9
|
+
import path from 'node:path'
|
|
10
|
+
import os from 'node:os'
|
|
11
|
+
import { getDataDir, getDbPath, getLogPath } from './db.js'
|
|
12
|
+
import { runClear } from './clear.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 查找 opencode 配置文件路径(优先 .jsonc,其次 .json)
|
|
16
|
+
* @returns {string|null}
|
|
17
|
+
*/
|
|
18
|
+
function findConfigFile() {
|
|
19
|
+
const dir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config')
|
|
20
|
+
const base = path.join(dir, 'opencode')
|
|
21
|
+
for (const file of ['opencode.jsonc', 'opencode.json']) {
|
|
22
|
+
const full = path.join(base, file)
|
|
23
|
+
if (fs.existsSync(full)) return full
|
|
24
|
+
}
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const opencodeHarness = {
|
|
29
|
+
name: 'opencode',
|
|
30
|
+
desc: 'opencode AI coding agent',
|
|
31
|
+
npmPackage: 'opencode-ai',
|
|
32
|
+
getConfigFile: findConfigFile,
|
|
33
|
+
getDataDir,
|
|
34
|
+
getDbPath,
|
|
35
|
+
getLogPath,
|
|
36
|
+
clear: runClear,
|
|
37
|
+
clearOptions: [
|
|
38
|
+
['-d, --days <n>', 'Keep sessions from the last n days (default 1)'],
|
|
39
|
+
['-h, --hours <n>', 'Keep sessions from the last n hours'],
|
|
40
|
+
['-n, --num <n>', 'Keep the most recent n sessions'],
|
|
41
|
+
['--dry-run', 'Preview only, do not modify anything'],
|
|
42
|
+
['-y, --yes', 'Skip the confirmation prompt'],
|
|
43
|
+
['--backup', 'Backup the DB before deleting (default: off)'],
|
|
44
|
+
['--db <path>', 'Override the opencode database path'],
|
|
45
|
+
],
|
|
46
|
+
}
|