@wavyx/pdcli 0.10.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 (47) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +7 -1
  3. package/oclif.manifest.json +6369 -1598
  4. package/package.json +29 -2
  5. package/src/commands/activity/type/list.js +36 -0
  6. package/src/commands/deal/convert.js +112 -0
  7. package/src/commands/deal/follower/add.js +31 -0
  8. package/src/commands/deal/follower/list.js +38 -0
  9. package/src/commands/deal/follower/remove.js +44 -0
  10. package/src/commands/deal/list.js +7 -4
  11. package/src/commands/deal/participant/add.js +31 -0
  12. package/src/commands/deal/participant/list.js +46 -0
  13. package/src/commands/deal/participant/remove.js +53 -0
  14. package/src/commands/deal/summary.js +69 -0
  15. package/src/commands/field/create.js +72 -0
  16. package/src/commands/field/delete.js +56 -0
  17. package/src/commands/field/get.js +10 -1
  18. package/src/commands/field/list.js +10 -1
  19. package/src/commands/field/option/add.js +49 -0
  20. package/src/commands/field/option/remove.js +72 -0
  21. package/src/commands/field/update.js +53 -0
  22. package/src/commands/file/delete.js +41 -0
  23. package/src/commands/file/update.js +39 -0
  24. package/src/commands/filter/delete.js +41 -0
  25. package/src/commands/lead/convert.js +103 -0
  26. package/src/commands/note/comment/add.js +31 -0
  27. package/src/commands/note/comment/delete.js +51 -0
  28. package/src/commands/note/comment/list.js +43 -0
  29. package/src/commands/note/comment/update.js +37 -0
  30. package/src/commands/org/follower/add.js +31 -0
  31. package/src/commands/org/follower/list.js +41 -0
  32. package/src/commands/org/follower/remove.js +50 -0
  33. package/src/commands/org/relationship/add.js +44 -0
  34. package/src/commands/org/relationship/list.js +52 -0
  35. package/src/commands/org/relationship/remove.js +46 -0
  36. package/src/commands/person/follower/add.js +31 -0
  37. package/src/commands/person/follower/list.js +38 -0
  38. package/src/commands/person/follower/remove.js +48 -0
  39. package/src/commands/project/list.js +10 -4
  40. package/src/commands/search.js +66 -9
  41. package/src/commands/task/create.js +44 -0
  42. package/src/commands/task/delete.js +41 -0
  43. package/src/commands/task/get.js +26 -0
  44. package/src/commands/task/list.js +60 -0
  45. package/src/commands/task/update.js +72 -0
  46. package/src/lib/client.js +19 -0
  47. package/src/lib/fields.js +43 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wavyx/pdcli",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -65,8 +65,14 @@
65
65
  "activity": {
66
66
  "description": "Activities (calls, meetings, tasks)"
67
67
  },
68
+ "activity:type": {
69
+ "description": "Activity types (key_string is what activity --type takes)"
70
+ },
71
+ "task": {
72
+ "description": "Tasks (project action items)"
73
+ },
68
74
  "field": {
69
- "description": "Custom-field discovery and name/key resolution"
75
+ "description": "Custom-field management: discovery, name/key resolution, create/update/delete"
70
76
  },
71
77
  "metrics": {
72
78
  "description": "Sales metrics and KPIs"
@@ -85,6 +91,27 @@
85
91
  },
86
92
  "deal:product": {
87
93
  "description": "Manage products (line items) on a deal"
94
+ },
95
+ "deal:participant": {
96
+ "description": "Manage extra people on a deal (buying committee)"
97
+ },
98
+ "deal:follower": {
99
+ "description": "Manage deal followers"
100
+ },
101
+ "person:follower": {
102
+ "description": "Manage person followers"
103
+ },
104
+ "org:follower": {
105
+ "description": "Manage organization followers"
106
+ },
107
+ "org:relationship": {
108
+ "description": "Manage parent/related links between organizations"
109
+ },
110
+ "field:option": {
111
+ "description": "Manage enum/set field options"
112
+ },
113
+ "note:comment": {
114
+ "description": "Manage comments on a note"
88
115
  }
89
116
  },
