@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 BaseCommand from '../../base-command.js'
3
+ import { clearFieldsCache, entityToFieldsPath } from '../../lib/fields.js'
4
+
5
+ export default class FieldUpdateCommand extends BaseCommand {
6
+ static description =
7
+ 'Update a custom field (field_code and field_type cannot change)'
8
+
9
+ static examples = [
10
+ '<%= config.bin %> field update deal dcf558aac1ae4e8c4f849ba5e668430d8df9be12 --name "New name"',
11
+ ]
12
+
13
+ static args = {
14
+ entity: Args.string({
15
+ required: true,
16
+ description: 'Entity type',
17
+ options: ['deal', 'person', 'org', 'organization', 'product'],
18
+ }),
19
+ field: Args.string({
20
+ required: true,
21
+ description: 'Field code (hashed key)',
22
+ }),
23
+ }
24
+
25
+ static flags = {
26
+ ...BaseCommand.baseFlags,
27
+ name: Flags.string({
28
+ required: true,
29
+ description: 'New field name (label)',
30
+ }),
31
+ }
32
+
33
+ async run() {
34
+ const { args, flags } = await this.parse(FieldUpdateCommand)
35
+
36
+ const path = `${entityToFieldsPath(args.entity)}/${args.field}`
37
+ const res = await this.apiClient.patch(path, {
38
+ body: { field_name: flags.name },
39
+ })
40
+
41
+ clearFieldsCache()
42
+
43
+ await this.outputResults(res.data, {
44
+ field_code: { header: 'Key' },
45
+ field_name: { header: 'Name' },
46
+ field_type: { header: 'Type' },
47
+ options: {
48
+ header: 'Options',
49
+ get: (row) => row.options?.map((o) => `${o.id}=${o.label}`).join(', '),
50
+ },
51
+ })
52
+ }
53
+ }
@@ -0,0 +1,41 @@
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 FileDeleteCommand extends BaseCommand {
8
+ static description = 'Delete a file'
9
+
10
+ static examples = [
11
+ '<%= config.bin %> file delete 5',
12
+ '<%= config.bin %> file delete 5 --yes',
13
+ ]
14
+
15
+ static args = {
16
+ id: Args.integer({ required: true, description: 'File ID' }),
17
+ }
18
+
19
+ static flags = {
20
+ ...BaseCommand.baseFlags,
21
+ yes: Flags.boolean({
22
+ char: 'y',
23
+ description: 'Skip the confirmation prompt',
24
+ default: false,
25
+ }),
26
+ }
27
+
28
+ async run() {
29
+ const { args, flags } = await this.parse(FileDeleteCommand)
30
+
31
+ const ok = await confirmAction(`Delete file ${args.id}?`, flags.yes, {
32
+ default: false,
33
+ })
34
+ if (!ok) {
35
+ throw new CliError('Aborted', { exitCode: 1 })
36
+ }
37
+
38
+ await this.apiClient.del(`/api/v1/files/${args.id}`)
39
+ this.log(chalk.green(`Deleted file ${args.id}`))
40
+ }
41
+ }
@@ -0,0 +1,39 @@
1
+ import { Args, Flags } from '@oclif/core'
2
+ import BaseCommand from '../../base-command.js'
3
+ import { outputRecord } from '../../lib/entity-view.js'
4
+ import { CliError } from '../../lib/errors.js'
5
+
6
+ export default class FileUpdateCommand extends BaseCommand {
7
+ static description = 'Update a file name and/or description'
8
+
9
+ static examples = [
10
+ '<%= config.bin %> file update 5 --name report.pdf',
11
+ '<%= config.bin %> file update 5 --description "Signed contract"',
12
+ ]
13
+
14
+ static args = {
15
+ id: Args.integer({ required: true, description: 'File ID' }),
16
+ }
17
+
18
+ static flags = {
19
+ ...BaseCommand.baseFlags,
20
+ name: Flags.string({ description: 'The visible name of the file' }),
21
+ description: Flags.string({ description: 'The description of the file' }),
22
+ }
23
+
24
+ async run() {
25
+ const { args, flags } = await this.parse(FileUpdateCommand)
26
+
27
+ if (flags.name == null && flags.description == null) {
28
+ throw new CliError('Pass at least one of --name or --description', {
29
+ exitCode: 64,
30
+ })
31
+ }
32
+
33
+ const res = await this.apiClient.putForm(`/api/v1/files/${args.id}`, {
34
+ name: flags.name,
35
+ description: flags.description,
36
+ })
37
+ await outputRecord(this, res.data)
38
+ }
39
+ }
@@ -0,0 +1,41 @@
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 FilterDeleteCommand extends BaseCommand {
8
+ static description = 'Delete a filter'
9
+
10
+ static examples = [
11
+ '<%= config.bin %> filter delete 5',
12
+ '<%= config.bin %> filter delete 5 --yes',
13
+ ]
14
+
15
+ static args = {
16
+ id: Args.integer({ required: true, description: 'Filter ID' }),
17
+ }
18
+
19
+ static flags = {
20
+ ...BaseCommand.baseFlags,
21
+ yes: Flags.boolean({
22
+ char: 'y',
23
+ description: 'Skip the confirmation prompt',
24
+ default: false,
25
+ }),
26
+ }
27
+
28
+ async run() {
29
+ const { args, flags } = await this.parse(FilterDeleteCommand)
30
+
31
+ const ok = await confirmAction(`Delete filter ${args.id}?`, flags.yes, {
32
+ default: false,
33
+ })
34
+ if (!ok) {
35
+ throw new CliError('Aborted', { exitCode: 1 })
36
+ }
37
+
38
+ await this.apiClient.del(`/api/v1/filters/${args.id}`)
39
+ this.log(chalk.green(`Deleted filter ${args.id}`))
40
+ }
41
+ }
@@ -3,12 +3,8 @@ import BaseCommand from '../base-command.js'
3
3
  import { collectPages } from '../lib/pagination.js'
