@wwkit/opm 1.0.13 → 1.0.15

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 CHANGED
@@ -67,6 +67,7 @@ pnpm opm <command> [args] [options]
67
67
  | `ftp <action>` | FTP 文件管理(上传/rsync/下载) |
68
68
  | `ensure <runtime> [version]` | 确保运行时已安装(node/php/python) |
69
69
  | `ping <host|proxy|registry>` | 验证主机、代理或所有 registry 的可访问性 |
70
+ | `env <action>` | 环境变量管理(list/get/set,跨平台:Unix profile / Windows user 级注册表) |
70
71
  | `config [section]` | 查看常用配置(proxy + 各 registry 段,段名动态生成) |
71
72
  | `opencode <action>` | opencode 运维命令(安装/升级/清理/env);裸 `opm opencode` 启动 TUI |
72
73
  | `harness <action>` | @wwkit/harness 插件管理(安装/升级/list/status/env/dir) |
@@ -532,6 +533,37 @@ opm ping registry -d # 输出完整 JSON 数组
532
533
  > `opm ping proxy` 从 `proxy.active` 解析代理 URL,提取主机名后 ping。
533
534
  > `opm ping registry` 遍历所有含 `registry` 配置的工具,并行 ping 各 active 地址的主机。
534
535
 
536
+ ### env
537
+
538
+ ```bash
539
+ opm env list # 列出 opm 管理的环境变量(含配置文件路径)
540
+ opm env list --all # 追加 process.env 全量,每条标注 managed: true/false
541
+ opm env get PATH # 根据 key 取值(先查 opm 管理源,回退 process.env)
542
+ opm env set FOO bar # 设置环境变量(替换已存在的)
543
+ opm env set MY_API_KEY secret123 # 设置 API 密钥
544
+ opm env search api # 按关键词匹配 key 或 value(忽略大小写)
545
+ opm env search api --all # 同时搜索 process.env
546
+ opm env help # 通用介绍
547
+ ```
548
+
549
+ 输出格式:默认输出**简化文本**(list/search 按来源分组:`配置文件路径` 行 + `key = value` 行;get/set 输出单行 `key = value`);加 `-j/--json` 输出完整 JSON。
550
+
551
+ ```bash
552
+ opm env list # 简化文本(按来源分组)
553
+ opm env list --json # JSON 数组
554
+ opm env get PATH # 简化:PATH = /usr/bin:/bin
555
+ opm env get PATH --json # JSON 对象(含 source/scope)
556
+ opm env set FOO bar # 简化:FOO = bar
557
+ opm env set FOO bar --json # JSON 对象(含 written/source)
558
+ opm env search api # 简化:匹配项按来源分组
559
+ opm env search api --json # JSON 数组
560
+ ```
561
+
562
+ > `list`/`search` 默认只作用于 `opm env set` 管理的变量(Unix 的 `# >>> wwkit NAME >>>` 标记块 / Windows manifest);`--all` 在其基础上追加 `process.env` 全量,并给每条标注 `managed: true/false` 与来源。
563
+ > `search <keyword>` 匹配 key 或 value 是否包含关键词(忽略大小写),可用 `-q/--query` 替代位置参数。
564
+ > 跨平台:Unix 读写 shell profile(`.zshrc`/`.bashrc`/`.profile`/fish config),用 `# >>> wwkit NAME >>>` 标记块管理;Windows 用 `setx` 写 user 级注册表(`HKCU\Environment`),`list` 用 `reg query` 读取。
565
+ > Windows 上 `setx` 不留标记,opm 通过 `~/.config/opm/env-managed.json` 记录哪些 user 级变量由 opm 管理;`set` 始终写入 user 级(无需管理员权限)。
566
+
535
567
  ### opencode
536
568
 
537
569
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wwkit/opm",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "author": "bluesliu <langcai163@163.com>",
5
5
  "description": "Unified CLI — package management (npm/pip/dnf/apt) + config view + opencode maintenance",
6
6
  "type": "module",
@@ -35,7 +35,16 @@
35
35
  "basic-ftp": "^6.2.1",
36
36
  "extract-zip": "^2.0.1",
37
37
  "systeminformation": "^5.23.0",
