@wwkit/opm 1.0.1 → 1.0.3

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,232 @@
1
+ /**
2
+ * proc 命令组 — 进程与系统信息管理
3
+ *
4
+ * 用法:
5
+ * opm proc info 系统信息(OS、CPU、内存、磁盘、运行时间)
6
+ * opm proc list 列出前 N 个进程(默认按内存排序,默认 10)
7
+ * opm proc search 按进程名或端口号搜索
8
+ * opm proc port 列出监听端口及其进程
9
+ * opm proc kill 终止进程(按 ID / 端口 / 名称)
10
+ * opm proc tree 显示进程树
11
+ * opm proc detail 显示指定 PID 的详细信息
12
+ *
13
+ * 基于 systeminformation(随包安装)+ netstat 回退获取端口映射。
14
+ */
15
+
16
+ import { parseFlags } from '../helpers/args.js'
17
+ import { output } from '../../formatter.js'
18
+ import {
19
+ getSystemInfo,
20
+ listProcesses,
21
+ searchProcesses,
22
+ listPorts,
23
+ killByPid,
24
+ killByPort,
25
+ killByName,
26
+ getProcessTree,
27
+ getProcessDetail,
28
+ } from '../../managers/proc.js'
29
+
30
+ const ACTIONS = {
31
+ info: { desc: 'Show system information (OS, CPU, memory, disk, uptime)' },
32
+ list: { desc: 'List top N processes by memory or CPU (default: 10, by mem)' },
33
+ search: { desc: 'Search processes by name or port' },
34
+ port: { desc: 'List listening ports and their processes' },
35
+ kill: { desc: 'Kill process by ID, port, or name' },
36
+ tree: { desc: 'Show process tree (by PID or all top-level)' },
37
+ detail: { desc: 'Show detailed info for a specific PID' },
38
+ help: { desc: 'Show this help' },
39
+ }
40
+
41
+ export class ProcGroup {
42
+ constructor() {
43
+ this.name = 'proc'
44
+ this.desc = 'Process & system info management (list/search/kill/port/tree)'
45
+ }
46
+
47
+ async run(argv) {
48
+ const [action, ...rest] = argv
49
+
50
+ if (!action || action === '-h' || action === '--help' || action === 'help') {
51
+ this.printHelp()
52
+ return
53
+ }
54
+
55
+ const parsed = parseFlags(rest)
56
+
57
+ switch (action) {
58
+ case 'info':
59
+ return this._info()
60
+ case 'list':
61
+ return this._list(parsed)
62
+ case 'search':
63
+ return this._search(parsed)
64
+ case 'port':
65
+ return this._port(parsed)
66
+ case 'kill':
67
+ return this._kill(parsed)
68
+ case 'tree':
69
+ return this._tree(parsed)
70
+ case 'detail':
71
+ return this._detail(parsed)
72
+ default:
73
+ console.error(`Unknown action: ${action}`)
74
+ this.printHelp()
75
+ process.exit(1)
76
+ }
77
+ }
78
+
79
+ async _info() {
80
+ const info = await getSystemInfo()
81
+ output(info)
82
+ }
83
+
84
+ async _list(parsed) {
85
+ const count = parseInt(parsed.flags.n || parsed.flags.count || '10', 10)
86
+ const n = (isNaN(count) || count < 1) ? 10 : count
87
+ const by = parsed.flags.b || parsed.flags.by || 'mem'
88
+ const sortBy = by === 'cpu' ? 'cpu' : 'mem'
89
+ const result = await listProcesses(n, sortBy)
90
+ output(result)
91
+ }
92
+
93
+ async _search(parsed) {
94
+ const name = parsed.flags.n || parsed.flags.name || ''
95
+ const port = parsed.flags.p || parsed.flags.port || ''
96
+ const exact = parsed.flags.exact === 'true'
97
+
98
+ if (!name && !port) {
99
+ console.error('Usage: opm proc search -n <name> [-p <port>] [--exact]')
100
+ console.error('At least one of -n/--name or -p/--port is required')
101
+ process.exit(1)
102
+ }
103
+
104
+ const result = await searchProcesses({ name: name || undefined, port: port || undefined, exact })
105
+ output(result)
106
+ }
107
+
108
+ async _port(parsed) {
109
+ const port = parsed.flags.p || parsed.flags.port || ''
110
+ const result = await listPorts(port || undefined)
111
+ output(result)
112
+ }
113
+
114
+ async _kill(parsed) {
115
+ const id = parsed.flags.i || parsed.flags.id || ''
116
+ const port = parsed.flags.p || parsed.flags.port || ''
117
+ const name = parsed.flags.n || parsed.flags.name || ''
118
+ const force = parsed.flags.f === 'true' || parsed.flags.force === 'true'
119
+ const yes = parsed.flags.y === 'true' || parsed.flags.yes === 'true'
120
+ const signal = force ? 'SIGKILL' : 'SIGTERM'
121
+
122
+ if (!id && !port && !name) {
123
+ console.error('Usage: opm proc kill -i <pid> | -p <port> | -n <name> [-f] [-y]')
124
+ console.error('At least one of -i/--id, -p/--port, or -n/--name is required')
125
+ process.exit(1)
126
+ }
127
+
128
+ if (!yes) {
129
+ const targets = []
130
+ if (id) targets.push(`PID ${id}`)
131
+ if (port) targets.push(`port ${port}`)
132
+ if (name) targets.push(`name "${name}"`)
133
+ const msg = `Kill process by ${targets.join(', ')} with ${signal}? [y/N] `
134
+ const answer = await this._confirm(msg)
135
+ if (!answer) {
136
+ output({ action: 'kill', signal, cancelled: true })
137
+ return
138
+ }
139
+ }
140
+
141
+ if (id) {
142
+ const pid = parseInt(id, 10)
143
+ const result = await killByPid(pid, signal)
144
+ output({ action: 'kill', signal, killed: [result] })
145
+ return
146
+ }
147
+
148
+ if (port) {
149
+ const portNum = parseInt(port, 10)
150
+ const result = await killByPort(portNum, signal)
151
+ output({ action: 'kill', signal, ...result })
152
+ return
153
+ }
154
+
155
+ if (name) {
156
+ const result = await killByName(name, signal)
157
+ output({ action: 'kill', signal, ...result })
158
+ return
159
+ }
160
+ }
161
+
162
+ async _tree(parsed) {
163
+ const pidArg = parsed.positional[0] || parsed.flags.pid || ''
164
+ const rootPid = pidArg ? parseInt(pidArg, 10) : null
165
+ const result = await getProcessTree(rootPid)
166
+ output(result)
167
+ }
168
+
169
+ async _detail(parsed) {
170
+ const pidArg = parsed.positional[0] || parsed.flags.pid || ''
171
+ if (!pidArg) {
172
+ console.error('Usage: opm proc detail <pid>')
173
+ process.exit(1)
174
+ }
175
+ const pid = parseInt(pidArg, 10)
176
+ const result = await getProcessDetail(pid)
177
+ output(result)
178
+ }
179
+
180
+ _confirm(msg) {
181
+ return new Promise((resolve) => {
182
+ process.stderr.write(msg)
183
+ process.stdin.setEncoding('utf8')
184
+ process.stdin.resume()
185
+ process.stdin.once('data', (data) => {
186
+ process.stdin.pause()
187
+ const answer = data.trim().toLowerCase()
188
+ resolve(answer === 'y' || answer === 'yes')
189
+ })
190
+ })
191
+ }
192
+
193
+ printHelp() {
194
+ const actionLines = Object.entries(ACTIONS)
195
+ .map(([sig, { desc }]) => ` ${sig.padEnd(12)} ${desc}`)
196
+ .join('\n')
197
+
198
+ console.log(`
199
+ Usage: opm proc <action> [options]
200
+
201
+ Actions:
202
+ ${actionLines}
203
+
204
+ Options:
205
+ -n, --count <n> Number of processes (list, default: 10)
206
+ -n, --name <name> Process name pattern (search/kill, fuzzy by default)
207
+ -b, --by <mem|cpu> Sort by memory or CPU (list, default: mem)
208
+ -p, --port <port> Port number (search/kill/port)
209
+ -i, --id <pid> Process ID (kill)
210
+ --exact Exact match (search, default: fuzzy)
211
+ -f, --force Use SIGKILL instead of SIGTERM (kill)
212
+ -y, --yes Skip confirmation prompt (kill)
213
+ -h, --help Show this help
214
+
215
+ Examples:
216
+ opm proc info
217
+ opm proc list
218
+ opm proc list -n 20 -b cpu
219
+ opm proc search -n node
220
+ opm proc search -n node -p 3000
221
+ opm proc search -n node --exact
222
+ opm proc port
223
+ opm proc port -p 8080
224
+ opm proc kill -i 1234
225
+ opm proc kill -p 8080 -f -y
226
+ opm proc kill -n node -y
227
+ opm proc tree
228
+ opm proc tree 1234
229
+ opm proc detail 1234
230
+ `)
231
+ }
232
+ }
@@ -2,9 +2,8 @@
2
2
  * uv 命令组 — 管理 uv 自身
