@seamapi/cli 0.0.1 → 0.4.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 (136) hide show
  1. package/README.md +59 -3
  2. package/bin/cli.d.ts +2 -0
  3. package/bin/cli.js +233 -0
  4. package/bin/cli.js.map +1 -0
  5. package/index.d.ts +2 -0
  6. package/index.js +5 -0
  7. package/index.js.map +1 -0
  8. package/lib/get-api-blueprint.d.ts +3 -0
  9. package/lib/get-api-blueprint.js +11 -0
  10. package/lib/get-api-blueprint.js.map +1 -0
  11. package/lib/get-command-blueprint-def.d.ts +2 -0
  12. package/lib/get-command-blueprint-def.js +11 -0
  13. package/lib/get-command-blueprint-def.js.map +1 -0
  14. package/lib/get-config-store.d.ts +2 -0
  15. package/lib/get-config-store.js +5 -0
  16. package/lib/get-config-store.js.map +1 -0
  17. package/lib/get-current-workspace-id.d.ts +1 -0
  18. package/lib/get-current-workspace-id.js +10 -0
  19. package/lib/get-current-workspace-id.js.map +1 -0
  20. package/lib/get-seam.d.ts +3 -0
  21. package/lib/get-seam.js +26 -0
  22. package/lib/get-seam.js.map +1 -0
  23. package/lib/get-server.d.ts +1 -0
  24. package/lib/get-server.js +7 -0
  25. package/lib/get-server.js.map +1 -0
  26. package/lib/interact-for-access-code.d.ts +3 -0
  27. package/lib/interact-for-access-code.js +19 -0
  28. package/lib/interact-for-access-code.js.map +1 -0
  29. package/lib/interact-for-acs-entrance.d.ts +1 -0
  30. package/lib/interact-for-acs-entrance.js +15 -0
  31. package/lib/interact-for-acs-entrance.js.map +1 -0
  32. package/lib/interact-for-acs-system.d.ts +1 -0
  33. package/lib/interact-for-acs-system.js +16 -0
  34. package/lib/interact-for-acs-system.js.map +1 -0
  35. package/lib/interact-for-acs-user.d.ts +1 -0
  36. package/lib/interact-for-acs-user.js +17 -0
  37. package/lib/interact-for-acs-user.js.map +1 -0
  38. package/lib/interact-for-action-attempt-poll.d.ts +2 -0
  39. package/lib/interact-for-action-attempt-poll.js +22 -0
  40. package/lib/interact-for-action-attempt-poll.js.map +1 -0
  41. package/lib/interact-for-array.d.ts +1 -0
  42. package/lib/interact-for-array.js +53 -0
  43. package/lib/interact-for-array.js.map +1 -0
  44. package/lib/interact-for-blueprint-object.d.ts +9 -0
  45. package/lib/interact-for-blueprint-object.js +235 -0
  46. package/lib/interact-for-blueprint-object.js.map +1 -0
  47. package/lib/interact-for-command-params.d.ts +5 -0
  48. package/lib/interact-for-command-params.js +11 -0
  49. package/lib/interact-for-command-params.js.map +1 -0
  50. package/lib/interact-for-command-selection.d.ts +2 -0
  51. package/lib/interact-for-command-selection.js +76 -0
  52. package/lib/interact-for-command-selection.js.map +1 -0
  53. package/lib/interact-for-connected-account.d.ts +1 -0
  54. package/lib/interact-for-connected-account.js +20 -0
  55. package/lib/interact-for-connected-account.js.map +1 -0
  56. package/lib/interact-for-custom-metadata.d.ts +6 -0
  57. package/lib/interact-for-custom-metadata.js +71 -0
  58. package/lib/interact-for-custom-metadata.js.map +1 -0
  59. package/lib/interact-for-device.d.ts +1 -0
  60. package/lib/interact-for-device.js +15 -0
  61. package/lib/interact-for-device.js.map +1 -0
  62. package/lib/interact-for-login.d.ts +1 -0
  63. package/lib/interact-for-login.js +39 -0
  64. package/lib/interact-for-login.js.map +1 -0
  65. package/lib/interact-for-resource.d.ts +11 -0
  66. package/lib/interact-for-resource.js +13 -0
  67. package/lib/interact-for-resource.js.map +1 -0
  68. package/lib/interact-for-server-selection.d.ts +1 -0
  69. package/lib/interact-for-server-selection.js +41 -0
  70. package/lib/interact-for-server-selection.js.map +1 -0
  71. package/lib/interact-for-timestamp.d.ts +1 -0
  72. package/lib/interact-for-timestamp.js +10 -0
  73. package/lib/interact-for-timestamp.js.map +1 -0
  74. package/lib/interact-for-use-remote-api-defs.d.ts +1 -0
  75. package/lib/interact-for-use-remote-api-defs.js +25 -0
  76. package/lib/interact-for-use-remote-api-defs.js.map +1 -0
  77. package/lib/interact-for-user-identity.d.ts +1 -0
  78. package/lib/interact-for-user-identity.js +15 -0
  79. package/lib/interact-for-user-identity.js.map +1 -0
  80. package/lib/interact-for-workspace-id.d.ts +1 -0
  81. package/lib/interact-for-workspace-id.js +31 -0
  82. package/lib/interact-for-workspace-id.js.map +1 -0
  83. package/lib/types.d.ts +5 -0
  84. package/lib/types.js +2 -0
  85. package/lib/types.js.map +1 -0
  86. package/lib/util/ellipsis.d.ts +1 -0
  87. package/lib/util/ellipsis.js +6 -0
  88. package/lib/util/ellipsis.js.map +1 -0
  89. package/lib/util/request-seam-api.d.ts +4 -0
  90. package/lib/util/request-seam-api.js +28 -0
  91. package/lib/util/request-seam-api.js.map +1 -0
  92. package/lib/util/with-loading.d.ts +1 -0
  93. package/lib/util/with-loading.js +14 -0
  94. package/lib/util/with-loading.js.map +1 -0
  95. package/lib/validate-token.d.ts +1 -0
  96. package/lib/validate-token.js +19 -0
  97. package/lib/validate-token.js.map +1 -0
  98. package/lib/version.d.ts +2 -0
  99. package/lib/version.js +3 -0
  100. package/lib/version.js.map +1 -0
  101. package/package.json +44 -18
  102. package/src/bin/cli.ts +265 -0
  103. package/src/index.ts +4 -1
  104. package/src/lib/get-api-blueprint.ts +17 -0
  105. package/src/lib/get-command-blueprint-def.ts +15 -0
  106. package/src/lib/get-config-store.ts +5 -0
  107. package/src/lib/get-current-workspace-id.ts +11 -0
  108. package/src/lib/get-seam.ts +43 -0
  109. package/src/lib/get-server.ts +9 -0
  110. package/src/lib/interact-for-access-code.ts +25 -0
  111. package/src/lib/interact-for-acs-entrance.ts +16 -0
  112. package/src/lib/interact-for-acs-system.ts +17 -0
  113. package/src/lib/interact-for-acs-user.ts +21 -0
  114. package/src/lib/interact-for-action-attempt-poll.ts +35 -0
  115. package/src/lib/interact-for-array.ts +61 -0
  116. package/src/lib/interact-for-blueprint-object.ts +274 -0
  117. package/src/lib/interact-for-command-params.ts +22 -0
  118. package/src/lib/interact-for-command-selection.ts +107 -0
  119. package/src/lib/interact-for-connected-account.ts +24 -0
  120. package/src/lib/interact-for-custom-metadata.ts +86 -0
  121. package/src/lib/interact-for-device.ts +15 -0
  122. package/src/lib/interact-for-login.ts +53 -0
  123. package/src/lib/interact-for-resource.ts +34 -0
  124. package/src/lib/interact-for-server-selection.ts +46 -0
  125. package/src/lib/interact-for-timestamp.ts +10 -0
  126. package/src/lib/interact-for-use-remote-api-defs.ts +27 -0
  127. package/src/lib/interact-for-user-identity.ts +16 -0
  128. package/src/lib/interact-for-workspace-id.ts +37 -0
  129. package/src/lib/types.ts +6 -0
  130. package/src/lib/util/ellipsis.ts +4 -0
  131. package/src/lib/util/request-seam-api.ts +43 -0
  132. package/src/lib/util/with-loading.ts +16 -0
  133. package/src/lib/validate-token.ts +28 -0
  134. package/src/lib/version.ts +3 -0
  135. package/src/lib/index.ts +0 -1
  136. package/src/lib/todo.ts +0 -1
