@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
@@ -0,0 +1,42 @@
1
+ import { getFields, makeResolver } from './fields.js'
2
+ import { flattenRecord } from './output/record.js'
3
+
4
+ /**
5
+ * Output a single entity record: raw JSON for scripting, or a transposed
6
+ * field/value table with custom-field hash keys and option IDs resolved.
7
+ * @param {import('../base-command.js').default} cmd
8
+ * @param {object} record
9
+ * @param {string} [entity] deal | person | org | activity | product — omit
10
+ * for entities without resolvable custom fields (notes, files, webhooks, …)
11
+ */
12
+ export async function outputRecord(cmd, record, entity) {
13
+ if (cmd.resolveFormat() === 'table') {
14
+ if (
15
+ entity &&
16
+ record.custom_fields &&
17
+ Object.keys(record.custom_fields).length
18
+ ) {
19
+ const defs = await getFields(cmd.apiClient, entity)
20
+ record = makeResolver(defs).resolveCustomFields(record)
21
+ }
22
+ await cmd.outputResults(flattenRecord(record), {
23
+ field: { header: 'Field' },
24
+ value: { header: 'Value' },
25
+ })
26
+ return
27
+ }
28
+
29
+ await cmd.outputResults(record, {})
30
+ }
31
+
32
+ /**
33
+ * Fetch field definitions only when --field entries are present.
34
+ * @param {import('../base-command.js').default} cmd
35
+ * @param {string} entity
36
+ * @param {string[]} [fieldFlags]
37
+ * @returns {Promise<object[] | undefined>}
38
+ */
39
+ export async function defsForFields(cmd, entity, fieldFlags) {
40
+ if (!fieldFlags?.length) return undefined
41
+ return getFields(cmd.apiClient, entity)
42
+ }
@@ -0,0 +1,110 @@
1
+ import { CliError } from './errors.js'
2
+
3
+ const NUMERIC_TYPES = new Set(['double', 'monetary', 'int'])
4
+
5
+ /**
6
+ * Build a write-request body from typed flags, repeatable --field entries,
7
+ * and a raw --body JSON string. Precedence: raw body < typed flags/fields.
8
+ *
9
+ * --field entries are "Name=Value" where Name is a field's human name, its
10
+ * field_code, or a 40-char custom-field hash. Enum labels resolve to option
11
+ * IDs; set values are comma-separated labels; numeric field types coerce to
12
+ * Number. Custom-field values nest under custom_fields (v2 shape).
13
+ *
14
+ * @param {object} options
15
+ * @param {Record<string, unknown>} [options.typed] API-named values from typed flags
16
+ * @param {string[]} [options.fields] repeatable --field "Name=Value" entries
17
+ * @param {string} [options.rawBody] raw JSON string from --body
18
+ * @param {object[]} [options.defs] field definitions (required when fields given)
19
+ * @returns {object} request body
20
+ */
21
+ export function buildWriteBody({
22
+ typed = {},
23
+ fields = [],
24
+ rawBody,
25
+ defs,
26
+ } = {}) {
27
+ let body = {}
28
+
29
+ if (rawBody) {
30
+ try {
31
+ body = JSON.parse(rawBody)
32
+ } catch (err) {
33
+ throw new CliError(`--body is not valid JSON: ${err.message}`, {
34
+ exitCode: 65,
35
+ })
36
+ }
37
+ }
38
+
39
+ for (const [key, value] of Object.entries(typed)) {
40
+ if (value !== undefined) body[key] = value
41
+ }
42
+
43
+ for (const entry of fields) {
44
+ const eq = entry.indexOf('=')
45
+ if (eq === -1) {
46
+ throw new CliError(`Invalid --field "${entry}". Expected Name=Value`, {
47
+ exitCode: 64,
48
+ })
49
+ }
50
+ const name = entry.slice(0, eq).trim()
51
+ const rawValue = entry.slice(eq + 1)
52
+
53
+ const def = findField(defs ?? [], name)
54
+ if (!def) {
55
+ throw new CliError(
56
+ `Unknown field "${name}". Run: pdcli field list <entity>`,
57
+ { exitCode: 65 },
58
+ )
59
+ }
60
+
61
+ const value = coerceValue(def, rawValue)
62
+
63
+ if (def.is_custom_field ?? isHashKey(def.field_code)) {
64
+ body.custom_fields ??= {}
65
+ body.custom_fields[def.field_code] = value
66
+ } else {
67
+ body[def.field_code] = value
68
+ }
69
+ }
70
+
71
+ return body
72
+ }
73
+
74
+ function isHashKey(s) {
75
+ return /^[a-f0-9]{40}$/.test(s)
76
+ }
77
+
78
+ function findField(defs, name) {
79
+ const lower = name.toLowerCase()
80
+ return defs.find(
81
+ (d) => d.field_name.toLowerCase() === lower || d.field_code === name,
82
+ )
83
+ }
84
+
85
+ function coerceValue(def, rawValue) {
86
+ if (def.field_type === 'enum') {
87
+ return resolveOption(def, rawValue)
88
+ }
89
+ if (def.field_type === 'set') {
90
+ return rawValue.split(',').map((label) => resolveOption(def, label.trim()))
91
+ }
92
+ if (NUMERIC_TYPES.has(def.field_type)) {
93
+ return Number(rawValue)
94
+ }
95
+ return rawValue
96
+ }
97
+
98
+ function resolveOption(def, label) {
99
+ const option = def.options?.find(
100
+ (o) => o.label.toLowerCase() === label.toLowerCase(),
101
+ )
102
+ if (!option) {
103
+ const valid = def.options?.map((o) => o.label).join(', ') ?? '(none)'
104
+ throw new CliError(
105
+ `Unknown option "${label}" for field "${def.field_name}". Valid: ${valid}`,
106
+ { exitCode: 65 },
107
+ )
108
+ }
109
+ return option.id
110
+ }
@@ -64,6 +64,53 @@ export async function deleteToken(profile) {
64
64
  }
