@wavyx/pdcli 0.1.0 → 0.3.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 (70) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/README.md +60 -11
  3. package/oclif.manifest.json +7101 -354
  4. package/package.json +4 -1
  5. package/src/base-command.js +65 -7
  6. package/src/commands/activity/create.js +63 -0
  7. package/src/commands/activity/delete.js +39 -0
  8. package/src/commands/activity/get.js +2 -17
  9. package/src/commands/activity/update.js +89 -0
  10. package/src/commands/api.js +6 -2
  11. package/src/commands/auth/login.js +102 -7
  12. package/src/commands/auth/logout.js +2 -1
  13. package/src/commands/auth/status.js +61 -13
  14. package/src/commands/backup.js +53 -0
  15. package/src/commands/deal/create.js +65 -0
  16. package/src/commands/deal/delete.js +39 -0
  17. package/src/commands/deal/get.js +2 -19
  18. package/src/commands/deal/update.js +79 -0
  19. package/src/commands/file/download.js +35 -0
  20. package/src/commands/file/get.js +26 -0
  21. package/src/commands/file/list.js +40 -0
  22. package/src/commands/file/upload.js +42 -0
  23. package/src/commands/filter/get.js +26 -0
  24. package/src/commands/filter/list.js +43 -0
  25. package/src/commands/goal/list.js +37 -0
  26. package/src/commands/lead/create.js +58 -0
  27. package/src/commands/lead/delete.js +39 -0
  28. package/src/commands/lead/get.js +26 -0
  29. package/src/commands/lead/list.js +50 -0
  30. package/src/commands/lead/update.js +71 -0
  31. package/src/commands/note/create.js +42 -0
  32. package/src/commands/note/delete.js +39 -0
  33. package/src/commands/note/get.js +26 -0
  34. package/src/commands/note/list.js +49 -0
  35. package/src/commands/note/update.js +45 -0
  36. package/src/commands/org/create.js +42 -0
  37. package/src/commands/org/delete.js +39 -0
  38. package/src/commands/org/get.js +2 -17
  39. package/src/commands/org/update.js +57 -0
  40. package/src/commands/person/create.js +54 -0
  41. package/src/commands/person/delete.js +39 -0
  42. package/src/commands/person/get.js +2 -17
  43. package/src/commands/person/update.js +69 -0
  44. package/src/commands/pipeline/get.js +26 -0
  45. package/src/commands/pipeline/list.js +37 -0
  46. package/src/commands/product/create.js +62 -0
  47. package/src/commands/product/delete.js +39 -0
  48. package/src/commands/product/get.js +26 -0
  49. package/src/commands/product/list.js +45 -0
  50. package/src/commands/product/update.js +77 -0
  51. package/src/commands/project/create.js +48 -0
  52. package/src/commands/project/delete.js +39 -0
  53. package/src/commands/project/get.js +26 -0
  54. package/src/commands/project/list.js +39 -0
  55. package/src/commands/project/update.js +63 -0
  56. package/src/commands/stage/get.js +26 -0
  57. package/src/commands/stage/list.js +41 -0
  58. package/src/commands/webhook/create.js +75 -0
  59. package/src/commands/webhook/delete.js +39 -0
  60. package/src/commands/webhook/list.js +33 -0
  61. package/src/lib/auth.js +227 -3
  62. package/src/lib/backup.js +122 -0
  63. package/src/lib/client.js +96 -6
  64. package/src/lib/confirm.js +10 -0
  65. package/src/lib/entity-view.js +42 -0
  66. package/src/lib/input.js +110 -0
  67. package/src/lib/keychain.js +47 -0
  68. package/src/lib/output/csv.js +26 -0
  69. package/src/lib/output/index.js +9 -1
  70. package/src/lib/output/yaml.js +9 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wavyx/pdcli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -100,6 +100,9 @@
100
100
  "cli-table3": "0.6.5",
101
101
  "conf": "15.1.0",
102
102
  "debug": "4.4.3",
103
+ "js-yaml": "^4.1.1",
104
+ "node-jq": "^6.3.1",
105
+ "open": "^11.0.0",
103
106
  "ora": "9.4.0",
