@wwkit/opm 1.0.8 → 1.0.9

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,169 @@
1
+ /**
2
+ * overlay 命令组 — 将本地开发源码覆盖到全局 npm 安装位置
3
+ *
4
+ * 用途:跳过发布流程,把最近修改的代码放到实际包安装位置用于实际测试。
5
+ *
6
+ * opm overlay 覆盖 cwd 包到全局安装
7
+ * opm overlay -f /path/to/pkg 指定源目录
8
+ * opm overlay -t /custom/node_modules 指定目标目录
9
+ * opm overlay --dry-run 预览将要复制的文件
10
+ *
11
+ * 逻辑:
12
+ * 1. 读取源目录 package.json 获取包名
13
+ * 2. 默认目标 = npm root -g,拼接 <globalRoot>/<pkgName>
14
+ * 3. 排除 node_modules/.git 等,逐文件复制(保留权限)
15
+ */
16
+
17
+ import fs from 'node:fs'
18
+ import path from 'node:path'
19
+ import { execSync } from 'node:child_process'
20
+ import { parseFlags } from '../helpers/args.js'
21
+ import { output } from '../../formatter.js'
22
+
23
+ const EXCLUDE = new Set(['node_modules', '.git', '.DS_Store', 'Thumbs.db'])
24
+
25
+ export class OverlayGroup {
26
+ constructor() {
27
+ this.name = 'overlay'
28
+ this.desc = 'Overlay local package source onto global npm install (bypass publish)'
29
+ }
30
+
31
+ async run(argv) {
32
+ const parsed = parseFlags(argv)
33
+
34
+ if (parsed.flags.help === 'true' || parsed.flags.h === 'true' || parsed.positional[0] === 'help') {
35
+ this.printHelp()
36
+ return
37
+ }
38
+
39
+ const dryRun = parsed.flags['dry-run'] === 'true'
40
+
41
+ const from = path.resolve(parsed.flags.f || parsed.flags.from || process.cwd())
42
+ if (!fs.existsSync(from) || !fs.statSync(from).isDirectory()) {
43
+ output({ error: 'Source directory does not exist or is not a directory', from })
44
+ process.exit(1)
45
+ }
46
+ const srcPkgPath = path.join(from, 'package.json')
47
+ if (!fs.existsSync(srcPkgPath)) {
48
+ output({ error: 'No package.json in source directory root (not an npm project)', from, hint: 'Use -f/--to to specify a directory containing package.json' })
49
+ process.exit(1)
50
+ }
51
+
52
+ const pkg = JSON.parse(fs.readFileSync(srcPkgPath, 'utf8'))
53
+ const pkgName = pkg.name
54
+ if (!pkgName) {
55
+ output({ error: 'package.json has no name field', from })
56
+ process.exit(1)
57
+ }
58
+
59
+ const to = parsed.flags.t || parsed.flags.to
60
+ let globalRoot
61
+ if (to) {
62
+ globalRoot = path.resolve(to)
63
+ if (path.basename(globalRoot) !== 'node_modules') {
64
+ output({ error: 'Target directory must end with "node_modules"', to: globalRoot, hint: 'Example: /usr/local/lib/node_modules' })
65
+ process.exit(1)
66
+ }
67
+ if (!fs.existsSync(globalRoot) || !fs.statSync(globalRoot).isDirectory()) {
68
+ output({ error: 'Target node_modules directory does not exist', to: globalRoot })
69
+ process.exit(1)
70
+ }
71
+ } else {
72
+ try {
73
+ globalRoot = execSync('npm root -g', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
74
+ } catch {
75
+ output({ error: 'Failed to determine npm global root', hint: 'Use -t/--to to specify target directory' })
76
+ process.exit(1)
77
+ }
78
+ }
79
+
80
+ const targetDir = path.join(globalRoot, pkgName)
81
+ if (!fs.existsSync(targetDir)) {
82
+ output({
83
+ error: 'Target package not found in global install',
84
+ package: pkgName,
85
+ target: targetDir,
86
+ hint: `Install first: npm install -g ${pkgName}`,
87
+ })
88
+ process.exit(1)
89
+ }
90
+
91
+ const files = this._collectFiles(from)
92
+
93
+ if (dryRun) {
94
+ output({
95
+ package: pkgName,
96
+ from,
97
+ to: targetDir,
98
+ dryRun: true,
99
+ fileCount: files.length,
100
+ files: files.map((f) => path.relative(from, f)),
101
+ })
102
+ return
103
+ }
104
+
105
+ let copied = 0
106
+ for (const srcPath of files) {
107
+ const rel = path.relative(from, srcPath)
108
+ const destPath = path.join(targetDir, rel)
109
+ fs.mkdirSync(path.dirname(destPath), { recursive: true })
110
+ const stat = fs.statSync(srcPath)
111
+ fs.copyFileSync(srcPath, destPath)
112
+ fs.chmodSync(destPath, stat.mode)
113
+ copied++
114
+ }
115
+
116
+ output({
117
+ package: pkgName,
118
+ from,
119
+ to: targetDir,
120
+ fileCount: copied,
121
+ })
122
+ }
123
+
124
+ /**
125
+ * 递归收集源目录下所有文件(排除 EXCLUDE 集合)
126
+ * @param {string} dir
127
+ * @returns {string[]}
128
+ * @private
129
+ */
130
+ _collectFiles(dir) {
131
+ const result = []
132
+ const walk = (d) => {
133
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
134
+ if (EXCLUDE.has(entry.name)) continue
135
+ const full = path.join(d, entry.name)
136
+ if (entry.isDirectory()) {
137
+ walk(full)
138
+ } else {
139
+ result.push(full)
140
+ }
141
+ }
142
+ }
143
+ walk(dir)
144
+ return result
145
+ }
146
+
147
+ printHelp() {
148
+ console.log(`
149
+ Usage: opm overlay [options]
150
+
151
+ Overlay local package source onto the global npm install location.
152
+ Copies all files (except node_modules/.git) from source to target,
153
+ overwriting existing files. Useful for testing local changes without
154
+ publishing.
155
+
156
+ Options:
157
+ -f, --from <dir> Source package directory (default: current directory)
158
+ -t, --to <dir> Target node_modules directory (default: npm root -g)
159
+ --dry-run Preview files to copy without writing
160
+ -h, --help Show this help
161
+
162
+ Examples:
163
+ opm overlay Overlay cwd package to global install
164
+ opm overlay -f /path/to/package Overlay specific package
165
+ opm overlay -t /custom/node_modules Overlay to custom target
166
+ opm overlay --dry-run Preview what would be copied
167
+ `)
168
+ }
169
+ }
@@ -50,14 +50,13 @@ const MANAGER_DESC = {
50
50
  }