38
- "@wwkit/shared": "1.0.17"
38
+ "@wwkit/shared": "1.0.20"
39
+ },
40
+ "devDependencies": {
41
+ "jest": "^29.7.0"
42
+ },
43
+ "jest": {
44
+ "transform": {},
45
+ "testMatch": [
46
+ "**/tests/**/*.test.js"
47
+ ]
39
48
  },
40
49
  "publishConfig": {
41
50
  "registry": "https://registry.npmjs.org/",
@@ -45,8 +54,8 @@
45
54
  "postinstall": "node scripts/postinstall.js",
46
55
  "check": "node --check bin/index.js",
47
56
  "start": "node bin/index.js",
48
- "test": "node --test \"tests/**/*.test.js\"",
49
- "test:all": "node --test \"tests/**/*.test.js\"",
57
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --no-cache tests",
58
+ "test:all": "node --experimental-vm-modules node_modules/jest/bin/jest.js --no-cache tests",
50
59
  "doc": "node -e \"import('./src/cli/doc.js').then(m => m.generateDocHtml({ open: false }))\"",
51
60
  "release": "node ../../packages/shared/bin/release-pkg.js"
52
61
  }
@@ -17,7 +17,7 @@ import path from 'node:path'
17
17
  import { parseFlags } from '../cli/helpers/args.js'
18
18
  import { output } from '../formatter.js'
19
19
  import { getActiveRegistry, setActiveRegistry, getPresets, getActiveProxy, getUserConfigDir } from '../config.js'
20
- import { fetchText, downloadToFile } from '../managers/http.js'
20
+ import { fetchText, downloadToFile } from '@wwkit/shared'
21
21
  import { getOs, OS_TYPES } from './platform.js'
22
22
  import { find as findExe } from './exefinder.js'
23
23
  import { verify as verifyDeps } from './depchecker.js'