104
107
  "undici": "8.3.0"
105
108
  },
@@ -1,7 +1,8 @@
1
1
  import { Command, Flags } from '@oclif/core'
2
2
  import { formatOutput } from './lib/output/index.js'
3
3
  import { loadConfig } from './lib/config.js'
4
- import { resolveCredentials } from './lib/auth.js'
4
+ import { resolveCredentials, refreshAccessToken } from './lib/auth.js'
5
+ import { setOAuthTokens } from './lib/keychain.js'
5
6
  import { createClient } from './lib/client.js'
6
7
  import { handleError } from './lib/errors.js'
7
8
 
@@ -11,7 +12,15 @@ export default class BaseCommand extends Command {
11
12
  char: 'o',
12
13
  description: 'Output format',
13
14
  helpGroup: 'GLOBAL',
14
- options: ['table', 'json'],
15
+ options: ['table', 'json', 'yaml', 'csv'],
16
+ }),
17
+ jq: Flags.string({
18
+ description: 'jq expression to filter JSON output',
19
+ helpGroup: 'GLOBAL',
20
+ }),
21
+ fields: Flags.string({
22
+ description: 'Comma-separated fields to display',
23
+ helpGroup: 'GLOBAL',
15
24
  }),
16
25
  profile: Flags.string({
17
26
  description: 'Named auth profile to use',
@@ -67,16 +76,45 @@ export default class BaseCommand extends Command {
67
76
 
68
77
  if (this.constructor.skipAuth) return
69
78
 
70
- const { companyDomain, token } = await resolveCredentials({
79
+ const creds = await resolveCredentials({
71
80
  flags,
72
81
  profile: this.activeProfile,
73
82
  })
74
- this.apiClient = createClient({
75
- companyDomain,
76
- token,
83
+
84
+ const common = {
77
85
  retry: !flags['no-retry'],
78
86
  timeout: flags.timeout,
79
87
  userAgent: `pdcli/${this.config.version}`,
88
+ }
89
+
90
+ if (creds.mode === 'oauth') {
91
+ this.apiClient = createClient({
92
+ ...common,
93
+ apiDomain: creds.apiDomain,
94
+ token: creds.token,
95
+ authMode: 'oauth',
96
+ onRefresh: async () => {
97
+ const refreshed = await refreshAccessToken({
98
+ refreshToken: creds.oauth.refreshToken,
99
+ clientId: creds.oauth.clientId,
100
+ clientSecret: creds.oauth.clientSecret,
101
+ })
102
+ await setOAuthTokens(this.activeProfile, {
103
+ ...creds.oauth,
104
+ accessToken: refreshed.accessToken,
105
+ refreshToken: refreshed.refreshToken,
106
+ expiresAt: Date.now() + refreshed.expiresIn * 1000,
107
+ })
108
+ return refreshed.accessToken
109
+ },
110
+ })
111
+ return
112
+ }
113
+
114
+ this.apiClient = createClient({
115
+ ...common,
116
+ companyDomain: creds.companyDomain,
117
+ token: creds.token,
80
118
  })
81
119
  }
82
120
 
@@ -90,7 +128,27 @@ export default class BaseCommand extends Command {
90
128
  * @param {Record<string, import('./lib/output/table.js').Column>} columns
91
129
  */
92
130
  async outputResults(data, columns) {
93
- formatOutput(data, columns, this.resolveFormat(), this)
131
+ if (this.flags.jq) {
132
+ // node-jq ships a native binary — load it only when actually used.
133
+ const { run } = await import('node-jq')
134
+ const input = JSON.stringify(Array.isArray(data) ? data : [data])
135
+ const result = await run(this.flags.jq, input, {
136
+ input: 'string',
137
+ output: 'pretty',
138
+ })
139
+ this.log(result)
140
+ return
141
+ }
142
+
143
+ let filteredColumns = columns
144
+ if (this.flags.fields && columns) {
145
+ const requested = this.flags.fields.split(',').map((f) => f.trim())
146
+ filteredColumns = Object.fromEntries(
147
+ Object.entries(columns).filter(([key]) => requested.includes(key)),
148
+ )
149
+ }
150
+
151
+ formatOutput(data, filteredColumns, this.resolveFormat(), this)
94
152
  }
95
153
 
96
154
  async catch(err) {
@@ -0,0 +1,63 @@
1
+ import { Flags } from '@oclif/core'
2
+ import BaseCommand from '../../base-command.js'
3
+ import { buildWriteBody } from '../../lib/input.js'
4
+ import { defsForFields, outputRecord } from '../../lib/entity-view.js'
5
+
6
+ export default class ActivityCreateCommand extends BaseCommand {
7
+ static description = 'Create an activity'
8
+
9
+ static examples = [
10
+ '<%= config.bin %> activity create --subject "Demo call" --type call --due-date 2026-06-10',
11
+ '<%= config.bin %> activity create --subject "Follow up" --field "Outcome=Positive"',
12
+ '<%= config.bin %> activity create --subject "Raw" --body \'{"priority":5}\'',
13
+ ]
14
+
15
+ static flags = {
16
+ ...BaseCommand.baseFlags,
17
+ subject: Flags.string({ required: true, description: 'Activity subject' }),
18
+ type: Flags.string({ default: 'task', description: 'Activity type' }),
19
+ 'due-date': Flags.string({ description: 'Due date (YYYY-MM-DD)' }),
20
+ 'due-time': Flags.string({ description: 'Due time (HH:MM)' }),
21
+ duration: Flags.string({ description: 'Duration (HH:MM)' }),
22
+ deal: Flags.integer({ description: 'Linked deal ID' }),
23
+ person: Flags.integer({ description: 'Linked person ID' }),
24
+ org: Flags.integer({ description: 'Linked organization ID' }),
25
+ owner: Flags.integer({ description: 'Owner (user) ID' }),
26
+ note: Flags.string({ description: 'Activity note' }),
27
+ done: Flags.boolean({ description: 'Mark the activity as done' }),
28
+ field: Flags.string({
29
+ multiple: true,
30
+ description: 'Custom/standard field as "Name=Value" (repeatable)',
31
+ }),
32
+ body: Flags.string({ description: 'Raw JSON body to merge (flags win)' }),
33
+ }
34
+
35
+ async run() {
36
+ const { flags } = await this.parse(ActivityCreateCommand)
37
+
38
+ const body = buildWriteBody({
39
+ typed: {
40
+ subject: flags.subject,
41
+ type: flags.type,
42
+ due_date: flags['due-date'],
43
+ due_time: flags['due-time'],
44
+ duration: flags.duration,
45
+ deal_id: flags.deal,
46
+ participants:
47
+ flags.person != null
48
+ ? [{ person_id: flags.person, primary: true }]
49
+ : undefined,
50
+ org_id: flags.org,
51
+ owner_id: flags.owner,
52
+ note: flags.note,
53
+ done: flags.done ? true : undefined,
54
+ },
55
+ fields: flags.field,
56
+ rawBody: flags.body,
57
+ defs: await defsForFields(this, 'activity', flags.field),
58
+ })
59
+
60
+ const res = await this.apiClient.post('/api/v2/activities', { body })
61
+ await outputRecord(this, res.data, 'activity')
62
+ }
63
+ }
@@ -0,0 +1,39 @@
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 ActivityDeleteCommand extends BaseCommand {
8
+ static description = 'Delete an activity'
9
+
10
+ static examples = [
11
+ '<%= config.bin %> activity delete 9',
12
+ '<%= config.bin %> activity delete 9 --yes',
13
+ ]
14
+
15
+ static args = {
16
+ id: Args.integer({ required: true, description: 'Activity 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(ActivityDeleteCommand)
30
+
31
+ const ok = await confirmAction(`Delete activity ${args.id}?`, flags.yes)
32
+ if (!ok) {
33
+ throw new CliError('Aborted', { exitCode: 1 })
34
+ }
35
+
36
+ await this.apiClient.del(`/api/v2/activities/${args.id}`)
37
+ this.log(chalk.green(`Deleted activity ${args.id}`))
38
+ }
39
+ }
@@ -1,7 +1,6 @@
1
1
  import { Args } from '@oclif/core'
2
2
  import BaseCommand from '../../base-command.js'
3
- import { getFields, makeResolver } from '../../lib/fields.js'
4
- import { flattenRecord } from '../../lib/output/record.js'
3
+ import { outputRecord } from '../../lib/entity-view.js'
5
4
 
6
5
  export default class ActivityGetCommand extends BaseCommand {
7
6
  static description = 'Get an activity by ID'
@@ -22,20 +21,6 @@ export default class ActivityGetCommand extends BaseCommand {
22
21
  async run() {
23
22
  const { args } = await this.parse(ActivityGetCommand)
24
23
  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, {})
24
+ await outputRecord(this, body.data, 'activity')
40
25
  }
41
26
  }
@@ -0,0 +1,89 @@
1
+ import { Args, Flags } from '@oclif/core'
2
+ import BaseCommand from '../../base-command.js'
3
+ import { buildWriteBody } from '../../lib/input.js'
4
+ import { defsForFields, outputRecord } from '../../lib/entity-view.js'
5
+ import { CliError } from '../../lib/errors.js'
6
+
7
+ export default class ActivityUpdateCommand extends BaseCommand {
8
+ static description =
9
+ 'Update an activity (v2 PATCH — only provided fields change)'
10
+
11
+ static examples = [
12
+ '<%= config.bin %> activity update 9 --subject "Renamed"',
13
+ '<%= config.bin %> activity update 9 --done',
14
+ '<%= config.bin %> activity update 9 --field "Outcome=Positive"',
15
+ ]
16
+
17
+ static args = {
18
+ id: Args.integer({ required: true, description: 'Activity ID' }),
19
+ }
20
+
21
+ static flags = {
22
+ ...BaseCommand.baseFlags,
23
+ subject: Flags.string({ description: 'Activity subject' }),
24
+ type: Flags.string({ description: 'Activity type' }),
25
+ 'due-date': Flags.string({ description: 'Due date (YYYY-MM-DD)' }),
26
+ 'due-time': Flags.string({ description: 'Due time (HH:MM)' }),
27
+ duration: Flags.string({ description: 'Duration (HH:MM)' }),
28
+ deal: Flags.integer({ description: 'Linked deal ID' }),
29
+ person: Flags.integer({ description: 'Linked person ID' }),
30
+ org: Flags.integer({ description: 'Linked organization ID' }),
31
+ owner: Flags.integer({ description: 'Owner (user) ID' }),
32
+ note: Flags.string({ description: 'Activity note' }),
33
+ done: Flags.boolean({
34
+ description: 'Mark the activity as done',
35
+ exclusive: ['undone'],
36
+ }),
37
+ undone: Flags.boolean({
38
+ description: 'Mark the activity as not done',
39
+ exclusive: ['done'],
40
+ }),
41
+ field: Flags.string({
42
+ multiple: true,
43
+ description: 'Custom/standard field as "Name=Value" (repeatable)',
44
+ }),
45
+ body: Flags.string({ description: 'Raw JSON body to merge (flags win)' }),
46
+ }
47
+
48
+ async run() {
49
+ const { args, flags } = await this.parse(ActivityUpdateCommand)
50
+
51
+ let done
52
+ if (flags.done) done = true
53
+ else if (flags.undone) done = false
54
+
55
+ const body = buildWriteBody({
56
+ typed: {
57
+ subject: flags.subject,
58
+ type: flags.type,
59
+ due_date: flags['due-date'],
60
+ due_time: flags['due-time'],
61
+ duration: flags.duration,
62
+ deal_id: flags.deal,
63
+ participants:
64
+ flags.person != null
65
+ ? [{ person_id: flags.person, primary: true }]
66
+ : undefined,
67
+ org_id: flags.org,
68
+ owner_id: flags.owner,
69
+ note: flags.note,
70
+ done,
71
+ },
72
+ fields: flags.field,
73
+ rawBody: flags.body,
74
+ defs: await defsForFields(this, 'activity', flags.field),
75
+ })
76
+
77
+ if (Object.keys(body).length === 0) {
78
+ throw new CliError(
79
+ 'Nothing to update — pass at least one field flag, --field, or --body',
80
+ { exitCode: 64 },
81
+ )
82
+ }
83
+
84
+ const res = await this.apiClient.patch(`/api/v2/activities/${args.id}`, {
85
+ body,
86
+ })
87
+ await outputRecord(this, res.data, 'activity')
88
+ }
89
+ }
@@ -53,8 +53,12 @@ export default class ApiCommand extends BaseCommand {
53
53
 
54
54
  const data = await this.apiClient[method](args.path, opts)
55
55
 
56
- if (data !== null) {
57
- this.log(JSON.stringify(data, null, 2))
56
+ if (data === null) return
57
+
58
+ if (this.flags.jq) {
59
+ await this.outputResults(data, {})
60
+ return
58
61
  }
62
+ this.log(JSON.stringify(data, null, 2))
59
63
  }
60
64
  }
@@ -3,21 +3,26 @@ import { input, password } from '@inquirer/prompts'
3
3
  import chalk from 'chalk'
4
4
  import ora from 'ora'
5
5
  import BaseCommand from '../../base-command.js'
6
- import { normalizeCompanyDomain, validateToken } from '../../lib/auth.js'
6
+ import {
7
+ authorizationCodeFlow,
8
+ normalizeCompanyDomain,
9
+ validateToken,
10
+ } from '../../lib/auth.js'
7
11
  import { createClient } from '../../lib/client.js'
8
- import { setToken } from '../../lib/keychain.js'
12
+ import { setToken, setOAuthTokens } from '../../lib/keychain.js'
9
13
  import { setProfileConfig } from '../../lib/config.js'
10
14
 
11
15
  export default class LoginCommand extends BaseCommand {
12
16
  static skipAuth = true
13
17
 
14
18
  static description =
15
- 'Authenticate with Pipedrive using your personal API token'
19
+ 'Authenticate with Pipedrive (personal API token, or OAuth with --oauth)'
16
20
 
17
21
  static examples = [
18
22
  '<%= config.bin %> auth login',
19
23
  '<%= config.bin %> auth login --company acme --api-token <token>',
20
- '<%= config.bin %> auth login --profile work',
24
+ '<%= config.bin %> auth login --oauth',
25
+ '<%= config.bin %> auth login --oauth --client-id <id> --client-secret <secret>',
21
26
  ]
22
27
 
23
28
  static flags = {
@@ -30,11 +35,39 @@ export default class LoginCommand extends BaseCommand {
30
35
  description:
31
36
  'Personal API token (app.pipedrive.com/settings/api). Prefer the prompt or env so the token stays out of shell history',
32
37
  }),
38
+ oauth: Flags.boolean({
39
+ description:
40
+ 'Use OAuth 2.0 via your own Developer Hub app (browser flow)',
41
+ default: false,
42
+ }),
43
+ 'client-id': Flags.string({
44
+ description: 'OAuth app client ID (--oauth; env PDCLI_CLIENT_ID)',
45
+ dependsOn: ['oauth'],
46
+ }),
47
+ 'client-secret': Flags.string({
48
+ description: 'OAuth app client secret (--oauth; env PDCLI_CLIENT_SECRET)',
49
+ dependsOn: ['oauth'],
50
+ }),
51
+ port: Flags.integer({
52
+ description:
53
+ "OAuth callback port — must match the app's registered callback URL (--oauth)",
54
+ default: 9999,
55
+ dependsOn: ['oauth'],
56
+ }),
33
57
  }
34
58
 
35
59
  async run() {
36
60
  const { flags } = await this.parse(LoginCommand)
37
61
 
62
+ if (flags.oauth) {
63
+ await this.#oauthLogin(flags)
64
+ return
65
+ }
66
+
67
+ await this.#tokenLogin(flags)
68
+ }
69
+
70
+ async #tokenLogin(flags) {
38
71
  const rawDomain =
39
72
  flags.company ??
40
73
  (await input({
@@ -58,15 +91,14 @@ export default class LoginCommand extends BaseCommand {
58
91
  userAgent: `pdcli/${this.config.version}`,
59
92
  })
60
93
  user = await validateToken(client)
94
+ } finally {
61
95
  spinner.stop()
62
- } catch (err) {
63
- spinner.stop()
64
- throw err
65
96
  }
66
97
 
67
98
  // Token only ever goes to the OS keychain; the domain lives in config.
68
99
  await setToken(this.activeProfile, token)
69
100
  setProfileConfig(this.activeProfile, 'company_domain', companyDomain)
101
+ setProfileConfig(this.activeProfile, 'auth_mode', 'token')
70
102
 
71
103
  this.log(
72
104
  chalk.green(
@@ -76,4 +108,67 @@ export default class LoginCommand extends BaseCommand {
76
108
  )
77
109
  this.log(chalk.dim(`Profile: ${this.activeProfile} — token in keychain`))
78
110
  }
111
+
112
+ async #oauthLogin(flags) {
113
+ const clientId =
114
+ flags['client-id'] ??
115
+ process.env.PDCLI_CLIENT_ID ??
116
+ (await input({
117
+ message: 'OAuth client ID (Developer Hub app):',
118
+ }))
119
+ const clientSecret =
120
+ flags['client-secret'] ??
121
+ process.env.PDCLI_CLIENT_SECRET ??
122
+ (await password({
123
+ message: 'OAuth client secret:',
124
+ mask: true,
125
+ }))
126
+
127
+ this.log('Opening your browser to authorize pdcli...')
128
+ const tokens = await authorizationCodeFlow({
129
+ clientId,
130
+ clientSecret,
131
+ port: flags.port,
132
+ })
133
+
134
+ const spinner = ora('Validating access token...').start()
135
+ let user
136
+ try {
137
+ const client = createClient({
138
+ apiDomain: tokens.apiDomain,
139
+ token: tokens.accessToken,
140
+ authMode: 'oauth',
141
+ userAgent: `pdcli/${this.config.version}`,
142
+ })
143
+ user = await validateToken(client)
144
+ } finally {
145
+ spinner.stop()
146
+ }
147
+
148
+ // The whole bundle — including the client secret — lives in the
149
+ // keychain. Config only records the mode and display domain.
150
+ await setOAuthTokens(this.activeProfile, {
151
+ accessToken: tokens.accessToken,
152
+ refreshToken: tokens.refreshToken,
153
+ expiresAt: Date.now() + tokens.expiresIn * 1000,
154
+ apiDomain: tokens.apiDomain,
155
+ clientId,
156
+ clientSecret,
157
+ })
158
+ const companyDomain = normalizeCompanyDomain(tokens.apiDomain)
159
+ setProfileConfig(this.activeProfile, 'auth_mode', 'oauth')
160
+ setProfileConfig(this.activeProfile, 'company_domain', companyDomain)
161
+
162
+ this.log(
163
+ chalk.green(
164
+ `Logged in via OAuth to ${chalk.cyan(tokens.apiDomain)} ` +
165
+ `as ${chalk.bold(user.name)} (${user.email})`,
166
+ ),
167
+ )
168
+ this.log(
169
+ chalk.dim(
170
+ `Profile: ${this.activeProfile} — tokens in keychain, auto-refresh on expiry`,
171
+ ),
172
+ )
173
+ }
79
174
  }
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk'
2
2
  import BaseCommand from '../../base-command.js'
3
- import { deleteToken } from '../../lib/keychain.js'
3
+ import { deleteToken, deleteOAuthTokens } from '../../lib/keychain.js'
4
4
 
5
5
  export default class LogoutCommand extends BaseCommand {
6
6
  static skipAuth = true
@@ -17,6 +17,7 @@ export default class LogoutCommand extends BaseCommand {
17
17
  await this.parse(LogoutCommand)
18
18
 
19
19
  await deleteToken(this.activeProfile)
20
+ await deleteOAuthTokens(this.activeProfile)
20
21
  this.log(
21
22
  chalk.green(`Logged out of profile ${chalk.cyan(this.activeProfile)}`),
22
23
  )
@@ -1,6 +1,10 @@
1
1
  import chalk from 'chalk'
2
2
  import BaseCommand from '../../base-command.js'
3
- import { getToken, isKeychainAvailable } from '../../lib/keychain.js'
3
+ import {
4
+ getToken,
5
+ getOAuthTokens,
6
+ isKeychainAvailable,
7
+ } from '../../lib/keychain.js'
4
8
  import { getProfileConfig } from '../../lib/config.js'
5
9
  import { companyDomainToBaseOrigin, validateToken } from '../../lib/auth.js'
6
10
  import { createClient } from '../../lib/client.js'
@@ -20,17 +24,24 @@ export default class StatusCommand extends BaseCommand {
20
24
  await this.parse(StatusCommand)
21
25
 
22
26
  const domain = getProfileConfig(this.activeProfile, 'company_domain')
23
- const token = await getToken(this.activeProfile)
27
+ const authMode = getProfileConfig(this.activeProfile, 'auth_mode')
24
28
  const keychainType = isKeychainAvailable() ? 'OS keychain' : 'unavailable'
25
29
 
26
30
  this.log(chalk.bold('Auth Status'))
27
31
  this.log('')
28
32
  this.log(` Profile: ${chalk.cyan(this.activeProfile)}`)
29
33
  this.log(` Keychain: ${keychainType}`)
34
+
35
+ if (authMode === 'oauth') {
36
+ await this.#oauthStatus()
37
+ return
38
+ }
39
+
30
40
  this.log(
31
41
  ` API host: ${domain ? companyDomainToBaseOrigin(domain) : chalk.dim('(not set)')}`,
32
42
  )
33
43
 
44
+ const token = await getToken(this.activeProfile)
34
45
  if (!token) {
35
46
  this.log(` Status: ${chalk.red('Not authenticated')}`)
36
47
  this.log('')
@@ -42,21 +53,58 @@ export default class StatusCommand extends BaseCommand {
42
53
 
43
54
  // Best-effort identity check — network errors are not fatal here.
44
55
  if (domain) {
45
- try {
46
- const client = createClient({
56
+ await this.#showIdentity(
57
+ createClient({
47
58
  companyDomain: domain,
48
59
  token,
49
60
  retry: false,
50
61
  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
- }
62
+ }),
63
+ )
64
+ }
65
+ }
66
+
67
+ async #oauthStatus() {
68
+ const tokens = await getOAuthTokens(this.activeProfile)
69
+
70
+ if (!tokens) {
71
+ this.log(` Auth mode: OAuth`)
72
+ this.log(` Status: ${chalk.red('Not authenticated')}`)
73
+ this.log('')
74
+ this.log(`Run ${chalk.cyan('pdcli auth login --oauth')} to authenticate.`)
75
+ return
76
+ }
77
+
78
+ const remainingMin = Math.max(
79
+ 0,
80
+ Math.round((tokens.expiresAt - Date.now()) / 60_000),
81
+ )
82
+ this.log(` API host: ${tokens.apiDomain}`)
83
+ this.log(` Auth mode: OAuth (auto-refresh)`)
84
+ this.log(
85
+ ` Token: ${chalk.green('present')} (expires in ${remainingMin}m)`,
86
+ )
87
+
88
+ await this.#showIdentity(
89
+ createClient({
90
+ apiDomain: tokens.apiDomain,
91
+ token: tokens.accessToken,
92
+ authMode: 'oauth',
93
+ retry: false,
94
+ userAgent: `pdcli/${this.config.version}`,
95
+ }),
96
+ )
97
+ }
98
+
99
+ async #showIdentity(client) {
100
+ try {
101
+ const user = await validateToken(client)
102
+ this.log('')
103
+ this.log(chalk.bold(' Authenticated User'))
104
+ if (user.name) this.log(` Name: ${user.name}`)
105
+ if (user.email) this.log(` Email: ${user.email}`)
106
+ } catch {
107
+ // Silently ignore — identity display is best-effort
60
108
  }
61
109
  }
62
110
  }