65
65
  }
66
66
 
67
+ /**
68
+ * @typedef {object} OAuthTokens
69
+ * @property {string} accessToken
70
+ * @property {string} refreshToken
71
+ * @property {number} expiresAt epoch ms
72
+ * @property {string} apiDomain e.g. https://acme.pipedrive.com
73
+ * @property {string} clientId
74
+ * @property {string} clientSecret kept in the keychain, never in config
75
+ */
76
+
77
+ /**
78
+ * @param {string} profile
79
+ * @returns {Promise<OAuthTokens | null>}
80
+ */
81
+ export async function getOAuthTokens(profile) {
82
+ if (!Entry) return null
83
+ const account = `${profile}/oauth`
84
+ try {
85
+ const raw = getEntry(account).getPassword()
86
+ return raw ? JSON.parse(raw) : null
87
+ } catch (err) {
88
+ debug('getOAuthTokens error: %s', err.message)
89
+ return null
90
+ }
91
+ }
92
+
93
+ /**
94
+ * @param {string} profile
95
+ * @param {OAuthTokens} tokens
96
+ */
97
+ export async function setOAuthTokens(profile, tokens) {
98
+ if (!Entry) keychainRequired()
99
+ const account = `${profile}/oauth`
100
+ getEntry(account).setPassword(JSON.stringify(tokens))
101
+ }
102
+
103
+ /** @param {string} profile */
104
+ export async function deleteOAuthTokens(profile) {
105
+ if (!Entry) return
106
+ const account = `${profile}/oauth`
107
+ try {
108
+ getEntry(account).deletePassword()
109
+ } catch (err) {
110
+ debug('deleteOAuthTokens error: %s', err.message)
111
+ }
112
+ }
113
+
67
114
  export function isKeychainAvailable() {
68
115
  return Entry !== null
69
116
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @param {object[]} data
3
+ * @param {Record<string, import('./table.js').Column>} columns
4
+ * @returns {string}
5
+ */
6
+ export function formatCsv(data, columns) {
7
+ if (!data || data.length === 0) return ''
8
+ const entries = Object.entries(columns)
9
+ const header = entries.map(([, col]) => col.header).join(',')
10
+ const rows = data.map((row) =>
11
+ entries
12
+ .map(([key, col]) => {
13
+ const val = col.get ? col.get(row) : row[col.key ?? key]
14
+ return csvEscape(val != null ? String(val) : '')
15
+ })
16
+ .join(','),
17
+ )
18
+ return [header, ...rows].join('\n')
19
+ }
20
+
21
+ function csvEscape(val) {
22
+ if (val.includes(',') || val.includes('"') || val.includes('\n')) {
23
+ return '"' + val.replace(/"/g, '""') + '"'
24
+ }
25
+ return val
26
+ }
@@ -1,10 +1,12 @@
1
1
  import { formatTable } from './table.js'
2
2
  import { formatJson } from './json.js'
3
+ import { formatYaml } from './yaml.js'
4
+ import { formatCsv } from './csv.js'
3
5
 
4
6
  /**
5
7
  * @param {object | object[]} data
6
8
  * @param {Record<string, import('./table.js').Column>} columns
7
- * @param {'table' | 'json'} format
9
+ * @param {'table' | 'json' | 'yaml' | 'csv'} format
8
10
  * @param {import('@oclif/core').Command} cmd
9
11
  */
10
12
  export function formatOutput(data, columns, format, cmd) {
@@ -14,6 +16,12 @@ export function formatOutput(data, columns, format, cmd) {
14
16
  case 'json':
15
17
  cmd.log(formatJson(data))
16
18
  break
19
+ case 'yaml':
20
+ cmd.log(formatYaml(data))
21
+ break
22
+ case 'csv':
23
+ cmd.log(formatCsv(items, columns))
24
+ break
17
25
  case 'table':
18
26
  default:
19
27
  cmd.log(formatTable(items, columns))
@@ -0,0 +1,9 @@
1
+ import yaml from 'js-yaml'
2
+
3
+ /**
4
+ * @param {unknown} data
5
+ * @returns {string}
6
+ */
7
+ export function formatYaml(data) {
8
+ return yaml.dump(data, { lineWidth: -1 }).trimEnd()
9
+ }