@@ -0,0 +1,451 @@
1
+ /**
2
+ * env 命令组 — 跨平台环境变量便捷管理
3
+ *
4
+ * 用法:
5
+ * opm env list [--all] 列出 opm 管理的环境变量(含配置文件路径);--all 追加 process.env 全量
6
+ * opm env get <key> 根据 key 取值(先查 opm 管理的配置源,回退 process.env)
7
+ * opm env set <key> <value> 设置值(替换已存在的)
8
+ * opm env help 通用介绍
9
+ *
10
+ * 跨平台策略(复用 @wwkit/shared 的 Shell):
11
+ * Unix: 读写 shell profile(bash/zsh/fish),用 `# >>> wwkit NAME >>>` 标记块管理
12
+ * Windows: setx 写 user 级注册表;list 用 reg query 读取;用 manifest 文件
13
+ * (~/.config/opm/env-managed.json)识别哪些 user 级变量由 opm 管理
14
+ * (setx 不留标记,无法从注册表本身区分)
15
+ */
16
+
17
+ import fs from 'node:fs'
18
+ import path from 'node:path'
19
+ import { spawnSync } from 'node:child_process'
20
+ import { Shell } from '@wwkit/shared'
21
+ import { parseFlags } from '../helpers/args.js'
22
+ import { output } from '../../formatter.js'
23
+ import { getUserConfigDir } from '../../config.js'
24
+
25
+ /** Windows 注册表环境变量键路径 */
26
+ const WIN_USER_KEY = 'HKCU\\Environment'
27
+
28
+ /** manifest 文件名(Windows 记录 opm 管理的 user 级变量 key) */
29
+ const MANIFEST_FILE = 'env-managed.json'
30
+
31
+ /**
32
+ * 反转 Shell.setEnvVar 的转义([$`\\"] → \\$&)
33
+ * @param {string} s
34
+ * @returns {string}
35
+ */
36
+ function unescapeEnvValue(s) {
37
+ return s.replace(/\\([$`\\"])/g, '$1')
38
+ }
39
+
40
+ /**
41
+ * 从一行 export/set 语句中提取变量值
42
+ * @param {string} line
43
+ * @param {string} key
44
+ * @returns {string|null}
45
+ */
46
+ function parseEnvLine(line, key) {
47
+ const exportQuoted = line.match(new RegExp(`^export\\s+${key}="(.*)"\\s*$`))
48
+ if (exportQuoted) return unescapeEnvValue(exportQuoted[1])
49
+ const fishQuoted = line.match(new RegExp(`^set -gx\\s+${key}\\s+"(.*)"\\s*$`))
50
+ if (fishQuoted) return unescapeEnvValue(fishQuoted[1])
51
+ const exportBare = line.match(new RegExp(`^export\\s+${key}=(\\S+)\\s*$`))
52
+ if (exportBare) return exportBare[1]
53
+ const fishBare = line.match(new RegExp(`^set -gx\\s+${key}\\s+(\\S+)\\s*$`))
54
+ if (fishBare) return fishBare[1]
55
+ return null
56
+ }
57
+
58
+ /**
59
+ * 解析 profile 文件内容中的 wwkit 标记块,提取 opm 管理的环境变量
60
+ * @param {string} content
61
+ * @returns {{ key: string, value: string }[]}
62
+ */
63
+ export function parseEnvBlocks(content) {
64
+ if (!content) return []
65
+ const lines = content.split('\n')
66
+ const result = []
67
+ let i = 0
68
+ while (i < lines.length) {
69
+ const marker = lines[i].match(/^# >>> wwkit (\S+) >>>/)
70
+ if (marker) {
71
+ const key = marker[1]
72
+ // 块体:标记行之后到下一个 wwkit 标记或 EOF 之间的非空非注释行
73
+ let j = i + 1
74
+ while (j < lines.length && lines[j].trim() === '') j++
75
+ if (j < lines.length && !lines[j].startsWith('# >>> wwkit')) {
76
+ const value = parseEnvLine(lines[j], key)
77
+ if (value !== null) result.push({ key, value })
78
+ }
79
+ i = j + 1
80
+ } else {
81
+ i++
82
+ }
83
+ }
84
+ return result
85
+ }
86
+
87
+ /**
88
+ * 解析 Windows `reg query` 输出,提取环境变量条目
89
+ * @param {string} output
90
+ * @returns {{ key: string, type: string, value: string }[]}
91
+ */
92
+ export function parseRegOutput(output) {
93
+ if (!output) return []
94
+ const result = []
95
+ for (const line of output.split(/\r?\n/)) {
96
+ const m = line.match(/^\s+(\S+)\s+(REG_\S+)\s+(.*)$/)
97
+ if (m) result.push({ key: m[1], type: m[2], value: m[3] })
98
+ }
99
+ return result
100
+ }
101
+
102
+ /**
103
+ * 校验环境变量名合法性(字母/下划线开头,后接字母数字下划线)
104
+ * @param {string} key
105
+ * @returns {boolean}
106
+ */
107
+ export function isValidEnvKey(key) {
108
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)
109
+ }
110
+
111
+ export class EnvGroup {
112
+ /**
113
+ * @param {{ shell?: Shell, configDir?: string }} [opts] - shell/configDir 注入用于测试
114
+ */
115
+ constructor(opts = {}) {
116
+ this.name = 'env'
117
+ this.desc = 'Manage environment variables (list/get/set) across platforms'
118
+ this.shell = opts.shell || new Shell()
119
+ this.configDir = opts.configDir || ''
120
+ }
121
+
122
+ /** @returns {string} manifest 文件绝对路径 */
123
+ _manifestPath() {
124
+ const dir = this.configDir || getUserConfigDir()
125
+ return path.join(dir, MANIFEST_FILE)
126
+ }
127
+
128
+ /**
129
+ * 读取 manifest 中的 opm 管理 key 集合(Windows 用)
130
+ * @returns {Set<string>}
131
+ */
132
+ _readManifest() {
133
+ try {
134
+ const raw = fs.readFileSync(this._manifestPath(), 'utf8')
135
+ const data = JSON.parse(raw)
136
+ return new Set(Array.isArray(data.keys) ? data.keys : [])
137
+ } catch {
138
+ return new Set()
139
+ }
140
+ }
141
+
142
+ /**
143
+ * 将 key 登记到 manifest(Windows set 时调用)
144
+ * @param {string} key
145
+ */
146
+ _addManagedKey(key) {
147
+ const keys = this._readManifest()
148
+ keys.add(key)
149
+ const dir = this.configDir || getUserConfigDir()
150
+ fs.mkdirSync(dir, { recursive: true })
151
+ fs.writeFileSync(this._manifestPath(), JSON.stringify({ keys: [...keys] }, null, 2), 'utf8')
152
+ }
153
+
154
+ /**
155
+ * 列出 opm 管理的环境变量(默认 list 的数据源)
156
+ * Unix: 解析所有 profile 的 wwkit 标记块
157
+ * Windows: 读 manifest → 查 user 级注册表当前值
158
+ * @returns {object[]}
159
+ */
160
+ _managedItems() {
161
+ return this.shell.isWindows ? this._listWindowsManaged() : this._listUnix()
162
+ }
163
+
164
+ _listUnix() {
165
+ const items = []
166
+ for (const { path: profilePath, syntax } of this.shell.getProfiles()) {
167
+ let content = ''
168
+ try {
169
+ content = fs.readFileSync(profilePath, 'utf8')
170
+ } catch {}
171
+ for (const { key, value } of parseEnvBlocks(content)) {
172
+ items.push({ key, value, scope: 'user', source: profilePath, syntax })
173
+ }
174
+ }
175
+ return items
176
+ }
177
+
178
+ _listWindowsManaged() {
179
+ const managedKeys = this._readManifest()
180
+ if (managedKeys.size === 0) return []
181
+ const userVars = this._regQuery(WIN_USER_KEY)
182
+ const byKey = new Map(userVars.map((v) => [v.key, v]))
183
+ const items = []
184
+ for (const key of managedKeys) {
185
+ const v = byKey.get(key)
186
+ if (v) {
187
+ items.push({ key, value: v.value, scope: 'user', source: 'registry (user)', type: v.type })
188
+ } else {
189
+ // manifest 登记过但注册表中已不存在(外部删除)——标注缺失
190
+ items.push({ key, value: null, scope: 'user', source: 'registry (user)', missing: true })
191
+ }
192
+ }
193
+ return items
194
+ }
195
+
196
+ _regQuery(keyPath) {
197
+ try {
198
+ const result = spawnSync('reg', ['query', keyPath], {
199
+ encoding: 'utf8',
200
+ stdio: ['pipe', 'pipe', 'pipe'],
201
+ })
202
+ if (result.status !== 0) return []
203
+ return parseRegOutput(result.stdout || '')
204
+ } catch {
205
+ return []
206
+ }
207
+ }
208
+
209
+ /**
210
+ * --all:opm 管理的 ∪ process.env 全量,每条标注 managed
211
+ * @returns {object[]}
212
+ */
213
+ _listAll() {
214
+ const managedItems = this._managedItems()
215
+ const seen = new Set()
216
+ const items = []
217
+ for (const m of managedItems) {
218
+ items.push({ key: m.key, value: m.value, managed: true, source: m.source, scope: m.scope })
219
+ seen.add(m.key)
220
+ }
221
+ for (const [key, value] of Object.entries(process.env)) {
222
+ if (!seen.has(key)) {
223
+ items.push({ key, value, managed: false, source: 'process.env' })
224
+ seen.add(key)
225
+ }
226
+ }
227
+ return items
228
+ }
229
+
230
+ async run(argv) {
231
+ const [action, ...rest] = argv
232
+
233
+ if (!action || action === '-h' || action === '--help' || action === 'help') {
234
+ this.printHelp()
235
+ return
236
+ }
237
+
238
+ const parsed = parseFlags(rest)
239
+
240
+ switch (action) {
241
+ case 'list':
242
+ return this._list(parsed)
243
+ case 'get':
244
+ return this._get(parsed)
245
+ case 'set':
246
+ return this._set(parsed)
247
+ case 'search':
248
+ return this._search(parsed)
249
+ default:
250
+ console.error(`Unknown action: ${action}`)
251
+ this.printHelp()
252
+ process.exit(1)
253
+ }
254
+ }
255
+
256
+ /**
257
+ * 是否输出 JSON(--json / -j)
258
+ * @param {{ flags: object, positional: string[] }} parsed
259
+ * @returns {boolean}
260
+ */
261
+ _useJson(parsed) {
262
+ return parsed.flags.json === 'true' || parsed.flags.j === 'true'
263
+ }
264
+
265
+ /**
266
+ * 统一输出:--json 走 JSON,否则走 plain 格式化函数
267
+ * @param {{ flags: object, positional: string[] }} parsed
268
+ * @param {*} data - JSON 输出数据
269
+ * @param {() => string} plain - 简化字符串输出函数
270
+ */
271
+ _out(parsed, data, plain) {
272
+ if (this._useJson(parsed)) {
273
+ output(data)
274
+ } else {
275
+ console.log(plain())
276
+ }
277
+ }
278
+
279
+ /**
280
+ * list 简化格式:按 source 分组,每组标题为来源,下面 key = value 行
281
+ * @param {object[]} items
282
+ * @returns {string}
283
+ */
284
+ _formatListPlain(items) {
285
+ const groups = []
286
+ const groupMap = new Map()
287
+ for (const item of items) {
288
+ const source = item.source || 'unknown'
289
+ if (!groupMap.has(source)) {
290
+ const g = { source, entries: [] }
291
+ groupMap.set(source, g)
292
+ groups.push(g)
293
+ }
294
+ groupMap.get(source).entries.push(item)
295
+ }
296
+ return groups
297
+ .map((g) => [
298
+ g.source,
299
+ ...g.entries.map((e) => `${e.key} = ${e.value === null || e.value === undefined ? '(not set)' : e.value}`),
300
+ ].join('\n'))
301
+ .join('\n\n')
302
+ }
303
+
304
+ /**
305
+ * 列出环境变量
306
+ * 默认: 只显示 opm 管理的(Unix 标记块 / Windows manifest)
307
+ * --all: 追加 process.env 全量,每条标注 managed: true/false
308
+ * 输出: 默认简化文本(按 source 分组),--json 输出 JSON
309
+ * @param {{ flags: object, positional: string[] }} parsed
310
+ */
311
+ async _list(parsed) {
312
+ const all = parsed.flags.all === 'true' || parsed.flags.a === 'true'
313
+ const items = all ? this._listAll() : this._managedItems()
314
+ this._out(parsed, items, () => this._formatListPlain(items))
315
+ }
316
+
317
+ /**
318
+ * 根据 key 取值:先查 opm 管理的配置源,回退 process.env
319
+ * 输出: 默认 `key = value`(未找到 `key = (not found)`),--json 输出 JSON
320
+ * @param {{ flags: object, positional: string[] }} parsed
321
+ */
322
+ async _get(parsed) {
323
+ const key = parsed.positional[0] || parsed.flags.k || parsed.flags.key || ''
324
+ if (!key) {
325
+ console.error('Usage: opm env get <key>')
326
+ console.error('Missing required <key>')
327
+ process.exit(1)
328
+ }
329
+
330
+ const configured = this._managedItems()
331
+ const hit = configured.find((e) => e.key === key)
332
+ if (hit) {
333
+ this._out(parsed, { key, value: hit.value, found: true, source: hit.source, scope: hit.scope }, () => `${key} = ${hit.value}`)
334
+ return
335
+ }
336
+
337
+ if (process.env[key] !== undefined) {
338
+ const value = process.env[key]
339
+ this._out(parsed, { key, value, found: true, source: 'process.env' }, () => `${key} = ${value}`)
340
+ return
341
+ }
342
+
343
+ this._out(parsed, { key, value: null, found: false }, () => `${key} = (not found)`)
344
+ }
345
+
346
+ /**
347
+ * 设置环境变量(替换已存在的)
348
+ * Unix: 写入 shell profile 标记块;Windows: setx 写 user 级注册表 + manifest 登记
349
+ * @param {{ flags: object, positional: string[] }} parsed
350
+ */
351
+ async _set(parsed) {
352
+ const key = parsed.positional[0] || parsed.flags.k || parsed.flags.key || ''
353
+ const value = parsed.positional[1] ?? parsed.flags.v ?? parsed.flags.value ?? ''
354
+
355
+ if (!key) {
356
+ console.error('Usage: opm env set <key> <value>')
357
+ console.error('Missing required <key>')
358
+ process.exit(1)
359
+ }
360
+ if (!isValidEnvKey(key)) {
361
+ console.error(`Invalid env key: "${key}" (must start with letter/underscore, only letters/digits/underscore)`)
362
+ process.exit(1)
363
+ }
364
+ if (parsed.positional.length < 2 && !(parsed.flags.v || parsed.flags.value)) {
365
+ console.error('Usage: opm env set <key> <value>')
366
+ console.error('Missing required <value>')
367
+ process.exit(1)
368
+ }
369
+
370
+ const written = this.shell.setEnvVar(key, value)
371
+ if (this.shell.isWindows) {
372
+ this._addManagedKey(key)
373
+ }
374
+ const source = this.shell.isWindows
375
+ ? 'registry (user)'
376
+ : this.shell.getProfiles().map((p) => p.path)
377
+ const result = { key, value, written, source }
378
+ this._out(parsed, result, () => `${key} = ${value}${written ? '' : ' (no change)'}`)
379
+ }
380
+
381
+ /**
382
+ * 按关键词搜索环境变量(匹配 key 或 value,忽略大小写)
383
+ * 默认: 在 opm 管理的变量中搜索;--all 扩展至 process.env 全量
384
+ * 输出: 默认简化文本(按 source 分组),--json 输出 JSON
385
+ * @param {{ flags: object, positional: string[] }} parsed
386
+ */
387
+ async _search(parsed) {
388
+ const keyword = parsed.positional[0] || parsed.flags.q || parsed.flags.query || ''
389
+ if (!keyword) {
390
+ console.error('Usage: opm env search <keyword>')
391
+ console.error('Missing required <keyword>')
392
+ process.exit(1)
393
+ }
394
+
395
+ const all = parsed.flags.all === 'true' || parsed.flags.a === 'true'
396
+ const source = all ? this._listAll() : this._managedItems()
397
+ const kw = keyword.toLowerCase()
398
+ const matches = source.filter((e) => {
399
+ const k = String(e.key).toLowerCase()
400
+ const v = e.value === null || e.value === undefined ? '' : String(e.value).toLowerCase()
401
+ return k.includes(kw) || v.includes(kw)
402
+ })
403
+ this._out(parsed, matches, () => this._formatListPlain(matches))
404
+ }
405
+
406
+ printHelp() {
407
+ console.log(`
408
+ Usage: opm env <action> [args] [options]
409
+
410
+ Actions:
411
+ list [--all] List opm-managed env vars with their config file path
412
+ (default: only vars set via "opm env set";
413
+ --all appends all process.env vars, each tagged managed: true/false)
414
+ get <key> Get value by key (checks opm-managed sources first, then process.env)
415
+ set <key> <value> Set value, replacing any existing one
416
+ (Unix: shell profile marker block; Windows: setx user-level registry)
417
+ search <keyword> [--all] Search env vars whose key or value contains keyword (case-insensitive)
418
+ (default: opm-managed vars; --all also searches process.env)
419
+ help Show this help
420
+
421
+ Options:
422
+ -k, --key <name> Key (alternative to positional for get/set)
423
+ -v, --value <val> Value (alternative to positional for set)
424
+ -q, --query <kw> Keyword (alternative to positional for search)
425
+ -a, --all Also list all process.env vars (tagged managed: true/false)
426
+ -j, --json Output JSON (default: plain text)
427
+ -h, --help Show this help
428
+
429
+ Output:
430
+ default (no -j) Plain text: list groups by source then "key = value" lines;
431
+ get/set print a single "key = value" line
432
+ -j / --json JSON object/array with full metadata
433
+
434
+ Platform behavior:
435
+ Unix read/write shell profiles (.zshrc/.bashrc/.profile/fish config)
436
+ via "# >>> wwkit NAME >>>" marker blocks
437
+ Windows read/write user-level registry (HKCU\\Environment); writes via setx;
438
+ opm-managed keys tracked in ~/.config/opm/env-managed.json
439
+
440
+ Examples:
441
+ opm env list
442
+ opm env list --all # all process.env vars, tagged managed
443
+ opm env get PATH
444
+ opm env set FOO bar
445
+ opm env set MY_API_KEY secret123
446
+ opm env search api # match key or value containing "api" (case-insensitive)
447
+ opm env search api --all # also search process.env
448
+ opm env help
449
+ `)
450
+ }
451
+ }