peachsim 0.1.630 → 0.1.632

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 (96) hide show
  1. package/README.md +9 -1
  2. package/cli/app-fonts.ts +23 -4
  3. package/cli/bridge-flow-runner.ts +35 -28
  4. package/cli/command-registry.ts +1 -0
  5. package/cli/commands/compat.ts +11 -60
  6. package/cli/commands/control.ts +12 -5
  7. package/cli/commands/detect.ts +289 -26
  8. package/cli/commands/inspect/core.ts +1 -1
  9. package/cli/commands/inspect.ts +4 -0
  10. package/cli/commands/maestro.ts +8 -2
  11. package/cli/commands/project-report.ts +386 -0
  12. package/cli/commands/projectReportContract.ts +42 -0
  13. package/cli/commands/report-issue.ts +38 -54
  14. package/cli/commands/setup.ts +6 -4
  15. package/cli/commands/telemetry.ts +8 -3
  16. package/cli/commands/test.ts +374 -133
  17. package/cli/drivers/playwright-core-pin.ts +10 -0
  18. package/cli/drivers/playwright-provisioning.ts +36 -1
  19. package/cli/drivers/playwright.ts +26 -19
  20. package/cli/main.ts +11 -1
  21. package/cli/outbound-endpoints.ts +2 -1
  22. package/cli/privacy.ts +1 -2
  23. package/cli/runtime-diagnostics.ts +51 -0
  24. package/cli/shell-init.ts +6 -1
  25. package/cli/telemetry.ts +78 -7
  26. package/cli/ws-bridge.ts +8 -2
  27. package/detox/index.ts +3 -8
  28. package/dist-lib/agent-daemon-client.cjs +1 -1
  29. package/dist-lib/agent-events.cjs +1 -1
  30. package/dist-lib/agent-identity.cjs +1 -1
  31. package/dist-lib/agent-sessions.cjs +1 -1
  32. package/dist-lib/attached-projects.cjs +1 -1
  33. package/dist-lib/auth/shared-session.cjs +1 -1
  34. package/dist-lib/backend-origin.cjs +1 -1
  35. package/dist-lib/beta.cjs +1 -1
  36. package/dist-lib/beta.mjs +1 -1
  37. package/dist-lib/bridge-constants.cjs +1 -1
  38. package/dist-lib/bridge-contract-input.cjs +1 -1
  39. package/dist-lib/bridge-contract-input.mjs +1 -1
  40. package/dist-lib/bridge-contract.cjs +1 -1
  41. package/dist-lib/bridge-contract.mjs +1 -1
  42. package/dist-lib/capture-contract.cjs +1 -1
  43. package/dist-lib/capture-contract.mjs +1 -1
  44. package/dist-lib/cli-constants.cjs +1 -1
  45. package/dist-lib/cloud-contract.cjs +1 -1
  46. package/dist-lib/cloud-contract.mjs +1 -1
  47. package/dist-lib/cloud-test-args.cjs +17 -5
  48. package/dist-lib/cloud-test-args.mjs +17 -5
  49. package/dist-lib/cloud.cjs +1 -1
  50. package/dist-lib/cloud.mjs +1 -1
  51. package/dist-lib/config.cjs +1 -1
  52. package/dist-lib/detox/index.cjs +2 -7
  53. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  54. package/dist-lib/home-paths.cjs +13 -5
  55. package/dist-lib/host/bridge-host.cjs +157 -113
  56. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  57. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  58. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  59. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  60. package/dist-lib/host/websocket-proxy.cjs +1 -1
  61. package/dist-lib/index.cjs +11 -2
  62. package/dist-lib/jump-to-source-babel.cjs +1 -1
  63. package/dist-lib/jump-to-source-native.cjs +1 -1
  64. package/dist-lib/kotlin-host.cjs +1 -1
  65. package/dist-lib/menu.cjs +11 -1
  66. package/dist-lib/menu.mjs +11 -1
  67. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  68. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  69. package/dist-lib/metro-production-bundle.cjs +1 -1
  70. package/dist-lib/metro-production-bundle.mjs +1 -1
  71. package/dist-lib/metro.cjs +1 -1
  72. package/dist-lib/native-app-identity.cjs +1 -1
  73. package/dist-lib/native-app-identity.mjs +1 -1
  74. package/dist-lib/profiles.cjs +1 -1
  75. package/dist-lib/public-brand.cjs +1 -1
  76. package/dist-lib/react-native-host-modules.cjs +1 -1
  77. package/dist-lib/react-native-host-modules.mjs +1 -1
  78. package/dist-lib/render-mode.cjs +1 -1
  79. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  80. package/dist-lib/sdk.cjs +3 -1
  81. package/dist-lib/sdk.mjs +2 -1
  82. package/dist-lib/skills.cjs +700 -329
  83. package/dist-lib/swift-compile.cjs +1 -1
  84. package/dist-lib/swift-compile.mjs +1 -1
  85. package/dist-lib/swift-package.cjs +1 -1
  86. package/dist-lib/swift-package.mjs +1 -1
  87. package/dist-lib/swift-submit.cjs +1 -1
  88. package/dist-lib/swift-submit.mjs +1 -1
  89. package/dist-lib/swift.cjs +443 -173
  90. package/dist-lib/vite-swift.cjs +1 -1
  91. package/dist-lib/vite.cjs +1 -1
  92. package/package.json +2 -1
  93. package/skills/contrast/SKILL.md +1 -0
  94. package/src/cloud-test-args.ts +18 -6
  95. package/src/home-paths.ts +19 -9
  96. package/src/menu.ts +11 -0