51
51
 
52
52
  const ACTIONS = {
53
- version: { desc: 'Show manager version' },
53
+ version: { desc: 'Show manager or package version' },
54
54
  installed: { desc: 'Check if the manager itself is installed' },
55
55
  registry: { desc: 'Query or set registry/mirror address' },
56
56
  search: { desc: 'Check if a package is available in the source' },
57
- view: { desc: 'Check if a package is already installed' },
57
+ view: { desc: 'Check package in global and project scopes' },
58
58
  info: { desc: 'Show package details' },
59
59
  versions: { desc: 'List all available versions of a package' },
60
- list: { desc: 'List all installed packages' },
61
60
  outdated: { desc: 'List outdated packages' },
62
61
  install: { desc: 'Install a package' },
63
62
  uninstall: { desc: 'Uninstall a package' },
@@ -105,8 +104,18 @@ export class PackageGroup {
105
104
 
106
105
  switch (action) {
107
106
  case 'version': {
108
- const result = await manager.getVersion()
109
- output((result.version || '').replace(/^v/, ''))
107
+ const name = positional[0]
108
+ if (name) {
109
+ const result = await manager.scopedInstalled(name, { proxy: opts.proxy })
110
+ output({
111
+ name: result.name,
112
+ global: { installed: result.global.installed, ...(result.global.version ? { version: result.global.version } : {}) },
113
+ project: { installed: result.project.installed, ...(result.project.version ? { version: result.project.version } : {}) },
114
+ })
115
+ } else {
116
+ const result = await manager.getVersion()
117
+ output((result.version || '').replace(/^v/, ''))
118
+ }
110
119
  break
111
120
  }
112
121
 
@@ -151,10 +160,10 @@ export class PackageGroup {
151
160
  case 'view': {
152
161
  const name = positional[0]
153
162
  if (!name) {
154
- console.error(`Usage: opm ${this.name} view <name> [-g]`)
163
+ console.error(`Usage: opm ${this.name} view <name>`)
155
164
  process.exit(1)
156
165
  }
157
- const result = await manager.isInstalled(name, { global: !!opts.global })
166
+ const result = await manager.scopedInstalled(name, { proxy: opts.proxy })
158
167
  output(result)
159
168
  break
160
169
  }
@@ -181,12 +190,6 @@ export class PackageGroup {
181
190
  break
182
191
  }
183
192
 
184
- case 'list': {
185
- const result = await manager.listInstalled({ global: !!opts.global })
186
- output(result)
187
- break
188
- }
189
-
190
193
  case 'outdated': {
191
194
  const result = await manager.listOutdated({ global: !!opts.global, proxy: opts.proxy })
192
195
  output(result)
@@ -254,16 +257,15 @@ export class PackageGroup {
254
257
  Usage: opm ${this.name} <action> [args] [options]
255
258
 
256
259
  Actions:
257
- version Show ${this.name} version
260
+ version [name] Show manager version, or package version in global+project scopes
258
261
  installed Check if ${this.name} itself is installed
259
262
  registry Show active registry (from opm config)
260
263
  registry set <url|preset> Set active registry (writes opm config)
261
264
  registry presets List available mirror presets
262
265
  search <name> [version] Check package availability in source
263
- view <name> [-g] Check if package is installed (local or global)
266
+ view <name> Show package in global and project scopes
264
267
  info <name> Show package details
265
268
  versions <name> List all available versions
266
- list [-g] List all installed packages
267
269
  outdated [-g] List outdated packages
268
270
  install <name> [version] Install a package
269
271
  uninstall <name> Uninstall a package
@@ -285,7 +287,6 @@ Examples:
285
287
  opm ${this.name} view <name>
286
288
  opm ${this.name} info <name>
287
289
  opm ${this.name} versions <name>
288
- opm ${this.name} list
289
290
  opm ${this.name} outdated
290
291
  opm ${this.name} install <name>
291
292
  opm ${this.name} uninstall <name>
@@ -363,7 +364,7 @@ export class RuntimeGroup extends PackageGroup {
363
364
  return
364
365
  }
365
366
 
366
- const runtimeActions = new Set(['use', 'current', 'default', 'installed', 'dir', 'install', 'upgrade', 'search', 'versions'])
367
+ const runtimeActions = new Set(['use', 'current', 'default', 'installed', 'dir', 'install', 'upgrade', 'search', 'versions', 'version', 'view', 'list'])
367
368
  if (runtimeActions.has(action)) {
368
369
  const manager = MANAGERS[this.name]()
369
370
  const parsed = parseFlags(rest)
@@ -465,6 +466,29 @@ export class RuntimeGroup extends PackageGroup {
465
466
  break
466
467
  }
467
468
 
469
+ case 'version': {
470
+ const result = await manager.getVersion()
471
+ output((result.version || '').replace(/^v/, ''))
472
+ break
473
+ }
474
+
475
+ case 'view': {
476
+ const version = positional[0]
477
+ if (!version) {
478
+ console.error(`Usage: opm ${this.name} view <version>`)
479
+ process.exit(1)
480
+ }
481
+ const result = await manager.isInstalled(version, { global: false })
482
+ output(result)
483
+ break
484
+ }
485
+
486
+ case 'list': {
487
+ const result = await manager.listInstalled({ global: false })
488
+ output(result)
489
+ break
490
+ }
491
+
468
492
  default:
469
493
  return super._dispatch(manager, action, parsed, opts)
470
494
  }
package/src/cli/index.js CHANGED
@@ -20,6 +20,7 @@ import { DockerGroup } from './groups/docker.js'
20
20
  import { EnsureGroup } from './groups/ensure.js'
21
21
  import { PingGroup } from './groups/ping.js'
22
22
  import { ProcGroup } from './groups/proc.js'
23
+ import { OverlayGroup } from './groups/overlay.js'
23
24
  import { FtpGroup } from '../tools/ftp/index.js'
24
25
  import { GitGroup } from '../tools/git/index.js'
25
26
  import { ShareGroup } from '../tools/share/index.js'
@@ -62,6 +63,7 @@ const GROUPS = {
62
63
  ensure: new EnsureGroup(),
63
64
  ping: new PingGroup(),
64
65
  proc: new ProcGroup(),
66
+ overlay: new OverlayGroup(),
65
67
  }
66
68
 
67
69
  class CLI {
package/src/config.json5 CHANGED
@@ -227,6 +227,7 @@
227
227
  env: {
228
228
  OPENCODE_CONFIG_DIR: "{pkg:@wwkit/harness}",
229
229
  OPENCODE_CONFIG_CONTENT: "",
230
+ OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS: "true",
230
231
  },
231
232
  },
232
233
 
@@ -21,7 +21,7 @@ const shell = new Shell()
21
21
  /**
22
22
  * opencode 专属环境变量名
23
23
  */
24
- const ENV_VARS = ['OPENCODE_CONFIG_DIR', 'OPENCODE_CONFIG_CONTENT']
24
+ const ENV_VARS = ['OPENCODE_CONFIG_DIR', 'OPENCODE_CONFIG_CONTENT', 'OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS']
25
25
 
26
26
  /**
27
27
  * 查找 opencode 配置文件路径(优先 .jsonc,其次 .json)
package/src/index.js CHANGED
@@ -21,6 +21,7 @@ export { WslGroup } from './cli/groups/wsl.js'
21
21
  export { DockerGroup } from './cli/groups/docker.js'
22
22
  export { PingGroup } from './cli/groups/ping.js'
23
23
  export { ProcGroup } from './cli/groups/proc.js'
24
+ export { OverlayGroup } from './cli/groups/overlay.js'
24
25
  export { FtpGroup } from './tools/ftp/index.js'
25
26
  export { GitGroup } from './tools/git/index.js'
26
27
  export { ShareGroup } from './tools/share/index.js'
@@ -249,6 +249,9 @@ export class AptManager extends PackageManager {
249
249
  }
250
250
 
251
251
  async isInstalled(name, opts = {}) {
252
+ if (opts.global === false) {
253
+ return { name, installed: false, hint: 'no project scope (system-level manager)' }
254
+ }
252
255
  const args = ['query', '-W', '-f=${Status} ${Version}', name]
253
256
  const out = this._exec(args, { allowNonZero: true })
254
257
  const m = out.trim().match(/^install ok installed\s+(\S+)$/)
@@ -323,18 +326,6 @@ export class AptManager extends PackageManager {
323
326
  return { name, versions, registry }
324
327
  }
325
328
 
326
- async listInstalled(opts = {}) {
327
- const out = this._exec(['list', '--installed'], { allowNonZero: true })
328
- const items = []
329
- for (const line of this._parseLines(out)) {
330
- const m = line.match(/^(\S+?)\/(?:\S+,)?now\s+(\S+)\s+(\S+)\s+\[/)
331
- if (m) {
332
- items.push({ name: m[1], version: m[2] })
333
- }
334
- }
335
- return items
336
- }
337
-
338
329
  async listOutdated(opts = {}) {
339
330
  const out = this._exec(['list', '--upgradable', ...this._proxyArgs(opts.proxy)], { allowNonZero: true })
340
331
  const items = []
@@ -81,13 +81,27 @@ export class PackageManager {
81
81
  /**
82
82
  * 查询包是否已安装
83
83
  * @param {string} name - 包名
84
- * @param {{ global?: boolean }} [opts] - 选项
85
- * @returns {Promise<InstalledResult>}
84
+ * @param {{ global?: boolean }} [opts] - 选项;global: true = 全局/系统作用域,false/缺省 = 项目/当前作用域
85
+ * @returns {Promise<InstalledResult>} - name/installed/version?/path?;单作用域管理器对不支持的作用域返回 { name, installed: false, hint }
86
86
  */
87
87
  async isInstalled(name, opts) {
88
88
  throw new Error(`${this.name}: isInstalled not implemented`)
89
89
  }
90
90
 
91
+ /**
92
+ * 查询包在全局与项目双作用域的安装状态
93
+ * @param {string} name - 包名
94
+ * @param {{ proxy?: string }} [opts] - 选项
95
+ * @returns {Promise<{ name: string, global: InstalledResult, project: InstalledResult }>}
96
+ */
97
+ async scopedInstalled(name, opts = {}) {
98
+ const [global, project] = await Promise.all([
99
+ this.isInstalled(name, { ...opts, global: true }),
100
+ this.isInstalled(name, { ...opts, global: false }),
101
+ ])
102
+ return { name, global, project }
103
+ }
104
+
91
105
  /**
92
106
  * 查询包详情
93
107
  * @param {string} name - 包名
@@ -110,6 +124,7 @@ export class PackageManager {
110
124
 
111
125
  /**
112
126
  * 列出所有已安装包
127
+ * // 仅运行时管理器(node/php/python)实现;包管理器不再实现此方法
113
128
  * @param {{ global?: boolean }} [opts] - 选项
114
129
  * @returns {Promise<InstalledItem[]>}
115
130
  */
@@ -299,6 +299,9 @@ export class BrewManager extends PackageManager {
299
299
  }
300
300
 
301
301
  async isInstalled(name, opts = {}) {
302
+ if (opts.global === false) {
303
+ return { name, installed: false, hint: 'no project scope (system-level manager)' }
304
+ }
302
305
  const out = this._exec(['list', '--versions', name], { allowNonZero: true })
303
306
  const items = this._parseListVersions(out)
304
307
  const hit = items.find((i) => i.name === name)
@@ -337,11 +340,6 @@ export class BrewManager extends PackageManager {
337
340
  return { name, versions, registry }
338
341
  }
339
342
 
340
- async listInstalled(opts = {}) {
341
- const out = this._exec(['list', '--versions'], { allowNonZero: true })
342
- return this._parseListVersions(out)
343
- }
344
-
345
343
  async listOutdated(opts = {}) {
346
344
  const out = this._exec(['outdated', '--json=v2'], { allowNonZero: true, proxy: opts.proxy })
347
345
  return this._parseOutdatedJson(out)
@@ -269,24 +269,6 @@ export class BunManager extends PackageManager {
269
269
  return { name, versions, registry }
270
270
  }
271
271
 
272
- async listInstalled(opts = {}) {
273
- const args = ['pm', 'ls']
274
- if (opts.global) args.push('-g')
275
- try {
276
- const out = this._exec(args, { allowNonZero: true })
277
- const items = []
278
- for (const line of out.split('\n')) {
279
- const m = line.match(/([^\s]+)@([\d.]+)/)
280
- if (m) {
281
- items.push({ name: m[1], version: m[2], ...(opts.global ? { global: true } : {}) })
282
- }
283
- }
284
- return items
285
- } catch {
286
- return []
287
- }
288
- }
289
-
290
272
  async listOutdated(opts = {}) {
291
273
  const args = ['outdated']
292
274
  if (opts.global) args.push('-g')
@@ -11,6 +11,7 @@
11
11
 
12
12
  import { execSync, spawnSync } from 'node:child_process'
13
13
  import fs from 'node:fs'
14
+ import os from 'node:os'
14
15
  import path from 'node:path'
15
16
  import { PackageManager } from './base.js'
16
17
  import { fetchText } from './http.js'
@@ -197,7 +198,71 @@ export class ComposerManager extends PackageManager {
197
198
  }
198
199
  }
199
200
 
201
+ /**
202
+ * 获取 composer 全局 home 目录
203
+ *
204
+ * 优先级:$COMPOSER_HOME → ~/.config/composer → ~/.composer(旧版)
205
+ * @returns {string}
206
+ * @private
207
+ */
208
+ _getComposerHome() {
209
+ if (process.env.COMPOSER_HOME) return process.env.COMPOSER_HOME
210
+ const xdg = path.join(os.homedir(), '.config', 'composer')
211
+ if (fs.existsSync(xdg)) return xdg
212
+ return path.join(os.homedir(), '.composer')
213
+ }
214
+
215
+ /**
216
+ * 读取 composer 全局 installed.json
217
+ * @returns {object[]|null}
218
+ * @private
219
+ */
220
+ _readGlobalInstalledJson() {
221
+ const home = this._getComposerHome()
222
+ const p = path.join(home, 'vendor', 'composer', 'installed.json')
223
+ if (!fs.existsSync(p)) return null
224
+ try {
225
+ const data = JSON.parse(fs.readFileSync(p, 'utf8'))
226
+ return Array.isArray(data.packages) ? data.packages : null
227
+ } catch {
228
+ return null
229
+ }
230
+ }
231
+
232
+ /**
233
+ * 通过 PATH 查找 bin 路径(Linux: which / Windows: where)
234
+ * @param {string} name - bin 名
235
+ * @returns {string|null} bin 路径或 null
236
+ * @private
237
+ */
238
+ _whichBin(name) {
239
+ const cmd = process.platform === 'win32' ? `where ${name}` : `which ${name}`
240
+ try {
241
+ const out = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
242
+ const lines = out.trim().split(/\r?\n/).filter(Boolean)
243
+ return lines[0] || null
244
+ } catch {
245
+ return null
246
+ }
247
+ }
248
+
200
249
  async isInstalled(name, opts = {}) {
250
+ if (opts.global === true) {
251
+ const packages = this._readGlobalInstalledJson()
252
+ if (packages) {
253
+ const pkg = packages.find((p) => p.name === name)
254
+ if (pkg) {
255
+ return { name, installed: true, version: pkg.version || 'unknown', global: true }
256
+ }
257
+ }
258
+
259
+ const binPath = this._whichBin(name)
260
+ if (binPath) {
261
+ return { name, installed: true, global: true, path: binPath }
262
+ }
263
+
264
+ return { name, installed: false, global: true }
265
+ }
201
266
  const packages = this._readInstalledJson()
202
267
  if (packages) {
203
268
  const pkg = packages.find((p) => p.name === name)
@@ -233,15 +298,6 @@ export class ComposerManager extends PackageManager {
233
298
  return { name, versions, registry }
234
299
  }
235
300
 
236
- async listInstalled(opts = {}) {
237
- const packages = this._readInstalledJson()
238
- if (!packages) return []
239
- return packages.map((p) => ({
240
- name: p.name,
241
- version: p.version || 'unknown',
242
- }))
243
- }
244
-
245
301
  async listOutdated(opts = {}) {
246
302
  const out = this._exec(['outdated', '--format=json'], { allowNonZero: true, proxy: opts.proxy })
247
303
  try {
@@ -240,6 +240,9 @@ export class DnfManager extends PackageManager {
240
240
  }
241
241
 
242
242
  async isInstalled(name, opts = {}) {
243
+ if (opts.global === false) {
244
+ return { name, installed: false, hint: 'no project scope (system-level manager)' }
245
+ }
243
246
  const args = ['repoquery', '--installed', '--qf', '%{VERSION}-%{RELEASE}', name]
244
247
  const out = this._exec(args, { allowNonZero: true })
245
248
  const versions = this._parseLines(out)
@@ -316,18 +319,6 @@ export class DnfManager extends PackageManager {
316
319
  return { name, versions, registry }
317
320
  }
318
321
 
319
- async listInstalled(opts = {}) {
320
- const out = this._exec(['repoquery', '--installed', '--qf', '%{NAME} %{VERSION}-%{RELEASE}'], { allowNonZero: true })
321
- const items = []
322
- for (const line of this._parseLines(out)) {
323
- const parts = line.split(/\s+/)
324
- if (parts.length >= 2) {
325
- items.push({ name: parts[0], version: parts[1] })
326
- }
327
- }
328
- return items
329
- }
330
-
331
322
  async listOutdated(opts = {}) {
332
323
  const args = ['list', '--upgrades', ...this._proxyArgs(opts.proxy)]
333
324
  const out = this._exec(args, { allowNonZero: true })