@wavyx/pdcli 0.1.0 → 0.2.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.
- package/CHANGELOG.md +21 -1
- package/README.md +46 -11
- package/oclif.manifest.json +2523 -240
- package/package.json +2 -1
- package/src/base-command.js +35 -5
- package/src/commands/activity/create.js +63 -0
- package/src/commands/activity/delete.js +39 -0
- package/src/commands/activity/get.js +2 -17
- package/src/commands/activity/update.js +89 -0
- package/src/commands/auth/login.js +102 -7
- package/src/commands/auth/logout.js +2 -1
- package/src/commands/auth/status.js +61 -13
- package/src/commands/deal/create.js +65 -0
- package/src/commands/deal/delete.js +39 -0
- package/src/commands/deal/get.js +2 -19
- package/src/commands/deal/update.js +79 -0
- package/src/commands/org/create.js +42 -0
- package/src/commands/org/delete.js +39 -0
- package/src/commands/org/get.js +2 -17
- package/src/commands/org/update.js +57 -0
- package/src/commands/person/create.js +54 -0
- package/src/commands/person/delete.js +39 -0
- package/src/commands/person/get.js +2 -17
- package/src/commands/person/update.js +69 -0
- package/src/commands/product/create.js +62 -0
- package/src/commands/product/delete.js +39 -0
- package/src/commands/product/get.js +26 -0
- package/src/commands/product/list.js +45 -0
- package/src/commands/product/update.js +77 -0
- package/src/lib/auth.js +227 -3
- package/src/lib/client.js +29 -6
- package/src/lib/confirm.js +10 -0
- package/src/lib/entity-view.js +37 -0
- package/src/lib/input.js +110 -0
- package/src/lib/keychain.js +47 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wavyx/pdcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -100,6 +100,7 @@
|
|
|
100
100
|
"cli-table3": "0.6.5",
|
|
101
101
|
"conf": "15.1.0",
|
|
102
102
|
"debug": "4.4.3",
|
|
103
|
+
"open": "^11.0.0",
|
|
103
104
|
"ora": "9.4.0",
|
|
104
105
|
"undici": "8.3.0"
|
|
105
106
|
},
|
package/src/base-command.js
CHANGED
|
@@ -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
|
|
|
@@ -67,16 +68,45 @@ export default class BaseCommand extends Command {
|
|
|
67
68
|
|
|
68
69
|
if (this.constructor.skipAuth) return
|
|
69
70
|
|
|
70
|
-
const
|
|
71
|
+
const creds = await resolveCredentials({
|
|
71
72
|
flags,
|
|
72
73
|
profile: this.activeProfile,
|
|
73
74
|
})
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
token,
|
|
75
|
+
|
|
76
|
+
const common = {
|
|
77
77
|
retry: !flags['no-retry'],
|
|
78
78
|
timeout: flags.timeout,
|
|
79
79
|
userAgent: `pdcli/${this.config.version}`,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (creds.mode === 'oauth') {
|
|
83
|
+
this.apiClient = createClient({
|
|
84
|
+
...common,
|
|
85
|
+
apiDomain: creds.apiDomain,
|
|
86
|
+
token: creds.token,
|
|
87
|
+
authMode: 'oauth',
|
|
88
|
+
onRefresh: async () => {
|
|
89
|
+
const refreshed = await refreshAccessToken({
|
|
90
|
+
refreshToken: creds.oauth.refreshToken,
|
|
91
|
+
clientId: creds.oauth.clientId,
|
|
92
|
+
clientSecret: creds.oauth.clientSecret,
|
|
93
|
+
})
|
|
94
|
+
await setOAuthTokens(this.activeProfile, {
|
|
95
|
+
...creds.oauth,
|
|
96
|
+
accessToken: refreshed.accessToken,
|
|
97
|
+
refreshToken: refreshed.refreshToken,
|
|
98
|
+
expiresAt: Date.now() + refreshed.expiresIn * 1000,
|
|
99
|
+
})
|
|
100
|
+
return refreshed.accessToken
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.apiClient = createClient({
|
|
107
|
+
...common,
|
|
108
|
+
companyDomain: creds.companyDomain,
|
|
109
|
+
token: creds.token,
|
|
80
110
|
})
|
|
81
111
|
}
|
|
82
112
|
|
|
@@ -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 {
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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 {
|
|
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
|
|
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 --
|
|
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 {
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
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 DealCreateCommand extends BaseCommand {
|
|
7
|
+
static description = 'Create a deal'
|
|
8
|
+
|
|
9
|
+
static examples = [
|
|
10
|
+
'<%= config.bin %> deal create --title "Acme renewal" --value 5000 --currency EUR',
|
|
11
|
+
'<%= config.bin %> deal create --title "Sized" --field "Deal Size=Large"',
|
|
12
|
+
'<%= config.bin %> deal create --title "Raw" --body \'{"probability":75}\'',
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
static flags = {
|
|
16
|
+
...BaseCommand.baseFlags,
|
|
17
|
+
title: Flags.string({ required: true, description: 'Deal title' }),
|
|
18
|
+
value: Flags.integer({ description: 'Deal value' }),
|
|
19
|
+
currency: Flags.string({ description: 'Deal currency (e.g. EUR)' }),
|
|
20
|
+
status: Flags.string({
|
|
21
|
+
description: 'Deal status',
|
|
22
|
+
options: ['open', 'won', 'lost'],
|
|
23
|
+
}),
|
|
24
|
+
stage: Flags.integer({ description: 'Stage ID' }),
|
|
25
|
+
pipeline: Flags.integer({ description: 'Pipeline ID' }),
|
|
26
|
+
person: Flags.integer({ description: 'Linked person ID' }),
|
|
27
|
+
org: Flags.integer({ description: 'Linked organization ID' }),
|
|
28
|
+
owner: Flags.integer({ description: 'Owner (user) ID' }),
|
|
29
|
+
probability: Flags.integer({ description: 'Success probability (0-100)' }),
|
|
30
|
+
'expected-close-date': Flags.string({
|
|
31
|
+
description: 'Expected close date (YYYY-MM-DD)',
|
|
32
|
+
}),
|
|
33
|
+
field: Flags.string({
|
|
34
|
+
multiple: true,
|
|
35
|
+
description: 'Custom/standard field as "Name=Value" (repeatable)',
|
|
36
|
+
}),
|
|
37
|
+
body: Flags.string({ description: 'Raw JSON body to merge (flags win)' }),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async run() {
|
|
41
|
+
const { flags } = await this.parse(DealCreateCommand)
|
|
42
|
+
|
|
43
|
+
const body = buildWriteBody({
|
|
44
|
+
typed: {
|
|
45
|
+
title: flags.title,
|
|
46
|
+
value: flags.value,
|
|
47
|
+
currency: flags.currency,
|
|
48
|
+
status: flags.status,
|
|
49
|
+
stage_id: flags.stage,
|
|
50
|
+
pipeline_id: flags.pipeline,
|
|
51
|
+
person_id: flags.person,
|
|
52
|
+
org_id: flags.org,
|
|
53
|
+
owner_id: flags.owner,
|
|
54
|
+
probability: flags.probability,
|
|
55
|
+
expected_close_date: flags['expected-close-date'],
|
|
56
|
+
},
|
|
57
|
+
fields: flags.field,
|
|
58
|
+
rawBody: flags.body,
|
|
59
|
+
defs: await defsForFields(this, 'deal', flags.field),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const res = await this.apiClient.post('/api/v2/deals', { body })
|
|
63
|
+
await outputRecord(this, res.data, 'deal')
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -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 DealDeleteCommand extends BaseCommand {
|
|
8
|
+
static description = 'Delete a deal'
|
|
9
|
+
|
|
10
|
+
static examples = [
|
|
11
|
+
'<%= config.bin %> deal delete 42',
|
|
12
|
+
'<%= config.bin %> deal delete 42 --yes',
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
static args = {
|
|
16
|
+
id: Args.integer({ required: true, description: 'Deal 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(DealDeleteCommand)
|
|
30
|
+
|
|
31
|
+
const ok = await confirmAction(`Delete deal ${args.id}?`, flags.yes)
|
|
32
|
+
if (!ok) {
|
|
33
|
+
throw new CliError('Aborted', { exitCode: 1 })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
await this.apiClient.del(`/api/v2/deals/${args.id}`)
|
|
37
|
+
this.log(chalk.green(`Deleted deal ${args.id}`))
|
|
38
|
+
}
|
|
39
|
+
}
|