@retailcrm/embed-ui 0.9.11 → 0.9.13

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/AGENTS.md CHANGED
@@ -105,4 +105,6 @@ yarn workspace @retailcrm/embed-ui-v1-components run storybook:build
105
105
 
106
106
  ## Notes
107
107
  - Do not assume legacy rules from other repositories (especially `omnica`) apply here.
108
+ - Keep imports grouped by type-only, external, internal alias, and relative blocks, separated from each other and alphabetized within each block: this improves scanability and reduces merge conflicts when multiple PRs add imports to the same file.
109
+ - When resolving lint issues, prefer running `eslint` with `--fix` first to avoid manual import reshuffling and unnecessary reading of repository-specific lint rules.
108
110
  - If repository policy is unclear, ask a short clarifying question before making irreversible actions.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,28 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## [0.9.13](https://github.com/retailcrm/embed-ui/compare/v0.9.12...v0.9.13) (2026-03-16)
5
+
6
+ ### Features
7
+
8
+ * Host query contract was added ([b2bfc48](https://github.com/retailcrm/embed-ui/commit/b2bfc486f667bb97efd8d47662e32dfd39d93119))
9
+ * **v1-components:** UiTable stories and tooling were added ([18c89ba](https://github.com/retailcrm/embed-ui/commit/18c89bac8c6481c889e0e0071cd140a6b74520c9))
10
+
11
+ ### Bug Fixes
12
+
13
+ * **v1-components:** UiTag attrs forwarding was corrected ([e3f653b](https://github.com/retailcrm/embed-ui/commit/e3f653bac555121d06d03f1bc2832296973c8aa9))
14
+ ## [0.9.12](https://github.com/retailcrm/embed-ui/compare/v0.9.11...v0.9.12) (2026-03-11)
15
+
16
+ ### Features
17
+
18
+ * Recursive updater scan and add mode were added ([72725e9](https://github.com/retailcrm/embed-ui/commit/72725e989fbfd8f5192ff226401ee7ad5501ad3e))
19
+ * **v1-components:** Remote component method delegates were typed ([4bfcd4c](https://github.com/retailcrm/embed-ui/commit/4bfcd4c7964f342f581701390a370e504ef213e4))
20
+ * **v1-components:** Vue remote tooling was applied to UiSelect ([2be366c](https://github.com/retailcrm/embed-ui/commit/2be366cec6a9dba32ff0ddb24b0716d6498880a0))
21
+
22
+ ### Bug Fixes
23
+
24
+ * **v1-components:** Test lint violations were corrected ([8c0968c](https://github.com/retailcrm/embed-ui/commit/8c0968ccfaaacd37538bb76e2382f253ba20287e))
25
+ * **v1-components:** UiSelect remote tests were aligned with remote tooling ([5c76150](https://github.com/retailcrm/embed-ui/commit/5c7615085c01e6c198527725c54f6f52d4764379))
4
26
  ## [0.9.11](https://github.com/retailcrm/embed-ui/compare/v0.9.11-alpha.7...v0.9.11) (2026-03-11)
5
27
  ## [0.9.11-alpha.7](https://github.com/retailcrm/embed-ui/compare/v0.9.11-alpha.6...v0.9.11-alpha.7) (2026-03-06)
6
28
 
package/Makefile CHANGED
@@ -30,6 +30,33 @@ build: .require-compose ## [Build][docker][heavy] Builds all workspaces
30
30
  $(YARN) workspaces foreach -A --topological-dev run build
31
31
  $(TARGET_OK)
32
32
 
33
+ .PHONY: storybook.build
34
+ storybook.build: .require-compose ## [Build][docker][heavy] Builds Storybook for v1-components
35
+ $(TARGET_HEADER)
36
+ $(YARN) workspace @retailcrm/embed-ui-v1-components run storybook:build
37
+ $(TARGET_OK)
38
+
39
+ .PHONY: storybook.serve
40
+ storybook.serve: .require-compose ## [Build][docker] Runs Storybook for v1-components
41
+ $(TARGET_HEADER)
42
+ $(COMPOSE) up v1-components
43
+
44
+ .PHONY: storybook.shot
45
+ storybook.shot: .require-compose ## [Research][docker] Captures a Storybook screenshot for v1-components docs/story page
46
+ $(TARGET_HEADER)
47
+ @$(COMPOSE) up -d v1-components
48
+ @UID=$$(id -u) GID=$$(id -g) $(COMPOSE) run --rm playwright \
49
+ yarn workspace @retailcrm/embed-ui-v1-components run storybook:shot \
50
+ --base-url http://v1-components:6006 \
51
+ --path "$(if $(story_path),$(story_path),/iframe.html?viewMode=docs&id=components-uitable--docs)" \
52
+ --output "$(if $(output),$(output),artifacts/storybook/UiTable.docs.png)" \
53
+ --wait-for-selector "$(if $(wait_for),$(wait_for),#storybook-docs)" \
54
+ --settle-ms "$(if $(settle_ms),$(settle_ms),2500)" \
55
+ --timeout-ms "$(if $(timeout_ms),$(timeout_ms),60000)" \
56
+ --viewport-width "$(if $(viewport_width),$(viewport_width),1600)" \
57
+ --viewport-height "$(if $(viewport_height),$(viewport_height),1600)"
58
+ $(TARGET_OK)
59
+
33
60
  .PHONY: prepare
34
61
  prepare: .require-compose ## [Build][docker][heavy] Runs prepare in all workspaces
35
62
  $(TARGET_HEADER)
package/README.md CHANGED
@@ -8,7 +8,8 @@ API и компоненты для создания расширений инт
8
8
 
9
9
  ## Обновление версий в целевом проекте
10
10
 
11
- Можно запустить бинарник через `npx`, чтобы обновить пакеты `@retailcrm/embed-ui*` в `package.json` целевого проекта
11
+ Можно запустить бинарник через `npx`, чтобы обновить пакеты `@retailcrm/embed-ui*` во всех `package.json`
12
+ текущего рабочего дерева или выбранного поддерева
12
13
  (`dependencies`, `devDependencies`, `peerDependencies`, `optionalDependencies`).
13
14
 
14
15
  ```bash
@@ -19,6 +20,16 @@ npx @retailcrm/embed-ui --target ./my-project --dry-run
19
20
  ```
20
21
 
21
22
  По умолчанию используется последняя версия из npm. Флаг `--exact` переключает формат обновления на точную версию.
23
+ CLI сохраняет исходные отступы и переводы строк в каждом изменяемом `package.json`.
24
+
25
+ Для точечного добавления пакетов только в один `package.json` есть флаг `--add`.
26
+ Если не передать `--packages`, CLI откроет интерактивный режим с выбором пакетов и кратким описанием.
27
+
28
+ ```bash
29
+ npx @retailcrm/embed-ui --add
30
+ npx @retailcrm/embed-ui --add --packages components,contexts
31
+ npx @retailcrm/embed-ui --add --target ./my-project --version 0.9.11
32
+ ```
22
33
 
23
34
  ## Цели встраивания
24
35
 
@@ -1,26 +1,82 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { createInterface } from 'node:readline/promises'
3
4
  import { execFileSync } from 'node:child_process'
4
5
  import fs from 'node:fs'
5
6
  import path from 'node:path'
7
+ import { pathToFileURL } from 'node:url'
6
8
  import process from 'node:process'
7
9
 
8
- const ROOT_PACKAGE = '@retailcrm/embed-ui'
9
- const TARGET_SECTIONS = [
10
+ export const ROOT_PACKAGE = '@retailcrm/embed-ui'
11
+ export const TARGET_SECTIONS = [
10
12
  'dependencies',
11
13
  'devDependencies',
12
14
  'peerDependencies',
13
15
  'optionalDependencies',
14
16
  ]
15
17
 
18
+ export const INSTALLABLE_PACKAGES = [
19
+ {
20
+ id: 'embed-ui',
21
+ name: ROOT_PACKAGE,
22
+ section: 'dependencies',
23
+ description: 'Базовый пакет с общим API и согласованными v1-зависимостями.',
24
+ },
25
+ {
26
+ id: 'components',
27
+ name: '@retailcrm/embed-ui-v1-components',
28
+ section: 'dependencies',
29
+ description: 'UI-компоненты для host/remote приложений.',
30
+ },
31
+ {
32
+ id: 'contexts',
33
+ name: '@retailcrm/embed-ui-v1-contexts',
34
+ section: 'dependencies',
35
+ description: 'Реактивные контексты RetailCRM JS API.',
36
+ },
37
+ {
38
+ id: 'types',
39
+ name: '@retailcrm/embed-ui-v1-types',
40
+ section: 'dependencies',
41
+ description: 'Базовые type declarations для RetailCRM JS API.',
42
+ },
43
+ {
44
+ id: 'testing',
45
+ name: '@retailcrm/embed-ui-v1-testing',
46
+ section: 'devDependencies',
47
+ description: 'Вспомогательные утилиты и типы для тестов интеграций.',
48
+ },
49
+ {
50
+ id: 'endpoint',
51
+ name: '@retailcrm/embed-ui-v1-endpoint',
52
+ section: 'dependencies',
53
+ description: 'Endpoint API для интеграций в RetailCRM.',
54
+ },
55
+ ]
56
+
57
+ const DEFAULT_INDENT = ' '
58
+ const DEFAULT_NEWLINE = '\n'
59
+ const SKIP_DIRECTORIES = new Set([
60
+ '.git',
61
+ '.hg',
62
+ '.svn',
63
+ '.yarn',
64
+ 'node_modules',
65
+ 'dist',
66
+ 'build',
67
+ 'coverage',
68
+ ])
69
+
16
70
  const HELP_TEXT = `Usage:
17
71
  npx @retailcrm/embed-ui [target] [version] [options]
18
72
 
19
73
  Options:
20
- -t, --target <path> Target project path (default: current directory)
74
+ -t, --target <path> Target path (default: current directory)
21
75
  -v, --version <ver> Target version. If omitted, latest npm version is used
22
76
  --exact Use exact version instead of range
23
77
  --dry-run Show changes without writing package.json
78
+ --add Add selected embed-ui packages into one package.json
79
+ --packages <list> Comma-separated package ids or names for --add
24
80
  -h, --help Show this help
25
81
 
26
82
  Examples:
@@ -28,17 +84,27 @@ Examples:
28
84
  npx @retailcrm/embed-ui --version 0.9.11
29
85
  npx @retailcrm/embed-ui ./my-project 0.9.11
30
86
  npx @retailcrm/embed-ui --target ./my-project --dry-run
87
+ npx @retailcrm/embed-ui --add
88
+ npx @retailcrm/embed-ui --add --packages components,contexts
31
89
  `
32
90
 
33
91
  const isSemverLike = (value) => /^v?\d+\.\d+\.\d+/.test(value)
34
92
  const stripLeadingV = (value) => value.replace(/^v/, '')
35
93
 
36
- const parseArgs = (argv) => {
94
+ const parsePackageList = (value) =>
95
+ value
96
+ .split(',')
97
+ .map((entry) => entry.trim())
98
+ .filter(Boolean)
99
+
100
+ export const parseArgs = (argv) => {
37
101
  const options = {
38
102
  target: process.cwd(),
39
103
  version: null,
40
104
  dryRun: false,
41
105
  exact: false,
106
+ add: false,
107
+ packages: null,
42
108
  }
43
109
 
44
110
  const positionals = []
@@ -73,6 +139,17 @@ const parseArgs = (argv) => {
73
139
  continue
74
140
  }
75
141
 
142
+ if (argument === '--packages') {
143
+ const value = argv[index + 1]
144
+ if (!value) {
145
+ throw new Error('Option --packages requires a value')
146
+ }
147
+
148
+ options.packages = parsePackageList(value)
149
+ index++
150
+ continue
151
+ }
152
+
76
153
  if (argument === '--dry-run') {
77
154
  options.dryRun = true
78
155
  continue
@@ -83,6 +160,11 @@ const parseArgs = (argv) => {
83
160
  continue
84
161
  }
85
162
 
163
+ if (argument === '--add') {
164
+ options.add = true
165
+ continue
166
+ }
167
+
86
168
  if (argument.startsWith('-')) {
87
169
  throw new Error(`Unknown option: ${argument}`)
88
170
  }
@@ -111,10 +193,14 @@ const parseArgs = (argv) => {
111
193
  options.version = stripLeadingV(positionals[1])
112
194
  }
113
195
 
196
+ if (options.packages && !options.add) {
197
+ throw new Error('Option --packages can only be used together with --add')
198
+ }
199
+
114
200
  return options
115
201
  }
116
202
 
117
- const resolveLatestVersion = () => {
203
+ export const resolveLatestVersion = () => {
118
204
  const output = execFileSync(
119
205
  'npm',
120
206
  ['view', ROOT_PACKAGE, 'version'],
@@ -131,10 +217,12 @@ const resolveLatestVersion = () => {
131
217
  return output
132
218
  }
133
219
 
134
- const isTargetPackage = (name) =>
220
+ export const isTargetPackage = (name) =>
135
221
  name === ROOT_PACKAGE || name.startsWith(`${ROOT_PACKAGE}-`)
136
222
 
137
- const formatRange = (currentRange, nextVersion, exact) => {
223
+ const createRange = (version, exact) => exact ? version : `^${version}`
224
+
225
+ export const formatRange = (currentRange, nextVersion, exact) => {
138
226
  if (exact) {
139
227
  return nextVersion
140
228
  }
@@ -154,18 +242,107 @@ const formatRange = (currentRange, nextVersion, exact) => {
154
242
  return `^${nextVersion}`
155
243
  }
156
244
 
157
- const main = () => {
158
- const options = parseArgs(process.argv.slice(2))
159
- const version = options.version ?? resolveLatestVersion()
245
+ export const detectFormatting = (source) => {
246
+ const newline = source.includes('\r\n') ? '\r\n' : DEFAULT_NEWLINE
247
+ const indentMatch = source.match(/\n([ \t]+)"/)
248
+
249
+ return {
250
+ indent: indentMatch?.[1] ?? DEFAULT_INDENT,
251
+ newline,
252
+ trailingNewline: source.endsWith('\n') || source.endsWith('\r\n'),
253
+ }
254
+ }
255
+
256
+ export const serializePackageJson = (packageJson, formatting) => {
257
+ const serialized = JSON.stringify(packageJson, null, formatting.indent)
258
+ .replace(/\n/g, formatting.newline)
259
+
260
+ return formatting.trailingNewline
261
+ ? `${serialized}${formatting.newline}`
262
+ : serialized
263
+ }
264
+
265
+ const ensureDirectoryExists = (targetPath) => {
266
+ if (!fs.existsSync(targetPath)) {
267
+ throw new Error(`Path not found: ${targetPath}`)
268
+ }
269
+
270
+ const stat = fs.statSync(targetPath)
271
+ if (!stat.isDirectory()) {
272
+ throw new Error(`Target is not a directory: ${targetPath}`)
273
+ }
274
+ }
275
+
276
+ export const resolvePackageJsonPath = (targetPath) => {
277
+ if (path.basename(targetPath) === 'package.json') {
278
+ if (!fs.existsSync(targetPath)) {
279
+ throw new Error(`package.json not found: ${targetPath}`)
280
+ }
281
+
282
+ return targetPath
283
+ }
284
+
285
+ const packageJsonPath = path.resolve(targetPath, 'package.json')
160
286
 
161
- const packageJsonPath = path.resolve(options.target, 'package.json')
162
287
  if (!fs.existsSync(packageJsonPath)) {
163
288
  throw new Error(`package.json not found: ${packageJsonPath}`)
164
289
  }
165
290
 
166
- const source = fs.readFileSync(packageJsonPath, 'utf8')
167
- const packageJson = JSON.parse(source)
291
+ return packageJsonPath
292
+ }
293
+
294
+ export const collectPackageJsonPaths = (targetPath) => {
295
+ const resolvedTarget = path.resolve(targetPath)
296
+
297
+ if (!fs.existsSync(resolvedTarget)) {
298
+ throw new Error(`Path not found: ${resolvedTarget}`)
299
+ }
300
+
301
+ if (path.basename(resolvedTarget) === 'package.json') {
302
+ return [resolvedTarget]
303
+ }
168
304
 
305
+ ensureDirectoryExists(resolvedTarget)
306
+
307
+ const packageJsonPaths = []
308
+
309
+ const visit = (directoryPath) => {
310
+ const packageJsonPath = path.join(directoryPath, 'package.json')
311
+ if (fs.existsSync(packageJsonPath) && fs.statSync(packageJsonPath).isFile()) {
312
+ packageJsonPaths.push(packageJsonPath)
313
+ }
314
+
315
+ for (const entry of fs.readdirSync(directoryPath, { withFileTypes: true })) {
316
+ if (!entry.isDirectory() || entry.isSymbolicLink()) {
317
+ continue
318
+ }
319
+
320
+ if (SKIP_DIRECTORIES.has(entry.name)) {
321
+ continue
322
+ }
323
+
324
+ visit(path.join(directoryPath, entry.name))
325
+ }
326
+ }
327
+
328
+ visit(resolvedTarget)
329
+
330
+ return packageJsonPaths.sort()
331
+ }
332
+
333
+ const findDependencySection = (packageJson, packageName) => {
334
+ for (const section of TARGET_SECTIONS) {
335
+ const dependencyMap = packageJson[section]
336
+
337
+ if (dependencyMap && typeof dependencyMap === 'object' && packageName in dependencyMap) {
338
+ return section
339
+ }
340
+ }
341
+
342
+ return null
343
+ }
344
+
345
+ export const updatePackageJson = (packageJson, version, exact) => {
169
346
  const updates = []
170
347
 
171
348
  for (const section of TARGET_SECTIONS) {
@@ -176,21 +353,18 @@ const main = () => {
176
353
  }
177
354
 
178
355
  for (const [name, currentRange] of Object.entries(dependencyMap)) {
179
- if (!isTargetPackage(name)) {
180
- continue
181
- }
182
-
183
- if (typeof currentRange !== 'string') {
356
+ if (!isTargetPackage(name) || typeof currentRange !== 'string') {
184
357
  continue
185
358
  }
186
359
 
187
- const nextRange = formatRange(currentRange, version, options.exact)
360
+ const nextRange = formatRange(currentRange, version, exact)
188
361
  if (nextRange === currentRange) {
189
362
  continue
190
363
  }
191
364
 
192
365
  dependencyMap[name] = nextRange
193
366
  updates.push({
367
+ type: 'update',
194
368
  section,
195
369
  name,
196
370
  currentRange,
@@ -199,31 +373,245 @@ const main = () => {
199
373
  }
200
374
  }
201
375
 
202
- if (updates.length === 0) {
203
- console.log(`No ${ROOT_PACKAGE}* dependencies found or changed in ${packageJsonPath}`)
376
+ return updates
377
+ }
378
+
379
+ export const resolveInstallPackages = (tokens) => {
380
+ const selectedPackages = []
381
+ const seen = new Set()
382
+
383
+ for (const token of tokens) {
384
+ const normalized = token.trim()
385
+ if (!normalized) {
386
+ continue
387
+ }
388
+
389
+ const numericIndex = Number(normalized)
390
+ const selectedPackage =
391
+ Number.isInteger(numericIndex) && numericIndex >= 1 && numericIndex <= INSTALLABLE_PACKAGES.length
392
+ ? INSTALLABLE_PACKAGES[numericIndex - 1]
393
+ : INSTALLABLE_PACKAGES.find((entry) => entry.id === normalized || entry.name === normalized)
394
+
395
+ if (!selectedPackage) {
396
+ const supported = INSTALLABLE_PACKAGES
397
+ .map((entry, index) => `${index + 1}/${entry.id}/${entry.name}`)
398
+ .join(', ')
399
+
400
+ throw new Error(`Unknown add target "${normalized}". Supported values: ${supported}`)
401
+ }
402
+
403
+ if (seen.has(selectedPackage.name)) {
404
+ continue
405
+ }
406
+
407
+ seen.add(selectedPackage.name)
408
+ selectedPackages.push(selectedPackage)
409
+ }
410
+
411
+ return selectedPackages
412
+ }
413
+
414
+ export const installPackages = (packageJson, packages, version, exact) => {
415
+ const updates = []
416
+
417
+ for (const selectedPackage of packages) {
418
+ const section = findDependencySection(packageJson, selectedPackage.name) ?? selectedPackage.section
419
+ const dependencyMap = packageJson[section] ?? {}
420
+
421
+ if (!(section in packageJson)) {
422
+ packageJson[section] = dependencyMap
423
+ }
424
+
425
+ const currentRange = dependencyMap[selectedPackage.name]
426
+ const nextRange = typeof currentRange === 'string'
427
+ ? formatRange(currentRange, version, exact)
428
+ : createRange(version, exact)
429
+
430
+ if (currentRange === nextRange) {
431
+ continue
432
+ }
433
+
434
+ dependencyMap[selectedPackage.name] = nextRange
435
+ updates.push({
436
+ type: typeof currentRange === 'string' ? 'update' : 'install',
437
+ section,
438
+ name: selectedPackage.name,
439
+ currentRange: typeof currentRange === 'string' ? currentRange : null,
440
+ nextRange,
441
+ })
442
+ }
443
+
444
+ return updates
445
+ }
446
+
447
+ export const promptForInstallSelection = async (packageJson) => {
448
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
449
+ throw new Error('Interactive add mode requires a TTY. Use --packages to select packages explicitly.')
450
+ }
451
+
452
+ console.log('Выберите пакеты для установки в текущий package.json:')
453
+ for (const [index, selectedPackage] of INSTALLABLE_PACKAGES.entries()) {
454
+ const currentSection = findDependencySection(packageJson, selectedPackage.name)
455
+ const installedHint = currentSection ? ` Уже есть в ${currentSection}.` : ''
456
+
457
+ console.log(` ${index + 1}. ${selectedPackage.name} (${selectedPackage.id})`)
458
+ console.log(` ${selectedPackage.description} Раздел по умолчанию: ${selectedPackage.section}.${installedHint}`)
459
+ }
460
+
461
+ const readline = createInterface({
462
+ input: process.stdin,
463
+ output: process.stdout,
464
+ })
465
+
466
+ try {
467
+ while (true) {
468
+ const answer = await readline.question(
469
+ 'Введите номера, ids или имена пакетов через запятую (например: 1,3 или components,types): '
470
+ )
471
+
472
+ const tokens = parsePackageList(answer)
473
+ if (tokens.length === 0) {
474
+ return []
475
+ }
476
+
477
+ try {
478
+ return resolveInstallPackages(tokens)
479
+ } catch (error) {
480
+ const message = error instanceof Error ? error.message : String(error)
481
+ console.error(message)
482
+ }
483
+ }
484
+ } finally {
485
+ readline.close()
486
+ }
487
+ }
488
+
489
+ const readPackageJson = (packageJsonPath) => {
490
+ const source = fs.readFileSync(packageJsonPath, 'utf8')
491
+
492
+ return {
493
+ formatting: detectFormatting(source),
494
+ packageJson: JSON.parse(source),
495
+ }
496
+ }
497
+
498
+ const writePackageJson = (packageJsonPath, packageJson, formatting) => {
499
+ fs.writeFileSync(packageJsonPath, serializePackageJson(packageJson, formatting), 'utf8')
500
+ }
501
+
502
+ const printChanges = (changes) => {
503
+ for (const change of changes) {
504
+ const prefix = change.type === 'install'
505
+ ? `${change.section}: ${change.name} -> ${change.nextRange}`
506
+ : `${change.section}: ${change.name} ${change.currentRange} -> ${change.nextRange}`
507
+
508
+ console.log(` ${prefix}`)
509
+ }
510
+ }
511
+
512
+ export const runUpdate = (options) => {
513
+ const version = options.version ?? resolveLatestVersion()
514
+ const packageJsonPaths = collectPackageJsonPaths(options.target)
515
+ const reports = []
516
+
517
+ for (const packageJsonPath of packageJsonPaths) {
518
+ const { formatting, packageJson } = readPackageJson(packageJsonPath)
519
+ const updates = updatePackageJson(packageJson, version, options.exact)
520
+
521
+ if (updates.length === 0) {
522
+ continue
523
+ }
524
+
525
+ if (!options.dryRun) {
526
+ writePackageJson(packageJsonPath, packageJson, formatting)
527
+ }
528
+
529
+ reports.push({ packageJsonPath, updates })
530
+ }
531
+
532
+ if (reports.length === 0) {
533
+ console.log(`No ${ROOT_PACKAGE}* dependencies found or changed under ${path.resolve(options.target)}`)
204
534
  return
205
535
  }
206
536
 
537
+ const totalUpdates = reports.reduce((sum, report) => sum + report.updates.length, 0)
538
+
207
539
  console.log(`Resolved version: ${version}`)
208
- for (const update of updates) {
209
- console.log(
210
- `${update.section}: ${update.name} ${update.currentRange} -> ${update.nextRange}`
211
- )
540
+ for (const report of reports) {
541
+ console.log(report.packageJsonPath)
542
+ printChanges(report.updates)
212
543
  }
213
544
 
545
+ if (options.dryRun) {
546
+ console.log('Dry run enabled, package.json files were not modified')
547
+ return
548
+ }
549
+
550
+ console.log(
551
+ `Updated ${totalUpdates} dependency entries in ${reports.length} package.json file(s) under ${path.resolve(options.target)}`
552
+ )
553
+ }
554
+
555
+ export const runAdd = async (options) => {
556
+ const version = options.version ?? resolveLatestVersion()
557
+ const packageJsonPath = resolvePackageJsonPath(path.resolve(options.target))
558
+ const { formatting, packageJson } = readPackageJson(packageJsonPath)
559
+ const selectedPackages = options.packages
560
+ ? resolveInstallPackages(options.packages)
561
+ : await promptForInstallSelection(packageJson)
562
+
563
+ if (selectedPackages.length === 0) {
564
+ console.log('Nothing selected, package.json was not modified')
565
+ return
566
+ }
567
+
568
+ const updates = installPackages(packageJson, selectedPackages, version, options.exact)
569
+
570
+ if (updates.length === 0) {
571
+ console.log(`Selected packages are already installed with matching ranges in ${packageJsonPath}`)
572
+ return
573
+ }
574
+
575
+ console.log(`Resolved version: ${version}`)
576
+ console.log(packageJsonPath)
577
+ printChanges(updates)
578
+
214
579
  if (options.dryRun) {
215
580
  console.log('Dry run enabled, package.json was not modified')
216
581
  return
217
582
  }
218
583
 
219
- fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8')
220
- console.log(`Updated ${updates.length} dependency entries in ${packageJsonPath}`)
584
+ writePackageJson(packageJsonPath, packageJson, formatting)
585
+ console.log(`Installed ${updates.length} package entries in ${packageJsonPath}`)
586
+ }
587
+
588
+ export const main = async (argv = process.argv.slice(2)) => {
589
+ const options = parseArgs(argv)
590
+
591
+ if (options.add) {
592
+ await runAdd(options)
593
+ return
594
+ }
595
+
596
+ runUpdate(options)
597
+ }
598
+
599
+ const isExecutedDirectly = () => {
600
+ const entryPath = process.argv[1]
601
+
602
+ if (!entryPath) {
603
+ return false
604
+ }
605
+
606
+ return pathToFileURL(path.resolve(entryPath)).href === import.meta.url
221
607
  }
222
608
 
223
- try {
224
- main()
225
- } catch (error) {
226
- const message = error instanceof Error ? error.message : String(error)
227
- console.error(message)
228
- process.exit(1)
609
+ if (isExecutedDirectly()) {
610
+ try {
611
+ await main()
612
+ } catch (error) {
613
+ const message = error instanceof Error ? error.message : String(error)
614
+ console.error(message)
615
+ process.exit(1)
616
+ }
229
617
  }