@wwkit/opm 1.0.9 → 1.0.11

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.
@@ -2,7 +2,12 @@
2
2
  * AI harness 统一命令组基类
3
3
  *
4
4
  * 各 harness(opencode / claude / …)共享同一套命令:
5
- * version / installed / install / uninstall / upgrade / config / clear / dir
5
+ * version / installed / install / uninstall / upgrade / config / clear / dir / env
6
+ * 内容型 harness(harness/@wwkit/harness 插件)额外提供:
7
+ * list(列出 skills/agents/commands) / status(安装 + env 健康检查)
8
+ *
9
+ * 支持的动作按 cfg 能力动态裁剪(_supportedActions):只有 cfg 提供了对应的
10
+ * 实现(list/status/clear/getConfigFile/getEnvVars)时才出现在帮助与路由中。
6
11
  *
7
12
  * install/uninstall/upgrade 委托 NpmManager(global),自动使用 opm 配置的
8
13
  * npm registry 镜像与 proxy,而非裸 npm。
@@ -25,7 +30,9 @@ const ACTIONS = {
25
30
  upgrade: { desc: 'Upgrade to latest via npm -g (uses opm npm registry + proxy)' },
26
31
  config: { desc: 'Show config file content' },
27
32
  clear: { desc: 'Clear sessions / data' },
28
- dir: { desc: 'Show data / db / log / config paths' },
33
+ dir: { desc: 'Show install / data / db / log / config paths' },
34
+ list: { desc: 'List installed content (skills/agents/commands)' },
35
+ status: { desc: 'Show install status + environment health' },
29
36
  env: { desc: 'Show harness environment variables' },
30
37
  help: { desc: 'Show this help' },
31
38
  }
