@wavyx/pdcli 0.1.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 (46) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +32 -0
  4. package/bin/run.js +20 -0
  5. package/oclif.manifest.json +2308 -0
  6. package/package.json +128 -0
  7. package/src/base-command.js +99 -0
  8. package/src/commands/activity/get.js +41 -0
  9. package/src/commands/activity/list.js +61 -0
  10. package/src/commands/api.js +60 -0
  11. package/src/commands/auth/login.js +79 -0
  12. package/src/commands/auth/logout.js +24 -0
  13. package/src/commands/auth/status.js +62 -0
  14. package/src/commands/config/get.js +30 -0
  15. package/src/commands/config/list.js +25 -0
  16. package/src/commands/config/set.js +29 -0
  17. package/src/commands/deal/get.js +43 -0
  18. package/src/commands/deal/list.js +62 -0
  19. package/src/commands/doctor.js +123 -0
  20. package/src/commands/field/get.js +58 -0
  21. package/src/commands/field/list.js +41 -0
  22. package/src/commands/org/get.js +41 -0
  23. package/src/commands/org/list.js +40 -0
  24. package/src/commands/person/get.js +41 -0
  25. package/src/commands/person/list.js +49 -0
  26. package/src/commands/profile/current.js +16 -0
  27. package/src/commands/profile/list.js +36 -0
  28. package/src/commands/profile/use.js +26 -0
  29. package/src/commands/search.js +67 -0
  30. package/src/commands/user/me.js +26 -0
  31. package/src/commands/version.js +24 -0
  32. package/src/hooks/command-not-found.js +15 -0
  33. package/src/hooks/init.js +7 -0
  34. package/src/hooks/prerun.js +7 -0
  35. package/src/lib/auth.js +95 -0
  36. package/src/lib/body.js +26 -0
  37. package/src/lib/client.js +184 -0
  38. package/src/lib/config.js +71 -0
  39. package/src/lib/errors.js +118 -0
  40. package/src/lib/fields.js +120 -0
  41. package/src/lib/keychain.js +69 -0
  42. package/src/lib/output/index.js +22 -0
  43. package/src/lib/output/json.js +7 -0
  44. package/src/lib/output/record.js +27 -0
  45. package/src/lib/output/table.js +40 -0
  46. package/src/lib/pagination.js +14 -0