4
4
  import { parsePeriod, formatApiDatetime } from '../lib/period.js'
5
5
  import { computeFunnel, computeExactFunnel } from '../lib/analytics.js'
6
- import { CliError, ApiError } from '../lib/errors.js'
7
-
8
- /** Token cost of one GET /deals/{id}/changelog request (rate-limit budget). */
9
- const CHANGELOG_COST = 20
10
- /** Above this deal count, mining gets expensive — warn before proceeding. */
11
- const MINE_WARN_THRESHOLD = 100
6
+ import { mineMany } from '../lib/changelog.js'
7
+ import { CliError } from '../lib/errors.js'
12
8
 
13
9
  export default class FunnelCommand extends BaseCommand {
14
10
  static description =
@@ -134,54 +130,16 @@ export default class FunnelCommand extends BaseCommand {
134
130
  }
135
131
 
136
132
  /**
137
- * Mine real stage transitions from each deal's v1 changelog. The changelog
138
- * uses a flat v2-style cursor (additional_data.next_cursor on a v1 path), so
139
- * the v2 pager works directly. Warns on stderr before mining a large set —
140
- * each request costs 20 tokens then lets the client's rate limiter pace it.
133
+ * Mine real stage transitions from each deal's v1 changelog (one request per
134
+ * deal, paced by the rate limiter), then compute the exact funnel. Mining,
135
+ * the large-set warning, and the skip-on-ApiError behavior live in the
136
+ * shared changelog lib so this command and `deal history` stay in step.
141
137
  * @param {object[]} deals deals to mine (current stage_id needed per deal)
142
138
  * @param {object[]} stages
143
139
  * @param {number} pipelineId
144
140
  */
145
141
  async mineExactFunnel(deals, stages, pipelineId) {
146
- if (deals.length > MINE_WARN_THRESHOLD) {
147
- process.stderr.write(
148
- `Mining stage history for ${deals.length} deals ` +
149
- `(~${deals.length} requests, ${CHANGELOG_COST} tokens each); ` +
150
- `rate limiting may slow this down.\n`,
151
- )
152
- }
153
-
154
- const transitionsByDeal = []
155
- let skipped = 0
156
- for (const deal of deals) {
157
- try {
158
- const rows = await collectPages(
159
- this.apiClient.pageV2(`/api/v1/deals/${deal.id}/changelog`, {
160
- limit: 500,
161
- }),
162
- )
163
- transitionsByDeal.push({
164
- dealId: deal.id,
165
- stageId: deal.stage_id,
166
- rows,
167
- })
168
- } catch (err) {
169
- // One bad changelog request must not abort the whole mine: skip the
170
- // deal, count it, and warn once after mining completes.
171
- if (err instanceof ApiError) {
172
- skipped++
173
- continue
174
- }
175
- throw err
176
- }
177
- }
178
-
179
- if (skipped > 0) {
180
- process.stderr.write(
181
- `skipped ${skipped} deal(s) whose changelog could not be fetched\n`,
182
- )
183
- }
184
-
142
+ const transitionsByDeal = await mineMany(this.apiClient, deals)
185
143
  return computeExactFunnel(transitionsByDeal, stages, { pipelineId })
186
144
  }
187
145
  }
