@wavyx/pdcli 0.3.0 → 0.4.0
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/CHANGELOG.md +17 -0
- package/README.md +14 -0
- package/oclif.manifest.json +464 -35
- package/package.json +1 -1
- package/src/commands/deal/bulk-update.js +131 -0
- package/src/commands/org/import.js +109 -0
- package/src/commands/person/import.js +118 -0
- package/src/lib/bulk.js +106 -0
- package/src/lib/csv-parse.js +88 -0
- package/src/lib/import.js +49 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to `pdcli` are documented here. Format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); versions follow
|
|
5
5
|
[SemVer](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.4.0] - 2026-06-04
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `deal bulk-update` — update many deals at once by `--ids`, a Pipedrive
|
|
12
|
+
saved `--filter`, or ids piped on stdin. Paced sequentially inside the
|
|
13
|
+
rate-limit burst window, confirms before writing (`--yes` to skip),
|
|
14
|
+
`--dry-run` previews targets, partial failures are listed per deal and
|
|
15
|
+
exit 1.
|
|
16
|
+
- `person import` / `org import` — bulk-create from CSV. Headers map to
|
|
17
|
+
fields, including custom fields by human name with option-label
|
|
18
|
+
resolution; `--dry-run` validates every row without writing.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- CI actions bumped to checkout/setup-node v5.
|
|
23
|
+
|
|
7
24
|
## [0.3.0] - 2026-06-04
|
|
8
25
|
|
|
9
26
|
### Added
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# pdcli
|
|
2
2
|
|
|
3
|
+
[](https://github.com/wavyx/pdcli/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/wavyx/pdcli)
|
|
5
|
+
[](https://www.npmjs.com/package/@wavyx/pdcli)
|
|
6
|
+
|
|
3
7
|
Command-line interface for [Pipedrive](https://www.pipedrive.com/) — fast, scriptable, built for terminals, CI pipelines, and AI agents.
|
|
4
8
|
|
|
5
9
|
> Not affiliated with or endorsed by Pipedrive.
|
|
@@ -55,6 +59,16 @@ pdcli deal create --title "Sized" --field "Deal Size=Large" --field "Score=4.5"
|
|
|
55
59
|
pdcli deal update 42 --body '{"probability":75}' # raw JSON escape hatch
|
|
56
60
|
```
|
|
57
61
|
|
|
62
|
+
## Bulk
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pdcli deal bulk-update --filter 9 --stage 5 # saved filter → stage move
|
|
66
|
+
pdcli deal bulk-update --ids 1,2,3 --status won --yes
|
|
67
|
+
pdcli deal list --status open --jq '.[].id' | pdcli deal bulk-update --owner 42
|
|
68
|
+
pdcli person import people.csv --dry-run # CSV headers map to fields,
|
|
69
|
+
pdcli person import people.csv # custom fields by name
|
|
70
|
+
```
|
|
71
|
+
|
|
58
72
|
## Files, webhooks, backup
|
|
59
73
|
|
|
60
74
|
```bash
|
package/oclif.manifest.json
CHANGED
|
@@ -1724,6 +1724,191 @@
|
|
|
1724
1724
|
"status.js"
|
|
1725
1725
|
]
|
|
1726
1726
|
},
|
|
1727
|
+
"deal:bulk-update": {
|
|
1728
|
+
"aliases": [],
|
|
1729
|
+
"args": {},
|
|
1730
|
+
"description": "Update many deals at once (by --ids, a saved --filter, or ids piped on stdin)",
|
|
1731
|
+
"examples": [
|
|
1732
|
+
"<%= config.bin %> deal bulk-update --ids 1,2,3 --stage 5",
|
|
1733
|
+
"<%= config.bin %> deal bulk-update --filter 9 --status won",
|
|
1734
|
+
"<%= config.bin %> deal list --status open --jq '.[].id' | <%= config.bin %> deal bulk-update --owner 42",
|
|
1735
|
+
"<%= config.bin %> deal bulk-update --filter 9 --stage 5 --dry-run"
|
|
1736
|
+
],
|
|
1737
|
+
"flags": {
|
|
1738
|
+
"output": {
|
|
1739
|
+
"char": "o",
|
|
1740
|
+
"description": "Output format",
|
|
1741
|
+
"helpGroup": "GLOBAL",
|
|
1742
|
+
"name": "output",
|
|
1743
|
+
"hasDynamicHelp": false,
|
|
1744
|
+
"multiple": false,
|
|
1745
|
+
"options": [
|
|
1746
|
+
"table",
|
|
1747
|
+
"json",
|
|
1748
|
+
"yaml",
|
|
1749
|
+
"csv"
|
|
1750
|
+
],
|
|
1751
|
+
"type": "option"
|
|
1752
|
+
},
|
|
1753
|
+
"jq": {
|
|
1754
|
+
"description": "jq expression to filter JSON output",
|
|
1755
|
+
"helpGroup": "GLOBAL",
|
|
1756
|
+
"name": "jq",
|
|
1757
|
+
"hasDynamicHelp": false,
|
|
1758
|
+
"multiple": false,
|
|
1759
|
+
"type": "option"
|
|
1760
|
+
},
|
|
1761
|
+
"fields": {
|
|
1762
|
+
"description": "Comma-separated fields to display",
|
|
1763
|
+
"helpGroup": "GLOBAL",
|
|
1764
|
+
"name": "fields",
|
|
1765
|
+
"hasDynamicHelp": false,
|
|
1766
|
+
"multiple": false,
|
|
1767
|
+
"type": "option"
|
|
1768
|
+
},
|
|
1769
|
+
"profile": {
|
|
1770
|
+
"description": "Named auth profile to use",
|
|
1771
|
+
"env": "PDCLI_PROFILE",
|
|
1772
|
+
"helpGroup": "GLOBAL",
|
|
1773
|
+
"name": "profile",
|
|
1774
|
+
"hasDynamicHelp": false,
|
|
1775
|
+
"multiple": false,
|
|
1776
|
+
"type": "option"
|
|
1777
|
+
},
|
|
1778
|
+
"no-color": {
|
|
1779
|
+
"description": "Disable color output",
|
|
1780
|
+
"helpGroup": "GLOBAL",
|
|
1781
|
+
"name": "no-color",
|
|
1782
|
+
"allowNo": false,
|
|
1783
|
+
"type": "boolean"
|
|
1784
|
+
},
|
|
1785
|
+
"verbose": {
|
|
1786
|
+
"description": "Show detailed API request/response on errors",
|
|
1787
|
+
"helpGroup": "GLOBAL",
|
|
1788
|
+
"name": "verbose",
|
|
1789
|
+
"allowNo": false,
|
|
1790
|
+
"type": "boolean"
|
|
1791
|
+
},
|
|
1792
|
+
"no-retry": {
|
|
1793
|
+
"description": "Disable automatic retry on rate limits and 5xx errors",
|
|
1794
|
+
"helpGroup": "GLOBAL",
|
|
1795
|
+
"name": "no-retry",
|
|
1796
|
+
"allowNo": false,
|
|
1797
|
+
"type": "boolean"
|
|
1798
|
+
},
|
|
1799
|
+
"timeout": {
|
|
1800
|
+
"description": "Request timeout in milliseconds",
|
|
1801
|
+
"helpGroup": "GLOBAL",
|
|
1802
|
+
"name": "timeout",
|
|
1803
|
+
"hasDynamicHelp": false,
|
|
1804
|
+
"multiple": false,
|
|
1805
|
+
"type": "option"
|
|
1806
|
+
},
|
|
1807
|
+
"limit": {
|
|
1808
|
+
"description": "Maximum number of items to return (lists)",
|
|
1809
|
+
"helpGroup": "GLOBAL",
|
|
1810
|
+
"name": "limit",
|
|
1811
|
+
"hasDynamicHelp": false,
|
|
1812
|
+
"multiple": false,
|
|
1813
|
+
"type": "option"
|
|
1814
|
+
},
|
|
1815
|
+
"ids": {
|
|
1816
|
+
"description": "Comma-separated deal IDs",
|
|
1817
|
+
"exclusive": [
|
|
1818
|
+
"filter"
|
|
1819
|
+
],
|
|
1820
|
+
"name": "ids",
|
|
1821
|
+
"hasDynamicHelp": false,
|
|
1822
|
+
"multiple": false,
|
|
1823
|
+
"type": "option"
|
|
1824
|
+
},
|
|
1825
|
+
"filter": {
|
|
1826
|
+
"description": "Pipedrive saved filter ID to select deals",
|
|
1827
|
+
"exclusive": [
|
|
1828
|
+
"ids"
|
|
1829
|
+
],
|
|
1830
|
+
"name": "filter",
|
|
1831
|
+
"hasDynamicHelp": false,
|
|
1832
|
+
"multiple": false,
|
|
1833
|
+
"type": "option"
|
|
1834
|
+
},
|
|
1835
|
+
"stage": {
|
|
1836
|
+
"description": "Move to stage ID",
|
|
1837
|
+
"name": "stage",
|
|
1838
|
+
"hasDynamicHelp": false,
|
|
1839
|
+
"multiple": false,
|
|
1840
|
+
"type": "option"
|
|
1841
|
+
},
|
|
1842
|
+
"pipeline": {
|
|
1843
|
+
"description": "Move to pipeline ID",
|
|
1844
|
+
"name": "pipeline",
|
|
1845
|
+
"hasDynamicHelp": false,
|
|
1846
|
+
"multiple": false,
|
|
1847
|
+
"type": "option"
|
|
1848
|
+
},
|
|
1849
|
+
"status": {
|
|
1850
|
+
"description": "Set status",
|
|
1851
|
+
"name": "status",
|
|
1852
|
+
"hasDynamicHelp": false,
|
|
1853
|
+
"multiple": false,
|
|
1854
|
+
"options": [
|
|
1855
|
+
"open",
|
|
1856
|
+
"won",
|
|
1857
|
+
"lost"
|
|
1858
|
+
],
|
|
1859
|
+
"type": "option"
|
|
1860
|
+
},
|
|
1861
|
+
"owner": {
|
|
1862
|
+
"description": "Assign owner (user) ID",
|
|
1863
|
+
"name": "owner",
|
|
1864
|
+
"hasDynamicHelp": false,
|
|
1865
|
+
"multiple": false,
|
|
1866
|
+
"type": "option"
|
|
1867
|
+
},
|
|
1868
|
+
"field": {
|
|
1869
|
+
"description": "Custom/standard field as \"Name=Value\" (repeatable)",
|
|
1870
|
+
"name": "field",
|
|
1871
|
+
"hasDynamicHelp": false,
|
|
1872
|
+
"multiple": true,
|
|
1873
|
+
"type": "option"
|
|
1874
|
+
},
|
|
1875
|
+
"body": {
|
|
1876
|
+
"description": "Raw JSON body to merge (flags win)",
|
|
1877
|
+
"name": "body",
|
|
1878
|
+
"hasDynamicHelp": false,
|
|
1879
|
+
"multiple": false,
|
|
1880
|
+
"type": "option"
|
|
1881
|
+
},
|
|
1882
|
+
"dry-run": {
|
|
1883
|
+
"description": "List the targets without updating anything",
|
|
1884
|
+
"name": "dry-run",
|
|
1885
|
+
"allowNo": false,
|
|
1886
|
+
"type": "boolean"
|
|
1887
|
+
},
|
|
1888
|
+
"yes": {
|
|
1889
|
+
"char": "y",
|
|
1890
|
+
"description": "Skip the confirmation prompt",
|
|
1891
|
+
"name": "yes",
|
|
1892
|
+
"allowNo": false,
|
|
1893
|
+
"type": "boolean"
|
|
1894
|
+
}
|
|
1895
|
+
},
|
|
1896
|
+
"hasDynamicHelp": false,
|
|
1897
|
+
"hiddenAliases": [],
|
|
1898
|
+
"id": "deal:bulk-update",
|
|
1899
|
+
"pluginAlias": "@wavyx/pdcli",
|
|
1900
|
+
"pluginName": "@wavyx/pdcli",
|
|
1901
|
+
"pluginType": "core",
|
|
1902
|
+
"strict": true,
|
|
1903
|
+
"enableJsonFlag": false,
|
|
1904
|
+
"isESM": true,
|
|
1905
|
+
"relativePath": [
|
|
1906
|
+
"src",
|
|
1907
|
+
"commands",
|
|
1908
|
+
"deal",
|
|
1909
|
+
"bulk-update.js"
|
|
1910
|
+
]
|
|
1911
|
+
},
|
|
1727
1912
|
"deal:create": {
|
|
1728
1913
|
"aliases": [],
|
|
1729
1914
|
"args": {},
|
|
@@ -5542,6 +5727,128 @@
|
|
|
5542
5727
|
"get.js"
|
|
5543
5728
|
]
|
|
5544
5729
|
},
|
|
5730
|
+
"org:import": {
|
|
5731
|
+
"aliases": [],
|
|
5732
|
+
"args": {
|
|
5733
|
+
"file": {
|
|
5734
|
+
"description": "CSV file path",
|
|
5735
|
+
"name": "file",
|
|
5736
|
+
"required": true
|
|
5737
|
+
}
|
|
5738
|
+
},
|
|
5739
|
+
"description": "Bulk-create organizations from a CSV (headers map to fields, custom fields by name)",
|
|
5740
|
+
"examples": [
|
|
5741
|
+
"<%= config.bin %> org import orgs.csv",
|
|
5742
|
+
"<%= config.bin %> org import orgs.csv --dry-run"
|
|
5743
|
+
],
|
|
5744
|
+
"flags": {
|
|
5745
|
+
"output": {
|
|
5746
|
+
"char": "o",
|
|
5747
|
+
"description": "Output format",
|
|
5748
|
+
"helpGroup": "GLOBAL",
|
|
5749
|
+
"name": "output",
|
|
5750
|
+
"hasDynamicHelp": false,
|
|
5751
|
+
"multiple": false,
|
|
5752
|
+
"options": [
|
|
5753
|
+
"table",
|
|
5754
|
+
"json",
|
|
5755
|
+
"yaml",
|
|
5756
|
+
"csv"
|
|
5757
|
+
],
|
|
5758
|
+
"type": "option"
|
|
5759
|
+
},
|
|
5760
|
+
"jq": {
|
|
5761
|
+
"description": "jq expression to filter JSON output",
|
|
5762
|
+
"helpGroup": "GLOBAL",
|
|
5763
|
+
"name": "jq",
|
|
5764
|
+
"hasDynamicHelp": false,
|
|
5765
|
+
"multiple": false,
|
|
5766
|
+
"type": "option"
|
|
5767
|
+
},
|
|
5768
|
+
"fields": {
|
|
5769
|
+
"description": "Comma-separated fields to display",
|
|
5770
|
+
"helpGroup": "GLOBAL",
|
|
5771
|
+
"name": "fields",
|
|
5772
|
+
"hasDynamicHelp": false,
|
|
5773
|
+
"multiple": false,
|
|
5774
|
+
"type": "option"
|
|
5775
|
+
},
|
|
5776
|
+
"profile": {
|
|
5777
|
+
"description": "Named auth profile to use",
|
|
5778
|
+
"env": "PDCLI_PROFILE",
|
|
5779
|
+
"helpGroup": "GLOBAL",
|
|
5780
|
+
"name": "profile",
|
|
5781
|
+
"hasDynamicHelp": false,
|
|
5782
|
+
"multiple": false,
|
|
5783
|
+
"type": "option"
|
|
5784
|
+
},
|
|
5785
|
+
"no-color": {
|
|
5786
|
+
"description": "Disable color output",
|
|
5787
|
+
"helpGroup": "GLOBAL",
|
|
5788
|
+
"name": "no-color",
|
|
5789
|
+
"allowNo": false,
|
|
5790
|
+
"type": "boolean"
|
|
5791
|
+
},
|
|
5792
|
+
"verbose": {
|
|
5793
|
+
"description": "Show detailed API request/response on errors",
|
|
5794
|
+
"helpGroup": "GLOBAL",
|
|
5795
|
+
"name": "verbose",
|
|
5796
|
+
"allowNo": false,
|
|
5797
|
+
"type": "boolean"
|
|
5798
|
+
},
|
|
5799
|
+
"no-retry": {
|
|
5800
|
+
"description": "Disable automatic retry on rate limits and 5xx errors",
|
|
5801
|
+
"helpGroup": "GLOBAL",
|
|
5802
|
+
"name": "no-retry",
|
|
5803
|
+
"allowNo": false,
|
|
5804
|
+
"type": "boolean"
|
|
5805
|
+
},
|
|
5806
|
+
"timeout": {
|
|
5807
|
+
"description": "Request timeout in milliseconds",
|
|
5808
|
+
"helpGroup": "GLOBAL",
|
|
5809
|
+
"name": "timeout",
|
|
5810
|
+
"hasDynamicHelp": false,
|
|
5811
|
+
"multiple": false,
|
|
5812
|
+
"type": "option"
|
|
5813
|
+
},
|
|
5814
|
+
"limit": {
|
|
5815
|
+
"description": "Maximum number of items to return (lists)",
|
|
5816
|
+
"helpGroup": "GLOBAL",
|
|
5817
|
+
"name": "limit",
|
|
5818
|
+
"hasDynamicHelp": false,
|
|
5819
|
+
"multiple": false,
|
|
5820
|
+
"type": "option"
|
|
5821
|
+
},
|
|
5822
|
+
"dry-run": {
|
|
5823
|
+
"description": "Validate every row without creating anything",
|
|
5824
|
+
"name": "dry-run",
|
|
5825
|
+
"allowNo": false,
|
|
5826
|
+
"type": "boolean"
|
|
5827
|
+
},
|
|
5828
|
+
"yes": {
|
|
5829
|
+
"char": "y",
|
|
5830
|
+
"description": "Skip the confirmation prompt",
|
|
5831
|
+
"name": "yes",
|
|
5832
|
+
"allowNo": false,
|
|
5833
|
+
"type": "boolean"
|
|
5834
|
+
}
|
|
5835
|
+
},
|
|
5836
|
+
"hasDynamicHelp": false,
|
|
5837
|
+
"hiddenAliases": [],
|
|
5838
|
+
"id": "org:import",
|
|
5839
|
+
"pluginAlias": "@wavyx/pdcli",
|
|
5840
|
+
"pluginName": "@wavyx/pdcli",
|
|
5841
|
+
"pluginType": "core",
|
|
5842
|
+
"strict": true,
|
|
5843
|
+
"enableJsonFlag": false,
|
|
5844
|
+
"isESM": true,
|
|
5845
|
+
"relativePath": [
|
|
5846
|
+
"src",
|
|
5847
|
+
"commands",
|
|
5848
|
+
"org",
|
|
5849
|
+
"import.js"
|
|
5850
|
+
]
|
|
5851
|
+
},
|
|
5545
5852
|
"org:list": {
|
|
5546
5853
|
"aliases": [],
|
|
5547
5854
|
"args": {},
|
|
@@ -6169,6 +6476,128 @@
|
|
|
6169
6476
|
"get.js"
|
|
6170
6477
|
]
|
|
6171
6478
|
},
|
|
6479
|
+
"person:import": {
|
|
6480
|
+
"aliases": [],
|
|
6481
|
+
"args": {
|
|
6482
|
+
"file": {
|
|
6483
|
+
"description": "CSV file path",
|
|
6484
|
+
"name": "file",
|
|
6485
|
+
"required": true
|
|
6486
|
+
}
|
|
6487
|
+
},
|
|
6488
|
+
"description": "Bulk-create persons from a CSV (headers map to fields, custom fields by name)",
|
|
6489
|
+
"examples": [
|
|
6490
|
+
"<%= config.bin %> person import people.csv",
|
|
6491
|
+
"<%= config.bin %> person import people.csv --dry-run"
|
|
6492
|
+
],
|
|
6493
|
+
"flags": {
|
|
6494
|
+
"output": {
|
|
6495
|
+
"char": "o",
|
|
6496
|
+
"description": "Output format",
|
|
6497
|
+
"helpGroup": "GLOBAL",
|
|
6498
|
+
"name": "output",
|
|
6499
|
+
"hasDynamicHelp": false,
|
|
6500
|
+
"multiple": false,
|
|
6501
|
+
"options": [
|
|
6502
|
+
"table",
|
|
6503
|
+
"json",
|
|
6504
|
+
"yaml",
|
|
6505
|
+
"csv"
|
|
6506
|
+
],
|
|
6507
|
+
"type": "option"
|
|
6508
|
+
},
|
|
6509
|
+
"jq": {
|
|
6510
|
+
"description": "jq expression to filter JSON output",
|
|
6511
|
+
"helpGroup": "GLOBAL",
|
|
6512
|
+
"name": "jq",
|
|
6513
|
+
"hasDynamicHelp": false,
|
|
6514
|
+
"multiple": false,
|
|
6515
|
+
"type": "option"
|
|
6516
|
+
},
|
|
6517
|
+
"fields": {
|
|
6518
|
+
"description": "Comma-separated fields to display",
|
|
6519
|
+
"helpGroup": "GLOBAL",
|
|
6520
|
+
"name": "fields",
|
|
6521
|
+
"hasDynamicHelp": false,
|
|
6522
|
+
"multiple": false,
|
|
6523
|
+
"type": "option"
|
|
6524
|
+
},
|
|
6525
|
+
"profile": {
|
|
6526
|
+
"description": "Named auth profile to use",
|
|
6527
|
+
"env": "PDCLI_PROFILE",
|
|
6528
|
+
"helpGroup": "GLOBAL",
|
|
6529
|
+
"name": "profile",
|
|
6530
|
+
"hasDynamicHelp": false,
|
|
6531
|
+
"multiple": false,
|
|
6532
|
+
"type": "option"
|
|
6533
|
+
},
|
|
6534
|
+
"no-color": {
|
|
6535
|
+
"description": "Disable color output",
|
|
6536
|
+
"helpGroup": "GLOBAL",
|
|
6537
|
+
"name": "no-color",
|
|
6538
|
+
"allowNo": false,
|
|
6539
|
+
"type": "boolean"
|
|
6540
|
+
},
|
|
6541
|
+
"verbose": {
|
|
6542
|
+
"description": "Show detailed API request/response on errors",
|
|
6543
|
+
"helpGroup": "GLOBAL",
|
|
6544
|
+
"name": "verbose",
|
|
6545
|
+
"allowNo": false,
|
|
6546
|
+
"type": "boolean"
|
|
6547
|
+
},
|
|
6548
|
+
"no-retry": {
|
|
6549
|
+
"description": "Disable automatic retry on rate limits and 5xx errors",
|
|
6550
|
+
"helpGroup": "GLOBAL",
|
|
6551
|
+
"name": "no-retry",
|
|
6552
|
+
"allowNo": false,
|
|
6553
|
+
"type": "boolean"
|
|
6554
|
+
},
|
|
6555
|
+
"timeout": {
|
|
6556
|
+
"description": "Request timeout in milliseconds",
|
|
6557
|
+
"helpGroup": "GLOBAL",
|
|
6558
|
+
"name": "timeout",
|
|
6559
|
+
"hasDynamicHelp": false,
|
|
6560
|
+
"multiple": false,
|
|
6561
|
+
"type": "option"
|
|
6562
|
+
},
|
|
6563
|
+
"limit": {
|
|
6564
|
+
"description": "Maximum number of items to return (lists)",
|
|
6565
|
+
"helpGroup": "GLOBAL",
|
|
6566
|
+
"name": "limit",
|
|
6567
|
+
"hasDynamicHelp": false,
|
|
6568
|
+
"multiple": false,
|
|
6569
|
+
"type": "option"
|
|
6570
|
+
},
|
|
6571
|
+
"dry-run": {
|
|
6572
|
+
"description": "Validate every row without creating anything",
|
|
6573
|
+
"name": "dry-run",
|
|
6574
|
+
"allowNo": false,
|
|
6575
|
+
"type": "boolean"
|
|
6576
|
+
},
|
|
6577
|
+
"yes": {
|
|
6578
|
+
"char": "y",
|
|
6579
|
+
"description": "Skip the confirmation prompt",
|
|
6580
|
+
"name": "yes",
|
|
6581
|
+
"allowNo": false,
|
|
6582
|
+
"type": "boolean"
|
|
6583
|
+
}
|
|
6584
|
+
},
|
|
6585
|
+
"hasDynamicHelp": false,
|
|
6586
|
+
"hiddenAliases": [],
|
|
6587
|
+
"id": "person:import",
|
|
6588
|
+
"pluginAlias": "@wavyx/pdcli",
|
|
6589
|
+
"pluginName": "@wavyx/pdcli",
|
|
6590
|
+
"pluginType": "core",
|
|
6591
|
+
"strict": true,
|
|
6592
|
+
"enableJsonFlag": false,
|
|
6593
|
+
"isESM": true,
|
|
6594
|
+
"relativePath": [
|
|
6595
|
+
"src",
|
|
6596
|
+
"commands",
|
|
6597
|
+
"person",
|
|
6598
|
+
"import.js"
|
|
6599
|
+
]
|
|
6600
|
+
},
|
|
6172
6601
|
"person:list": {
|
|
6173
6602
|
"aliases": [],
|
|
6174
6603
|
"args": {},
|
|
@@ -8329,19 +8758,12 @@
|
|
|
8329
8758
|
"update.js"
|
|
8330
8759
|
]
|
|
8331
8760
|
},
|
|
8332
|
-
"
|
|
8761
|
+
"user:me": {
|
|
8333
8762
|
"aliases": [],
|
|
8334
|
-
"args": {
|
|
8335
|
-
|
|
8336
|
-
"description": "Stage ID",
|
|
8337
|
-
"name": "id",
|
|
8338
|
-
"required": true
|
|
8339
|
-
}
|
|
8340
|
-
},
|
|
8341
|
-
"description": "Get a stage by ID",
|
|
8763
|
+
"args": {},
|
|
8764
|
+
"description": "Show the authenticated user",
|
|
8342
8765
|
"examples": [
|
|
8343
|
-
"<%= config.bin %>
|
|
8344
|
-
"<%= config.bin %> stage get 5 --output json"
|
|
8766
|
+
"<%= config.bin %> user me"
|
|
8345
8767
|
],
|
|
8346
8768
|
"flags": {
|
|
8347
8769
|
"output": {
|
|
@@ -8424,7 +8846,7 @@
|
|
|
8424
8846
|
},
|
|
8425
8847
|
"hasDynamicHelp": false,
|
|
8426
8848
|
"hiddenAliases": [],
|
|
8427
|
-
"id": "
|
|
8849
|
+
"id": "user:me",
|
|
8428
8850
|
"pluginAlias": "@wavyx/pdcli",
|
|
8429
8851
|
"pluginName": "@wavyx/pdcli",
|
|
8430
8852
|
"pluginType": "core",
|
|
@@ -8434,17 +8856,23 @@
|
|
|
8434
8856
|
"relativePath": [
|
|
8435
8857
|
"src",
|
|
8436
8858
|
"commands",
|
|
8437
|
-
"
|
|
8438
|
-
"
|
|
8859
|
+
"user",
|
|
8860
|
+
"me.js"
|
|
8439
8861
|
]
|
|
8440
8862
|
},
|
|
8441
|
-
"stage:
|
|
8863
|
+
"stage:get": {
|
|
8442
8864
|
"aliases": [],
|
|
8443
|
-
"args": {
|
|
8444
|
-
|
|
8865
|
+
"args": {
|
|
8866
|
+
"id": {
|
|
8867
|
+
"description": "Stage ID",
|
|
8868
|
+
"name": "id",
|
|
8869
|
+
"required": true
|
|
8870
|
+
}
|
|
8871
|
+
},
|
|
8872
|
+
"description": "Get a stage by ID",
|
|
8445
8873
|
"examples": [
|
|
8446
|
-
"<%= config.bin %> stage
|
|
8447
|
-
"<%= config.bin %> stage
|
|
8874
|
+
"<%= config.bin %> stage get 5",
|
|
8875
|
+
"<%= config.bin %> stage get 5 --output json"
|
|
8448
8876
|
],
|
|
8449
8877
|
"flags": {
|
|
8450
8878
|
"output": {
|
|
@@ -8523,18 +8951,11 @@
|
|
|
8523
8951
|
"hasDynamicHelp": false,
|
|
8524
8952
|
"multiple": false,
|
|
8525
8953
|
"type": "option"
|
|
8526
|
-
},
|
|
8527
|
-
"pipeline": {
|
|
8528
|
-
"description": "Filter by pipeline ID",
|
|
8529
|
-
"name": "pipeline",
|
|
8530
|
-
"hasDynamicHelp": false,
|
|
8531
|
-
"multiple": false,
|
|
8532
|
-
"type": "option"
|
|
8533
8954
|
}
|
|
8534
8955
|
},
|
|
8535
8956
|
"hasDynamicHelp": false,
|
|
8536
8957
|
"hiddenAliases": [],
|
|
8537
|
-
"id": "stage:
|
|
8958
|
+
"id": "stage:get",
|
|
8538
8959
|
"pluginAlias": "@wavyx/pdcli",
|
|
8539
8960
|
"pluginName": "@wavyx/pdcli",
|
|
8540
8961
|
"pluginType": "core",
|
|
@@ -8545,15 +8966,16 @@
|
|
|
8545
8966
|
"src",
|
|
8546
8967
|
"commands",
|
|
8547
8968
|
"stage",
|
|
8548
|
-
"
|
|
8969
|
+
"get.js"
|
|
8549
8970
|
]
|
|
8550
8971
|
},
|
|
8551
|
-
"
|
|
8972
|
+
"stage:list": {
|
|
8552
8973
|
"aliases": [],
|
|
8553
8974
|
"args": {},
|
|
8554
|
-
"description": "
|
|
8975
|
+
"description": "List stages",
|
|
8555
8976
|
"examples": [
|
|
8556
|
-
"<%= config.bin %>
|
|
8977
|
+
"<%= config.bin %> stage list",
|
|
8978
|
+
"<%= config.bin %> stage list --pipeline 1 --output json"
|
|
8557
8979
|
],
|
|
8558
8980
|
"flags": {
|
|
8559
8981
|
"output": {
|
|
@@ -8632,11 +9054,18 @@
|
|
|
8632
9054
|
"hasDynamicHelp": false,
|
|
8633
9055
|
"multiple": false,
|
|
8634
9056
|
"type": "option"
|
|
9057
|
+
},
|
|
9058
|
+
"pipeline": {
|
|
9059
|
+
"description": "Filter by pipeline ID",
|
|
9060
|
+
"name": "pipeline",
|
|
9061
|
+
"hasDynamicHelp": false,
|
|
9062
|
+
"multiple": false,
|
|
9063
|
+
"type": "option"
|
|
8635
9064
|
}
|
|
8636
9065
|
},
|
|
8637
9066
|
"hasDynamicHelp": false,
|
|
8638
9067
|
"hiddenAliases": [],
|
|
8639
|
-
"id": "
|
|
9068
|
+
"id": "stage:list",
|
|
8640
9069
|
"pluginAlias": "@wavyx/pdcli",
|
|
8641
9070
|
"pluginName": "@wavyx/pdcli",
|
|
8642
9071
|
"pluginType": "core",
|
|
@@ -8646,8 +9075,8 @@
|
|
|
8646
9075
|
"relativePath": [
|
|
8647
9076
|
"src",
|
|
8648
9077
|
"commands",
|
|
8649
|
-
"
|
|
8650
|
-
"
|
|
9078
|
+
"stage",
|
|
9079
|
+
"list.js"
|
|
8651
9080
|
]
|
|
8652
9081
|
},
|
|
8653
9082
|
"webhook:create": {
|
|
@@ -9051,5 +9480,5 @@
|
|
|
9051
9480
|
]
|
|
9052
9481
|
}
|
|
9053
9482
|
},
|
|
9054
|
-
"version": "0.
|
|
9483
|
+
"version": "0.4.0"
|
|
9055
9484
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import ora from 'ora'
|
|
4
|
+
import BaseCommand from '../../base-command.js'
|
|
5
|
+
import { resolveTargets, bulkRun } from '../../lib/bulk.js'
|
|
6
|
+
import { buildWriteBody } from '../../lib/input.js'
|
|
7
|
+
import { defsForFields } from '../../lib/entity-view.js'
|
|
8
|
+
import { confirmAction } from '../../lib/confirm.js'
|
|
9
|
+
import { CliError } from '../../lib/errors.js'
|
|
10
|
+
|
|
11
|
+
export default class DealBulkUpdateCommand extends BaseCommand {
|
|
12
|
+
static description =
|
|
13
|
+
'Update many deals at once (by --ids, a saved --filter, or ids piped on stdin)'
|
|
14
|
+
|
|
15
|
+
static examples = [
|
|
16
|
+
'<%= config.bin %> deal bulk-update --ids 1,2,3 --stage 5',
|
|
17
|
+
'<%= config.bin %> deal bulk-update --filter 9 --status won',
|
|
18
|
+
"<%= config.bin %> deal list --status open --jq '.[].id' | <%= config.bin %> deal bulk-update --owner 42",
|
|
19
|
+
'<%= config.bin %> deal bulk-update --filter 9 --stage 5 --dry-run',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
static flags = {
|
|
23
|
+
...BaseCommand.baseFlags,
|
|
24
|
+
ids: Flags.string({
|
|
25
|
+
description: 'Comma-separated deal IDs',
|
|
26
|
+
exclusive: ['filter'],
|
|
27
|
+
}),
|
|
28
|
+
filter: Flags.integer({
|
|
29
|
+
description: 'Pipedrive saved filter ID to select deals',
|
|
30
|
+
exclusive: ['ids'],
|
|
31
|
+
}),
|
|
32
|
+
stage: Flags.integer({ description: 'Move to stage ID' }),
|
|
33
|
+
pipeline: Flags.integer({ description: 'Move to pipeline ID' }),
|
|
34
|
+
status: Flags.string({
|
|
35
|
+
description: 'Set status',
|
|
36
|
+
options: ['open', 'won', 'lost'],
|
|
37
|
+
}),
|
|
38
|
+
owner: Flags.integer({ description: 'Assign owner (user) ID' }),
|
|
39
|
+
field: Flags.string({
|
|
40
|
+
multiple: true,
|
|
41
|
+
description: 'Custom/standard field as "Name=Value" (repeatable)',
|
|
42
|
+
}),
|
|
43
|
+
body: Flags.string({ description: 'Raw JSON body to merge (flags win)' }),
|
|
44
|
+
'dry-run': Flags.boolean({
|
|
45
|
+
description: 'List the targets without updating anything',
|
|
46
|
+
default: false,
|
|
47
|
+
}),
|
|
48
|
+
yes: Flags.boolean({
|
|
49
|
+
char: 'y',
|
|
50
|
+
description: 'Skip the confirmation prompt',
|
|
51
|
+
default: false,
|
|
52
|
+
}),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async run() {
|
|
56
|
+
const { flags } = await this.parse(DealBulkUpdateCommand)
|
|
57
|
+
|
|
58
|
+
const body = buildWriteBody({
|
|
59
|
+
typed: {
|
|
60
|
+
stage_id: flags.stage,
|
|
61
|
+
pipeline_id: flags.pipeline,
|
|
62
|
+
status: flags.status,
|
|
63
|
+
owner_id: flags.owner,
|
|
64
|
+
},
|
|
65
|
+
fields: flags.field,
|
|
66
|
+
rawBody: flags.body,
|
|
67
|
+
defs: await defsForFields(this, 'deal', flags.field),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
if (Object.keys(body).length === 0) {
|
|
71
|
+
throw new CliError(
|
|
72
|
+
'Nothing to update — pass at least one change flag, --field, or --body',
|
|
73
|
+
{ exitCode: 64 },
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const targets = await resolveTargets(
|
|
78
|
+
{ ids: flags.ids, filter: flags.filter },
|
|
79
|
+
this.apiClient,
|
|
80
|
+
'/api/v2/deals',
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if (flags['dry-run']) {
|
|
84
|
+
this.log(
|
|
85
|
+
`Would update ${chalk.bold(targets.length)} deals: ${targets.join(', ')}`,
|
|
86
|
+
)
|
|
87
|
+
this.log(chalk.dim(`Change: ${JSON.stringify(body)}`))
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const ok = await confirmAction(
|
|
92
|
+
`Update ${targets.length} deals with ${JSON.stringify(body)}?`,
|
|
93
|
+
flags.yes,
|
|
94
|
+
)
|
|
95
|
+
if (!ok) {
|
|
96
|
+
throw new CliError('Aborted', { exitCode: 1 })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const spinner = ora(`Updating ${targets.length} deals...`).start()
|
|
100
|
+
let summary
|
|
101
|
+
try {
|
|
102
|
+
summary = await bulkRun(
|
|
103
|
+
targets,
|
|
104
|
+
(id) => this.apiClient.patch(`/api/v2/deals/${id}`, { body }),
|
|
105
|
+
{
|
|
106
|
+
onProgress: (done, total) => {
|
|
107
|
+
spinner.text = `Updating deals ${done}/${total}`
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
} finally {
|
|
112
|
+
spinner.stop()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.log(
|
|
116
|
+
chalk.green(
|
|
117
|
+
`Updated ${summary.succeeded.length}/${targets.length} deals`,
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if (summary.failed.length > 0) {
|
|
122
|
+
for (const { item, error } of summary.failed) {
|
|
123
|
+
this.log(chalk.red(` ✘ deal ${item}: ${error}`))
|
|
124
|
+
}
|
|
125
|
+
throw new CliError(
|
|
126
|
+
`${summary.failed.length} of ${targets.length} updates failed`,
|
|
127
|
+
{ exitCode: 1 },
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { Args, Flags } from '@oclif/core'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import ora from 'ora'
|
|
5
|
+
import BaseCommand from '../../base-command.js'
|
|
6
|
+
import { parseCsv } from '../../lib/csv-parse.js'
|
|
7
|
+
import { prepareImportBodies } from '../../lib/import.js'
|
|
8
|
+
import { bulkRun } from '../../lib/bulk.js'
|
|
9
|
+
import { getFields } from '../../lib/fields.js'
|
|
10
|
+
import { confirmAction } from '../../lib/confirm.js'
|
|
11
|
+
import { CliError } from '../../lib/errors.js'
|
|
12
|
+
|
|
13
|
+
const SPECIAL_COLUMNS = {
|
|
14
|
+
name: (typed, value) => {
|
|
15
|
+
typed.name = value
|
|
16
|
+
},
|
|
17
|
+
owner_id: (typed, value) => {
|
|
18
|
+
typed.owner_id = Number(value)
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default class OrgImportCommand extends BaseCommand {
|
|
23
|
+
static description =
|
|
24
|
+
'Bulk-create organizations from a CSV (headers map to fields, custom fields by name)'
|
|
25
|
+
|
|
26
|
+
static examples = [
|
|
27
|
+
'<%= config.bin %> org import orgs.csv',
|
|
28
|
+
'<%= config.bin %> org import orgs.csv --dry-run',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
static args = {
|
|
32
|
+
file: Args.string({ required: true, description: 'CSV file path' }),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static flags = {
|
|
36
|
+
...BaseCommand.baseFlags,
|
|
37
|
+
'dry-run': Flags.boolean({
|
|
38
|
+
description: 'Validate every row without creating anything',
|
|
39
|
+
default: false,
|
|
40
|
+
}),
|
|
41
|
+
yes: Flags.boolean({
|
|
42
|
+
char: 'y',
|
|
43
|
+
description: 'Skip the confirmation prompt',
|
|
44
|
+
default: false,
|
|
45
|
+
}),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async run() {
|
|
49
|
+
const { args, flags } = await this.parse(OrgImportCommand)
|
|
50
|
+
|
|
51
|
+
const { headers, rows } = parseCsv(readFileSync(args.file, 'utf8'))
|
|
52
|
+
if (!headers.some((h) => h.toLowerCase() === 'name')) {
|
|
53
|
+
throw new CliError('CSV must include a "name" column', { exitCode: 64 })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const needsDefs = headers.some((h) => !(h.toLowerCase() in SPECIAL_COLUMNS))
|
|
57
|
+
const bodies = prepareImportBodies({
|
|
58
|
+
headers,
|
|
59
|
+
rows,
|
|
60
|
+
specialColumns: SPECIAL_COLUMNS,
|
|
61
|
+
defs: needsDefs ? await getFields(this.apiClient, 'org') : [],
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
if (flags['dry-run']) {
|
|
65
|
+
this.log(chalk.green(`${bodies.length} rows valid — nothing created`))
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const ok = await confirmAction(
|
|
70
|
+
`Create ${bodies.length} organizations from ${args.file}?`,
|
|
71
|
+
flags.yes,
|
|
72
|
+
)
|
|
73
|
+
if (!ok) {
|
|
74
|
+
throw new CliError('Aborted', { exitCode: 1 })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const spinner = ora(`Importing ${bodies.length} organizations...`).start()
|
|
78
|
+
let summary
|
|
79
|
+
try {
|
|
80
|
+
summary = await bulkRun(
|
|
81
|
+
bodies,
|
|
82
|
+
(body) => this.apiClient.post('/api/v2/organizations', { body }),
|
|
83
|
+
{
|
|
84
|
+
onProgress: (done, total) => {
|
|
85
|
+
spinner.text = `Importing organizations ${done}/${total}`
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
} finally {
|
|
90
|
+
spinner.stop()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.log(
|
|
94
|
+
chalk.green(
|
|
95
|
+
`Imported ${summary.succeeded.length}/${bodies.length} organizations`,
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if (summary.failed.length > 0) {
|
|
100
|
+
for (const { item, error } of summary.failed) {
|
|
101
|
+
this.log(chalk.red(` ✘ ${item.name ?? '(unnamed)'}: ${error}`))
|
|
102
|
+
}
|
|
103
|
+
throw new CliError(
|
|
104
|
+
`${summary.failed.length} of ${bodies.length} rows failed`,
|
|
105
|
+
{ exitCode: 1 },
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { Args, Flags } from '@oclif/core'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import ora from 'ora'
|
|
5
|
+
import BaseCommand from '../../base-command.js'
|
|
6
|
+
import { parseCsv } from '../../lib/csv-parse.js'
|
|
7
|
+
import { prepareImportBodies } from '../../lib/import.js'
|
|
8
|
+
import { bulkRun } from '../../lib/bulk.js'
|
|
9
|
+
import { getFields } from '../../lib/fields.js'
|
|
10
|
+
import { confirmAction } from '../../lib/confirm.js'
|
|
11
|
+
import { CliError } from '../../lib/errors.js'
|
|
12
|
+
|
|
13
|
+
const SPECIAL_COLUMNS = {
|
|
14
|
+
name: (typed, value) => {
|
|
15
|
+
typed.name = value
|
|
16
|
+
},
|
|
17
|
+
email: (typed, value) => {
|
|
18
|
+
typed.emails = [{ value, primary: true }]
|
|
19
|
+
},
|
|
20
|
+
phone: (typed, value) => {
|
|
21
|
+
typed.phones = [{ value, primary: true }]
|
|
22
|
+
},
|
|
23
|
+
org_id: (typed, value) => {
|
|
24
|
+
typed.org_id = Number(value)
|
|
25
|
+
},
|
|
26
|
+
owner_id: (typed, value) => {
|
|
27
|
+
typed.owner_id = Number(value)
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default class PersonImportCommand extends BaseCommand {
|
|
32
|
+
static description =
|
|
33
|
+
'Bulk-create persons from a CSV (headers map to fields, custom fields by name)'
|
|
34
|
+
|
|
35
|
+
static examples = [
|
|
36
|
+
'<%= config.bin %> person import people.csv',
|
|
37
|
+
'<%= config.bin %> person import people.csv --dry-run',
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
static args = {
|
|
41
|
+
file: Args.string({ required: true, description: 'CSV file path' }),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static flags = {
|
|
45
|
+
...BaseCommand.baseFlags,
|
|
46
|
+
'dry-run': Flags.boolean({
|
|
47
|
+
description: 'Validate every row without creating anything',
|
|
48
|
+
default: false,
|
|
49
|
+
}),
|
|
50
|
+
yes: Flags.boolean({
|
|
51
|
+
char: 'y',
|
|
52
|
+
description: 'Skip the confirmation prompt',
|
|
53
|
+
default: false,
|
|
54
|
+
}),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async run() {
|
|
58
|
+
const { args, flags } = await this.parse(PersonImportCommand)
|
|
59
|
+
|
|
60
|
+
const { headers, rows } = parseCsv(readFileSync(args.file, 'utf8'))
|
|
61
|
+
if (!headers.some((h) => h.toLowerCase() === 'name')) {
|
|
62
|
+
throw new CliError('CSV must include a "name" column', { exitCode: 64 })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const needsDefs = headers.some((h) => !(h.toLowerCase() in SPECIAL_COLUMNS))
|
|
66
|
+
const bodies = prepareImportBodies({
|
|
67
|
+
headers,
|
|
68
|
+
rows,
|
|
69
|
+
specialColumns: SPECIAL_COLUMNS,
|
|
70
|
+
defs: needsDefs ? await getFields(this.apiClient, 'person') : [],
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
if (flags['dry-run']) {
|
|
74
|
+
this.log(chalk.green(`${bodies.length} rows valid — nothing created`))
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const ok = await confirmAction(
|
|
79
|
+
`Create ${bodies.length} persons from ${args.file}?`,
|
|
80
|
+
flags.yes,
|
|
81
|
+
)
|
|
82
|
+
if (!ok) {
|
|
83
|
+
throw new CliError('Aborted', { exitCode: 1 })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const spinner = ora(`Importing ${bodies.length} persons...`).start()
|
|
87
|
+
let summary
|
|
88
|
+
try {
|
|
89
|
+
summary = await bulkRun(
|
|
90
|
+
bodies,
|
|
91
|
+
(body) => this.apiClient.post('/api/v2/persons', { body }),
|
|
92
|
+
{
|
|
93
|
+
onProgress: (done, total) => {
|
|
94
|
+
spinner.text = `Importing persons ${done}/${total}`
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
} finally {
|
|
99
|
+
spinner.stop()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.log(
|
|
103
|
+
chalk.green(
|
|
104
|
+
`Imported ${summary.succeeded.length}/${bodies.length} persons`,
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if (summary.failed.length > 0) {
|
|
109
|
+
for (const { item, error } of summary.failed) {
|
|
110
|
+
this.log(chalk.red(` ✘ ${item.name ?? '(unnamed)'}: ${error}`))
|
|
111
|
+
}
|
|
112
|
+
throw new CliError(
|
|
113
|
+
`${summary.failed.length} of ${bodies.length} rows failed`,
|
|
114
|
+
{ exitCode: 1 },
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/lib/bulk.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import createDebug from 'debug'
|
|
2
|
+
import { CliError } from './errors.js'
|
|
3
|
+
|
|
4
|
+
const debug = createDebug('pd:bulk')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the target ids for a bulk operation from exactly one selector:
|
|
8
|
+
* --ids "1,2,3", a Pipedrive saved filter (--filter <id>), or piped stdin
|
|
9
|
+
* (newline-separated ids, a JSON array of ids, or JSON objects with an id).
|
|
10
|
+
* @param {object} selectors
|
|
11
|
+
* @param {string} [selectors.ids]
|
|
12
|
+
* @param {number} [selectors.filter]
|
|
13
|
+
* @param {NodeJS.ReadStream} [selectors.stdin] defaults to process.stdin
|
|
14
|
+
* @param {ReturnType<import('./client.js').createClient>} client
|
|
15
|
+
* @param {string} listPath v2 list endpoint supporting filter_id (e.g. /api/v2/deals)
|
|
16
|
+
* @returns {Promise<number[]>}
|
|
17
|
+
*/
|
|
18
|
+
export async function resolveTargets(
|
|
19
|
+
{ ids, filter, stdin = process.stdin },
|
|
20
|
+
client,
|
|
21
|
+
listPath,
|
|
22
|
+
) {
|
|
23
|
+
if (ids) {
|
|
24
|
+
return ids.split(',').map(parseId)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (filter != null) {
|
|
28
|
+
debug('resolving targets from filter %d', filter)
|
|
29
|
+
const targets = []
|
|
30
|
+
for await (const item of client.pageV2(listPath, {
|
|
31
|
+
filter_id: filter,
|
|
32
|
+
limit: 500,
|
|
33
|
+
})) {
|
|
34
|
+
targets.push(item.id)
|
|
35
|
+
}
|
|
36
|
+
return targets
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!stdin.isTTY) {
|
|
40
|
+
const chunks = []
|
|
41
|
+
for await (const chunk of stdin) chunks.push(chunk)
|
|
42
|
+
const text = Buffer.concat(chunks).toString('utf8').trim()
|
|
43
|
+
if (text.startsWith('[')) {
|
|
44
|
+
const parsed = JSON.parse(text)
|
|
45
|
+
return parsed.map((entry) =>
|
|
46
|
+
typeof entry === 'object' ? entry.id : parseId(String(entry)),
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
return text.split('\n').map(parseId)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw new CliError(
|
|
53
|
+
'No targets — pass --ids, --filter, or pipe ids on stdin',
|
|
54
|
+
{ exitCode: 64 },
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseId(raw) {
|
|
59
|
+
const id = Number(raw.trim())
|
|
60
|
+
if (!Number.isInteger(id)) {
|
|
61
|
+
throw new CliError(`Invalid id "${raw.trim()}" — expected an integer`, {
|
|
62
|
+
exitCode: 64,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
return id
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sleep(ms) {
|
|
69
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Run an async operation per item, sequentially with a pacing gap so bulk
|
|
74
|
+
* writes stay inside Pipedrive's 2-second burst window (writes cost 10
|
|
75
|
+
* tokens each; the client's 429 backoff covers anything that slips through).
|
|
76
|
+
* Per-item failures are collected, never thrown.
|
|
77
|
+
* @template T
|
|
78
|
+
* @param {T[]} items
|
|
79
|
+
* @param {(item: T) => Promise<unknown>} operation
|
|
80
|
+
* @param {object} [options]
|
|
81
|
+
* @param {number} [options.gapMs] delay between requests (default 200)
|
|
82
|
+
* @param {(done: number, total: number) => void} [options.onProgress]
|
|
83
|
+
* @returns {Promise<{ succeeded: { item: T, result: unknown }[], failed: { item: T, error: string }[] }>}
|
|
84
|
+
*/
|
|
85
|
+
export async function bulkRun(
|
|
86
|
+
items,
|
|
87
|
+
operation,
|
|
88
|
+
{ gapMs = 200, onProgress } = {},
|
|
89
|
+
) {
|
|
90
|
+
const succeeded = []
|
|
91
|
+
const failed = []
|
|
92
|
+
|
|
93
|
+
for (const [index, item] of items.entries()) {
|
|
94
|
+
if (index > 0 && gapMs > 0) await sleep(gapMs)
|
|
95
|
+
try {
|
|
96
|
+
const result = await operation(item)
|
|
97
|
+
succeeded.push({ item, result })
|
|
98
|
+
} catch (err) {
|
|
99
|
+
debug('bulk item %o failed: %s', item, err.message)
|
|
100
|
+
failed.push({ item, error: err.message })
|
|
101
|
+
}
|
|
102
|
+
onProgress?.(index + 1, items.length)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { succeeded, failed }
|
|
106
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { CliError } from './errors.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal RFC 4180 CSV parser (quoted fields, escaped quotes, embedded
|
|
5
|
+
* commas/newlines, CRLF). First record is the header row.
|
|
6
|
+
* @param {string} text
|
|
7
|
+
* @returns {{ headers: string[], rows: string[][] }}
|
|
8
|
+
*/
|
|
9
|
+
export function parseCsv(text) {
|
|
10
|
+
const records = []
|
|
11
|
+
let record = []
|
|
12
|
+
let field = ''
|
|
13
|
+
let inQuotes = false
|
|
14
|
+
let i = 0
|
|
15
|
+
|
|
16
|
+
while (i < text.length) {
|
|
17
|
+
const char = text[i]
|
|
18
|
+
|
|
19
|
+
if (inQuotes) {
|
|
20
|
+
if (char === '"') {
|
|
21
|
+
if (text[i + 1] === '"') {
|
|
22
|
+
field += '"'
|
|
23
|
+
i += 2
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
inQuotes = false
|
|
27
|
+
i++
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
field += char
|
|
31
|
+
i++
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (char === '"') {
|
|
36
|
+
inQuotes = true
|
|
37
|
+
i++
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
if (char === ',') {
|
|
41
|
+
record.push(field)
|
|
42
|
+
field = ''
|
|
43
|
+
i++
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
if (char === '\n' || char === '\r') {
|
|
47
|
+
if (char === '\r' && text[i + 1] === '\n') i++
|
|
48
|
+
record.push(field)
|
|
49
|
+
field = ''
|
|
50
|
+
if (record.length > 1 || record[0] !== '') records.push(record)
|
|
51
|
+
record = []
|
|
52
|
+
i++
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
field += char
|
|
56
|
+
i++
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (inQuotes) {
|
|
60
|
+
throw new CliError('Unterminated quoted field in CSV', { exitCode: 65 })
|
|
61
|
+
}
|
|
62
|
+
if (field !== '' || record.length > 0) {
|
|
63
|
+
// A tail record only reaches here when non-empty (a bare trailing
|
|
64
|
+
// newline never starts a record), so push unconditionally.
|
|
65
|
+
record.push(field)
|
|
66
|
+
records.push(record)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (records.length === 0) {
|
|
70
|
+
throw new CliError('CSV file is empty', { exitCode: 65 })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const [headers, ...rows] = records
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
headers,
|
|
77
|
+
rows: rows.map((row, index) => {
|
|
78
|
+
if (row.length > headers.length) {
|
|
79
|
+
throw new CliError(
|
|
80
|
+
`CSV row ${index + 2} has ${row.length} cells but the header has ${headers.length}`,
|
|
81
|
+
{ exitCode: 65 },
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
while (row.length < headers.length) row.push('')
|
|
85
|
+
return row
|
|
86
|
+
}),
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { buildWriteBody } from './input.js'
|
|
2
|
+
import { CliError } from './errors.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Turn parsed CSV rows into write bodies. Special columns (matched
|
|
6
|
+
* case-insensitively) build typed values; every other header resolves
|
|
7
|
+
* through the entity's field definitions — names, hash keys, and option
|
|
8
|
+
* labels included. Empty cells are skipped.
|
|
9
|
+
* @param {object} options
|
|
10
|
+
* @param {string[]} options.headers
|
|
11
|
+
* @param {string[][]} options.rows
|
|
12
|
+
* @param {Record<string, (typed: object, value: string) => void>} [options.specialColumns]
|
|
13
|
+
* @param {object[]} [options.defs] field definitions for non-special headers
|
|
14
|
+
* @returns {object[]} one request body per row
|
|
15
|
+
*/
|
|
16
|
+
export function prepareImportBodies({
|
|
17
|
+
headers,
|
|
18
|
+
rows,
|
|
19
|
+
specialColumns = {},
|
|
20
|
+
defs,
|
|
21
|
+
}) {
|
|
22
|
+
const specials = Object.fromEntries(
|
|
23
|
+
Object.entries(specialColumns).map(([k, v]) => [k.toLowerCase(), v]),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return rows.map((row, index) => {
|
|
27
|
+
const typed = {}
|
|
28
|
+
const fields = []
|
|
29
|
+
|
|
30
|
+
headers.forEach((header, i) => {
|
|
31
|
+
const value = row[i]
|
|
32
|
+
if (value === '') return
|
|
33
|
+
const special = specials[header.toLowerCase()]
|
|
34
|
+
if (special) {
|
|
35
|
+
special(typed, value)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
fields.push(`${header}=${value}`)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
return buildWriteBody({ typed, fields, defs })
|
|
43
|
+
} catch (err) {
|
|
44
|
+
throw new CliError(`CSV row ${index + 2}: ${err.message}`, {
|
|
45
|
+
exitCode: 65,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|