package/package.json ADDED
@@ -0,0 +1,128 @@
1
+ {
2
+ "name": "@wavyx/pdcli",
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "Command-line interface for Pipedrive",
8
+ "type": "module",
9
+ "license": "MIT",
10
+ "author": "Eric Rodriguez",
11
+ "homepage": "https://github.com/wavyx/pdcli",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/wavyx/pdcli.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/wavyx/pdcli/issues"
18
+ },
19
+ "bin": {
20
+ "pdcli": "./bin/run.js"
21
+ },
22
+ "files": [
23
+ "bin",
24
+ "src",
25
+ "oclif.manifest.json",
26
+ "CHANGELOG.md"
27
+ ],
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "keywords": [
32
+ "pipedrive",
33
+ "crm",
34
+ "cli",
35
+ "oclif",
36
+ "sales",
37
+ "pipeline",
38
+ "automation"
39
+ ],
40
+ "oclif": {
41
+ "bin": "pdcli",
42
+ "dirname": "pdcli",
43
+ "commands": {
44
+ "strategy": "pattern",
45
+ "target": "./src/commands",
46
+ "globPatterns": [
47
+ "**/*.js",
48
+ "!**/*.test.js"
49
+ ]
50
+ },
51
+ "topicSeparator": " ",
52
+ "topics": {
53
+ "auth": {
54
+ "description": "Authentication management"
55
+ },
56
+ "deal": {
57
+ "description": "Deals"
58
+ },
59
+ "person": {
60
+ "description": "Persons (contacts)"
61
+ },
62
+ "org": {
63
+ "description": "Organizations"
64
+ },
65
+ "activity": {
66
+ "description": "Activities (calls, meetings, tasks)"
67
+ },
68
+ "field": {
69
+ "description": "Custom-field discovery and name/key resolution"
70
+ },
71
+ "user": {
72
+ "description": "Users"
73
+ },
74
+ "profile": {
75
+ "description": "Profile management"
76
+ },
77
+ "config": {
78
+ "description": "Configuration"
79
+ }
80
+ },
81
+ "hooks": {
82
+ "init": "./src/hooks/init",
83
+ "prerun": "./src/hooks/prerun",
84
+ "command_not_found": "./src/hooks/command-not-found"
85
+ },
86
+ "plugins": [
87
+ "@oclif/plugin-help",
88
+ "@oclif/plugin-plugins",
89
+ "@oclif/plugin-autocomplete"
90
+ ]
91
+ },
92
+ "dependencies": {
93
+ "@inquirer/prompts": "8.5.0",
94
+ "@napi-rs/keyring": "1.3.0",
95
+ "@oclif/core": "4.11.4",
96
+ "@oclif/plugin-autocomplete": "3.2.50",
97
+ "@oclif/plugin-help": "6.2.49",
98
+ "@oclif/plugin-plugins": "5.4.69",
99
+ "chalk": "5.6.2",
100
+ "cli-table3": "0.6.5",
101
+ "conf": "15.1.0",
102
+ "debug": "4.4.3",
103
+ "ora": "9.4.0",
104
+ "undici": "8.3.0"
105
+ },
106
+ "devDependencies": {
107
+ "@eslint/js": "10.0.1",
108
+ "@oclif/test": "4.1.18",
109
+ "@vitest/coverage-v8": "4.1.7",
110
+ "eslint": "10.4.0",
111
+ "globals": "17.6.0",
112
+ "nock": "14.0.15",
113
+ "oclif": "4.23.8",
114
+ "prettier": "3.8.3",
115
+ "vitest": "4.1.7"
116
+ },
117
+ "scripts": {
118
+ "build": "oclif manifest",
119
+ "docs:commands": "node scripts/gen-commands.mjs",
120
+ "lint": "eslint . && prettier --check .",
121
+ "lint:fix": "eslint --fix . && prettier --write .",
122
+ "test": "vitest run",
123
+ "test:watch": "vitest",
124
+ "test:coverage": "vitest run --coverage",
125
+ "prepare": "npm run build",
126
+ "postpack": "rm -f oclif.manifest.json"
127
+ }
128
+ }
@@ -0,0 +1,99 @@
1
+ import { Command, Flags } from '@oclif/core'
2
+ import { formatOutput } from './lib/output/index.js'
3
+ import { loadConfig } from './lib/config.js'
4
+ import { resolveCredentials } from './lib/auth.js'
5
+ import { createClient } from './lib/client.js'
6
+ import { handleError } from './lib/errors.js'
7
+
8
+ export default class BaseCommand extends Command {
9
+ static baseFlags = {
10
+ output: Flags.string({
11
+ char: 'o',
12
+ description: 'Output format',
13
+ helpGroup: 'GLOBAL',
14
+ options: ['table', 'json'],
15
+ }),
16
+ profile: Flags.string({
17
+ description: 'Named auth profile to use',
18
+ helpGroup: 'GLOBAL',
19
+ env: 'PDCLI_PROFILE',
20
+ }),
21
+ 'no-color': Flags.boolean({
22
+ description: 'Disable color output',
23
+ helpGroup: 'GLOBAL',
24
+ }),
25
+ verbose: Flags.boolean({
26
+ description: 'Show detailed API request/response on errors',
27
+ helpGroup: 'GLOBAL',
28
+ default: false,
29
+ }),
30
+ 'no-retry': Flags.boolean({
31
+ description: 'Disable automatic retry on rate limits and 5xx errors',
32
+ helpGroup: 'GLOBAL',
33
+ default: false,
34
+ }),
35
+ timeout: Flags.integer({
36
+ description: 'Request timeout in milliseconds',
37
+ helpGroup: 'GLOBAL',
38
+ }),
39
+ limit: Flags.integer({
40
+ description: 'Maximum number of items to return (lists)',
41
+ helpGroup: 'GLOBAL',
42
+ }),
43
+ }
44
+
45
+ /** @type {string} */
46
+ activeProfile
47
+ /** @type {ReturnType<import('./lib/client.js').createClient>} */
48
+ apiClient
49
+
50
+ async init() {
51
+ await super.init()
52
+ const { flags } = await this.parse(/** @type {any} */ (this.constructor))
53
+ this.flags = flags
54
+
55
+ if (flags['no-color'] || process.env.NO_COLOR) {
56
+ process.env.FORCE_COLOR = '0'
57
+ }
58
+
59
+ if (flags.verbose) {
60
+ process.env.DEBUG = process.env.DEBUG
61
+ ? `${process.env.DEBUG},pd:*`
62
+ : 'pd:*'
63
+ }
64
+
65
+ const config = loadConfig(flags.profile)
66
+ this.activeProfile = config.activeProfile
67
+
68
+ if (this.constructor.skipAuth) return
69
+
70
+ const { companyDomain, token } = await resolveCredentials({
71
+ flags,
72
+ profile: this.activeProfile,
73
+ })
74
+ this.apiClient = createClient({
75
+ companyDomain,
76
+ token,
77
+ retry: !flags['no-retry'],
78
+ timeout: flags.timeout,
79
+ userAgent: `pdcli/${this.config.version}`,
80
+ })
81
+ }
82
+
83
+ /** Effective output format: explicit flag, else table in a TTY, json piped. */
84
+ resolveFormat() {
85
+ return this.flags.output ?? (process.stdout.isTTY ? 'table' : 'json')
86
+ }
87
+
88
+ /**
89
+ * @param {object | object[]} data
90
+ * @param {Record<string, import('./lib/output/table.js').Column>} columns
91
+ */
92
+ async outputResults(data, columns) {
93
+ formatOutput(data, columns, this.resolveFormat(), this)
94
+ }
95
+
96
+ async catch(err) {
97
+ handleError(err, this)
98
+ }
99
+ }
@@ -0,0 +1,41 @@
1
+ import { Args } from '@oclif/core'
2
+ import BaseCommand from '../../base-command.js'
3
+ import { getFields, makeResolver } from '../../lib/fields.js'
4
+ import { flattenRecord } from '../../lib/output/record.js'
5
+
6
+ export default class ActivityGetCommand extends BaseCommand {
7
+ static description = 'Get an activity by ID'
8
+
9
+ static examples = [
10
+ '<%= config.bin %> activity get 9',
11
+ '<%= config.bin %> activity get 9 --output json',
12
+ ]
13
+
14
+ static flags = {
15
+ ...BaseCommand.baseFlags,
16
+ }
17
+
18
+ static args = {
19
+ id: Args.integer({ required: true, description: 'Activity ID' }),
20
+ }
21
+
22
+ async run() {
23
+ const { args } = await this.parse(ActivityGetCommand)
24
+ const body = await this.apiClient.get(`/api/v2/activities/${args.id}`)
25
+ let record = body.data
26
+
27
+ if (this.resolveFormat() === 'table') {
28
+ if (record.custom_fields && Object.keys(record.custom_fields).length) {
29
+ const defs = await getFields(this.apiClient, 'activity')
30
+ record = makeResolver(defs).resolveCustomFields(record)
31
+ }
32
+ await this.outputResults(flattenRecord(record), {
33
+ field: { header: 'Field' },
34
+ value: { header: 'Value' },
35
+ })
36
+ return
37
+ }
38
+
39
+ await this.outputResults(record, {})
40
+ }
41
+ }
@@ -0,0 +1,61 @@
1
+ import { 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
+ subject: { header: 'Subject' },
8
+ type: { header: 'Type' },
9
+ due_date: { header: 'Due' },
10
+ done: { header: 'Done' },
11
+ deal_id: { header: 'Deal' },
12
+ owner_id: { header: 'Owner' },
13
+ }
14
+
15
+ export default class ActivityListCommand extends BaseCommand {
16
+ static description = 'List activities'
17
+
18
+ static examples = [
19
+ '<%= config.bin %> activity list',
20
+ '<%= config.bin %> activity list --todo --deal 42',
21
+ '<%= config.bin %> activity list --type call --output json',
22
+ ]
23
+
24
+ static flags = {
25
+ ...BaseCommand.baseFlags,
26
+ owner: Flags.integer({ description: 'Filter by owner (user) ID' }),
27
+ deal: Flags.integer({ description: 'Filter by deal ID' }),
28
+ person: Flags.integer({ description: 'Filter by person ID' }),
29
+ org: Flags.integer({ description: 'Filter by organization ID' }),
30
+ type: Flags.string({ description: 'Filter by activity type key' }),
31
+ done: Flags.boolean({
32
+ description: 'Only completed activities',
33
+ exclusive: ['todo'],
34
+ }),
35
+ todo: Flags.boolean({
36
+ description: 'Only open (not done) activities',
37
+ exclusive: ['done'],
38
+ }),
39
+ }
40
+
41
+ async run() {
42
+ const { flags } = await this.parse(ActivityListCommand)
43
+ const limit = flags.limit ?? 100
44
+
45
+ const query = {
46
+ owner_id: flags.owner,
47
+ deal_id: flags.deal,
48
+ person_id: flags.person,
49
+ org_id: flags.org,
50
+ type: flags.type,
51
+ done: flags.done ? true : flags.todo ? false : undefined,
52
+ limit: Math.min(limit, 100),
53
+ }
54
+
55
+ const items = await collectPages(
56
+ this.apiClient.pageV2('/api/v2/activities', query),
57
+ limit,
58
+ )
59
+ await this.outputResults(items, columns)
60
+ }
61
+ }
@@ -0,0 +1,60 @@
1
+ import { Args, Flags } from '@oclif/core'
2
+ import BaseCommand from '../base-command.js'
3
+ import { resolveBody } from '../lib/body.js'
4
+
5
+ export default class ApiCommand extends BaseCommand {
6
+ static description =
7
+ 'Make a raw API request (host-locked to your Pipedrive company domain)'
8
+
9
+ static examples = [
10
+ '<%= config.bin %> api GET /api/v2/deals',
11
+ '<%= config.bin %> api GET /api/v1/currencies',
12
+ '<%= config.bin %> api POST /api/v2/deals --body \'{"title":"New deal"}\'',
13
+ '<%= config.bin %> api DELETE /api/v1/webhooks/1',
14
+ ]
15
+
16
+ static flags = {
17
+ ...BaseCommand.baseFlags,
18
+ body: Flags.string({
19
+ description: 'Request body (JSON string, @file, or pipe stdin)',
20
+ }),
21
+ }
22
+
23
+ static args = {
24
+ method: Args.string({
25
+ required: true,
26
+ description: 'HTTP method',
27
+ options: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
28
+ }),
29
+ path: Args.string({
30
+ required: true,
31
+ description: 'API path (e.g. /api/v2/deals — v1 and v2 both work)',
32
+ }),
33
+ }
34
+
35
+ async run() {
36
+ const { args, flags } = await this.parse(ApiCommand)
37
+
38
+ const methodMap = {
39
+ GET: 'get',
40
+ POST: 'post',
41
+ PUT: 'put',
42
+ PATCH: 'patch',
43
+ DELETE: 'del',
44
+ }
45
+
46
+ const method = methodMap[args.method]
47
+ const opts = {}
48
+
49
+ if (flags.body && !['GET', 'DELETE'].includes(args.method)) {
50
+ const bodyText = await resolveBody(flags)
51
+ opts.body = JSON.parse(bodyText)
52
+ }
53
+
54
+ const data = await this.apiClient[method](args.path, opts)
55
+
56
+ if (data !== null) {
57
+ this.log(JSON.stringify(data, null, 2))
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,79 @@
1
+ import { Flags } from '@oclif/core'
2
+ import { input, password } from '@inquirer/prompts'
3
+ import chalk from 'chalk'
4
+ import ora from 'ora'
5
+ import BaseCommand from '../../base-command.js'
6
+ import { normalizeCompanyDomain, validateToken } from '../../lib/auth.js'
7
+ import { createClient } from '../../lib/client.js'
8
+ import { setToken } from '../../lib/keychain.js'
9
+ import { setProfileConfig } from '../../lib/config.js'
10
+
11
+ export default class LoginCommand extends BaseCommand {
12
+ static skipAuth = true
13
+
14
+ static description =
15
+ 'Authenticate with Pipedrive using your personal API token'
16
+
17
+ static examples = [
18
+ '<%= config.bin %> auth login',
19
+ '<%= config.bin %> auth login --company acme --api-token <token>',
20
+ '<%= config.bin %> auth login --profile work',
21
+ ]
22
+
23
+ static flags = {
24
+ ...BaseCommand.baseFlags,
25
+ company: Flags.string({
26
+ description:
27
+ 'Company domain ("acme" from acme.pipedrive.com — full URL accepted)',
28
+ }),
29
+ 'api-token': Flags.string({
30
+ description:
31
+ 'Personal API token (app.pipedrive.com/settings/api). Prefer the prompt or env so the token stays out of shell history',
32
+ }),
33
+ }
34
+
35
+ async run() {
36
+ const { flags } = await this.parse(LoginCommand)
37
+
38
+ const rawDomain =
39
+ flags.company ??
40
+ (await input({
41
+ message: 'Company domain (e.g. "acme" from acme.pipedrive.com):',
42
+ }))
43
+ const companyDomain = normalizeCompanyDomain(rawDomain)
44
+
45
+ const token =
46
+ flags['api-token'] ??
47
+ (await password({
48
+ message: 'API token (app.pipedrive.com/settings/api):',
49
+ mask: true,
50
+ }))
51
+
52
+ const spinner = ora('Validating token...').start()
53
+ let user
54
+ try {
55
+ const client = createClient({
56
+ companyDomain,
57
+ token,
58
+ userAgent: `pdcli/${this.config.version}`,
59
+ })
60
+ user = await validateToken(client)
61
+ spinner.stop()
62
+ } catch (err) {
63
+ spinner.stop()
64
+ throw err
65
+ }
66
+
67
+ // Token only ever goes to the OS keychain; the domain lives in config.
68
+ await setToken(this.activeProfile, token)
69
+ setProfileConfig(this.activeProfile, 'company_domain', companyDomain)
70
+
71
+ this.log(
72
+ chalk.green(
73
+ `Logged in to ${chalk.cyan(`${companyDomain}.pipedrive.com`)} ` +
74
+ `as ${chalk.bold(user.name)} (${user.email})`,
75
+ ),
76
+ )
77
+ this.log(chalk.dim(`Profile: ${this.activeProfile} — token in keychain`))
78
+ }
79
+ }
@@ -0,0 +1,24 @@
1
+ import chalk from 'chalk'
2
+ import BaseCommand from '../../base-command.js'
3
+ import { deleteToken } from '../../lib/keychain.js'
4
+
5
+ export default class LogoutCommand extends BaseCommand {
6
+ static skipAuth = true
7
+
8
+ static description = 'Log out and remove the stored API token'
9
+
10
+ static examples = ['<%= config.bin %> auth logout']
11
+
12
+ static flags = {
13
+ ...BaseCommand.baseFlags,
14
+ }
15
+
16
+ async run() {
17
+ await this.parse(LogoutCommand)
18
+
19
+ await deleteToken(this.activeProfile)
20
+ this.log(
21
+ chalk.green(`Logged out of profile ${chalk.cyan(this.activeProfile)}`),
22
+ )
23
+ }
24
+ }
@@ -0,0 +1,62 @@
1
+ import chalk from 'chalk'
2
+ import BaseCommand from '../../base-command.js'
3
+ import { getToken, isKeychainAvailable } from '../../lib/keychain.js'
4
+ import { getProfileConfig } from '../../lib/config.js'
5
+ import { companyDomainToBaseOrigin, validateToken } from '../../lib/auth.js'
6
+ import { createClient } from '../../lib/client.js'
7
+
8
+ export default class StatusCommand extends BaseCommand {
9
+ static skipAuth = true
10
+
11
+ static description = 'Show current authentication status'
12
+
13
+ static examples = ['<%= config.bin %> auth status']
14
+
15
+ static flags = {
16
+ ...BaseCommand.baseFlags,
17
+ }
18
+
19
+ async run() {
20
+ await this.parse(StatusCommand)
21
+
22
+ const domain = getProfileConfig(this.activeProfile, 'company_domain')
23
+ const token = await getToken(this.activeProfile)
24
+ const keychainType = isKeychainAvailable() ? 'OS keychain' : 'unavailable'
25
+
26
+ this.log(chalk.bold('Auth Status'))
27
+ this.log('')
28
+ this.log(` Profile: ${chalk.cyan(this.activeProfile)}`)
29
+ this.log(` Keychain: ${keychainType}`)
30
+ this.log(
31
+ ` API host: ${domain ? companyDomainToBaseOrigin(domain) : chalk.dim('(not set)')}`,
32
+ )
33
+
34
+ if (!token) {
35
+ this.log(` Status: ${chalk.red('Not authenticated')}`)
36
+ this.log('')
37
+ this.log(`Run ${chalk.cyan('pdcli auth login')} to authenticate.`)
38
+ return
39
+ }
40
+
41
+ this.log(` Token: ${chalk.green('present')} (keychain)`)
42
+
43
+ // Best-effort identity check — network errors are not fatal here.
44
+ if (domain) {
45
+ try {
46
+ const client = createClient({
47
+ companyDomain: domain,
48
+ token,
49
+ retry: false,
50
+ userAgent: `pdcli/${this.config.version}`,
51
+ })
52
+ const user = await validateToken(client)
53
+ this.log('')
54
+ this.log(chalk.bold(' Authenticated User'))
55
+ if (user.name) this.log(` Name: ${user.name}`)
56
+ if (user.email) this.log(` Email: ${user.email}`)
57
+ } catch {
58
+ // Silently ignore — identity display is best-effort
59
+ }
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,30 @@
1
+ import { Args } from '@oclif/core'
2
+ import BaseCommand from '../../base-command.js'
3
+ import { getProfileConfig } from '../../lib/config.js'
4
+
5
+ export default class ConfigGetCommand extends BaseCommand {
6
+ static skipAuth = true
7
+
8
+ static description = 'Get a config value for the active profile'
9
+
10
+ static examples = [
11
+ '<%= config.bin %> config get company_domain',
12
+ '<%= config.bin %> config get default_output',
13
+ ]
14
+
15
+ static args = {
16
+ key: Args.string({ required: true, description: 'Config key to read' }),
17
+ }
18
+
19
+ async run() {
20
+ const { args } = await this.parse(ConfigGetCommand)
21
+
22
+ const value = getProfileConfig(this.activeProfile, args.key)
23
+
24
+ if (value === undefined) {
25
+ this.log('not set')
26
+ } else {
27
+ this.log(String(value))
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,25 @@
1
+ import chalk from 'chalk'
2
+ import BaseCommand from '../../base-command.js'
3
+ import { getProfileData } from '../../lib/config.js'
4
+
5
+ export default class ConfigListCommand extends BaseCommand {
6
+ static skipAuth = true
7
+
8
+ static description = 'List all config for the active profile'
9
+
10
+ static examples = ['<%= config.bin %> config list']
11
+
12
+ async run() {
13
+ const data = getProfileData(this.activeProfile)
14
+ const entries = Object.entries(data)
15
+
16
+ if (entries.length === 0) {
17
+ this.log(`No config set for profile ${chalk.cyan(this.activeProfile)}`)
18
+ return
19
+ }
20
+
21
+ for (const [key, value] of entries) {
22
+ this.log(`${key}=${value}`)
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,29 @@
1
+ import { Args } from '@oclif/core'
2
+ import chalk from 'chalk'
3
+ import BaseCommand from '../../base-command.js'
4
+ import { setProfileConfig } from '../../lib/config.js'
5
+
6
+ export default class ConfigSetCommand extends BaseCommand {
7
+ static skipAuth = true
8
+
9
+ static description = 'Set a config value for the active profile'
10
+
11
+ static examples = [
12
+ '<%= config.bin %> config set company_domain acme',
13
+ '<%= config.bin %> config set default_output json',
14
+ ]
15
+
16
+ static args = {
17
+ key: Args.string({ required: true, description: 'Config key to set' }),
18
+ value: Args.string({ required: true, description: 'Value to assign' }),
19
+ }
20
+
21
+ async run() {
22
+ const { args } = await this.parse(ConfigSetCommand)
23
+
24
+ setProfileConfig(this.activeProfile, args.key, args.value)
25
+ this.log(
26
+ `Set ${chalk.cyan(args.key)} = ${chalk.green(args.value)} for profile ${chalk.cyan(this.activeProfile)}`,
27
+ )
28
+ }
29
+ }
@@ -0,0 +1,43 @@
1
+ import { Args } from '@oclif/core'
2
+ import BaseCommand from '../../base-command.js'
3
+ import { getFields, makeResolver } from '../../lib/fields.js'
4
+ import { flattenRecord } from '../../lib/output/record.js'
5
+
6
+ export default class DealGetCommand extends BaseCommand {
7
+ static description = 'Get a deal by ID'
8
+
9
+ static examples = [
10
+ '<%= config.bin %> deal get 42',
11
+ '<%= config.bin %> deal get 42 --output json',
12
+ ]
13
+
14
+ static flags = {
15
+ ...BaseCommand.baseFlags,
16
+ }
17
+
18
+ static args = {
19
+ id: Args.integer({ required: true, description: 'Deal ID' }),
20
+ }
21
+
22
+ async run() {
23
+ const { args } = await this.parse(DealGetCommand)
24
+ const body = await this.apiClient.get(`/api/v2/deals/${args.id}`)
25
+ let record = body.data
26
+
27
+ if (this.resolveFormat() === 'table') {
28
+ // Table view resolves custom-field hash keys to names and option IDs
29
+ // to labels; JSON output stays raw for scripting.
30
+ if (record.custom_fields && Object.keys(record.custom_fields).length) {
31
+ const defs = await getFields(this.apiClient, 'deal')
32
+ record = makeResolver(defs).resolveCustomFields(record)
33
+ }
34
+ await this.outputResults(flattenRecord(record), {
35
+ field: { header: 'Field' },
36
+ value: { header: 'Value' },
37
+ })
38
+ return
39
+ }
40
+
41
+ await this.outputResults(record, {})
42
+ }
43
+ }