@@ -1,11 +1,21 @@
1
1
  // project detection — finds project root, detects framework, scans monorepo workspaces
2
2
 
3
- import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
4
- import { basename, dirname, join } from 'path'
3
+ import {
4
+ existsSync,
5
+ lstatSync,
6
+ opendirSync,
7
+ readFileSync,
8
+ readdirSync,
9
+ statSync,
10
+ } from 'node:fs'
11
+ import { basename, dirname, join } from 'node:path'
12
+ import JSON5 from 'json5'
13
+ import { parse as parseYaml } from 'yaml'
14
+ import type { DependencyVersion, ProjectManifest } from './projectReportContract'
5
15
 
6
16
  export type Framework = 'one' | 'expo' | 'bare' | 'unknown'
7
17
  export type PackageManager = 'bun' | 'pnpm' | 'npm' | 'yarn'
8
- export type MonorepoType = 'turbo' | 'pnpm' | 'nx' | 'npm-workspaces' | 'single'
18
+ export type MonorepoType = 'turbo' | 'pnpm' | 'nx' | 'lerna' | 'npm-workspaces' | 'single'
9
19
 
10
20
  export interface DetectedProject {
11
21
  dir: string
@@ -25,7 +35,8 @@ export interface DetectedWorkspace {
25
35
 
26
36
  const CONFIG_EXTENSIONS = ['ts', 'js', 'cjs', 'mjs', 'mts', 'cts']
27
37
 
28
- export function findProjectRoot(cwd: string): string {
38
+ export function findProjectRoot(cwd: string, includeNativeRepository = false): string {
39
+ let swiftRoot: string | undefined
29
40
  let dir = cwd
30
41
  while (true) {
31
42
  if (
@@ -37,22 +48,30 @@ export function findProjectRoot(cwd: string): string {
37
48
  return dir
38
49
  }
39
50
 
40
- const pkgPath = join(dir, 'package.json')
41
- if (existsSync(pkgPath)) {
42
- try {
43
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
44
- if (pkg.workspaces) return dir
45
- } catch {}
46
- }
51
+ if (metadataValue(readProjectJson(join(dir, 'package.json')), 'workspaces'))
52
+ return dir
47
53
 
54
+ if (includeNativeRepository) {
55
+ if (existsSync(join(dir, '.git'))) return dir
56
+ if (!swiftRoot && existsSync(join(dir, 'Package.swift'))) swiftRoot = dir
57
+ }
48
58
  const parent = dirname(dir)
49
59
  if (parent === dir) break
50
60
  dir = parent
51
61
  }
52
- return cwd
62
+ return swiftRoot ?? cwd
53
63
  }
54
64
 
55
65
  export function detectPackageManager(root: string): PackageManager {
66
+ const manager = metadataValue(
67
+ readProjectJson(join(root, 'package.json')),
68
+ 'packageManager',
69
+ )
70
+ if (typeof manager === 'string') {
71
+ const name = manager.split('@')[0]
72
+ if (name === 'bun' || name === 'pnpm' || name === 'npm' || name === 'yarn')
73
+ return name
74
+ }
56
75
  if (existsSync(join(root, 'bun.lock')) || existsSync(join(root, 'bun.lockb'))) {
57
76
  return 'bun'
58
77
  }
@@ -66,13 +85,9 @@ export function detectMonorepoType(root: string): MonorepoType {
66
85
  if (existsSync(join(root, 'nx.json'))) return 'nx'
67
86
  if (existsSync(join(root, 'pnpm-workspace.yaml'))) return 'pnpm'
68
87
 
69
- const pkgPath = join(root, 'package.json')
70
- if (existsSync(pkgPath)) {
71
- try {
72
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
73
- if (pkg.workspaces) return 'npm-workspaces'
74
- } catch {}
75
- }
88
+ if (existsSync(join(root, 'lerna.json'))) return 'lerna'
89
+ if (metadataValue(readProjectJson(join(root, 'package.json')), 'workspaces'))
90
+ return 'npm-workspaces'
76
91
  return 'single'
77
92
  }
78
93
 
@@ -87,14 +102,14 @@ export function detectProject(dir: string): DetectedProject | null {
87
102
  const pkgPath = join(dir, 'package.json')
88
103
  if (!existsSync(pkgPath)) return null
89
104
 
90
- let pkg: any
91
- try {
92
- pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
93
- } catch {
94
- return null
95
- }
105
+ const pkg = readProjectManifest(dir)
106
+ if (!pkg) return null
96
107
 
97
108
  const deps = { ...pkg.dependencies, ...pkg.devDependencies }
109
+ const devCommand = metadataValue(
110
+ metadataValue(readProjectJson(pkgPath), 'scripts'),
111
+ 'dev',
112
+ )
98
113
  const framework = detectFramework(deps)
99
114
 
100
115
  if (framework === 'unknown') return null
@@ -112,7 +127,7 @@ export function detectProject(dir: string): DetectedProject | null {
112
127
  hasMetroConfig: CONFIG_EXTENSIONS.some((ext) =>
113
128
  existsSync(join(dir, `metro.config.${ext}`)),
114
129
  ),
115
- devCommand: pkg.scripts?.dev || null,
130
+ devCommand: typeof devCommand === 'string' ? devCommand : null,
116
131
  }
117
132
  }
118
133
 
@@ -199,3 +214,251 @@ export function scanWorkspace(root: string): DetectedWorkspace {
199
214
 
200
215
  return { root, type, packageManager, apps }
201
216
  }
217
+
218
+ // reports inspect metadata without evaluating app config or following directory
219
+ // symlinks. this also finds native projects outside package-manager workspaces.
220
+ const REPORT_SKIP_DIRS = new Set([
221
+ 'node_modules',
222
+ 'Pods',
223
+ 'Carthage',
224
+ 'build',
225
+ 'dist',
226
+ 'dist-lib',
227
+ 'DerivedData',
228
+ 'vendor',
229
+ 'coverage',
230
+ 'target',
231
+ 'tmp',
232
+ 'cache',
233
+ 'artifacts',
234
+ 'test-results',
235
+ 'playwright-report',
236
+ ])
237
+
238
+ export function scanRepository(root: string): {
239
+ directories: string[]
240
+ truncated: boolean
241
+ unreadable: number
242
+ } {
243
+ const directories: string[] = []
244
+ const pending = [{ dir: root, depth: 0 }]
245
+ let entries = 0
246
+ let cursor = 0
247
+ let truncated = false
248
+ let unreadable = 0
249
+ while (cursor < pending.length && entries < 200_000 && directories.length < 10_000) {
250
+ const next = pending[cursor++]
251
+ if (!next) break
252
+ directories.push(next.dir)
253
+ let handle: ReturnType<typeof opendirSync>
254
+ try {
255
+ handle = opendirSync(next.dir)
256
+ } catch {
257
+ unreadable++
258
+ continue
259
+ }
260
+ try {
261
+ let entry
262
+ while (entries < 200_000 && (entry = handle.readSync())) {
263
+ entries++
264
+ if (
265
+ !entry.isDirectory() ||
266
+ entry.name.startsWith('.') ||
267
+ REPORT_SKIP_DIRS.has(entry.name)
268
+ )
269
+ continue
270
+ if (next.depth >= 12) {
271
+ truncated = true
272
+ continue
273
+ }
274
+ pending.push({ dir: join(next.dir, entry.name), depth: next.depth + 1 })
275
+ }
276
+ } finally {
277
+ handle.closeSync()
278
+ }
279
+ }
280
+ return {
281
+ directories: directories.sort(),
282
+ truncated: truncated || cursor < pending.length || entries >= 200_000,
283
+ unreadable,
284
+ }
285
+ }
286
+
287
+ // inspect only regular, bounded metadata files. errors are data, never a reason
288
+ // to execute project code. symlinks may point outside the checkout.
289
+ export function readProjectMetadata(path: string): string | null {
290
+ try {
291
+ const info = lstatSync(path)
292
+ if (!info.isFile() || info.size > 4 * 1024 * 1024) return null
293
+ return readFileSync(path, 'utf8')
294
+ } catch {
295
+ return null
296
+ }
297
+ }
298
+
299
+ export function metadataValue(value: unknown, key: string): unknown {
300
+ if (!value || typeof value !== 'object') return undefined
301
+ return Object.getOwnPropertyDescriptor(value, key)?.value
302
+ }
303
+
304
+ export function readProjectJson(path: string): unknown {
305
+ const text = readProjectMetadata(path)
306
+ if (text === null) return null
307
+ try {
308
+ const value: unknown = JSON.parse(text)
309
+ return value
310
+ } catch {
311
+ return null
312
+ }
313
+ }
314
+
315
+ export function readProjectManifest(dir: string): ProjectManifest | null {
316
+ const parsed = readProjectJson(join(dir, 'package.json'))
317
+ if (!parsed || typeof parsed !== 'object') return null
318
+ const record = (value: unknown): Record<string, string> => {
319
+ if (!value || typeof value !== 'object') return {}
320
+ return Object.fromEntries(
321
+ Object.entries(value).filter(
322
+ (entry): entry is [string, string] =>
323
+ typeof entry[1] === 'string' &&
324
+ /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i.test(entry[0]) &&
325
+ entry[0] !== '.' &&
326
+ entry[0] !== '..',
327
+ ),
328
+ )
329
+ }
330
+ const name = metadataValue(parsed, 'name')
331
+ return {
332
+ ...(typeof name === 'string' ? { name } : {}),
333
+ dependencies: record(metadataValue(parsed, 'dependencies')),
334
+ devDependencies: record(metadataValue(parsed, 'devDependencies')),
335
+ }
336
+ }
337
+
338
+ // resolve each dependency at its nearest installation/lockfile boundary, so an
339
+ // uninstalled nested project never inherits another app's hoisted version.
340
+ export function resolveManifestVersions(
341
+ dir: string,
342
+ manifest: ProjectManifest,
343
+ locks = new Map<string, Map<string, string>>(),
344
+ ): Record<string, DependencyVersion> {
345
+ const resolveVersion = (name: string, declared: string): DependencyVersion => {
346
+ if (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i.test(name))
347
+ return { version: declared, source: 'declared' }
348
+ let current = dir
349
+ while (true) {
350
+ // installed packages commonly use symlinks; read just this known manifest,
351
+ // never traverse their dependency trees.
352
+ try {
353
+ const file = join(current, 'node_modules', name, 'package.json')
354
+ if (statSync(file).size <= 256 * 1024) {
355
+ const value: unknown = JSON.parse(readFileSync(file, 'utf8'))
356
+ const version = metadataValue(value, 'version')
357
+ if (typeof version === 'string') return { version, source: 'installed' }
358
+ }
359
+ } catch {}
360
+ let lock = locks.get(current)
361
+ if (!lock) {
362
+ lock = readLockedVersions(current)
363
+ locks.set(current, lock)
364
+ }
365
+ const version = lock.get(name)
366
+ if (version) return { version, source: 'lockfile' }
367
+ if (
368
+ [
369
+ 'bun.lock',
370
+ 'bun.lockb',
371
+ 'package-lock.json',
372
+ 'pnpm-lock.yaml',
373
+ 'yarn.lock',
374
+ ].some((file) => existsSync(join(current, file)))
375
+ )
376
+ break
377
+ const parent = dirname(current)
378
+ if (parent === current) break
379
+ current = parent
380
+ }
381
+ return { version: declared, source: 'declared' }
382
+ }
383
+ return Object.fromEntries(
384
+ Object.entries({ ...manifest.dependencies, ...manifest.devDependencies }).map(
385
+ ([name, declared]) => [name, resolveVersion(name, declared)],
386
+ ),
387
+ )
388
+ }
389
+
390
+ function readLockedVersions(dir: string): Map<string, string> {
391
+ const versions = new Map<string, string>()
392
+ const put = (name: string, value: unknown) => {
393
+ if (typeof value !== 'string') return
394
+ // multiple resolutions without an installed tree are ambiguous; report the
395
+ // declared range rather than choosing a transitive package's version.
396
+ const before = versions.get(name)
397
+ versions.set(name, before === undefined || before === value ? value : '')
398
+ }
399
+ const bun = readProjectMetadata(join(dir, 'bun.lock'))
400
+ if (bun) {
401
+ try {
402
+ const rootVersions = new Map<string, string>()
403
+ const parsed: unknown = JSON5.parse(bun)
404
+ const packages = metadataValue(parsed, 'packages')
405
+ if (packages && typeof packages === 'object') {
406
+ for (const [key, tuple] of Object.entries(packages)) {
407
+ if (!Array.isArray(tuple) || typeof tuple[0] !== 'string') continue
408
+ const match = /^((?:@[^/]+\/)?[^@]+)@([\d][\w.+-]*)$/.exec(tuple[0])
409
+ if (match) put(match[1], match[2])
410
+ // bun's root resolution wins over nested duplicate package keys.
411
+ if (match && key === match[1]) rootVersions.set(match[1], match[2])
412
+ }
413
+ }
414
+ for (const [name, version] of rootVersions) versions.set(name, version)
415
+ } catch {}
416
+ }
417
+ const npm = metadataValue(readProjectJson(join(dir, 'package-lock.json')), 'packages')
418
+ if (npm && typeof npm === 'object') {
419
+ for (const [key, value] of Object.entries(npm)) {
420
+ if (key.startsWith('node_modules/') && !key.slice(13).includes('/node_modules/'))
421
+ put(key.slice(13), metadataValue(value, 'version'))
422
+ }
423
+ }
424
+ const pnpm = readProjectMetadata(join(dir, 'pnpm-lock.yaml'))
425
+ if (pnpm) {
426
+ try {
427
+ const parsed: unknown = parseYaml(pnpm)
428
+ const packages = metadataValue(parsed, 'packages')
429
+ if (packages && typeof packages === 'object')
430
+ for (const key of Object.keys(packages)) {
431
+ const match = /^\/?((?:@[^/]+\/)?[^@]+)@([\d][\w.+-]*)/.exec(key)
432
+ if (match) put(match[1], match[2])
433
+ }
434
+ } catch {}
435
+ }
436
+ const yarn = readProjectMetadata(join(dir, 'yarn.lock'))
437
+ if (yarn) {
438
+ let names: string[] = []
439
+ for (const line of yarn.split('\n')) {
440
+ if (/^\S.*:\s*$/.test(line))
441
+ names = [...line.matchAll(/(?:^|[", ]+)((?:@[^/\s]+\/)?[^@" ,]+)@/g)].map(
442
+ (match) => match[1],
443
+ )
444
+ const version = /^\s+version[ :]+["']?([\d][\w.+-]*)/.exec(line)
445
+ if (version) for (const name of names) put(name, version[1])
446
+ }
447
+ }
448
+ return versions
449
+ }
450
+
451
+ export function installedManifest(
452
+ dir: string,
453
+ manifest: ProjectManifest,
454
+ ): ProjectManifest {
455
+ const versions = resolveManifestVersions(dir, manifest)
456
+ const mapped = (deps: Record<string, string> | undefined) =>
457
+ deps &&
458
+ Object.fromEntries(Object.keys(deps).map((name) => [name, versions[name].version]))
459
+ return {
460
+ name: manifest.name,
461
+ dependencies: mapped(manifest.dependencies),
462
+ devDependencies: mapped(manifest.devDependencies),
463
+ }
464
+ }
@@ -1287,7 +1287,7 @@ export interface ConsoleEntry {
1287
1287
  // One root error boundary in the tenant worker) can be live and screaming in
1288
1288
  // the driver's console while the agent's `sootsim_errors` reports nothing.
1289
1289
  // Merge both, dedup, so worker + root-error-boundary failures always surface.
1290
- function consoleEntriesEval(level: 'error' | 'warn', limit: number): string {
1290
+ export function consoleEntriesEval(level: 'error' | 'warn', limit: number): string {
1291
1291
  const getter = level === 'error' ? 'getErrors' : 'getWarnings'
1292
1292
  return `(() => {
1293
1293
  const out = []
@@ -20,6 +20,8 @@ import { shouldPrintInspectNotice } from '../inspect-notice-state'
20
20
  import { levenshtein } from '../levenshtein'
21
21
  import { TOP_LEVEL_RUNTIME_COMMANDS } from '../parse-args'
22
22
  import { rethrowIfExit, rnxExit } from '../run-rnx'
23
+ import { collectRuntimeDiagnostics } from '../runtime-diagnostics'
24
+ import { trackCliError } from '../telemetry'
23
25
  import {
24
26
  type BridgeTransportOption,
25
27
  callInBridge,
@@ -4639,6 +4641,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
4639
4641
  }
4640
4642
  } catch (err: any) {
4641
4643
  rethrowIfExit(err)
4644
+ trackCliError(err)
4642
4645
  const message = err instanceof Error ? err.message : String(err)
4643
4646
  console.error(` ${subcommand ?? 'peach'} failed: ${message}`)
4644
4647
  const unknownSim = /^no sim connected with id ([^;]+)(?:; connected sims: .+)?$/.exec(
@@ -4704,6 +4707,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
4704
4707
  }
4705
4708
  rnxExit(1)
4706
4709
  } finally {
4710
+ if (subcommand !== 'list') await collectRuntimeDiagnostics(bridge)
4707
4711
  bridge.close()
4708
4712
  }
4709
4713
  }
@@ -182,7 +182,11 @@ export function applyEnvFlags(args: string[]): string[] {
182
182
  const out: string[] = []
183
183
  for (let i = 0; i < args.length; i++) {
184
184
  const arg = args[i] as string
185
- const inline = arg.startsWith('--env=') ? arg.slice('--env='.length) : null
185
+ const inline = arg.startsWith('--env=')
186
+ ? arg.slice('--env='.length)
187
+ : arg.startsWith('-e=')
188
+ ? arg.slice('-e='.length)
189
+ : null
186
190
  if (arg === '--env' || arg === '-e' || inline !== null) {
187
191
  const kv = inline ?? args[i + 1]
188
192
  if (!kv || !kv.includes('=')) {
@@ -244,7 +248,9 @@ const MAESTRO_COMPAT_VALUE_FLAGS = new Set([
244
248
  '--screen-size',
245
249
  ])
246
250
 
247
- function splitTagList(value: string): string[] {
251
+ // shared with the cloud submitter, which resolves tag selection before any
252
+ // hosted job exists the same way a local workspace run does.
253
+ export function splitTagList(value: string): string[] {
248
254
  return value
249
255
  .split(',')
250
256
  .map((tag) => tag.trim())