@wavyx/pdcli 0.9.0 → 0.11.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +15 -0
  3. package/oclif.manifest.json +7590 -1565
  4. package/package.json +32 -2
  5. package/src/commands/activity/list.js +47 -5
  6. package/src/commands/activity/type/list.js +36 -0
  7. package/src/commands/deal/convert.js +112 -0
  8. package/src/commands/deal/follower/add.js +31 -0
  9. package/src/commands/deal/follower/list.js +38 -0
  10. package/src/commands/deal/follower/remove.js +44 -0
  11. package/src/commands/deal/history.js +73 -0
  12. package/src/commands/deal/list.js +47 -6
  13. package/src/commands/deal/participant/add.js +31 -0
  14. package/src/commands/deal/participant/list.js +46 -0
  15. package/src/commands/deal/participant/remove.js +53 -0
  16. package/src/commands/deal/product/add.js +69 -0
  17. package/src/commands/deal/product/list.js +56 -0
  18. package/src/commands/deal/product/remove.js +52 -0
  19. package/src/commands/deal/product/update.js +78 -0
  20. package/src/commands/deal/summary.js +69 -0
  21. package/src/commands/field/create.js +72 -0
  22. package/src/commands/field/delete.js +56 -0
  23. package/src/commands/field/get.js +10 -1
  24. package/src/commands/field/list.js +10 -1
  25. package/src/commands/field/option/add.js +49 -0
  26. package/src/commands/field/option/remove.js +72 -0
  27. package/src/commands/field/update.js +53 -0
  28. package/src/commands/file/delete.js +41 -0
  29. package/src/commands/file/update.js +39 -0
  30. package/src/commands/filter/delete.js +41 -0
  31. package/src/commands/funnel.js +7 -49
  32. package/src/commands/lead/convert.js +103 -0
  33. package/src/commands/note/comment/add.js +31 -0
  34. package/src/commands/note/comment/delete.js +51 -0
  35. package/src/commands/note/comment/list.js +43 -0
  36. package/src/commands/note/comment/update.js +37 -0
  37. package/src/commands/org/follower/add.js +31 -0
  38. package/src/commands/org/follower/list.js +41 -0
  39. package/src/commands/org/follower/remove.js +50 -0
  40. package/src/commands/org/list.js +40 -2
  41. package/src/commands/org/relationship/add.js +44 -0
  42. package/src/commands/org/relationship/list.js +52 -0
  43. package/src/commands/org/relationship/remove.js +46 -0
  44. package/src/commands/person/follower/add.js +31 -0
  45. package/src/commands/person/follower/list.js +38 -0
  46. package/src/commands/person/follower/remove.js +48 -0
  47. package/src/commands/person/list.js +40 -2
  48. package/src/commands/product/list.js +35 -2
  49. package/src/commands/project/list.js +10 -4
  50. package/src/commands/search.js +66 -9
  51. package/src/commands/task/create.js +44 -0
  52. package/src/commands/task/delete.js +41 -0
  53. package/src/commands/task/get.js +26 -0
  54. package/src/commands/task/list.js +60 -0
  55. package/src/commands/task/update.js +72 -0
  56. package/src/commands/user/find.js +50 -0
  57. package/src/commands/user/list.js +36 -0
  58. package/src/commands/webhook/create.js +8 -2
  59. package/src/lib/changelog.js +85 -0
  60. package/src/lib/client.js +44 -0
  61. package/src/lib/fields.js +43 -12
