@seamapi/cli 0.10.0 → 0.12.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 (115) hide show
  1. package/README.md +118 -0
  2. package/bin/cli.js +78 -100
  3. package/bin/cli.js.map +1 -1
  4. package/completions/seam.bash +11 -0
  5. package/completions/seam.fish +11 -0
  6. package/completions/seam.zsh +13 -0
  7. package/lib/command-spec.d.ts +53 -0
  8. package/lib/command-spec.js +290 -0
  9. package/lib/command-spec.js.map +1 -0
  10. package/lib/completion/describe.d.ts +10 -0
  11. package/lib/completion/describe.js +16 -0
  12. package/lib/completion/describe.js.map +1 -0
  13. package/lib/completion/index.d.ts +20 -0
  14. package/lib/completion/index.js +65 -0
  15. package/lib/completion/index.js.map +1 -0
  16. package/lib/completion/render-bash.d.ts +2 -0
  17. package/lib/completion/render-bash.js +98 -0
  18. package/lib/completion/render-bash.js.map +1 -0
  19. package/lib/completion/render-fish.d.ts +2 -0
  20. package/lib/completion/render-fish.js +58 -0
  21. package/lib/completion/render-fish.js.map +1 -0
  22. package/lib/completion/render-zsh.d.ts +2 -0
  23. package/lib/completion/render-zsh.js +121 -0
  24. package/lib/completion/render-zsh.js.map +1 -0
  25. package/lib/get-response-key.d.ts +9 -0
  26. package/lib/get-response-key.js +21 -0
  27. package/lib/get-response-key.js.map +1 -0
  28. package/lib/interact-for-action-attempt-poll.js +4 -3
  29. package/lib/interact-for-action-attempt-poll.js.map +1 -1
  30. package/lib/interact-for-array.js +9 -7
  31. package/lib/interact-for-array.js.map +1 -1
  32. package/lib/interact-for-blueprint-object.js +9 -8
  33. package/lib/interact-for-blueprint-object.js.map +1 -1
  34. package/lib/interact-for-command-selection.js +2 -2
  35. package/lib/interact-for-command-selection.js.map +1 -1
  36. package/lib/interact-for-custom-metadata.js +10 -8
  37. package/lib/interact-for-custom-metadata.js.map +1 -1
  38. package/lib/interact-for-login.js +8 -6
  39. package/lib/interact-for-login.js.map +1 -1
  40. package/lib/interact-for-resource.js +2 -2
  41. package/lib/interact-for-resource.js.map +1 -1
  42. package/lib/interact-for-server-selection.js +7 -5
  43. package/lib/interact-for-server-selection.js.map +1 -1
  44. package/lib/interact-for-timestamp.js +2 -2
  45. package/lib/interact-for-timestamp.js.map +1 -1
  46. package/lib/interact-for-use-remote-api-defs.js +4 -3
  47. package/lib/interact-for-use-remote-api-defs.js.map +1 -1
  48. package/lib/interact-for-workspace-id.js +2 -2
  49. package/lib/interact-for-workspace-id.js.map +1 -1
  50. package/lib/output/create-memory-output.d.ts +13 -0
  51. package/lib/output/create-memory-output.js +22 -0
  52. package/lib/output/create-memory-output.js.map +1 -0
  53. package/lib/output/create-output.d.ts +50 -0
  54. package/lib/output/create-output.js +34 -0
  55. package/lib/output/create-output.js.map +1 -0
  56. package/lib/output/get-output.d.ts +12 -0
  57. package/lib/output/get-output.js +21 -0
  58. package/lib/output/get-output.js.map +1 -0
  59. package/lib/output/resolve-output-format.d.ts +16 -0
  60. package/lib/output/resolve-output-format.js +23 -0
  61. package/lib/output/resolve-output-format.js.map +1 -0
  62. package/lib/output/select-response-payload.d.ts +16 -0
  63. package/lib/output/select-response-payload.js +33 -0
  64. package/lib/output/select-response-payload.js.map +1 -0
  65. package/lib/render-help.d.ts +8 -0
  66. package/lib/render-help.js +150 -0
  67. package/lib/render-help.js.map +1 -0
  68. package/lib/util/cli-args.d.ts +16 -1
  69. package/lib/util/cli-args.js +18 -2
  70. package/lib/util/cli-args.js.map +1 -1
  71. package/lib/util/prompt.d.ts +17 -0
  72. package/lib/util/prompt.js +25 -0
  73. package/lib/util/prompt.js.map +1 -0
  74. package/lib/util/read-stdin-json.d.ts +22 -0
  75. package/lib/util/read-stdin-json.js +42 -0
  76. package/lib/util/read-stdin-json.js.map +1 -0
  77. package/lib/util/request-seam-api.d.ts +5 -2
  78. package/lib/util/request-seam-api.js +13 -15
  79. package/lib/util/request-seam-api.js.map +1 -1
  80. package/lib/util/with-loading.js +6 -1
  81. package/lib/util/with-loading.js.map +1 -1
  82. package/lib/version.d.ts +1 -1
  83. package/lib/version.js +1 -1
  84. package/package.json +2 -1
  85. package/src/bin/cli.ts +100 -106
  86. package/src/lib/command-spec.ts +400 -0
  87. package/src/lib/completion/describe.ts +21 -0
  88. package/src/lib/completion/index.ts +82 -0
  89. package/src/lib/completion/render-bash.ts +125 -0
  90. package/src/lib/completion/render-fish.ts +80 -0
  91. package/src/lib/completion/render-zsh.ts +157 -0
  92. package/src/lib/get-response-key.ts +25 -0
  93. package/src/lib/interact-for-action-attempt-poll.ts +4 -3
  94. package/src/lib/interact-for-array.ts +9 -7
  95. package/src/lib/interact-for-blueprint-object.ts +9 -8
  96. package/src/lib/interact-for-command-selection.ts +2 -3
  97. package/src/lib/interact-for-custom-metadata.ts +10 -8
  98. package/src/lib/interact-for-login.ts +8 -6
  99. package/src/lib/interact-for-resource.ts +2 -3
  100. package/src/lib/interact-for-server-selection.ts +7 -6
  101. package/src/lib/interact-for-timestamp.ts +2 -2
  102. package/src/lib/interact-for-use-remote-api-defs.ts +4 -4
  103. package/src/lib/interact-for-workspace-id.ts +2 -2
  104. package/src/lib/output/create-memory-output.ts +39 -0
  105. package/src/lib/output/create-output.ts +104 -0
  106. package/src/lib/output/get-output.ts +24 -0
  107. package/src/lib/output/resolve-output-format.ts +33 -0
  108. package/src/lib/output/select-response-payload.ts +56 -0
  109. package/src/lib/render-help.ts +197 -0
  110. package/src/lib/util/cli-args.ts +32 -2
  111. package/src/lib/util/prompt.ts +38 -0
  112. package/src/lib/util/read-stdin-json.ts +59 -0
  113. package/src/lib/util/request-seam-api.ts +24 -20
  114. package/src/lib/util/with-loading.ts +9 -1
  115. package/src/lib/version.ts +1 -1