@@ -0,0 +1,103 @@
1
+ import { Args, Flags } from '@oclif/core'
2
+ import chalk from 'chalk'
3
+ import BaseCommand from '../../base-command.js'
4
+ import { CliError } from '../../lib/errors.js'
5
+
6
+ const POLL_INTERVAL_MS = 2000
7
+
8
+ function defaultSleep(ms) {
9
+ return new Promise((resolve) => setTimeout(resolve, ms))
10
+ }
11
+
12
+ export default class LeadConvertCommand extends BaseCommand {
13
+ static description =
14
+ 'Convert a lead to a deal. The conversion runs as an async job; ' +
15
+ 'use --wait to poll until it finishes. On success the lead is deleted.'
16
+
17
+ static examples = [
18
+ '<%= config.bin %> lead convert adf21080-0e10-11eb-879b-05d71fb426ec',
19
+ '<%= config.bin %> lead convert adf21080-0e10-11eb-879b-05d71fb426ec --stage 7',
20
+ '<%= config.bin %> lead convert adf21080-0e10-11eb-879b-05d71fb426ec --wait',
21
+ ]
22
+
23
+ static args = {
24
+ id: Args.string({ required: true, description: 'Lead ID (UUID)' }),
25
+ }
26
+
27
+ static flags = {
28
+ ...BaseCommand.baseFlags,
29
+ stage: Flags.integer({
30
+ description: 'Stage ID for the new deal (a pipeline is inferred from it)',
31
+ }),
32
+ pipeline: Flags.integer({
33
+ description: 'Pipeline ID for the new deal (ignored when --stage is set)',
34
+ }),
35
+ wait: Flags.boolean({
36
+ description: 'Poll the conversion status until it finishes',
37
+ default: false,
38
+ }),
39
+ 'timeout-secs': Flags.integer({
40
+ description: 'Max seconds to poll when --wait is set',
41
+ default: 30,
42
+ }),
43
+ }
44
+
45
+ /** Overridable in tests so polling never waits in real time. */
46
+ static sleepFn = defaultSleep
47
+
48
+ async run() {
49
+ const { args, flags } = await this.parse(LeadConvertCommand)
50
+
51
+ const body = {}
52
+ if (flags.stage !== undefined) body.stage_id = flags.stage
53
+ if (flags.pipeline !== undefined) body.pipeline_id = flags.pipeline
54
+
55
+ const res = await this.apiClient.post(
56
+ `/api/v2/leads/${args.id}/convert/deal`,
57
+ { body },
58
+ )
59
+ const conversionId = res.data?.conversion_id
60
+
61
+ if (!flags.wait) {
62
+ this.log(chalk.green(`Conversion started: ${conversionId}`))
63
+ this.log(
64
+ `Check status: ${this.config.bin} api GET ` +
65
+ `/api/v2/leads/${args.id}/convert/status/${conversionId}`,
66
+ )
67
+ return
68
+ }
69
+
70
+ const timeoutMs = flags['timeout-secs'] * 1000
71
+ const sleep = LeadConvertCommand.sleepFn
72
+ let elapsed = 0
73
+ while (true) {
74
+ const status = await this.apiClient.get(
75
+ `/api/v2/leads/${args.id}/convert/status/${conversionId}`,
76
+ )
77
+ const state = status.data?.status
78
+ if (state === 'completed') {
79
+ this.log(
80
+ chalk.green(
81
+ `Conversion completed: lead ${args.id} → deal ${status.data?.deal_id}`,
82
+ ),
83
+ )
84
+ return
85
+ }
86
+ if (state === 'failed' || state === 'rejected') {
87
+ throw new CliError(`Conversion ${state} for lead ${args.id}`, {
88
+ exitCode: 70,
89
+ })
90
+ }
91
+ if (elapsed + POLL_INTERVAL_MS > timeoutMs) {
92
+ throw new CliError(
93
+ `Timed out after ${flags['timeout-secs']}s waiting for conversion ` +
94
+ `${conversionId} (last status: ${state}). ` +
95
+ `Check status: ${this.config.bin} api GET /api/v2/leads/${args.id}/convert/status/${conversionId}`,
96
+ { exitCode: 1 },
97
+ )
98
+ }
99
+ await sleep(POLL_INTERVAL_MS)
100
+ elapsed += POLL_INTERVAL_MS
101
+ }
102
+ }
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 NoteCommentAddCommand extends BaseCommand {
6
+ static description = 'Add a comment to a note'
7
+
8
+ static examples = [
9
+ '<%= config.bin %> note comment add 5 --content "Nice work"',
10
+ '<%= config.bin %> note comment add 5 --content "Reviewed" --output json',
11
+ ]
12
+
13
+ static args = {
14
+ noteId: Args.integer({ required: true, description: 'Note ID' }),
15
+ }
16
+
17
+ static flags = {
18
+ ...BaseCommand.baseFlags,
19
+ content: Flags.string({ required: true, description: 'Comment content' }),
20
+ }
21
+
22
+ async run() {
23
+ const { args, flags } = await this.parse(NoteCommentAddCommand)
24
+
25
+ const res = await this.apiClient.post(
26
+ `/api/v1/notes/${args.noteId}/comments`,
27
+ { body: { content: flags.content } },
28
+ )
29
+ await outputRecord(this, res.data)
30
+ }
31
+ }
@@ -0,0 +1,51 @@
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 NoteCommentDeleteCommand extends BaseCommand {
8
+ static description = 'Delete a comment from a note'
9
+
10
+ static examples = [
11
+ '<%= config.bin %> note comment delete 5 --comment <uuid>',
12
+ '<%= config.bin %> note comment delete 5 --comment <uuid> --yes',
13
+ ]
14
+
15
+ static args = {
16
+ noteId: Args.integer({ required: true, description: 'Note ID' }),
17
+ }
18
+
19
+ static flags = {
20
+ ...BaseCommand.baseFlags,
21
+ comment: Flags.string({
22
+ required: true,
23
+ description: 'Comment ID (UUID)',
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(NoteCommentDeleteCommand)
34
+
35
+ const ok = await confirmAction(
36
+ `Delete comment ${flags.comment} from note ${args.noteId}?`,
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/notes/${args.noteId}/comments/${flags.comment}`,
46
+ )
47
+ this.log(
48
+ chalk.green(`Deleted comment ${flags.comment} from note ${args.noteId}`),
49
+ )
50
+ }
51
+ }
@@ -0,0 +1,43 @@
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
+ uuid: { header: 'ID' },
7
+ content: {
8
+ header: 'Content',
9
+ get: (row) => (row.content ?? '').slice(0, 60),
10
+ },
11
+ user_id: { header: 'User' },
12
+ add_time: { header: 'Created' },
13
+ }
14
+
15
+ export default class NoteCommentListCommand extends BaseCommand {
16
+ static description = 'List comments on a note'
17
+
18
+ static examples = [
19
+ '<%= config.bin %> note comment list 5',
20
+ '<%= config.bin %> note comment list 5 --output json',
21
+ ]
22
+
23
+ static args = {
24
+ noteId: Args.integer({ required: true, description: 'Note ID' }),
25
+ }
26
+
27
+ static flags = {
28
+ ...BaseCommand.baseFlags,
29
+ }
30
+
31
+ async run() {
32
+ const { args, flags } = await this.parse(NoteCommentListCommand)
33
+ const limit = flags.limit ?? 100
34
+
35
+ const query = { limit: Math.min(limit, 100) }
36
+
37
+ const items = await collectPages(
38
+ this.apiClient.pageV1(`/api/v1/notes/${args.noteId}/comments`, query),
39
+ limit,
40
+ )
41
+ await this.outputResults(items, columns)
42
+ }
43
+ }
@@ -0,0 +1,37 @@
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 NoteCommentUpdateCommand extends BaseCommand {
6
+ static description = 'Update a comment on a note'
7
+
8
+ static examples = [
9
+ '<%= config.bin %> note comment update 5 --comment <uuid> --content "Edited"',
10
+ ]
11
+
12
+ static args = {
13
+ noteId: Args.integer({ required: true, description: 'Note ID' }),
14
+ }
15
+
16
+ static flags = {
17
+ ...BaseCommand.baseFlags,
18
+ comment: Flags.string({
19
+ required: true,
20
+ description: 'Comment ID (UUID)',
21
+ }),
22
+ content: Flags.string({
23
+ required: true,
24
+ description: 'New comment content',
25
+ }),
26
+ }
27
+
28
+ async run() {
29
+ const { args, flags } = await this.parse(NoteCommentUpdateCommand)
30
+
31
+ const res = await this.apiClient.put(
32
+ `/api/v1/notes/${args.noteId}/comments/${flags.comment}`,
33
+ { body: { content: flags.content } },
34
+ )
35
+ await outputRecord(this, res.data)
36
+ }
37
+ }
@@ -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 OrgFollowerAddCommand extends BaseCommand {
6
+ static description = 'Add a follower (user) to an organization'
7
+
8
+ static examples = [
9
+ '<%= config.bin %> org follower add 42 --user 5',
10
+ '<%= config.bin %> org follower add 42 --user 5 --output json',
11
+ ]
12
+
13
+ static args = {
14
+ id: Args.integer({ required: true, description: 'Organization 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(OrgFollowerAddCommand)
24
+
25
+ const res = await this.apiClient.post(
26
+ `/api/v2/organizations/${args.id}/followers`,
27
+ { body: { user_id: flags.user } },
28
+ )
29
+ await outputRecord(this, res.data)
30
+ }
31
+ }
@@ -0,0 +1,41 @@
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 OrgFollowerListCommand extends BaseCommand {
11
+ static description = 'List followers of an organization'
12
+
13
+ static examples = [
14
+ '<%= config.bin %> org follower list 42',
15
+ '<%= config.bin %> org follower list 42 --output json',
16
+ ]
17
+
18
+ static args = {
19
+ id: Args.integer({ required: true, description: 'Organization ID' }),
20
+ }
21
+
22
+ static flags = {
23
+ ...BaseCommand.baseFlags,
24
+ }
25
+
26
+ async run() {
27
+ const { args, flags } = await this.parse(OrgFollowerListCommand)
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(
34
+ `/api/v2/organizations/${args.id}/followers`,
35
+ query,
36
+ ),
37
+ limit,
38
+ )
39
+ await this.outputResults(items, columns)
40
+ }
41
+ }
@@ -0,0 +1,50 @@
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 OrgFollowerRemoveCommand extends BaseCommand {
8
+ static description = 'Remove a follower from an organization'
9
+
10
+ static examples = [
11
+ '<%= config.bin %> org follower remove 42 --user 5',
12
+ '<%= config.bin %> org follower remove 42 --user 5 --yes',
13
+ ]
14
+
15
+ static args = {
16
+ id: Args.integer({ required: true, description: 'Organization 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(OrgFollowerRemoveCommand)
31
+
32
+ const ok = await confirmAction(
33
+ `Remove follower ${flags.user} from organization ${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(
42
+ `/api/v2/organizations/${args.id}/followers/${flags.user}`,
43
+ )
44
+ this.log(
45
+ chalk.green(
46
+ `Removed follower ${flags.user} from organization ${args.id}`,
47
+ ),
48
+ )
49
+ }
50
+ }