@@ -0,0 +1,53 @@
1
+ import { Args, Flags } from '@oclif/core'
2
+ import chalk from 'chalk'
3
+ import BaseCommand from '../../../base-command.js'
4
+ import { confirmAction } from '../../../lib/confirm.js'
5
+ import { CliError } from '../../../lib/errors.js'
6
+
7
+ export default class DealParticipantRemoveCommand extends BaseCommand {
8
+ static description = 'Remove a participant from a deal'
9
+
10
+ static examples = [
11
+ '<%= config.bin %> deal participant remove 42 --participant 3',
12
+ '<%= config.bin %> deal participant remove 42 --participant 3 --yes',
13
+ ]
14
+
15
+ static args = {
16
+ id: Args.integer({ required: true, description: 'Deal ID' }),
17
+ }
18
+
19
+ static flags = {
20
+ ...BaseCommand.baseFlags,
21
+ participant: Flags.integer({
22
+ required: true,
23
+ description: 'Deal-participant ID',
24
+ }),
25
+ yes: Flags.boolean({
26
+ char: 'y',
27
+ description: 'Skip the confirmation prompt',
28
+ default: false,
29
+ }),
30
+ }
31
+
32
+ async run() {
33
+ const { args, flags } = await this.parse(DealParticipantRemoveCommand)
34
+
35
+ const ok = await confirmAction(
36
+ `Remove participant ${flags.participant} from deal ${args.id}?`,
37
+ flags.yes,
38
+ { default: false },
39
+ )
40
+ if (!ok) {
41
+ throw new CliError('Aborted', { exitCode: 1 })
42
+ }
43
+
44
+ await this.apiClient.del(
45
+ `/api/v1/deals/${args.id}/participants/${flags.participant}`,
46
+ )
47
+ this.log(
48
+ chalk.green(
49
+ `Removed participant ${flags.participant} from deal ${args.id}`,
50
+ ),
51
+ )
52
+ }
53
+ }
@@ -0,0 +1,69 @@
1
+ import { Args, Flags } from '@oclif/core'
2
+ import BaseCommand from '../../../base-command.js'
3
+ import { CliError } from '../../../lib/errors.js'
4
+ import { buildWriteBody } from '../../../lib/input.js'
5
+ import { outputRecord } from '../../../lib/entity-view.js'
6
+
7
+ /** Coerce a numeric flag, failing with a clean input error on garbage. */
8
+ function num(name, value) {
9
+ if (value === undefined) return undefined
10
+ const n = Number(value)
11
+ if (!Number.isFinite(n)) {
12
+ throw new CliError(`Invalid number for --${name}: "${value}"`, {
13
+ exitCode: 64,
14
+ })
15
+ }
16
+ return n
17
+ }
18
+
19
+ export default class DealProductAddCommand extends BaseCommand {
20
+ static description = 'Attach a product to a deal'
21
+
22
+ static examples = [
23
+ '<%= config.bin %> deal product add 42 --product 10 --price 90',
24
+ '<%= config.bin %> deal product add 42 --product 10 --price 90 --quantity 3',
25
+ '<%= config.bin %> deal product add 42 --product 10 --price 90 --discount 10 --discount-type percentage',
26
+ ]
27
+
28
+ static args = {
29
+ id: Args.integer({ required: true, description: 'Deal ID' }),
30
+ }
31
+
32
+ static flags = {
33
+ ...BaseCommand.baseFlags,
34
+ product: Flags.integer({ required: true, description: 'Product ID' }),
35
+ price: Flags.string({
36
+ required: true,
37
+ description: 'Item price (per unit)',
38
+ }),
39
+ quantity: Flags.string({ description: 'Quantity', default: '1' }),
40
+ discount: Flags.string({ description: 'Discount value' }),
41
+ 'discount-type': Flags.string({
42
+ description: 'Discount type',
43
+ options: ['percentage', 'amount'],
44
+ }),
45
+ tax: Flags.string({ description: 'Product tax percentage' }),
46
+ comments: Flags.string({ description: 'Comments' }),
47
+ }
48
+
49
+ async run() {
50
+ const { args, flags } = await this.parse(DealProductAddCommand)
51
+
52
+ const body = buildWriteBody({
53
+ typed: {
54
+ product_id: flags.product,
55
+ item_price: num('price', flags.price),
56
+ quantity: num('quantity', flags.quantity),
57
+ discount: num('discount', flags.discount),
58
+ discount_type: flags['discount-type'],
59
+ tax: num('tax', flags.tax),
60
+ comments: flags.comments,
61
+ },
62
+ })
63
+
64
+ const res = await this.apiClient.post(`/api/v2/deals/${args.id}/products`, {
65
+ body,
66
+ })
67
+ await outputRecord(this, res.data)
68
+ }
69
+ }
@@ -0,0 +1,56 @@
1
+ import { Args, Flags } from '@oclif/core'
2
+ import BaseCommand from '../../../base-command.js'
3
+ import { collectPages } from '../../../lib/pagination.js'
4
+
5
+ const columns = {
6
+ id: { header: 'ID' },
7
+ product_id: { header: 'Product' },
8
+ name: { header: 'Name' },
9
+ item_price: { header: 'Item price' },
10
+ quantity: { header: 'Qty' },
11
+ discount: { header: 'Discount' },
12
+ sum: { header: 'Sum' },
13
+ }
14
+
15
+ export default class DealProductListCommand extends BaseCommand {
16
+ static description = 'List products attached to a deal'
17
+
18
+ static examples = [
19
+ '<%= config.bin %> deal product list 42',
20
+ '<%= config.bin %> deal product list 42 --sort-by add_time --sort-direction desc',
21
+ '<%= config.bin %> deal product list 42 --output json',
22
+ ]
23
+
24
+ static args = {
25
+ id: Args.integer({ required: true, description: 'Deal ID' }),
26
+ }
27
+
28
+ static flags = {
29
+ ...BaseCommand.baseFlags,
30
+ 'sort-by': Flags.string({
31
+ description: 'Field to sort by',
32
+ options: ['id', 'add_time', 'update_time', 'order_nr'],
33
+ }),
34
+ 'sort-direction': Flags.string({
35
+ description: 'Sort direction',
36
+ options: ['asc', 'desc'],
37
+ }),
38
+ }
39
+
40
+ async run() {
41
+ const { args, flags } = await this.parse(DealProductListCommand)
42
+ const limit = flags.limit ?? 500
43
+
44
+ const query = {
45
+ sort_by: flags['sort-by'],
46
+ sort_direction: flags['sort-direction'],
47
+ limit: Math.min(limit, 500),
48
+ }
49
+
50
+ const items = await collectPages(
51
+ this.apiClient.pageV2(`/api/v2/deals/${args.id}/products`, query),
52
+ limit,
53
+ )
54
+ await this.outputResults(items, columns)
55
+ }
56
+ }
@@ -0,0 +1,52 @@
1
+ import { Args, Flags } from '@oclif/core'
2
+ import chalk from 'chalk'
3
+ import BaseCommand from '../../../base-command.js'
4
+ import { confirmAction } from '../../../lib/confirm.js'
5
+ import { CliError } from '../../../lib/errors.js'
6
+
7
+ export default class DealProductRemoveCommand extends BaseCommand {
8
+ static description = 'Remove a product attached to a deal'
9
+
10
+ static examples = [
11
+ '<%= config.bin %> deal product remove 42 --attachment 3',
12
+ '<%= config.bin %> deal product remove 42 --attachment 3 --yes',
13
+ ]
14
+
15
+ static args = {
16
+ id: Args.integer({ required: true, description: 'Deal ID' }),
17
+ }
18
+
19
+ static flags = {
20
+ ...BaseCommand.baseFlags,
21
+ attachment: Flags.integer({
22
+ required: true,
23
+ description: 'Deal-product (attachment) ID',
24
+ }),
25
+ yes: Flags.boolean({
26
+ char: 'y',
27
+ description: 'Skip the confirmation prompt',
28
+ default: false,
29
+ }),
30
+ }
31
+
32
+ async run() {
33
+ const { args, flags } = await this.parse(DealProductRemoveCommand)
34
+
35
+ const ok = await confirmAction(
36
+ `Remove product attachment ${flags.attachment} from deal ${args.id}?`,
37
+ flags.yes,
38
+ )
39
+ if (!ok) {
40
+ throw new CliError('Aborted', { exitCode: 1 })
41
+ }
42
+
43
+ await this.apiClient.del(
44
+ `/api/v2/deals/${args.id}/products/${flags.attachment}`,
45
+ )
46
+ this.log(
47
+ chalk.green(
48
+ `Removed product attachment ${flags.attachment} from deal ${args.id}`,
49
+ ),
50
+ )
51
+ }
52
+ }
@@ -0,0 +1,78 @@
1
+ import { Args, Flags } from '@oclif/core'
2
+ import BaseCommand from '../../../base-command.js'
3
+ import { buildWriteBody } from '../../../lib/input.js'
4
+ import { outputRecord } from '../../../lib/entity-view.js'
5
+ import { CliError } from '../../../lib/errors.js'
6
+
7
+ /** Coerce a numeric flag, failing with a clean input error on garbage. */
8
+ function num(name, value) {
9
+ if (value === undefined) return undefined
10
+ const n = Number(value)
11
+ if (!Number.isFinite(n)) {
12
+ throw new CliError(`Invalid number for --${name}: "${value}"`, {
13
+ exitCode: 64,
14
+ })
15
+ }
16
+ return n
17
+ }
18
+
19
+ export default class DealProductUpdateCommand extends BaseCommand {
20
+ static description =
21
+ 'Update a product attached to a deal (v2 PATCH — only provided fields change)'
22
+
23
+ static examples = [
24
+ '<%= config.bin %> deal product update 42 --attachment 3 --quantity 5',
25
+ '<%= config.bin %> deal product update 42 --attachment 3 --price 120',
26
+ '<%= config.bin %> deal product update 42 --attachment 3 --discount 15 --discount-type amount',
27
+ ]
28
+
29
+ static args = {
30
+ id: Args.integer({ required: true, description: 'Deal ID' }),
31
+ }
32
+
33
+ static flags = {
34
+ ...BaseCommand.baseFlags,
35
+ attachment: Flags.integer({
36
+ required: true,
37
+ description: 'Deal-product (attachment) ID',
38
+ }),
39
+ product: Flags.integer({ description: 'Product ID' }),
40
+ price: Flags.string({ description: 'Item price (per unit)' }),
41
+ quantity: Flags.string({ description: 'Quantity' }),
42
+ discount: Flags.string({ description: 'Discount value' }),
43
+ 'discount-type': Flags.string({
44
+ description: 'Discount type',
45
+ options: ['percentage', 'amount'],
46
+ }),
47
+ tax: Flags.string({ description: 'Product tax percentage' }),
48
+ comments: Flags.string({ description: 'Comments' }),
49
+ }
50
+
51
+ async run() {
52
+ const { args, flags } = await this.parse(DealProductUpdateCommand)
53
+
54
+ const body = buildWriteBody({
55
+ typed: {
56
+ product_id: flags.product,
57
+ item_price: num('price', flags.price),
58
+ quantity: num('quantity', flags.quantity),
59
+ discount: num('discount', flags.discount),
60
+ discount_type: flags['discount-type'],
61
+ tax: num('tax', flags.tax),
62
+ comments: flags.comments,
63
+ },
64
+ })
65
+
66
+ if (Object.keys(body).length === 0) {
67
+ throw new CliError('Nothing to update — pass at least one field flag', {
68
+ exitCode: 64,
69
+ })
70
+ }
71
+
72
+ const res = await this.apiClient.patch(
73
+ `/api/v2/deals/${args.id}/products/${flags.attachment}`,
74
+ { body },
75
+ )
76
+ await outputRecord(this, res.data)
77
+ }
78
+ }
@@ -0,0 +1,69 @@
1
+ import { Flags } from '@oclif/core'
2
+ import BaseCommand from '../../base-command.js'
3
+
4
+ const columns = {
5
+ currency: { header: 'Currency' },
6
+ total: { header: 'Total' },
7
+ weighted: { header: 'Weighted' },
8
+ count: { header: 'Count' },
9
+ }
10
+
11
+ export default class DealSummaryCommand extends BaseCommand {
12
+ static description = 'Summary of open/won/lost deals, totalled per currency'
13
+
14
+ static examples = [
15
+ '<%= config.bin %> deal summary',
16
+ '<%= config.bin %> deal summary --status open --pipeline 1',
17
+ '<%= config.bin %> deal summary --output json',
18
+ ]
19
+
20
+ static flags = {
21
+ ...BaseCommand.baseFlags,
22
+ status: Flags.string({
23
+ description: 'Filter by status',
24
+ options: ['open', 'won', 'lost'],
25
+ }),
26
+ pipeline: Flags.integer({ description: 'Filter by pipeline ID' }),
27
+ stage: Flags.integer({ description: 'Filter by stage ID' }),
28
+ filter: Flags.integer({ description: 'Filter by saved filter ID' }),
29
+ }
30
+
31
+ async run() {
32
+ const { flags } = await this.parse(DealSummaryCommand)
33
+
34
+ // Summary lives on v1; the v2 wrapper does not exist. Cost: 40 tokens.
35
+ const body = await this.apiClient.get('/api/v1/deals/summary', {
36
+ query: {
37
+ status: flags.status,
38
+ pipeline_id: flags.pipeline,
39
+ stage_id: flags.stage,
40
+ filter_id: flags.filter,
41
+ },
42
+ })
43
+
44
+ const data = body.data ?? {}
45
+
46
+ // Non-table formats stay raw for scriptability: emit the data object as-is
47
+ // (per-currency totals, grand totals, counts).
48
+ if (this.resolveFormat() !== 'table') {
49
+ await this.outputResults(data, columns)
50
+ return
51
+ }
52
+
53
+ // Table view: one row per deal currency. Totals come pre-formatted from the
54
+ // API (`value_formatted`, e.g. "€10") — render those rather than re-deriving
55
+ // currency symbols. Weighted totals are keyed by the same currency code.
56
+ const values = data.values_total ?? {}
57
+ const weighted = data.weighted_values_total ?? {}
58
+ // `get` returning undefined renders as an empty cell, so no `?? ''` tail.
59
+ const rows = Object.entries(values).map(([currency, totals]) => ({
60
+ currency,
61
+ total: totals.value_formatted ?? totals.value,
62
+ weighted:
63
+ weighted[currency]?.value_formatted ?? weighted[currency]?.value,
64
+ count: totals.count,
65
+ }))
66
+
67
+ await this.outputResults(rows, columns)
68
+ }
69
+ }
@@ -0,0 +1,72 @@
1
+ import { Args, Flags } from '@oclif/core'
2
+ import BaseCommand from '../../base-command.js'
3
+ import { clearFieldsCache, entityToFieldsPath } from '../../lib/fields.js'
4
+ import { CliError } from '../../lib/errors.js'
5
+
6
+ /** Option-bearing field types that require --options on creation. */
7
+ const OPTION_TYPES = new Set(['enum', 'set'])
8
+
9
+ export default class FieldCreateCommand extends BaseCommand {
10
+ static description = 'Create a custom field on an entity'
11
+
12
+ static examples = [
13
+ '<%= config.bin %> field create deal --name "Budget" --type double',
14
+ '<%= config.bin %> field create person --name "Tier" --type enum --options "Gold,Silver,Bronze"',
15
+ ]
16
+
17
+ static args = {
18
+ entity: Args.string({
19
+ required: true,
20
+ description: 'Entity type',
21
+ // Custom fields are writable on core v2 entities only — activity fields
22
+ // are read-only, and lead/note have no v2 write path.
23
+ options: ['deal', 'person', 'org', 'organization', 'product'],
24
+ }),
25
+ }
26
+
27
+ static flags = {
28
+ ...BaseCommand.baseFlags,
29
+ name: Flags.string({ required: true, description: 'Field name (label)' }),
30
+ type: Flags.string({
31
+ required: true,
32
+ description: 'Field type (e.g. varchar, double, monetary, enum, set)',
33
+ }),
34
+ options: Flags.string({
35
+ description: 'Comma-separated option labels (required for enum/set)',
36
+ }),
37
+ }
38
+
39
+ async run() {
40
+ const { args, flags } = await this.parse(FieldCreateCommand)
41
+
42
+ const isOptionType = OPTION_TYPES.has(flags.type)
43
+ if (isOptionType && !flags.options) {
44
+ throw new CliError(
45
+ `Field type "${flags.type}" requires --options "A,B,C"`,
46
+ { exitCode: 64 },
47
+ )
48
+ }
49
+
50
+ const body = { field_name: flags.name, field_type: flags.type }
51
+ if (isOptionType) {
52
+ body.options = flags.options
53
+ .split(',')
54
+ .map((label) => ({ label: label.trim() }))
55
+ }
56
+
57
+ const path = entityToFieldsPath(args.entity)
58
+ const res = await this.apiClient.post(path, { body })
59
+
60
+ clearFieldsCache()
61
+
62
+ await this.outputResults(res.data, {
63
+ field_code: { header: 'Key' },
64
+ field_name: { header: 'Name' },
65
+ field_type: { header: 'Type' },
66
+ options: {
67
+ header: 'Options',
68
+ get: (row) => row.options?.map((o) => `${o.id}=${o.label}`).join(', '),
69
+ },
70
+ })
71
+ }
72
+ }
@@ -0,0 +1,56 @@
1
+ import { Args, Flags } from '@oclif/core'
2
+ import chalk from 'chalk'
3
+ import BaseCommand from '../../base-command.js'
4
+ import { confirmAction } from '../../lib/confirm.js'
5
+ import { clearFieldsCache, entityToFieldsPath } from '../../lib/fields.js'
6
+ import { CliError } from '../../lib/errors.js'
7
+
8
+ export default class FieldDeleteCommand extends BaseCommand {
9
+ static description = 'Delete a custom field (data stored on records is lost)'
10
+
11
+ static examples = [
12
+ '<%= config.bin %> field delete deal dcf558aac1ae4e8c4f849ba5e668430d8df9be12',
13
+ '<%= config.bin %> field delete deal dcf558aac1ae4e8c4f849ba5e668430d8df9be12 --yes',
14
+ ]
15
+
16
+ static args = {
17
+ entity: Args.string({
18
+ required: true,
19
+ description: 'Entity type',
20
+ options: ['deal', 'person', 'org', 'organization', 'product'],
21
+ }),
22
+ field: Args.string({
23
+ required: true,
24
+ description: 'Field code (hashed key)',
25
+ }),
26
+ }
27
+
28
+ static flags = {
29
+ ...BaseCommand.baseFlags,
30
+ yes: Flags.boolean({
31
+ char: 'y',
32
+ description: 'Skip the confirmation prompt',
33
+ default: false,
34
+ }),
35
+ }
36
+
37
+ async run() {
38
+ const { args, flags } = await this.parse(FieldDeleteCommand)
39
+
40
+ const ok = await confirmAction(
41
+ `Delete field ${args.field} on ${args.entity}? ` +
42
+ `All data stored in this field on existing records is lost.`,
43
+ flags.yes,
44
+ { default: false },
45
+ )
46
+ if (!ok) {
47
+ throw new CliError('Aborted', { exitCode: 1 })
48
+ }
49
+
50
+ await this.apiClient.del(`${entityToFieldsPath(args.entity)}/${args.field}`)
51
+
52
+ clearFieldsCache()
53
+
54
+ this.log(chalk.green(`Deleted field ${args.field} on ${args.entity}`))
55
+ }
56
+ }
@@ -15,7 +15,16 @@ export default class FieldGetCommand extends BaseCommand {
15
15
  entity: Args.string({
16
16
  required: true,
17
17
  description: 'Entity type',
18
- options: ['deal', 'person', 'org', 'organization', 'product', 'activity'],
18
+ options: [
19
+ 'deal',
20
+ 'person',
21
+ 'org',
22
+ 'organization',
23
+ 'product',
24
+ 'activity',
25
+ 'lead',
26
+ 'note',
27
+ ],
19
28
  }),
20
29
  field: Args.string({
21
30
  required: true,
@@ -15,7 +15,16 @@ export default class FieldListCommand extends BaseCommand {
15
15
  entity: Args.string({
16
16
  required: true,
17
17
  description: 'Entity type',
18
- options: ['deal', 'person', 'org', 'organization', 'product', 'activity'],
18
+ options: [
19
+ 'deal',
20
+ 'person',
21
+ 'org',
22
+ 'organization',
23
+ 'product',
24
+ 'activity',
25
+ 'lead',
26
+ 'note',
27
+ ],
19
28
  }),
20
29
  }
21
30
 
@@ -0,0 +1,49 @@
1
+ import { Args, Flags } from '@oclif/core'
2
+ import BaseCommand from '../../../base-command.js'
3
+ import { clearFieldsCache, entityToFieldsPath } from '../../../lib/fields.js'
4
+
5
+ export default class FieldOptionAddCommand extends BaseCommand {
6
+ static description = 'Add an option to an enum/set custom field'
7
+
8
+ static examples = [
9
+ '<%= config.bin %> field option add deal dcf558aac1ae4e8c4f849ba5e668430d8df9be12 --label "Critical"',
10
+ ]
11
+
12
+ static args = {
13
+ entity: Args.string({
14
+ required: true,
15
+ description: 'Entity type',
16
+ options: ['deal', 'person', 'org', 'organization', 'product'],
17
+ }),
18
+ field: Args.string({
19
+ required: true,
20
+ description: 'Field code (hashed key)',
21
+ }),
22
+ }
23
+
24
+ static flags = {
25
+ ...BaseCommand.baseFlags,
26
+ label: Flags.string({
27
+ required: true,
28
+ description: 'Label for the new option',
29
+ }),
30
+ }
31
+
32
+ async run() {
33
+ const { args, flags } = await this.parse(FieldOptionAddCommand)
34
+
35
+ const res = await this.apiClient.post(
36
+ `${entityToFieldsPath(args.entity)}/${args.field}/options`,
37
+ { body: [{ label: flags.label }] },
38
+ )
39
+
40
+ // Option ids feed the per-run resolver — invalidate so the new option
41
+ // resolves immediately within this process.
42
+ clearFieldsCache()
43
+
44
+ await this.outputResults(res.data ?? [], {
45
+ id: { header: 'ID' },
46
+ label: { header: 'Label' },
47
+ })
48
+ }
49
+ }
@@ -0,0 +1,72 @@
1
+ import { Args, Flags } from '@oclif/core'
2
+ import BaseCommand from '../../../base-command.js'
3
+ import { confirmAction } from '../../../lib/confirm.js'
4
+ import { clearFieldsCache, entityToFieldsPath } from '../../../lib/fields.js'
5
+ import { CliError } from '../../../lib/errors.js'
6
+
7
+ export default class FieldOptionRemoveCommand extends BaseCommand {
8
+ static description =
9
+ 'Remove an option from an enum/set custom field (records lose the value)'
10
+
11
+ static examples = [
12
+ '<%= config.bin %> field option remove deal dcf558aac1ae4e8c4f849ba5e668430d8df9be12 --option 4',
13
+ ]
14
+
15
+ static args = {
16
+ entity: Args.string({
17
+ required: true,
18
+ description: 'Entity type',
19
+ options: ['deal', 'person', 'org', 'organization', 'product'],
20
+ }),
21
+ field: Args.string({
22
+ required: true,
23
+ description: 'Field code (hashed key)',
24
+ }),
25
+ }
26
+
27
+ static flags = {
28
+ ...BaseCommand.baseFlags,
29
+ option: Flags.string({
30
+ required: true,
31
+ description: 'Option ID to remove (see field get)',
32
+ }),
33
+ yes: Flags.boolean({
34
+ char: 'y',
35
+ description: 'Skip the confirmation prompt',
36
+ default: false,
37
+ }),
38
+ }
39
+
40
+ async run() {
41
+ const { args, flags } = await this.parse(FieldOptionRemoveCommand)
42
+
43
+ const optionId = Number(flags.option)
44
+ if (!Number.isInteger(optionId)) {
45
+ throw new CliError(`Invalid option id: "${flags.option}"`, {
46
+ exitCode: 64,
47
+ })
48
+ }
49
+
50
+ const ok = await confirmAction(
51
+ `Remove option ${optionId} from field ${args.field} on ${args.entity}? ` +
52
+ `Records using it lose the value.`,
53
+ flags.yes,
54
+ { default: false },
55
+ )
56
+ if (!ok) {
57
+ throw new CliError('Aborted', { exitCode: 1 })
58
+ }
59
+
60
+ const res = await this.apiClient.del(
61
+ `${entityToFieldsPath(args.entity)}/${args.field}/options`,
62
+ { body: [{ id: optionId }] },
63
+ )
64
+
65
+ clearFieldsCache()
66
+
67
+ await this.outputResults(res.data ?? [], {
68
+ id: { header: 'ID' },
69
+ label: { header: 'Label' },
70
+ })
71
+ }
72
+ }