@@ -0,0 +1,157 @@
1
+ import {
2
+ type CommandFlag,
3
+ type CommandSpec,
4
+ flagTokens,
5
+ } from '../command-spec.js'
6
+ import { describeForShell } from './describe.js'
7
+
8
+ export const renderZshCompletion = (spec: CommandSpec): string => {
9
+ const valuelessTokens = spec.globalFlags
10
+ .filter(({ takesValue }) => !takesValue)
11
+ .flatMap(flagTokens)
12
+ .sort()
13
+
14
+ return `${[
15
+ header,
16
+ renderCase('_seam_subcommands', subcommandBranches(spec)),
17
+ renderCase('_seam_flags', flagBranches(spec)),
18
+ renderCase('_seam_flag_values', flagValueBranches(spec)),
19
+ `_seam_global_flags() {\n _seam_reply+=(${describeFlags(spec.globalFlags)})\n}`,
20
+ completionFunction(valuelessTokens),
21
+ dispatch,
22
+ ].join('\n\n')}\n`
23
+ }
24
+
25
+ const header = `#compdef seam
26
+
27
+ # zsh completion for the seam command.
28
+ #
29
+ # Generated by @seamapi/cli from the Seam API definitions.
30
+ # Do not edit: regenerate with 'seam completion zsh'.
31
+ #
32
+ # Load it for the current shell with
33
+ #
34
+ # source <(seam completion zsh)
35
+ #
36
+ # or install it for every shell with
37
+ #
38
+ # seam completion zsh > "\${fpath[1]}/_seam"`
39
+
40
+ const completionFunction = (valuelessTokens: string[]): string =>
41
+ `_seam() {
42
+ local -a _seam_reply
43
+ local -a valueless
44
+ local command previous word
45
+ local -i index
46
+
47
+ valueless=(${valuelessTokens.join(' ')})
48
+
49
+ # The command path is the run of words before the first flag.
50
+ command=''
51
+ for (( index = 2; index < CURRENT; index++ )); do
52
+ word="\${words[index]}"
53
+ if [[ "$word" == -* ]]; then
54
+ break
55
+ fi
56
+ command="\${command:+$command }$word"
57
+ done
58
+
59
+ previous=''
60
+ if (( CURRENT > 1 )); then
61
+ previous="\${words[CURRENT - 1]}"
62
+ fi
63
+
64
+ # Completing the value of a flag that takes one.
65
+ if [[ "$previous" == -* ]] && (( \${valueless[(Ie)$previous]} == 0 )); then
66
+ _seam_flag_values "$command $previous"
67
+ if (( \${#_seam_reply} )); then
68
+ _describe -t values 'value' _seam_reply
69
+ fi
70
+ return
71
+ fi
72
+
73
+ if [[ "\${words[CURRENT]}" == -* ]]; then
74
+ _seam_flags "$command"
75
+ _seam_global_flags
76
+ _describe -t options 'option' _seam_reply
77
+ return
78
+ fi
79
+
80
+ _seam_subcommands "$command"
81
+ if (( \${#_seam_reply} )); then
82
+ _describe -t commands 'command' _seam_reply
83
+ return
84
+ fi
85
+
86
+ _seam_flags "$command"
87
+ _seam_global_flags
88
+ _describe -t options 'option' _seam_reply
89
+ }`
90
+
91
+ // The script runs in three ways. Autoloaded from fpath as _seam, it must
92
+ // complete the in-flight request: funcstack holds _seam. Evaluated by the
93
+ // loader stub inside the autoloaded _seam, the same applies, but eval pushes
94
+ // '(eval)' onto funcstack, so search the whole stack rather than the top.
95
+ // Sourced into a shell, funcstack holds no _seam: register with compdef.
96
+ const dispatch = `if (( \${funcstack[(I)_seam]} )); then
97
+ _seam "$@"
98
+ else
99
+ compdef _seam seam
100
+ fi`
101
+
102
+ interface Branch {
103
+ pattern: string
104
+ entries: string[]
105
+ }
106
+
107
+ const subcommandBranches = (spec: CommandSpec): Branch[] =>
108
+ spec.groups.map((group) => ({
109
+ pattern: group.path.join(' '),
110
+ entries: group.subcommands.map(({ name, description }) =>
111
+ describe(name, description),
112
+ ),
113
+ }))
114
+
115
+ const flagBranches = (spec: CommandSpec): Branch[] =>
116
+ spec.commands
117
+ .filter(({ flags }) => flags.length > 0)
118
+ .map((command) => ({
119
+ pattern: command.path.join(' '),
120
+ entries: [describeFlags(command.flags)],
121
+ }))
122
+
123
+ const flagValueBranches = (spec: CommandSpec): Branch[] =>
124
+ spec.commands.flatMap((command) =>
125
+ command.flags
126
+ .filter(({ values }) => values.length > 0)
127
+ .flatMap((flag) =>
128
+ flagTokens(flag).map((token) => ({
129
+ pattern: `${command.path.join(' ')} ${token}`,
130
+ entries: flag.values.map((value) => `'${value}'`),
131
+ })),
132
+ ),
133
+ )
134
+
135
+ const describeFlags = (flags: CommandFlag[]): string =>
136
+ flags
137
+ .flatMap((flag) =>
138
+ flagTokens(flag).map((token) => describe(token, flag.description)),
139
+ )
140
+ .join(' ')
141
+
142
+ const describe = (value: string, description: string): string => {
143
+ const summary = describeForShell(description)
144
+ return summary === '' ? `'${value}'` : `'${value}:${summary}'`
145
+ }
146
+
147
+ const renderCase = (name: string, branches: Branch[]): string =>
148
+ [
149
+ `${name}() {`,
150
+ ` case "$1" in`,
151
+ ...branches.map(
152
+ ({ pattern, entries }) =>
153
+ ` ('${pattern}') _seam_reply+=(${entries.join(' ')}) ;;`,
154
+ ),
155
+ ` esac`,
156
+ `}`,
157
+ ].join('\n')
@@ -0,0 +1,25 @@
1
+ import { getCommandBlueprintDef } from './get-command-blueprint-def.js'
2
+ import type { ContextHelpers } from './types.js'
3
+
4
+ /**
5
+ * The top level response key documented for a command,
6
+ * e.g., `devices` for `seam devices list`.
7
+ *
8
+ * Returns null when the command is not in the blueprint,
9
+ * or when it does not respond with a resource.
10
+ */
11
+ export const getResponseKey = (
12
+ command: string[],
13
+ ctx: ContextHelpers,
14
+ ): string | null => {
15
+ let endpoint
16
+ try {
17
+ endpoint = getCommandBlueprintDef(command, ctx)
18
+ } catch {
19
+ return null
20
+ }
21
+
22
+ if (endpoint.response.responseType === 'void') return null
23
+
24
+ return endpoint.response.responseKey
25
+ }
@@ -1,14 +1,15 @@
1
1
  import type { ActionAttemptsGetResponse } from '@seamapi/http/connect'