90
117
  "hooks": {
@@ -0,0 +1,36 @@
1
+ import BaseCommand from '../../../base-command.js'
2
+
3
+ const columns = {
4
+ id: { header: 'ID' },
5
+ key_string: { header: 'Key' },
6
+ name: { header: 'Name' },
7
+ active: {
8
+ header: 'Active',
9
+ get: (row) => (row.active_flag ? 'yes' : 'no'),
10
+ },
11
+ }
12
+
13
+ export default class ActivityTypeListCommand extends BaseCommand {
14
+ static description =
15
+ 'List activity types. The Key (key_string) is what `activity --type` takes.'
16
+
17
+ static examples = [
18
+ '<%= config.bin %> activity type list',
19
+ '<%= config.bin %> activity type list --output json',
20
+ ]
21
+
22
+ static flags = {
23
+ ...BaseCommand.baseFlags,
24
+ }
25
+
26
+ async run() {
27
+ const { flags } = await this.parse(ActivityTypeListCommand)
28
+
29
+ // Activity types are v1-only and returned in one unpaginated array.
30
+ const body = await this.apiClient.get('/api/v1/activityTypes')
31
+ let types = body.data ?? []
32
+ if (flags.limit != null) types = types.slice(0, flags.limit)
33
+
34
+ await this.outputResults(types, columns)
35
+ }
36
+ }
@@ -0,0 +1,112 @@
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
+ const POLL_INTERVAL_MS = 2000
8
+
9
+ function defaultSleep(ms) {
10
+ return new Promise((resolve) => setTimeout(resolve, ms))
11
+ }
12
+
13
+ export default class DealConvertCommand extends BaseCommand {
14
+ static description =
15
+ 'Convert a deal to a lead. The conversion runs as an async job; ' +
16
+ 'use --wait to poll until it finishes. WARNING: on success the source ' +
17
+ 'deal is deleted.'
18
+
19
+ static examples = [
20
+ '<%= config.bin %> deal convert 42',
21
+ '<%= config.bin %> deal convert 42 --yes',
22
+ '<%= config.bin %> deal convert 42 --wait',
23
+ ]
24
+
25
+ static args = {
26
+ id: Args.integer({ required: true, description: 'Deal ID' }),
27
+ }
28
+
29
+ static flags = {
30
+ ...BaseCommand.baseFlags,
31
+ wait: Flags.boolean({
32
+ description: 'Poll the conversion status until it finishes',
33
+ default: false,
34
+ }),
35
+ 'timeout-secs': Flags.integer({
36
+ description: 'Max seconds to poll when --wait is set',
37
+ default: 30,
38
+ }),
39
+ yes: Flags.boolean({
40
+ char: 'y',
41
+ description: 'Skip the confirmation prompt',
42
+ default: false,
43
+ }),
44
+ }
45
+
46
+ /** Overridable in tests so polling never waits in real time. */
47
+ static sleepFn = defaultSleep
48
+
49
+ async run() {
50
+ const { args, flags } = await this.parse(DealConvertCommand)
51
+
52
+ const ok = await confirmAction(
53
+ `Convert deal ${args.id} to a lead? Deal ${args.id} will be DELETED on success.`,
54
+ flags.yes,
55
+ { default: false },
56
+ )
57
+ if (!ok) {
58
+ throw new CliError('Aborted', { exitCode: 1 })
59
+ }
60
+
61
+ const res = await this.apiClient.post(
62
+ `/api/v2/deals/${args.id}/convert/lead`,
63
+ // The endpoint takes no parameters, but the client sets a JSON
64
+ // content-type — the API 400s on an empty body, so send {}.
65
+ { body: {} },
66
+ )
67
+ const conversionId = res.data?.conversion_id
68
+
69
+ if (!flags.wait) {
70
+ this.log(chalk.green(`Conversion started: ${conversionId}`))
71
+ this.log(
72
+ `Check status: ${this.config.bin} api GET ` +
73
+ `/api/v2/deals/${args.id}/convert/status/${conversionId}`,
74
+ )
75
+ return
76
+ }
77
+
78
+ const timeoutMs = flags['timeout-secs'] * 1000
79
+ const sleep = DealConvertCommand.sleepFn
80
+ let elapsed = 0
81
+ while (true) {
82
+ const status = await this.apiClient.get(
83
+ `/api/v2/deals/${args.id}/convert/status/${conversionId}`,
84
+ )
85
+ const state = status.data?.status
86
+ if (state === 'completed') {
87
+ this.log(
88
+ chalk.green(
89
+ `Conversion completed: deal ${args.id} → lead ${status.data?.lead_id}`,
90
+ ),
91
+ )
92
+ return
93
+ }
94
+ if (state === 'failed' || state === 'rejected') {
95
+ throw new CliError(`Conversion ${state} for deal ${args.id}`, {
96
+ exitCode: 70,
97
+ })
98
+ }
99
+ if (elapsed + POLL_INTERVAL_MS > timeoutMs) {
100
+ throw new CliError(
101
+ `Timed out after ${flags['timeout-secs']}s waiting for conversion ` +
102
+ `${conversionId} (last status: ${state}). ` +
103
+ `Check status: ${this.config.bin} api GET ` +
104
+ `/api/v2/deals/${args.id}/convert/status/${conversionId}`,
105
+ { exitCode: 1 },
106
+ )
107
+ }
108
+ await sleep(POLL_INTERVAL_MS)
109
+ elapsed += POLL_INTERVAL_MS
110
+ }
111
+ }
112
+ }
@@ -0,0 +1,31 @@
1
+ import { Args, Flags } from '@oclif/core'
2
+ import BaseCommand from '../../../base-command.js'
3
+ import { outputRecord } from '../../../lib/entity-view.js'
4
+
5
+ export default class DealFollowerAddCommand extends BaseCommand {
6
+ static description = 'Add a follower (user) to a deal'
7
+
8
+ static examples = [
9
+ '<%= config.bin %> deal follower add 42 --user 5',
10
+ '<%= config.bin %> deal follower add 42 --user 5 --output json',
11
+ ]
12
+
13
+ static args = {
14
+ id: Args.integer({ required: true, description: 'Deal ID' }),
15
+ }
16
+
17
+ static flags = {
18
+ ...BaseCommand.baseFlags,
19
+ user: Flags.integer({ required: true, description: 'User ID' }),
20
+ }
21
+
22
+ async run() {
23
+ const { args, flags } = await this.parse(DealFollowerAddCommand)
24
+
25
+ const res = await this.apiClient.post(
26
+ `/api/v2/deals/${args.id}/followers`,
27
+ { body: { user_id: flags.user } },
28
+ )
29
+ await outputRecord(this, res.data)
30
+ }
31
+ }
@@ -0,0 +1,38 @@
1
+ import { Args } from '@oclif/core'
2
+ import BaseCommand from '../../../base-command.js'
3
+ import { collectPages } from '../../../lib/pagination.js'
4
+
5
+ const columns = {
6
+ user_id: { header: 'User' },
7
+ add_time: { header: 'Added' },
8
+ }
9
+
10
+ export default class DealFollowerListCommand extends BaseCommand {
11
+ static description = 'List followers of a deal'
12
+
13
+ static examples = [
14
+ '<%= config.bin %> deal follower list 42',
15
+ '<%= config.bin %> deal follower list 42 --output json',
16
+ ]
17
+
18
+ static args = {
19
+ id: Args.integer({ required: true, description: 'Deal ID' }),
20
+ }
21
+
22
+ static flags = {
23
+ ...BaseCommand.baseFlags,
24
+ }
25
+
26
+ async run() {
27
+ const { args, flags } = await this.parse(DealFollowerListCommand)
28
+ const limit = flags.limit ?? 500
29
+
30
+ const query = { limit: Math.min(limit, 500) }
31
+
32
+ const items = await collectPages(
33
+ this.apiClient.pageV2(`/api/v2/deals/${args.id}/followers`, query),
34
+ limit,
35
+ )
36
+ await this.outputResults(items, columns)
37
+ }
38
+ }
@@ -0,0 +1,44 @@
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 DealFollowerRemoveCommand extends BaseCommand {
8
+ static description = 'Remove a follower from a deal'
9
+
10
+ static examples = [
11
+ '<%= config.bin %> deal follower remove 42 --user 5',
12
+ '<%= config.bin %> deal follower remove 42 --user 5 --yes',
13
+ ]
14
+
15
+ static args = {
16
+ id: Args.integer({ required: true, description: 'Deal ID' }),
17
+ }
18
+
19
+ static flags = {
20
+ ...BaseCommand.baseFlags,
21
+ user: Flags.integer({ required: true, description: 'User ID' }),
22
+ yes: Flags.boolean({
23
+ char: 'y',
24
+ description: 'Skip the confirmation prompt',
25
+ default: false,
26
+ }),
27
+ }
28
+
29
+ async run() {
30
+ const { args, flags } = await this.parse(DealFollowerRemoveCommand)
31
+
32
+ const ok = await confirmAction(
33
+ `Remove follower ${flags.user} from deal ${args.id}?`,
34
+ flags.yes,
35
+ { default: false },
36
+ )
37
+ if (!ok) {
38
+ throw new CliError('Aborted', { exitCode: 1 })
39
+ }
40
+
41
+ await this.apiClient.del(`/api/v2/deals/${args.id}/followers/${flags.user}`)
42
+ this.log(chalk.green(`Removed follower ${flags.user} from deal ${args.id}`))
43
+ }
44
+ }
@@ -29,6 +29,10 @@ export default class DealListCommand extends BaseCommand {
29
29
 
30
30
  static flags = {
31
31
  ...BaseCommand.baseFlags,
32
+ archived: Flags.boolean({
33
+ description: 'List archived deals instead of active ones',
34
+ default: false,
35
+ }),
32
36
  status: Flags.string({
33
37
  description: 'Filter by status',
34
38
  options: ['open', 'won', 'lost', 'deleted'],
@@ -91,10 +95,9 @@ export default class DealListCommand extends BaseCommand {
91
95
  limit: Math.min(limit, 500),
92
96
  }
93
97
 
94
- const items = await collectPages(
95
- this.apiClient.pageV2('/api/v2/deals', query),
96
- limit,
97
- )
98
+ // Archived deals share the same params and cursor pager as active deals.
99
+ const path = flags.archived ? '/api/v2/deals/archived' : '/api/v2/deals'
100
+ const items = await collectPages(this.apiClient.pageV2(path, query), limit)
98
101
  await this.outputResults(items, columns, { entity: 'deal' })
99
102
  }
100
103
  }
@@ -0,0 +1,31 @@
1
+ import { Args, Flags } from '@oclif/core'
2
+ import BaseCommand from '../../../base-command.js'
3
+ import { outputRecord } from '../../../lib/entity-view.js'
4
+
5
+ export default class DealParticipantAddCommand extends BaseCommand {
6
+ static description = 'Add a participant (person) to a deal'
7
+
8
+ static examples = [
9
+ '<%= config.bin %> deal participant add 42 --person 10',
10
+ '<%= config.bin %> deal participant add 42 --person 10 --output json',
11
+ ]
12
+
13
+ static args = {
14
+ id: Args.integer({ required: true, description: 'Deal ID' }),
15
+ }
16
+
17
+ static flags = {
18
+ ...BaseCommand.baseFlags,
19
+ person: Flags.integer({ required: true, description: 'Person ID' }),
20
+ }
21
+
22
+ async run() {
23
+ const { args, flags } = await this.parse(DealParticipantAddCommand)
24
+
25
+ const res = await this.apiClient.post(
26
+ `/api/v1/deals/${args.id}/participants`,
27
+ { body: { person_id: flags.person } },
28
+ )
29
+ await outputRecord(this, res.data)
30
+ }
31
+ }
@@ -0,0 +1,46 @@
1
+ import { Args } 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
+ person_id: {
8
+ header: 'Person',
9
+ get: (row) => row.person_id?.value ?? '',
10
+ },
11
+ name: {
12
+ header: 'Name',
13
+ get: (row) => row.person_id?.name ?? '',
14
+ },
15
+ add_time: { header: 'Added' },
16
+ }
17
+
18
+ export default class DealParticipantListCommand extends BaseCommand {
19
+ static description = 'List participants of a deal'
20
+
21
+ static examples = [
22
+ '<%= config.bin %> deal participant list 42',
23
+ '<%= config.bin %> deal participant list 42 --output json',
24
+ ]
25
+
26
+ static args = {
27
+ id: Args.integer({ required: true, description: 'Deal ID' }),
28
+ }
29
+
30
+ static flags = {
31
+ ...BaseCommand.baseFlags,
32
+ }
33
+
34
+ async run() {
35
+ const { args, flags } = await this.parse(DealParticipantListCommand)
36
+ const limit = flags.limit ?? 100
37
+
38
+ const query = { limit: Math.min(limit, 500) }
39
+
40
+ const items = await collectPages(
41
+ this.apiClient.pageV1(`/api/v1/deals/${args.id}/participants`, query),
42
+ limit,
43
+ )
44
+ await this.outputResults(items, columns)
45
+ }
46
+ }
@@ -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 { 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
+ }