@@ -35,12 +42,15 @@ const ACTIONS = {
35
42
  * @property {string} name - harness 名(命令名)
36
43
  * @property {string} desc - 描述
37
44
  * @property {string} npmPackage - npm 全局包名
38
- * @property {() => string|null} getConfigFile - 配置文件路径
39
- * @property {() => string} getDataDir - 数据目录
40
- * @property {() => string} getDbPath - DB 路径
41
- * @property {() => string} getLogPath - 日志路径
42
- * @property {(argv: string[]) => Promise<void>} clear - 清理实现
45
+ * @property {() => string|null} [getConfigFile] - 配置文件路径
46
+ * @property {() => string} [getDataDir] - 数据目录
47
+ * @property {() => string} [getDbPath] - DB 路径
48
+ * @property {() => string} [getLogPath] - 日志路径
49
+ * @property {(argv: string[]) => Promise<void>} [clear] - 清理实现
43
50
  * @property {string[][]} [clearOptions] - clear 子选项帮助 [flag, desc]
51
+ * @property {() => Promise<Object>} [list] - 列出包内容(skills/agents/commands)
52
+ * @property {() => Promise<Object>} [status] - 安装/环境健康检查
53
+ * @property {() => Promise<Object>} [dirInfo] - 自定义 dir 输出(覆盖默认 npm install 路径)
44
54
  */
45
55
 
46
56
  export class HarnessGroup {
@@ -62,9 +72,19 @@ export class HarnessGroup {
62
72
  }
63
73
 
64
74
  if (!action || !ACTIONS[action]) {
75
+ if (!this.cfg.open) {
76
+ this.printHelp()
77
+ return
78
+ }
65
79
  return this._open(argv)
66
80
  }
67
81
 
82
+ if (!this._supportedActions().includes(action)) {
83
+ console.error(`Action "${action}" is not supported for ${this.cfg.name}`)
84
+ this.printHelp()
85
+ process.exit(1)
86
+ }
87
+
68
88
  switch (action) {
69
89
  case 'version':
70
90
  return this._version()
@@ -82,6 +102,10 @@ export class HarnessGroup {
82
102
  return this._clear(rest)
83
103
  case 'dir':
84
104
  return this._dir()
105
+ case 'list':
106
+ return this._list()
107
+ case 'status':
108
+ return this._status()
85
109
  case 'env':
86
110
  return this._env()
87
111
  default:
@@ -91,6 +115,23 @@ export class HarnessGroup {
91
115
  }
92
116
  }
93
117
 
118
+ /**
119
+ * 本 harness 支持的动作列表(按 cfg 能力动态裁剪)
120
+ * @returns {string[]}
121
+ * @private
122
+ */
123
+ _supportedActions() {
124
+ return Object.keys(ACTIONS).filter((sig) => {
125
+ if (sig === 'help') return true
126
+ if (sig === 'list') return Boolean(this.cfg.list)
127
+ if (sig === 'status') return Boolean(this.cfg.status)
128
+ if (sig === 'clear') return Boolean(this.cfg.clear)
129
+ if (sig === 'config') return Boolean(this.cfg.getConfigFile)
130
+ if (sig === 'env') return Boolean(this.cfg.getEnvVars)
131
+ return true
132
+ })
133
+ }
134
+
94
135
  /**
95
136
  * 解析 proxy(-p/--proxy 优先,否则回退配置 active proxy)
96
137
  * @param {string[]} argv
@@ -152,6 +193,22 @@ export class HarnessGroup {
152
193
  output({ file, content })
153
194
  }
154
195
 
196
+ async _list() {
197
+ if (!this.cfg.list) {
198
+ output({ name: this.cfg.name, error: 'list not supported for this harness' })
199
+ return
200
+ }
201
+ output(await this.cfg.list())
202
+ }
203
+
204
+ async _status() {
205
+ if (!this.cfg.status) {
206
+ output({ name: this.cfg.name, error: 'status not supported for this harness' })
207
+ return
208
+ }
209
+ output(await this.cfg.status())
210
+ }
211
+
155
212
  async _clear(rest) {
156
213
  const parsed = parseFlags(rest)
157
214
  if (parsed.flags.help === 'true') {
@@ -162,6 +219,10 @@ export class HarnessGroup {
162
219
  }
163
220
 
164
221
  async _dir() {
222
+ if (this.cfg.dirInfo) {
223
+ output(await this.cfg.dirInfo())
224
+ return
225
+ }
165
226
  const npm = new NpmManager()
166
227
  const npmResult = await npm.isInstalled(this.cfg.npmPackage, { global: true })
167
228
  const configFile = this.cfg.getConfigFile()
@@ -218,18 +279,34 @@ export class HarnessGroup {
218
279
  }
219
280
 
220
281
  printHelp() {
221
- const actionLines = Object.entries(ACTIONS)
222
- .map(([sig, { desc }]) => ` ${sig.padEnd(12)} ${desc}`)
282
+ const actionLines = this._supportedActions()
283
+ .map((sig) => ` ${sig.padEnd(12)} ${ACTIONS[sig].desc}`)
223
284
  .join('\n')
224
285
 
225
286
  let clearSection = ''
226
- if (this.cfg.clearOptions && this.cfg.clearOptions.length > 0) {
287
+ if (this._supportedActions().includes('clear') && this.cfg.clearOptions && this.cfg.clearOptions.length > 0) {
227
288
  const optLines = this.cfg.clearOptions
228
289
  .map(([flag, desc]) => ` ${flag.padEnd(20)} ${desc}`)
229
290
  .join('\n')
230
291
  clearSection = `\nClear options:\n${optLines}\n`
231
292
  }
232
293
 
294
+ const supported = this._supportedActions()
295
+ const examples = [
296
+ ' opm ' + this.cfg.name + ' version',
297
+ ' opm ' + this.cfg.name + ' install',
298
+ ' opm ' + this.cfg.name + ' upgrade',
299
+ ...(supported.includes('list') ? [` opm ${this.cfg.name} list`] : []),
300
+ ...(supported.includes('status') ? [` opm ${this.cfg.name} status`] : []),
301
+ ...(supported.includes('env') ? [` opm ${this.cfg.name} env`] : []),
302
+ ...(supported.includes('dir') ? [` opm ${this.cfg.name} dir`] : []),
303
+ ]
304
+ if (this.cfg.open) {
305
+ examples.push(` opm ${this.cfg.name} Launch TUI in current directory`)
306
+ examples.push(` opm ${this.cfg.name} -d /path/to/project`)
307
+ examples.push(` opm ${this.cfg.name} -c config --agent work Pass through args to opencode`)
308
+ }
309
+
233
310
  console.log(`
234
311
  Usage: opm ${this.cfg.name} <action> [options]
235
312
 
@@ -242,16 +319,7 @@ Options:
242
319
  -h, --help Show this help
243
320
 
244
321
  Examples:
245
- opm ${this.cfg.name} version
246
- opm ${this.cfg.name} install
247
- opm ${this.cfg.name} upgrade
248
- opm ${this.cfg.name} config
249
- opm ${this.cfg.name} clear
250
- opm ${this.cfg.name} dir
251
- opm ${this.cfg.name} env
252
- opm ${this.cfg.name} Launch TUI in current directory
253
- opm ${this.cfg.name} -d /path/to/project
254
- opm ${this.cfg.name} -c config --agent work Pass through args to opencode
322
+ ${examples.join('\n')}
255
323
  `)
256
324
  }
257
325
  }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @wwkit/harness harness 配置
3
+ *
4
+ * @wwkit/harness 是 opencode 插件包(skills/agents/commands/scripts),
5
+ * 通过 npm 全局安装;postinstall 设置 OPENCODE_CONFIG_DIR 指向包根目录,
6
+ * 使 opencode 自动加载其 skills/agents/commands。
7
+ *
8
+ * 与 opencode(AI agent CLI)不同:无独立配置文件、无 DB/日志/会话,
9
+ * 因此不支持 config/clear;而提供 list(列出包内容)与 status(健康检查)。
10
+ */
11
+
12
+ import fs from 'node:fs'
13
+ import path from 'node:path'
14
+ import { NpmManager } from '../../managers/npm.js'
15
+
16
+ const CONTENT_DIRS = ['skills', 'agents', 'commands']
17
+ const ENV_VARS = ['OPENCODE_CONFIG_DIR', 'OPENCODE_EXPERIMENTAL_LSP_TOOL']
18
+ const NAME = 'harness'
19
+ const NPM_PACKAGE = '@wwkit/harness'
20
+
21
+ /**
22
+ * 已安装的 @wwkit/harness 全局包信息
23
+ * @returns {Promise<{ installed: boolean, version?: string, path?: string }>}
24
+ * @private
25
+ */
26
+ async function getInstalled() {
27
+ const npm = new NpmManager()
28
+ return npm.isInstalled(NPM_PACKAGE, { global: true })
29
+ }
30
+
31
+ /**
32
+ * 读取 env 变量当前值
33
+ * @returns {Record<string, string>}
34
+ */
35
+ export function getEnvVars() {
36
+ const result = {}
37
+ for (const key of ENV_VARS) {
38
+ result[key] = process.env[key] || ''
39
+ }
40
+ return result
41
+ }
42
+
43
+ /**
44
+ * 列出包根目录下 skills/agents/commands 内容
45
+ * @param {string} root - 已安装包根目录
46
+ * @returns {Record<string, string[]>}
47
+ */
48
+ export function readContentDirs(root) {
49
+ const result = {}
50
+ for (const dir of CONTENT_DIRS) {
51
+ const full = path.join(root, dir)
52
+ result[dir] = fs.existsSync(full) ? fs.readdirSync(full).sort() : []
53
+ }
54
+ return result
55
+ }
56
+
57
+ /**
58
+ * list:列出安装包提供的 skills/agents/commands
59
+ * @returns {Promise<Object>}
60
+ */
61
+ async function list() {
62
+ const installed = await getInstalled()
63
+ if (!installed.installed || !installed.path) {
64
+ return { name: NAME, installed: false, hint: `Run \`opm ${NAME} install\` to install ${NPM_PACKAGE}` }
65
+ }
66
+ const content = readContentDirs(installed.path)
67
+ const counts = {}
68
+ for (const dir of CONTENT_DIRS) counts[dir] = content[dir].length
69
+ return { name: NAME, installed: true, version: installed.version, install: installed.path, counts, ...content }
70
+ }
71
+
72
+ /**
73
+ * status:健康检查(安装 + env + 内容目录完整性)
74
+ * @returns {Promise<Object>}
75
+ */
76
+ async function status() {
77
+ const installed = await getInstalled()
78
+ const env = getEnvVars()
79
+ if (!installed.installed || !installed.path) {
80
+ return {
81
+ name: NAME,
82
+ installed: false,
83
+ ok: false,
84
+ env,
85
+ hint: `Run \`opm ${NAME} install\` to install ${NPM_PACKAGE}`,
86
+ }
87
+ }
88
+ const content = readContentDirs(installed.path)
89
+ const contentOk = {}
90
+ for (const dir of CONTENT_DIRS) contentOk[dir] = content[dir].length > 0
91
+ const envDirOk = env.OPENCODE_CONFIG_DIR === installed.path
92
+ const envLspOk = env.OPENCODE_EXPERIMENTAL_LSP_TOOL === 'true'
93
+ const ok = envDirOk && envLspOk && Object.values(contentOk).every(Boolean)
94
+ return {
95
+ name: NAME,
96
+ installed: true,
97
+ version: installed.version,
98
+ ok,
99
+ envDirOk,
100
+ envLspOk,
101
+ contentDirs: contentOk,
102
+ env,
103
+ ...(!ok ? { hint: 'Environment or content is not fully ready, see fields above' } : {}),
104
+ }
105
+ }
106
+
107
+ /**
108
+ * dir:安装路径 + skills/agents/commands 目录
109
+ * @returns {Promise<Object>}
110
+ */
111
+ async function dirInfo() {
112
+ const installed = await getInstalled()
113
+ if (!installed.installed || !installed.path) {
114
+ return { name: NAME, install: '', hint: `Run \`opm ${NAME} install\` to install ${NPM_PACKAGE}` }
115
+ }
116
+ const paths = { install: installed.path }
117
+ for (const dir of CONTENT_DIRS) {
118
+ paths[dir] = path.join(installed.path, dir)
119
+ }
120
+ return { name: NAME, ...paths }
121
+ }
122
+
123
+ export const harnessCfg = {
124
+ name: NAME,
125
+ desc: '@wwkit/harness opencode plugin (skills/agents/commands)',
126
+ npmPackage: NPM_PACKAGE,
127
+ envVars: ENV_VARS,
128
+ getEnvVars,
129
+ list,
130
+ status,
131
+ dirInfo,
132
+ }
@@ -4,7 +4,7 @@
4
4
  * opencode 是 AI coding agent,通过 npm 全局包 opencode-ai 安装(binary: opencode)。
5
5
  * 数据目录跨平台(db.js),会话清理复用 clear.js。
6
6
  * env 命令管理 OPENCODE_CONFIG_DIR / OPENCODE_CONFIG_CONTENT 环境变量。
7
- * open 命令在指定工作目录启动 opencode TUI(自动注入 env 变量)。
7
+ * 裸 `opm opencode`(或带透传 args)在指定工作目录启动 opencode TUI(自动注入 env 变量)。
8
8
  */
9
9
 
10
10
  import fs from 'node:fs'
package/src/index.js CHANGED
@@ -11,6 +11,7 @@ export { PackageGroup, RuntimeGroup } from './cli/groups/package.js'
11
11
  export { ConfigGroup } from './cli/groups/config.js'
12
12
  export { HarnessGroup } from './harnesses/base.js'
13
13
  export { opencodeHarness } from './harnesses/opencode/index.js'
14
+ export { harnessCfg } from './harnesses/harness/index.js'
14
15
  export { BinaryGroup } from './binaries/base.js'
15
16
  export { CftGroup } from './binaries/cft/index.js'
16
17
  export { NvmGroup } from './cli/groups/nvm.js'
@@ -68,6 +68,22 @@ export function installNvm(proxy) {
68
68
  }
69
69
  }
70
70
 
71
+ /**
72
+ * 解析 nvm ls 输出为已安装版本列表
73
+ * @param {string} out
74
+ * @returns {{ name: string, version: string, current: boolean }[]}
75
+ */
76
+ export function parseNvmInstalled(out) {
77
+ const items = []
78
+ for (const line of out.split('\n')) {
79
+ const m = line.match(/^(->)?\s*v(\d+\.\d+\.\d+)\b/)
80
+ if (m) {
81
+ items.push({ name: `v${m[2]}`, version: m[2], current: !!m[1] })
82
+ }
83
+ }
84
+ return items
85
+ }
86
+
71
87
  export class NodeManager extends RuntimeManager {
72
88
  constructor() {
73
89
  super({
@@ -127,14 +143,7 @@ export class NodeManager extends RuntimeManager {
127
143
  * @private
128
144
  */
129
145
  _parseInstalled(out) {
130
- const items = []
131
- for (const line of out.split('\n')) {
132
- const m = line.match(/^(->)?\s*v(\d+\.\d+\.\d+)\b/)
133
- if (m) {
134
- items.push({ name: `v${m[2]}`, version: m[2], current: !!m[1] })
135
- }
136
- }
137
- return items
146
+ return parseNvmInstalled(out)
138
147
  }
139
148
 
140
149
  /**
@@ -70,6 +70,22 @@ export function installPhpenv() {
70
70
  }
71
71
  }
72
72
 
73
+ /**
74
+ * 解析 phpenv versions 输出为已安装版本列表
75
+ * @param {string} out
76
+ * @returns {{ name: string, version: string, current: boolean }[]}
77
+ */
78
+ export function parsePhpenvInstalled(out) {
79
+ const items = []
80
+ for (const line of out.split('\n')) {
81
+ const m = line.match(/^(\*)?\s+(\d+\.\d+\.\d+)\b/)
82
+ if (m) {
83
+ items.push({ name: m[2], version: m[2], current: !!m[1] })
84
+ }
85
+ }
86
+ return items
87
+ }
88
+
73
89
  export class PhpManager extends RuntimeManager {
74
90
  constructor() {
75
91
  super({
@@ -134,14 +150,7 @@ export class PhpManager extends RuntimeManager {
134
150
  * @private
135
151
  */
136
152
  _parseInstalled(out) {
137
- const items = []
138
- for (const line of out.split('\n')) {
139
- const m = line.match(/^(\*)?\s+(\d+\.\d+\.\d+)\b/)
140
- if (m) {
141
- items.push({ name: m[2], version: m[2], current: !!m[1] })
142
- }
143
- }
144
- return items
153
+ return parsePhpenvInstalled(out)
145
154
  }
146
155
 
147
156
  /**
@@ -67,6 +67,23 @@ export function installUv(proxy) {
67
67
  return { action: 'installed', hint: 'Restart shell or ensure ~/.local/bin is in PATH' }
68
68
  }
69
69
 
70
+ /**
71
+ * 解析 uv python list --only-installed 输出为已安装版本列表
72
+ * 输出格式: cpython-3.12.4-linux-x86_64-gnu /path/to/python
73
+ * @param {string} out
74
+ * @returns {{ name: string, version: string, current: boolean }[]}
75
+ */
76
+ export function parseUvInstalled(out) {
77
+ const items = []
78
+ for (const line of out.split('\n')) {
79
+ const m = line.match(/cpython-(\d+\.\d+\.\d+)/)
80
+ if (m) {
81
+ items.push({ name: m[1], version: m[1], current: false })
82
+ }
83
+ }
84
+ return items
85
+ }
86
+
70
87
  export class PythonManager extends RuntimeManager {
71
88
  constructor() {
72
89
  super({
@@ -129,14 +146,7 @@ export class PythonManager extends RuntimeManager {
129
146
  * @private
130
147
  */
131
148
  _parseInstalled(out) {
132
- const items = []
133
- for (const line of out.split('\n')) {
134
- const m = line.match(/cpython-(\d+\.\d+\.\d+)/)
135
- if (m) {
136
- items.push({ name: m[1], version: m[1], current: false })
137
- }
138
- }
139
- return items
149
+ return parseUvInstalled(out)
140
150
  }
141
151
 
142
152
  /**