2
- import prompts from 'prompts'
3
2
 
4
3
  import { getSeam } from './get-seam.js'
4
+ import { getOutput } from './output/get-output.js'
5
+ import { prompt } from './util/prompt.js'
5
6
  import { withLoading } from './util/with-loading.js'
6
7
 
7
8
  export const interactForActionAttemptPoll = async (
8
9
  action_attempt: ActionAttemptsGetResponse['action_attempt'],
9
10
  ) => {
10
11
  if (action_attempt.status === 'pending') {
11
- const { poll_for_action_attempt } = await prompts({
12
+ const { poll_for_action_attempt } = await prompt({
12
13
  name: 'poll_for_action_attempt',
13
14
  message: "Would you like to poll the action attempt until it's ready?",
14
15
  type: 'toggle',
@@ -29,7 +30,7 @@ export const interactForActionAttemptPoll = async (
29
30
  { waitForActionAttempt: { pollingInterval: 240, timeout: 10_000 } },
30
31
  ),
31
32
  )
32
- console.dir(updated_action_attempt, { depth: null })
33
+ getOutput().data({ action_attempt: updated_action_attempt })
33
34
  }
34
35
  }
35
36
  }
@@ -1,19 +1,21 @@
1
- import prompts from 'prompts'
1
+ import { getOutput } from './output/get-output.js'
2
+ import { prompt } from './util/prompt.js'
2
3
 
3
4
  export const interactForArray = async (
4
5
  array: string[],
5
6
  message: string,
6
7
  ): Promise<string[]> => {
7
8
  const updatedArray = [...array]
9
+ const output = getOutput()
8
10
 
9
11
  const displayList = () => {
10
- console.log(`${message} Current list:`)
12
+ output.info(`${message} Current list:`)
11
13
  if (updatedArray.length > 0) {
12
14
  updatedArray.forEach((item, index) => {
13
- console.log(`${index + 1}: ${item}`)
15
+ output.info(`${index + 1}: ${item}`)
14
16
  })
15
17
  } else {
16
- console.log('The list is currently empty.')
18
+ output.info('The list is currently empty.')
17
19
  }
18
20
  }
19
21
 
@@ -21,7 +23,7 @@ export const interactForArray = async (
21
23
  do {
22
24
  displayList()
23
25
 
24
- const response = await prompts({
26
+ const response = await prompt({
25
27
  type: 'select',
26
28
  name: 'action',
27
29
  message: 'Choose an action:',
@@ -35,7 +37,7 @@ export const interactForArray = async (
35
37
  action = response.action
36
38
 
37
39
  if (action === 'add') {
38
- const { newItem } = await prompts({
40
+ const { newItem } = await prompt({
39
41
  type: 'text',
40
42
  name: 'newItem',
41
43
  message: 'Enter the new item:',
@@ -44,7 +46,7 @@ export const interactForArray = async (
44
46
  updatedArray.push(newItem)
45
47
  }
46
48
  } else if (action === 'remove') {
47
- const { index } = await prompts({
49
+ const { index } = await prompt({
48
50
  type: 'number',
49
51
  name: 'index',
50
52
  message: 'Enter the index of the item to remove:',
@@ -1,5 +1,4 @@
1
1
  import type { Parameter } from '@seamapi/blueprint'
2
- import prompts from 'prompts'
3
2
 
4
3
  import { interactForAccessCode } from './interact-for-access-code.js'
5
4
  import { interactForAcsEntrance } from './interact-for-acs-entrance.js'
@@ -11,9 +10,11 @@ import { interactForCustomMetadata } from './interact-for-custom-metadata.js'
11
10
  import { interactForDevice } from './interact-for-device.js'
12
11
  import { interactForTimestamp } from './interact-for-timestamp.js'
13
12
  import { interactForUserIdentity } from './interact-for-user-identity.js'
13
+ import { getOutput } from './output/get-output.js'
14
14
  import type { ContextHelpers } from './types.js'
15
15
  import { NonInteractiveError, toArgName } from './util/cli-args.js'
16
16
  import { ellipsis } from './util/ellipsis.js'
17
+ import { prompt } from './util/prompt.js'
17
18
 
18
19
  const ergonomicPropOrder = [
19
20
  'name',
@@ -82,8 +83,8 @@ export const interactForBlueprintObject = async (
82
83
  ? `Editing "${args.subPropertyPath}"`
83
84
  : `[${cmdPath}] Parameters`
84
85
 
85
- console.log('')
86
- const { paramToEdit } = await prompts({
86
+ getOutput().info()
87
+ const { paramToEdit } = await prompt({
87
88
  name: 'paramToEdit',
88
89
  message: parameterSelectionMessage,
89
90
  type: 'autocomplete',
@@ -203,7 +204,7 @@ export const interactForBlueprintObject = async (
203
204
  value = await interactForTimestamp()
204
205
  } else {
205
206
  value = (
206
- await prompts({
207
+ await prompt({
207
208
  name: 'value',
208
209
  message: `${paramToEdit}:`,
209
210
  type: 'text',
@@ -214,7 +215,7 @@ export const interactForBlueprintObject = async (
214
215
  return interactForBlueprintObject(args, ctx)
215
216
  } else if (prop.format === 'enum') {
216
217
  const value = (
217
- await prompts({
218
+ await prompt({
218
219
  name: 'value',
219
220
  message: `${paramToEdit}:`,
220
221
  type: 'select',
@@ -227,7 +228,7 @@ export const interactForBlueprintObject = async (
227
228
  args.params[paramToEdit] = value
228
229
  return interactForBlueprintObject(args, ctx)
229
230
  } else if (prop.format === 'boolean') {
230
- const { value } = await prompts({
231
+ const { value } = await prompt({
231
232
  name: 'value',
232
233
  message: `${paramToEdit}:`,
233
234
  type: 'toggle',
@@ -241,7 +242,7 @@ export const interactForBlueprintObject = async (
241
242
  return interactForBlueprintObject(args, ctx)
242
243
  } else if (prop.format === 'list' && prop.itemFormat === 'enum') {
243
244
  const value = (
244
- await prompts({
245
+ await prompt({
245
246
  name: 'value',
246
247
  message: `${paramToEdit}:`,
247
248
  type: 'autocompleteMultiselect',
@@ -272,7 +273,7 @@ export const interactForBlueprintObject = async (
272
273
  )
273
274
  return interactForBlueprintObject(args, ctx)
274
275
  } else if (prop.format === 'number') {
275
- const { value } = await prompts({
276
+ const { value } = await prompt({
276
277
  name: 'value',
277
278
  message: `${paramToEdit}:`,
278
279
  type: 'number',
@@ -1,9 +1,8 @@
1
1
  import { isDeepStrictEqual as isEqual } from 'node:util'
2
2
 
3
- import prompts from 'prompts'
4
-
5
3
  import type { ContextHelpers } from './types.js'
6
4
  import { NonInteractiveError } from './util/cli-args.js'
5
+ import { prompt } from './util/prompt.js'
7
6
 
8
7
  const uniqBy = <T>(items: T[], keyOf: (item: T) => unknown): T[] => {
9
8
  const seen = new Set<unknown>()
@@ -93,7 +92,7 @@ export async function interactForCommandSelection(
93
92
 
94
93
  const commandPathStr = commandPath.join('/').replace(/-/g, '_')
95
94
 
96
- const res = await prompts({
95
+ const res = await prompt({
97
96
  name: 'Command',
98
97
  type: 'autocomplete',
99
98
  choices: [
@@ -1,4 +1,5 @@
1
- import prompts from 'prompts'
1
+ import { getOutput } from './output/get-output.js'
2
+ import { prompt } from './util/prompt.js'
2
3
 
3
4
  // Structurally the CustomMetadata of @seamapi/types, spelled out here so the
4
5
  // published declarations do not depend on a development-only package.
@@ -12,15 +13,16 @@ export const interactForCustomMetadata = async (
12
13
  custom_metadata: CustomMetadata,
13
14
  ) => {
14
15
  const updated_custom_metadata: UpdatedCustomMetadata = { ...custom_metadata }
16
+ const output = getOutput()
15
17
 
16
18
  const displayCurrentCustomMetadata = () => {
17
- console.log('custom_metadata:')
19
+ output.info('custom_metadata:')
18
20
  if (Object.keys(updated_custom_metadata).length > 0) {
19
21
  Object.keys(updated_custom_metadata).forEach((key, index) => {
20
- console.log(`${index + 1}: ${key}: ${updated_custom_metadata[key]}`)
22
+ output.info(`${index + 1}: ${key}: ${updated_custom_metadata[key]}`)
21
23
  })
22
24
  } else {
23
- console.log('The custom metadata param is empty.')
25
+ output.info('The custom metadata param is empty.')
24
26
  }
25
27
  }
26
28
 
@@ -29,7 +31,7 @@ export const interactForCustomMetadata = async (
29
31
  do {
30
32
  displayCurrentCustomMetadata()
31
33
 
32
- const response = await prompts({
34
+ const response = await prompt({
33
35
  type: 'select',
34
36
  name: 'action',
35
37
  message: 'Choose an action:',
@@ -43,13 +45,13 @@ export const interactForCustomMetadata = async (
43
45
  action = response.action
44
46
 
45
47
  if (action === 'add') {
46
- const { newKey } = await prompts({
48
+ const { newKey } = await prompt({
47
49
  type: 'text',
48
50
  name: 'newKey',
49
51
  message: 'Enter a key to add or edit:',
50
52
  })
51
53
 
52
- let { newValue } = await prompts({
54
+ let { newValue } = await prompt({
53
55
  type: 'text',
54
56
  name: 'newValue',
55
57
  message: 'Enter the new value to add or edit (or null to delete):',
@@ -65,7 +67,7 @@ export const interactForCustomMetadata = async (
65
67
  }
66
68
  }
67
69
  } else if (action === 'remove') {
68
- const { custom_key_to_remove } = await prompts({
70
+ const { custom_key_to_remove } = await prompt({
69
71
  type: 'select',
70
72
  name: 'custom_key_to_remove',
71
73
  message: 'Choose a key-value pair to remove from params:',
@@ -1,33 +1,35 @@
1
1
  import { isApiKey, isPersonalAccessToken } from '@seamapi/http/connect'
2
2
  import chalk from 'chalk'
3
- import prompts from 'prompts'
4
3
 
5
4
  import { getConfigStore } from './config/index.js'
6
5
  import { getServer } from './get-server.js'
7
6
  import { interactForWorkspaceId } from './interact-for-workspace-id.js'
7
+ import { getOutput } from './output/get-output.js'
8
+ import { prompt } from './util/prompt.js'
8
9
  import { withLoading } from './util/with-loading.js'
9
10
  import { validateToken } from './validate-token.js'
10
11
 
11
12
  export const interactForLogin = async () => {
12
13
  const config = await getConfigStore()
14
+ const output = getOutput()
13
15
 
14
16
  if (getServer().includes('localhost')) {
15
- console.log(
17
+ output.info(
16
18
  `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
19
  )
18
20
  } else {
19
- console.log(
21
+ output.info(
20
22
  `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
23
  )
22
24
  }
23
25
 
24
- console.log(
26
+ output.info(
25
27
  chalk.gray(
26
28
  '> Note: You can enter an API Key here for single-workspace access',
27
29
  ),
28
30
  )
29
31
 
30
- const { pat } = await prompts({
32
+ const { pat } = await prompt({
31
33
  name: 'pat',
32
34
  type: 'text',
33
35
  message: 'Personal Access Token:',
@@ -49,5 +51,5 @@ export const interactForLogin = async () => {
49
51
  }
50
52
 
51
53
  config.set(`${getServer()}.pat`, token)
52
- console.log(`Token saved! You may begin using the CLI!`)
54
+ output.info(`Token saved! You may begin using the CLI!`)
53
55
  }
@@ -1,5 +1,4 @@
1
- import prompts from 'prompts'
2
-
1
+ import { prompt } from './util/prompt.js'
3
2
  import { withLoading } from './util/with-loading.js'
4
3
 
5
4
  export interface ResourceChoice {
@@ -23,7 +22,7 @@ export const interactForResource = async <Resource>({
23
22
  `Fetching ${resourceName.replace(/_/g, ' ')}s...`,
24
23
  fetchResources,
25
24
  )
26
- const { resourceId } = await prompts({
25
+ const { resourceId } = await prompt({
27
26
  name: 'resourceId',
28
27
  type: 'autocomplete',
29
28
  message,
@@ -1,9 +1,9 @@
1
1
  import { randomBytes } from 'node:crypto'
2
2
 
3
- import prompts from 'prompts'
4
-
5
3
  import { getConfigStore } from './config/index.js'
6
4
  import { getServer } from './get-server.js'
5
+ import { getOutput } from './output/get-output.js'
6
+ import { prompt } from './util/prompt.js'
7
7
 
8
8
  export async function interactForServerSelection() {
9
9
  const servers = [
@@ -12,7 +12,7 @@ export async function interactForServerSelection() {
12
12
  'https://fakeseamconnect.seam.vc',
13
13
  ]
14
14
 
15
- const { server } = await prompts([
15
+ const { server } = await prompt([
16
16
  {
17
17
  type: 'select',
18
18
  name: 'server',
@@ -22,8 +22,9 @@ export async function interactForServerSelection() {
22
22
  ])
23
23
 
24
24
  const config = getConfigStore()
25
+ const output = getOutput()
25
26
  if (server === servers[2]) {
26
- let { userUrlSeed } = await prompts([
27
+ let { userUrlSeed } = await prompt([
27
28
  {
28
29
  type: 'text',
29
30
  name: 'userUrlSeed',
@@ -37,10 +38,10 @@ export async function interactForServerSelection() {
37
38
  }
38
39
  config.set('server', `https://${userUrlSeed}.fakeseamconnect.seam.vc`)
39
40
  config.set(`${getServer()}.pat`, `seam_apikey1_token`)
40
- console.log(`PAT set to use fakeseamconnect with "seam_apikey1_token"`)
41
+ output.info(`PAT set to use fakeseamconnect with "seam_apikey1_token"`)
41
42
  } else {
42
43
  config.set('server', server)
43
44
  }
44
45
  config.delete('current_workspace_id')
45
- console.log(`Server set to ${server}`)
46
+ output.info(`Server set to ${server}`)
46
47
  }
@@ -1,6 +1,6 @@
1
- import prompts from 'prompts'
1
+ import { prompt } from './util/prompt.js'
2
2
  export const interactForTimestamp = async () => {
3
- const { timestamp } = await prompts({
3
+ const { timestamp } = await prompt({
4
4
  name: 'timestamp',
5
5
  type: 'date',
6
6
  message: 'Enter a timestamp:',
@@ -1,9 +1,9 @@
1
- import prompts from 'prompts'
2
-
3
1
  import { getConfigStore } from './config/index.js'
2
+ import { getOutput } from './output/get-output.js'
3
+ import { prompt } from './util/prompt.js'
4
4
 
5
5
  export async function interactForUseRemoteApiDefs() {
6
- const { use_remote_api_defs } = await prompts([
6
+ const { use_remote_api_defs } = await prompt([
7
7
  {
8
8
  type: 'select',
9
9
  name: 'use_remote_api_defs',
@@ -23,5 +23,5 @@ export async function interactForUseRemoteApiDefs() {
23
23
 
24
24
  const config = getConfigStore()
25
25
  config.set('use_remote_api_defs', use_remote_api_defs)
26
- console.log(`Use remote API Definitions: ${use_remote_api_defs}`)
26
+ getOutput().info(`Use remote API Definitions: ${use_remote_api_defs}`)
27
27
  }
@@ -1,9 +1,9 @@
1
1
  import { SeamHttpWithoutWorkspace } from '@seamapi/http/connect'
2
- import prompts from 'prompts'
3
2
 
4
3
  import { getConfigStore } from './config/index.js'
5
4
  import { getSeamMultiWorkspace } from './get-seam.js'
6
5
  import { getServer } from './get-server.js'
6
+ import { prompt } from './util/prompt.js'
7
7
  import { withLoading } from './util/with-loading.js'
8
8
 
9
9
  export const interactForWorkspaceId = async (personalAccessToken?: string) => {
@@ -17,7 +17,7 @@ export const interactForWorkspaceId = async (personalAccessToken?: string) => {
17
17
  const workspaces = await withLoading('Fetching workspaces...', () =>
18
18
  seam.workspaces.list(),
19
19
  )
20
- const { workspaceId } = await prompts({
20
+ const { workspaceId } = await prompt({
21
21
  name: 'workspaceId',
22
22
  type: 'select',
23
23
  message: 'Select a workspace:',
@@ -0,0 +1,39 @@
1
+ import {
2
+ createOutput,
3
+ type CreateOutputOptions,
4
+ type Output,
5
+ type OutputStream,
6
+ } from './create-output.js'
7
+
8
+ export interface MemoryOutput {
9
+ output: Output
10
+ /** Everything written to stdout so far. */
11
+ stdout: () => string
12
+ /** Everything written to stderr so far. */
13
+ stderr: () => string
14
+ }
15
+
16
+ const createMemoryStream = (): OutputStream & { read: () => string } => {
17
+ const chunks: string[] = []
18
+ return {
19
+ write: (chunk: string) => chunks.push(chunk),
20
+ read: () => chunks.join(''),
21
+ }
22
+ }
23
+
24
+ /**
25
+ * An {@link Output} that captures writes in memory instead of
26
+ * touching the process streams, for asserting on CLI output.
27
+ */
28
+ export const createMemoryOutput = (
29
+ options: Omit<CreateOutputOptions, 'stdout' | 'stderr'> = {},
30
+ ): MemoryOutput => {
31
+ const stdout = createMemoryStream()
32
+ const stderr = createMemoryStream()
33
+
34
+ return {
35
+ output: createOutput({ ...options, stdout, stderr }),
36
+ stdout: stdout.read,
37
+ stderr: stderr.read,
38
+ }
39
+ }