@@ -0,0 +1,274 @@
1
+ import type { Parameter } from '@seamapi/blueprint'
2
+ import prompts from 'prompts'
3
+
4
+ import { interactForAccessCode } from './interact-for-access-code.js'
5
+ import { interactForAcsEntrance } from './interact-for-acs-entrance.js'
6
+ import { interactForAcsSystem } from './interact-for-acs-system.js'
7
+ import { interactForAcsUser } from './interact-for-acs-user.js'
8
+ import { interactForArray } from './interact-for-array.js'
9
+ import { interactForConnectedAccount } from './interact-for-connected-account.js'
10
+ import { interactForCustomMetadata } from './interact-for-custom-metadata.js'
11
+ import { interactForDevice } from './interact-for-device.js'
12
+ import { interactForTimestamp } from './interact-for-timestamp.js'
13
+ import { interactForUserIdentity } from './interact-for-user-identity.js'
14
+ import type { ContextHelpers } from './types.js'
15
+ import { ellipsis } from './util/ellipsis.js'
16
+
17
+ const ergonomicPropOrder = [
18
+ 'name',
19
+ 'connected_account_id',
20
+ 'device_id',
21
+ 'access_code_id',
22
+ 'user_identity_id',
23
+ 'code',
24
+ 'starts_at',
25
+ 'ends_at',
26
+ ]
27
+
28
+ export const interactForBlueprintObject = async (
29
+ args: {
30
+ command: string[]
31
+ parameters: Parameter[]
32
+ params: Record<string, any>
33
+ isSubProperty?: boolean
34
+ subPropertyPath?: string
35
+ },
36
+ ctx: ContextHelpers,
37
+ ): Promise<any> => {
38
+ // Clone args and args params so that we can mutate it
39
+ args = { ...args, params: { ...args.params } }
40
+
41
+ const properties = Object.fromEntries(
42
+ args.parameters.map((parameter) => [parameter.name, parameter]),
43
+ )
44
+ const required = args.parameters
45
+ .filter((parameter) => parameter.isRequired)
46
+ .map((parameter) => parameter.name)
47
+
48
+ const haveAllRequiredParams = required.every((k) => args.params[k])
49
+
50
+ const should_auto_submit =
51
+ !ctx.is_interactive && haveAllRequiredParams && !args.isSubProperty
52
+ if (should_auto_submit) {
53
+ return args.params
54
+ }
55
+
56
+ const propSortScore = (prop: string) => {
57
+ if (required.includes(prop)) return 100 - ergonomicPropOrder.indexOf(prop)
58
+ if (args.params[prop] !== undefined) {
59
+ return 50 - Object.keys(args.params).indexOf(prop)
60
+ }
61
+ return ergonomicPropOrder.indexOf(prop)
62
+ }
63
+
64
+ const cmdPath = `/${args.command.join('/').replace(/-/g, '_')}`
65
+ const parameterSelectionMessage = args.isSubProperty
66
+ ? `Editing "${args.subPropertyPath}"`
67
+ : `[${cmdPath}] Parameters`
68
+
69
+ console.log('')
70
+ const { paramToEdit } = await prompts({
71
+ name: 'paramToEdit',
72
+ message: parameterSelectionMessage,
73
+ type: 'autocomplete',
74
+ choices: [
75
+ ...(haveAllRequiredParams && !args.isSubProperty
76
+ ? [
77
+ {
78
+ value: 'done',
79
+ title: `[Make API Call] ${cmdPath}`,
80
+ },
81
+ ]
82
+ : []),
83
+ ...(haveAllRequiredParams && args.isSubProperty
84
+ ? [
85
+ {
86
+ title: `[Save]`,
87
+ value: 'done',
88
+ },
89
+ ]
90
+ : []),
91
+ ...Object.keys(properties)
92
+ .map((k) => {
93
+ return {
94
+ title: k + (required.includes(k) ? '*' : ''),
95
+ value: k,
96
+ description:
97
+ args.params[k] !== undefined
98
+ ? typeof args.params[k] === 'object'
99
+ ? ellipsis(JSON.stringify(args.params[k]), 60)
100
+ : `[${args.params[k]}]`
101
+ : undefined,
102
+ }
103
+ })
104
+ .sort((a, b) => propSortScore(b.value) - propSortScore(a.value)),
105
+ ...(args.isSubProperty
106
+ ? [
107
+ {
108
+ title: `[Leave Empty]`,
109
+ value: 'empty',
110
+ },
111
+ ]
112
+ : []),
113
+ {
114
+ title: `[Back]`,
115
+ value: 'back',
116
+ },
117
+ ],
118
+ })
119
+
120
+ if (paramToEdit === 'empty') {
121
+ return undefined
122
+ }
123
+
124
+ if (paramToEdit === 'done') {
125
+ // TODO check for required
126
+ return args.params
127
+ }
128
+
129
+ if (paramToEdit === 'back') {
130
+ if (args.subPropertyPath) {
131
+ return args.params
132
+ }
133
+
134
+ return '[Back]'
135
+ }
136
+
137
+ const prop = properties[paramToEdit]
138
+
139
+ if (paramToEdit === 'device_id') {
140
+ args.params[paramToEdit] = await interactForDevice()
141
+ return interactForBlueprintObject(args, ctx)
142
+ } else if (paramToEdit === 'access_code_id') {
143
+ args.params[paramToEdit] = await interactForAccessCode(args.params as any)
144
+ return interactForBlueprintObject(args, ctx)
145
+ } else if (paramToEdit === 'connected_account_id') {
146
+ const connected_account_id = await interactForConnectedAccount()
147
+ args.params[paramToEdit] = connected_account_id
148
+ return interactForBlueprintObject(args, ctx)
149
+ } else if (
150
+ paramToEdit === 'user_identity_id' ||
151
+ paramToEdit === 'user_identity_ids'
152
+ ) {
153
+ const user_identity_id = await interactForUserIdentity()
154
+ args.params[paramToEdit] =
155
+ paramToEdit === 'user_identity_ids'
156
+ ? [user_identity_id]
157
+ : user_identity_id
158
+ return interactForBlueprintObject(args, ctx)
159
+ } else if (paramToEdit.endsWith('acs_system_id')) {
160
+ args.params[paramToEdit] = await interactForAcsSystem()
161
+ return interactForBlueprintObject(args, ctx)
162
+ } else if (paramToEdit.endsWith('acs_user_id')) {
163
+ args.params[paramToEdit] = await interactForAcsUser()
164
+ return interactForBlueprintObject(args, ctx)
165
+ } else if (paramToEdit.endsWith('acs_entrance_id')) {
166
+ args.params['acs_entrance_id'] = await interactForAcsEntrance()
167
+ return interactForBlueprintObject(args, ctx)
168
+ } else if (
169
+ paramToEdit.endsWith('_at') ||
170
+ paramToEdit === 'since' ||
171
+ paramToEdit.endsWith('_before') ||
172
+ paramToEdit.endsWith('_after')
173
+ ) {
174
+ args.params[paramToEdit] = await interactForTimestamp()
175
+ return interactForBlueprintObject(args, ctx)
176
+ } else if (paramToEdit === 'custom_metadata') {
177
+ args.params[paramToEdit] = await interactForCustomMetadata(
178
+ args.params[paramToEdit] || {},
179
+ )
180
+ return interactForBlueprintObject(args, ctx)
181
+ }
182
+
183
+ if (prop) {
184
+ if (['string', 'id', 'datetime'].includes(prop.format)) {
185
+ let value
186
+ if (prop.format === 'datetime') {
187
+ value = await interactForTimestamp()
188
+ } else {
189
+ value = (
190
+ await prompts({
191
+ name: 'value',
192
+ message: `${paramToEdit}:`,
193
+ type: 'text',
194
+ })
195
+ ).value
196
+ }
197
+ args.params[paramToEdit] = value
198
+ return interactForBlueprintObject(args, ctx)
199
+ } else if (prop.format === 'enum') {
200
+ const value = (
201
+ await prompts({
202
+ name: 'value',
203
+ message: `${paramToEdit}:`,
204
+ type: 'select',
205
+ choices: prop.values.map((v) => ({
206
+ title: v.name,
207
+ value: v.name,
208
+ })),
209
+ })
210
+ ).value
211
+ args.params[paramToEdit] = value
212
+ return interactForBlueprintObject(args, ctx)
213
+ } else if (prop.format === 'boolean') {
214
+ const { value } = await prompts({
215
+ name: 'value',
216
+ message: `${paramToEdit}:`,
217
+ type: 'toggle',
218
+ initial: true,
219
+ active: 'true',
220
+ inactive: 'false',
221
+ })
222
+
223
+ args.params[paramToEdit] = value
224
+
225
+ return interactForBlueprintObject(args, ctx)
226
+ } else if (prop.format === 'list' && prop.itemFormat === 'enum') {
227
+ const value = (
228
+ await prompts({
229
+ name: 'value',
230
+ message: `${paramToEdit}:`,
231
+ type: 'autocompleteMultiselect',
232
+ choices: prop.itemEnumValues.map((v) => ({
233
+ title: v.name,
234
+ value: v.name,
235
+ })),
236
+ })
237
+ ).value
238
+ args.params[paramToEdit] = value
239
+ return interactForBlueprintObject(args, ctx)
240
+ } else if (prop.format === 'list') {
241
+ args.params[paramToEdit] = await interactForArray(
242
+ args.params[paramToEdit] || [],
243
+ `Edit the list for ${paramToEdit}`,
244
+ )
245
+ return interactForBlueprintObject(args, ctx)
246
+ } else if (prop.format === 'object') {
247
+ args.params[paramToEdit] = await interactForBlueprintObject(
248
+ {
249
+ command: args.command,
250
+ params: {},
251
+ parameters: prop.parameters,
252
+ isSubProperty: true,
253
+ subPropertyPath: paramToEdit,
254
+ },
255
+ ctx,
256
+ )
257
+ return interactForBlueprintObject(args, ctx)
258
+ } else if (prop.format === 'number') {
259
+ const { value } = await prompts({
260
+ name: 'value',
261
+ message: `${paramToEdit}:`,
262
+ type: 'number',
263
+ })
264
+
265
+ args.params[paramToEdit] = value
266
+
267
+ return interactForBlueprintObject(args, ctx)
268
+ }
269
+ }
270
+
271
+ throw new Error(
272
+ `Didn't know how to handle Blueprint parameter for property: "${paramToEdit}"`,
273
+ )
274
+ }
@@ -0,0 +1,22 @@
1
+ import { getCommandBlueprintDef } from './get-command-blueprint-def.js'
2
+ import { interactForBlueprintObject } from './interact-for-blueprint-object.js'
3
+ import type { ContextHelpers } from './types.js'
4
+
5
+ export const interactForCommandParams = async (
6
+ args: {
7
+ command: string[]
8
+ params: Record<string, any>
9
+ },
10
+ ctx: ContextHelpers,
11
+ ): Promise<any> => {
12
+ const endpoint = getCommandBlueprintDef(args.command, ctx)
13
+
14
+ return interactForBlueprintObject(
15
+ {
16
+ command: args.command,
17
+ params: args.params,
18
+ parameters: endpoint.request.parameters,
19
+ },
20
+ ctx,
21
+ )
22
+ }
@@ -0,0 +1,107 @@
1
+ import { isDeepStrictEqual as isEqual } from 'node:util'
2
+
3
+ import prompts from 'prompts'
4
+
5
+ import type { ContextHelpers } from './types.js'
6
+
7
+ const uniqBy = <T>(items: T[], keyOf: (item: T) => unknown): T[] => {
8
+ const seen = new Set<unknown>()
9
+ return items.filter((item) => {
10
+ const key = keyOf(item)
11
+ if (seen.has(key)) return false
12
+ seen.add(key)
13
+ return true
14
+ })
15
+ }
16
+
17
+ const ergonomicOrder = ['create', 'list', 'get', 'update', 'unlock_door']
18
+
19
+ function ergonomicSort(aStr: string, bStr: string) {
20
+ let a = ergonomicOrder.indexOf(aStr)
21
+ if (a === -1) a = ergonomicOrder.length
22
+ let b = ergonomicOrder.indexOf(bStr)
23
+ if (b === -1) b = ergonomicOrder.length
24
+
25
+ return a > b ? 1 : a < b ? -1 : 0
26
+ }
27
+
28
+ export async function interactForCommandSelection(
29
+ commandPath: string[],
30
+ helpers: ContextHelpers,
31
+ ) {
32
+ const commands = helpers.blueprint.routes
33
+ .flatMap((route) => route.endpoints)
34
+ .map((endpoint) =>
35
+ endpoint.path.replace(/_/g, '-').replace(/^\//, '').split('/'),
36
+ )
37
+ .concat([
38
+ ['login'],
39
+ ['logout'],
40
+ ['config', 'reveal-location'],
41
+ ['config', 'use-remote-api-defs'],
42
+ ['select', 'workspace'],
43
+ ['select', 'server'],
44
+ ['health', 'get-health'],
45
+ ])
46
+
47
+ const possibleCommands = uniqBy(
48
+ commandPath.length === 0
49
+ ? commands
50
+ : commands.filter((cmd) =>
51
+ isEqual(cmd.slice(0, commandPath.length), commandPath),
52
+ ),
53
+ (v) => v[commandPath.length],
54
+ )
55
+
56
+ if (possibleCommands.length === 0) {
57
+ throw new Error('No possible commands')
58
+ }
59
+
60
+ if (
61
+ possibleCommands.length === 1 &&
62
+ possibleCommands[0]?.length === commandPath.length
63
+ ) {
64
+ return commandPath
65
+ }
66
+
67
+ // Add dynamic 'back' command for sub-commands to allow returning
68
+ // to previous level.
69
+ if (commandPath.length > 0) {
70
+ possibleCommands.push([...commandPath, '[Back]'])
71
+ }
72
+
73
+ const commandPathStr = commandPath.join('/').replace(/-/g, '_')
74
+
75
+ const res = await prompts({
76
+ name: 'Command',
77
+ type: 'autocomplete',
78
+ choices: [
79
+ ...possibleCommands.map((cmd) => ({
80
+ title:
81
+ cmd?.[commandPath.length] ?? `[Call /${commandPathStr} Directly]`,
82
+ value: cmd?.[commandPath.length] ?? '<none>',
83
+ })),
84
+ ].sort((a, b) => ergonomicSort(a.value, b.value)),
85
+ message: `Select a command: /${commandPathStr}`,
86
+ })
87
+
88
+ if (res?.Command === undefined) {
89
+ throw new Error('Bailed')
90
+ }
91
+
92
+ if (res?.Command === '<none>') {
93
+ return commandPath
94
+ }
95
+
96
+ const newCommandPath = [...commandPath, res.Command]
97
+
98
+ const fullCommand = possibleCommands.find((cmd) =>
99
+ isEqual(newCommandPath, cmd),
100
+ )
101
+
102
+ if (!fullCommand) {
103
+ return interactForCommandSelection(newCommandPath, helpers)
104
+ }
105
+
106
+ return fullCommand
107
+ }
@@ -0,0 +1,24 @@
1
+ import { getSeam } from './get-seam.js'
2
+ import { interactForResource } from './interact-for-resource.js'
3
+ export const interactForConnectedAccount = async () => {
4
+ const seam = await getSeam()
5
+
6
+ return interactForResource({
7
+ resourceName: 'connected_account',
8
+ fetchResources: () => seam.connectedAccounts.list(),
9
+ toChoice: (connectedAccount) => {
10
+ const identifiers = Object.values(
11
+ connectedAccount.user_identifier ?? {},
12
+ ).filter((value): value is string => typeof value === 'string')
13
+
14
+ return {
15
+ title:
16
+ connectedAccount.user_identifier?.email ??
17
+ identifiers[0] ??
18
+ '<No Name>',
19
+ value: connectedAccount.connected_account_id,
20
+ description: `${connectedAccount.account_type} ${connectedAccount.connected_account_id}`,
21
+ }
22
+ },
23
+ })
24
+ }
@@ -0,0 +1,86 @@
1
+ import type { CustomMetadata } from '@seamapi/types/connect'
2
+ import prompts from 'prompts'
3
+
4
+ type UpdatedCustomMetadata = {
5
+ [x: string]: string | boolean | null
6
+ }
7
+
8
+ export const interactForCustomMetadata = async (
9
+ custom_metadata: CustomMetadata,
10
+ ) => {
11
+ const updated_custom_metadata: UpdatedCustomMetadata = { ...custom_metadata }
12
+
13
+ const displayCurrentCustomMetadata = () => {
14
+ console.log('custom_metadata:')
15
+ if (Object.keys(updated_custom_metadata).length > 0) {
16
+ Object.keys(updated_custom_metadata).forEach((key, index) => {
17
+ console.log(`${index + 1}: ${key}: ${updated_custom_metadata[key]}`)
18
+ })
19
+ } else {
20
+ console.log('The custom metadata param is empty.')
21
+ }
22
+ }
23
+
24
+ let action: string
25
+
26
+ do {
27
+ displayCurrentCustomMetadata()
28
+
29
+ const response = await prompts({
30
+ type: 'select',
31
+ name: 'action',
32
+ message: 'Choose an action:',
33
+ choices: [
34
+ { title: 'Add an item to params', value: 'add' },
35
+ { title: 'Remove an item from params', value: 'remove' },
36
+ { title: 'Finish editing params', value: 'done' },
37
+ ],
38
+ })
39
+
40
+ action = response.action
41
+
42
+ if (action === 'add') {
43
+ const { newKey } = await prompts({
44
+ type: 'text',
45
+ name: 'newKey',
46
+ message: 'Enter a key to add or edit:',
47
+ })
48
+
49
+ let { newValue } = await prompts({
50
+ type: 'text',
51
+ name: 'newValue',
52
+ message: 'Enter the new value to add or edit (or null to delete):',
53
+ })
54
+ if (newKey) {
55
+ if (newValue === 'false' || newValue === 'true') {
56
+ newValue = Boolean(newValue)
57
+ }
58
+ if (newValue === 'null') {
59
+ updated_custom_metadata[newKey] = null
60
+ } else {
61
+ updated_custom_metadata[newKey] = newValue
62
+ }
63
+ }
64
+ } else if (action === 'remove') {
65
+ const { custom_key_to_remove } = await prompts({
66
+ type: 'select',
67
+ name: 'custom_key_to_remove',
68
+ message: 'Choose a key-value pair to remove from params:',
69
+ choices: Object.keys(updated_custom_metadata).map(
70
+ (custom_metadata_key) => {
71
+ return {
72
+ title: `${custom_metadata_key}: ${updated_custom_metadata[custom_metadata_key]}`,
73
+ value: custom_metadata_key,
74
+ }
75
+ },
76
+ ),
77
+ })
78
+
79
+ if (custom_key_to_remove) {
80
+ delete custom_metadata[custom_key_to_remove]
81
+ }
82
+ }
83
+ } while (action !== 'done')
84
+
85
+ return updated_custom_metadata
86
+ }
@@ -0,0 +1,15 @@
1
+ import { getSeam } from './get-seam.js'
2
+ import { interactForResource } from './interact-for-resource.js'
3
+ export const interactForDevice = async () => {
4
+ const seam = await getSeam()
5
+
6
+ return interactForResource({
7
+ resourceName: 'device',
8
+ fetchResources: () => seam.devices.list(),
9
+ toChoice: (device) => ({
10
+ title: device.properties.name ?? '<No Name>',
11
+ value: device.device_id,
12
+ description: `${device.device_type} ${device.device_id}`,
13
+ }),
14
+ })
15
+ }
@@ -0,0 +1,53 @@
1
+ import { isApiKey, isPersonalAccessToken } from '@seamapi/http/connect'
2
+ import chalk from 'chalk'
3
+ import prompts from 'prompts'
4
+
5
+ import { getConfigStore } from './get-config-store.js'
6
+ import { getServer } from './get-server.js'
7
+ import { interactForWorkspaceId } from './interact-for-workspace-id.js'
8
+ import { withLoading } from './util/with-loading.js'
9
+ import { validateToken } from './validate-token.js'
10
+
11
+ export const interactForLogin = async () => {
12
+ const config = await getConfigStore()
13
+
14
+ if (getServer().includes('localhost')) {
15
+ console.log(
16
+ `You're using a local Seam Connect instance, you can enter the API Key to your local user, you can create a new user from:\n\n${getServer()}/admin/create_user_with_api_key`,
17
+ )
18
+ } else {
19
+ console.log(
20
+ `To login, navigate to the URL below and create a new Personal Access Token (PAT) and paste the PAT in the provided box:\n\nhttps://console.seam.co/settings/access-tokens\n\n`,
21
+ )
22
+ }
23
+
24
+ console.log(
25
+ chalk.gray(
26
+ '> Note: You can enter an API Key here for single-workspace access',
27
+ ),
28
+ )
29
+
30
+ const { pat } = await prompts({
31
+ name: 'pat',
32
+ type: 'text',
33
+ message: 'Personal Access Token:',
34
+ })
35
+ const token = pat?.trim()
36
+
37
+ if (!token) {
38
+ throw new Error('No token provided')
39
+ }
40
+
41
+ if (isPersonalAccessToken(token)) {
42
+ await interactForWorkspaceId(token)
43
+ } else if (isApiKey(token)) {
44
+ await withLoading('Validating API key...', () => validateToken(token))
45
+ } else {
46
+ throw new Error(
47
+ 'Invalid token: expected a Personal Access Token or API Key',
48
+ )
49
+ }
50
+
51
+ config.set(`${getServer()}.pat`, token)
52
+ console.log(`Token saved! You may begin using the CLI!`)
53
+ }
@@ -0,0 +1,34 @@
1
+ import prompts from 'prompts'
2
+
3
+ import { withLoading } from './util/with-loading.js'
4
+
5
+ export interface ResourceChoice {
6
+ title: string
7
+ value: string
8
+ description?: string
9
+ }
10
+
11
+ export const interactForResource = async <Resource>({
12
+ resourceName,
13
+ fetchResources,
14
+ toChoice,
15
+ message = `Select a ${resourceName}:`,
16
+ }: {
17
+ resourceName: string
18
+ fetchResources: () => Promise<Resource[]>
19
+ toChoice: (resource: Resource) => ResourceChoice
20
+ message?: string | undefined
21
+ }) => {
22
+ const resources = await withLoading(
23
+ `Fetching ${resourceName.replace(/_/g, ' ')}s...`,
24
+ fetchResources,
25
+ )
26
+ const { resourceId } = await prompts({
27
+ name: 'resourceId',
28
+ type: 'autocomplete',
29
+ message,
30
+ choices: resources.map(toChoice),
31
+ })
32
+
33
+ return resourceId as string
34
+ }
@@ -0,0 +1,46 @@
1
+ import { randomBytes } from 'node:crypto'
2
+
3
+ import prompts from 'prompts'
4
+
5
+ import { getConfigStore } from './get-config-store.js'
6
+ import { getServer } from './get-server.js'
7
+
8
+ export async function interactForServerSelection() {
9
+ const servers = [
10
+ 'http://localhost:3020',
11
+ 'https://connect.getseam.com',
12
+ 'https://fakeseamconnect.seam.vc',
13
+ ]
14
+
15
+ const { server } = await prompts([
16
+ {
17
+ type: 'select',
18
+ name: 'server',
19
+ message: 'Select a server:',
20
+ choices: servers.map((server) => ({ title: server, value: server })),
21
+ },
22
+ ])
23
+
24
+ const config = getConfigStore()
25
+ if (server === servers[2]) {
26
+ let { userUrlSeed } = await prompts([
27
+ {
28
+ type: 'text',
29
+ name: 'userUrlSeed',
30
+ message:
31
+ 'You can input a custom server URL or leave this field empty to use a new fakeserver.',
32
+ },
33
+ ])
34
+
35
+ if (userUrlSeed.trim().length === 0) {
36
+ userUrlSeed = randomBytes(5).toString('hex')
37
+ }
38
+ config.set('server', `https://${userUrlSeed}.fakeseamconnect.seam.vc`)
39
+ config.set(`${getServer()}.pat`, `seam_apikey1_token`)
40
+ console.log(`PAT set to use fakeseamconnect with "seam_apikey1_token"`)
41
+ } else {
42
+ config.set('server', server)
43
+ }
44
+ config.delete('current_workspace_id')
45
+ console.log(`Server set to ${server}`)
46
+ }
@@ -0,0 +1,10 @@
1
+ import prompts from 'prompts'
2
+ export const interactForTimestamp = async () => {
3
+ const { timestamp } = await prompts({
4
+ name: 'timestamp',
5
+ type: 'date',
6
+ message: 'Enter a timestamp:',
7
+ })
8
+
9
+ return timestamp.toISOString()
10
+ }