@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.
Files changed (62) hide show
  1. package/README.md +381 -0
  2. package/bin/index.js +9 -0
  3. package/docs/managers/apt.md +45 -0
  4. package/docs/managers/brew.md +92 -0
  5. package/docs/managers/bun.md +50 -0
  6. package/docs/managers/cft.md +57 -0
  7. package/docs/managers/composer.md +96 -0
  8. package/docs/managers/config.md +32 -0
  9. package/docs/managers/dnf.md +46 -0
  10. package/docs/managers/node.md +49 -0
  11. package/docs/managers/npm.md +100 -0
  12. package/docs/managers/nvm.md +34 -0
  13. package/docs/managers/php.md +48 -0
  14. package/docs/managers/phpenv.md +34 -0
  15. package/docs/managers/pip.md +98 -0
  16. package/docs/managers/python.md +53 -0
  17. package/docs/managers/uv.md +27 -0
  18. package/docs/opm.html +570 -0
  19. package/docs/packages/opencode.md +41 -0
  20. package/package.json +51 -0
  21. package/scripts/postinstall.js +7 -0
  22. package/src/binaries/base.js +508 -0
  23. package/src/binaries/cft/index.js +35 -0
  24. package/src/binaries/depchecker.js +172 -0
  25. package/src/binaries/exefinder.js +110 -0
  26. package/src/binaries/platform.js +17 -0
  27. package/src/cli/doc.js +335 -0
  28. package/src/cli/groups/config.js +78 -0
  29. package/src/cli/groups/ensure.js +96 -0
  30. package/src/cli/groups/homebrew.js +73 -0
  31. package/src/cli/groups/nvm.js +73 -0
  32. package/src/cli/groups/package.js +514 -0
  33. package/src/cli/groups/phpenv.js +89 -0
  34. package/src/cli/groups/uv.js +73 -0
  35. package/src/cli/groups/version-manager.js +186 -0
  36. package/src/cli/helpers/args.js +49 -0
  37. package/src/cli/index.js +129 -0
  38. package/src/config.js +133 -0
  39. package/src/config.json5 +158 -0
  40. package/src/formatter.js +13 -0
  41. package/src/harnesses/base.js +205 -0
  42. package/src/harnesses/opencode/clear.js +239 -0
  43. package/src/harnesses/opencode/db.js +39 -0
  44. package/src/harnesses/opencode/index.js +46 -0
  45. package/src/harnesses/opencode/sqlite.js +200 -0
  46. package/src/index.js +46 -0
  47. package/src/managers/apt.js +377 -0
  48. package/src/managers/base.js +167 -0
  49. package/src/managers/brew.js +374 -0
  50. package/src/managers/bun.js +342 -0
  51. package/src/managers/composer.js +280 -0
  52. package/src/managers/dnf.js +370 -0
  53. package/src/managers/http.js +432 -0
  54. package/src/managers/node.js +150 -0
  55. package/src/managers/npm.js +473 -0
  56. package/src/managers/php.js +157 -0
  57. package/src/managers/pip.js +555 -0
  58. package/src/managers/python.js +180 -0
  59. package/src/managers/runtime.js +309 -0
  60. package/src/managers/winget.js +268 -0
  61. package/src/presets.js +24 -0
  62. 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
+ }