3
3
  *
4
4
  * uv 是 Astral 出品的快速 Python 包/版本管理器(Rust 实现)。
5
- * 安装方式:curl 安装脚本(非 git clone);
5
+ * 安装方式:curl 安装脚本(非 git clone);脚本 URL 由 uv.registry.active 提供;
6
6
  * upgrade = uv self update;uninstall = rm 二进制 + 数据目录。
7
- * uv 无镜像源,故无 registry action。
8
7
  */
9
8
 
10
9
  import { execSync, spawnSync } from 'node:child_process'
@@ -14,15 +13,11 @@ import os from 'node:os'
14
13
  import { ToolManagerGroup } from './version-manager.js'
15
14
  import { installUv, getUvBin } from '../../managers/python.js'
16
15
 
17
- const UV_INSTALL_URL = 'https://astral.sh/uv/install.sh'
18
- const UV_INSTALL_PS_URL = 'https://astral.sh/uv/install.ps1'
19
-
20
16
  export class UvGroup extends ToolManagerGroup {
21
17
  constructor() {
22
18
  super({
23
19
  name: 'uv',
24
20
  desc: 'uv version manager (manage uv itself)',
25
- hasRegistry: false,
26
21
  isInstalled: () => {
27
22
  try {
28
23
  execSync(`${getUvBin()} --version`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
@@ -34,7 +29,7 @@ export class UvGroup extends ToolManagerGroup {
34
29
  getPath: () => getUvBin(),
35
30
  versionCmd: () => `${getUvBin()} --version`,
36
31
  parseVersion: (out) => out.trim(),
37
- install: () => installUv(),
32
+ install: (proxy) => installUv(proxy),
38
33
  upgrade: () => {
39
34
  execSync(`${getUvBin()} self update`, { stdio: 'inherit' })
40
35
  return { action: 'upgraded' }
@@ -54,7 +49,10 @@ export class UvGroup extends ToolManagerGroup {
54
49
  actions: [
55
50
  ['version', 'Show uv version'],
56
51
  ['installed', 'Check if uv is installed'],
57
- ['install', 'Install uv (curl installer from astral.sh)'],
52
+ ['registry', 'Show active uv install script URL (from opm config)'],
53
+ ['registry set <url|preset>', 'Set active uv install script URL (writes opm config)'],
54
+ ['registry presets', 'List available mirror presets'],
55
+ ['install', 'Install uv (curl installer)'],
58
56
  ['upgrade', 'Update uv (uv self update)'],
59
57
  ['uninstall', 'Remove uv (~/.local/bin/uv + ~/.local/share/uv)'],
60
58
  ['dir', 'Show uv install directory'],
@@ -63,6 +61,9 @@ export class UvGroup extends ToolManagerGroup {
63
61
  examples: [
64
62
  'opm uv version',
65
63
  'opm uv installed',
64
+ 'opm uv registry',
65
+ 'opm uv registry set official',
66
+ 'opm uv registry presets',
66
67
  'opm uv install',
67
68
  'opm uv upgrade',
68
69
  'opm uv dir',
@@ -7,9 +7,10 @@
7
7
  */
8
8
 
9
9
  import { execSync } from 'node:child_process'
10
+ import readline from 'node:readline'
10
11
  import { parseFlags } from '../helpers/args.js'
11
12
  import { Shell } from '@wwkit/shared'
12
- import { getActiveRegistry, setActiveRegistry, getPresets } from '../../config.js'
13
+ import { getActiveRegistry, setActiveRegistry, getPresets, getActiveProxy } from '../../config.js'
13
14
  import { output } from '../../formatter.js'
14
15
 
15
16
  const shell = new Shell()
@@ -24,7 +25,7 @@ const shell = new Shell()
24
25
  * @property {() => string} getPath - view 展示的安装路径
25
26
  * @property {() => string} versionCmd - 返回查询版本的 shell 命令
26
27
  * @property {(out: string) => string} [parseVersion] - 解析版本输出
27
- * @property {() => Object} install - 安装并返回结果字段(失败时 throw)
28
+ * @property {(proxy?: string) => Object} install - 安装并返回结果字段(失败时 throw)
28
29
  * @property {() => Object} upgrade - 升级并返回结果字段(失败时 throw)
29
30
  * @property {() => Object} uninstall - 卸载并返回结果字段(失败时 throw)
30
31
  * @property {string} usage - 帮助 usage 行
@@ -44,7 +45,8 @@ export class ToolManagerGroup {
44
45
  }
45
46
 
46
47
  async run(argv) {
47
- const [action, ...rest] = argv
48
+ const parsed = parseFlags(argv)
49
+ const [action, ...rest] = parsed.positional
48
50
 
49
51
  if (!action || action === '-h' || action === '--help' || action === 'help') {
50
52
  this.printHelp()
@@ -57,6 +59,13 @@ export class ToolManagerGroup {
57
59
  process.exit(1)
58
60
  }
59
61
 
62
+ let proxy = ''
63
+ if ('p' in parsed.flags || 'proxy' in parsed.flags) {
64
+ let raw = parsed.flags.proxy || parsed.flags.p || ''
65
+ if (raw === 'true') raw = ''
66
+ proxy = raw || getActiveProxy()
67
+ }
68
+
60
69
  switch (action) {
61
70
  case 'version':
62
71
  return this._version()
@@ -65,11 +74,11 @@ export class ToolManagerGroup {
65
74
  case 'registry':
66
75
  return this._registry(parseFlags(rest))
67
76
  case 'install':
68
- return this._install()
77
+ return this._install(proxy)
69
78
  case 'upgrade':
70
79
  return this._upgrade()
71
80
  case 'uninstall':
72
- return this._uninstall()
81
+ return this._uninstall(parsed)
73
82
  case 'dir':
74
83
  return this._dir()
75
84
  default:
@@ -110,13 +119,15 @@ export class ToolManagerGroup {
110
119
  }
111
120
  }
112
121
 
113
- _install() {
122
+ _install(proxy) {
114
123
  if (this.cfg.isInstalled()) {
115
124
  output({ name: this.name, installed: true, hint: `${this.name} already installed, run \`opm ${this.name} upgrade\` to update` })
116
125
  return
117
126
  }
118
127
  try {
119
- output({ name: this.name, installed: true, ...(this.cfg.install() || {}) })
128
+ const installFn = this.cfg.install
129
+ const result = installFn.length >= 1 ? installFn(proxy) : installFn()
130
+ output({ name: this.name, installed: true, ...(result || {}) })
120
131
  } catch (err) {
121
132
  console.error(err.message || `Failed to install ${this.name}`)
122
133
  process.exit(1)
@@ -136,11 +147,26 @@ export class ToolManagerGroup {
136
147
  }
137
148
  }
138
149
 
139
- _uninstall() {
150
+ async _uninstall(parsed) {
140
151
  if (!this.cfg.isInstalled()) {
141
152
  output({ name: this.name, installed: false, hint: `${this.name} not installed` })
142
153
  return
143
154
  }
155
+ const force = parsed.flags.y || parsed.flags.yes || parsed.flags.force
156
+ if (force !== 'true' && force !== true) {
157
+ console.error(`WARNING: This will remove ${this.name} and all its data.`)
158
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
159
+ const answer = await new Promise((resolve) => {
160
+ rl.question(`Are you sure? [y/N] `, (ans) => {
161
+ rl.close()
162
+ resolve(ans.trim().toLowerCase())
163
+ })
164
+ })
165
+ if (answer !== 'y' && answer !== 'yes') {
166
+ output({ name: this.name, installed: true, hint: 'Uninstall cancelled' })
167
+ return
168
+ }
169
+ }
144
170
  try {
145
171
  output({ name: this.name, ...(this.cfg.uninstall() || {}) })
146
172
  } catch (err) {
@@ -0,0 +1,220 @@
1
+ /**
2
+ * wsl 命令组 — 管理 WSL2 自身与发行版
3
+ *
4
+ * 继承 ToolManagerGroup 复用 version/installed/upgrade/uninstall/dir,
5
+ * 额外增加发行版管理动作(list/available/remove/start/stop/stop-all/
6
+ * set-default/default/status/open)与带可选参数的 install。
7
+ * 仅 Windows 可用;wsl.exe 输出为 UTF-16LE,需 override _version/_installed。
8
+ */
9
+
10
+ import { execSync } from 'node:child_process'
11
+ import { ToolManagerGroup } from './version-manager.js'
12
+ import { parseFlags } from '../helpers/args.js'
13
+ import { output } from '../../formatter.js'
14
+ import {
15
+ isWslInstalled,
16
+ getWslVersion,
17
+ installWsl,
18
+ updateWsl,
19
+ uninstallWsl,
20
+ getWslDir,
21
+ listDistros,
22
+ listAvailable,
23
+ installDistro,
24
+ removeDistro,
25
+ startDistro,
26
+ stopDistro,
27
+ stopAll,
28
+ setDefaultDistro,
29
+ getDefaultDistro,
30
+ getWslStatus,
31
+ openWsl,
32
+ } from '../../managers/wsl.js'
33
+
34
+ export class WslGroup extends ToolManagerGroup {
35
+ constructor() {
36
+ super({
37
+ name: 'wsl',
38
+ desc: 'WSL2 manager (install/upgrade/list/open distros)',
39
+ hasRegistry: false,
40
+ isInstalled: () => isWslInstalled(),
41
+ getPath: () => getWslDir(),
42
+ versionCmd: () => 'wsl.exe --version',
43
+ parseVersion: (out) => out.trim().split('\n')[0].trim(),
44
+ install: () => installWsl(),
45
+ upgrade: () => updateWsl(),
46
+ uninstall: () => uninstallWsl(),
47
+ usage: 'opm wsl <action> [args] [options]',
48
+ actions: [
49
+ ['version', 'Show WSL version'],
50
+ ['installed', 'Check if WSL is installed'],
51
+ ['install [distro]', 'Install WSL2 (or a specific distribution)'],
52
+ ['upgrade', 'Update WSL kernel (wsl --update)'],
53
+ ['uninstall', 'Show uninstall instructions'],
54
+ ['dir', 'Show WSL data directory'],
55
+ ['list', 'List installed distributions'],
56
+ ['available', 'List available distributions'],
57
+ ['remove <distro>', 'Remove a distribution'],
58
+ ['start [distro]', 'Start a distribution in current terminal'],
59
+ ['stop <distro>', 'Stop a distribution'],
60
+ ['stop-all', 'Stop all distributions'],
61
+ ['set-default <distro>', 'Set default distribution'],
62
+ ['default', 'Show default distribution'],
63
+ ['status', 'Show WSL status'],
64
+ ['open [distro]', 'Open PowerShell window and enter WSL'],
65
+ ['help', 'Show this help'],
66
+ ],
67
+ examples: [
68
+ 'opm wsl version',
69
+ 'opm wsl installed',
70
+ 'opm wsl install',
71
+ 'opm wsl install Ubuntu',
72
+ 'opm wsl list',
73
+ 'opm wsl available',
74
+ 'opm wsl open',
75
+ 'opm wsl open Ubuntu',
76
+ 'opm wsl start Ubuntu',
77
+ 'opm wsl stop Ubuntu',
78
+ 'opm wsl stop-all',
79
+ 'opm wsl set-default Ubuntu',
80
+ 'opm wsl default',
81
+ 'opm wsl status',
82
+ 'opm wsl dir',
83
+ ],
84
+ })
85
+ }
86
+
87
+ async run(argv) {
88
+ const [action, ...rest] = argv
89
+
90
+ if (!action || action === '-h' || action === '--help' || action === 'help') {
91
+ this.printHelp()
92
+ return
93
+ }
94
+
95
+ switch (action) {
96
+ case 'list':
97
+ return this._list()
98
+ case 'available':
99
+ return this._available()
100
+ case 'remove':
101
+ return this._remove(parseFlags(rest))
102
+ case 'start':
103
+ return this._start(parseFlags(rest))
104
+ case 'stop':
105
+ return this._stop(parseFlags(rest))
106
+ case 'stop-all':
107
+ return this._stopAll()
108
+ case 'set-default':
109
+ return this._setDefault(parseFlags(rest))
110
+ case 'default':
111
+ return this._default()
112
+ case 'status':
113
+ return this._status()
114
+ case 'open':
115
+ return this._open(parseFlags(rest))
116
+ case 'install':
117
+ return this._installDistro(parseFlags(rest))
118
+ }
119
+
120
+ return super.run(argv)
121
+ }
122
+
123
+ _version() {
124
+ if (!isWslInstalled()) {
125
+ output('')
126
+ return
127
+ }
128
+ try {
129
+ output(getWslVersion())
130
+ } catch {
131
+ output('')
132
+ }
133
+ }
134
+
135
+ _installed() {
136
+ const installed = isWslInstalled()
137
+ const result = { name: 'wsl', installed }
138
+ if (installed) {
139
+ result.path = getWslDir()
140
+ try {
141
+ result.version = getWslVersion()
142
+ } catch {}
143
+ }
144
+ output(result)
145
+ }
146
+
147
+ _installDistro(parsed) {
148
+ const distro = parsed.positional[0]
149
+ if (distro) {
150
+ output(installDistro(distro))
151
+ return
152
+ }
153
+ if (isWslInstalled()) {
154
+ output({ name: 'wsl', installed: true, hint: 'WSL already installed, use "opm wsl install <distro>" to install a distribution' })
155
+ return
156
+ }
157
+ output(installWsl())
158
+ }
159
+
160
+ _list() {
161
+ output(listDistros())
162
+ }
163
+
164
+ _available() {
165
+ output(listAvailable().map((name) => ({ name })))
166
+ }
167
+
168
+ _remove(parsed) {
169
+ const distro = parsed.positional[0]
170
+ if (!distro) {
171
+ console.error('Usage: opm wsl remove <distro>')
172
+ process.exit(1)
173
+ }
174
+ output(removeDistro(distro))
175
+ }
176
+
177
+ _start(parsed) {
178
+ const distro = parsed.positional[0] || getDefaultDistro()
179
+ if (!distro) {
180
+ console.error('Usage: opm wsl start [distro]')
181
+ process.exit(1)
182
+ }
183
+ output(startDistro(distro))
184
+ }
185
+
186
+ _stop(parsed) {
187
+ const distro = parsed.positional[0]
188
+ if (!distro) {
189
+ console.error('Usage: opm wsl stop <distro>')
190
+ process.exit(1)
191
+ }
192
+ output(stopDistro(distro))
193
+ }
194
+
195
+ _stopAll() {
196
+ output(stopAll())
197
+ }
198
+
199
+ _setDefault(parsed) {
200
+ const distro = parsed.positional[0]
201
+ if (!distro) {
202
+ console.error('Usage: opm wsl set-default <distro>')
203
+ process.exit(1)
204
+ }
205
+ output(setDefaultDistro(distro))
206
+ }
207
+
208
+ _default() {
209
+ output({ default: getDefaultDistro() })
210
+ }
211
+
212
+ _status() {
213
+ output(getWslStatus())
214
+ }
215
+
216
+ _open(parsed) {
217
+ const distro = parsed.positional[0] || getDefaultDistro()
218
+ output(openWsl(distro || ''))
219
+ }
220
+ }
package/src/cli/index.js CHANGED
@@ -15,7 +15,13 @@ import { NvmGroup } from './groups/nvm.js'
15
15
  import { PhpenvGroup } from './groups/phpenv.js'
16
16
  import { UvGroup } from './groups/uv.js'
17
17
  import { HomebrewGroup } from './groups/homebrew.js'
18
+ import { WslGroup } from './groups/wsl.js'
19
+ import { DockerGroup } from './groups/docker.js'
18
20
  import { EnsureGroup } from './groups/ensure.js'
21
+ import { PingGroup } from './groups/ping.js'
22
+ import { ProcGroup } from './groups/proc.js'
23
+ import { FtpGroup } from '../tools/ftp/index.js'
24
+ import { GitGroup } from '../tools/git/index.js'
19
25
  import { HarnessGroup } from '../harnesses/base.js'
20
26
  import { opencodeHarness } from '../harnesses/opencode/index.js'
21
27
  import { CftGroup } from '../binaries/cft/index.js'
@@ -33,6 +39,10 @@ const GROUPS = {
33
39
  apt: new PackageGroup('apt'),
34
40
  brew: new PackageGroup('brew'),
35
41
  homebrew: new HomebrewGroup(),
42
+ wsl: new WslGroup(),
43
+ docker: new DockerGroup(),
44
+ ftp: new FtpGroup(),
45
+ git: new GitGroup(),
36
46
  composer: new PackageGroup('composer'),
37
47
  bun: new PackageGroup('bun'),
38
48
  winget: new PackageGroup('winget'),
@@ -46,6 +56,8 @@ const GROUPS = {
46
56
  opencode: new HarnessGroup(opencodeHarness),
47
57
  cft: new CftGroup(),
48
58
  ensure: new EnsureGroup(),
59
+ ping: new PingGroup(),
60
+ proc: new ProcGroup(),
49
61
  }
50
62
 
51
63
  class CLI {