@membranehq/cli 0.1.1 → 0.1.3

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 (78) hide show
  1. package/dist/index.js +140 -140
  2. package/package.json +16 -4
  3. package/.turbo/turbo-build.log +0 -9
  4. package/CHANGELOG.md +0 -7
  5. package/scripts/add-shebang.sh +0 -6
  6. package/scripts/prepare-package-json.ts +0 -29
  7. package/src/agent.tsx +0 -50
  8. package/src/cli.ts +0 -72
  9. package/src/commands/open.command.ts +0 -51
  10. package/src/commands/pull.command.ts +0 -75
  11. package/src/commands/push.command.ts +0 -79
  12. package/src/commands/test.command.ts +0 -99
  13. package/src/components/AddMcpServerScreen.tsx +0 -215
  14. package/src/components/AgentStatus.tsx +0 -15
  15. package/src/components/Main.tsx +0 -64
  16. package/src/components/OverviewSection.tsx +0 -24
  17. package/src/components/PersonalAccessTokenInput.tsx +0 -56
  18. package/src/components/RecentChanges.tsx +0 -65
  19. package/src/components/SelectWorkspace.tsx +0 -112
  20. package/src/components/Setup.tsx +0 -121
  21. package/src/components/WorkspaceStatus.tsx +0 -61
  22. package/src/contexts/FileWatcherContext.tsx +0 -81
  23. package/src/index.ts +0 -27
  24. package/src/legacy/commands/pullWorkspace.ts +0 -70
  25. package/src/legacy/commands/pushWorkspace.ts +0 -246
  26. package/src/legacy/integrationElements.ts +0 -78
  27. package/src/legacy/push/types.ts +0 -17
  28. package/src/legacy/reader/index.ts +0 -113
  29. package/src/legacy/types.ts +0 -17
  30. package/src/legacy/util.ts +0 -149
  31. package/src/legacy/workspace-elements/connectors.ts +0 -397
  32. package/src/legacy/workspace-elements/index.ts +0 -27
  33. package/src/legacy/workspace-tools/commands/pullWorkspace.ts +0 -70
  34. package/src/legacy/workspace-tools/integrationElements.ts +0 -78
  35. package/src/legacy/workspace-tools/util.ts +0 -149
  36. package/src/mcp/server-status.ts +0 -27
  37. package/src/mcp/server.ts +0 -36
  38. package/src/mcp/tools/getTestAccessToken.ts +0 -32
  39. package/src/modules/api/account-api-client.ts +0 -89
  40. package/src/modules/api/index.ts +0 -3
  41. package/src/modules/api/membrane-api-client.ts +0 -116
  42. package/src/modules/api/workspace-api-client.ts +0 -11
  43. package/src/modules/config/cwd-context.tsx +0 -11
  44. package/src/modules/config/project/getAgentVersion.ts +0 -16
  45. package/src/modules/config/project/index.ts +0 -8
  46. package/src/modules/config/project/paths.ts +0 -25
  47. package/src/modules/config/project/readProjectConfig.ts +0 -27
  48. package/src/modules/config/project/useProjectConfig.tsx +0 -103
  49. package/src/modules/config/system/index.ts +0 -35
  50. package/src/modules/file-watcher/index.ts +0 -166
  51. package/src/modules/file-watcher/types.ts +0 -14
  52. package/src/modules/setup/steps.ts +0 -9
  53. package/src/modules/setup/useSetup.ts +0 -16
  54. package/src/modules/status/useStatus.ts +0 -16
  55. package/src/modules/workspace-element-service/constants.ts +0 -121
  56. package/src/modules/workspace-element-service/getTypeAndKeyFromPath.ts +0 -69
  57. package/src/modules/workspace-element-service/index.ts +0 -304
  58. package/src/testing/environment.ts +0 -172
  59. package/src/testing/runners/base.runner.ts +0 -27
  60. package/src/testing/runners/test.runner.ts +0 -123
  61. package/src/testing/scripts/generate-test-report.ts +0 -757
  62. package/src/testing/test-suites/base.ts +0 -92
  63. package/src/testing/test-suites/data-collection.ts +0 -128
  64. package/src/testing/testers/base.ts +0 -115
  65. package/src/testing/testers/create.ts +0 -273
  66. package/src/testing/testers/delete.ts +0 -155
  67. package/src/testing/testers/find-by-id.ts +0 -135
  68. package/src/testing/testers/list.ts +0 -110
  69. package/src/testing/testers/match.ts +0 -149
  70. package/src/testing/testers/search.ts +0 -148
  71. package/src/testing/testers/spec.ts +0 -30
  72. package/src/testing/testers/update.ts +0 -284
  73. package/src/utils/auth.ts +0 -19
  74. package/src/utils/constants.ts +0 -27
  75. package/src/utils/fields.ts +0 -83
  76. package/src/utils/logger.ts +0 -106
  77. package/src/utils/templating.ts +0 -50
  78. package/tsconfig.json +0 -21
@@ -1,215 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
-
4
- import { useState, useEffect } from 'react'
5
- import { Box, Text, useApp, useInput } from 'ink'
6
- import SelectInput from 'ink-select-input'
7
-
8
- import { useCwd } from '../modules/config/cwd-context'
9
-
10
- const AGENTS = [
11
- { label: 'Cursor', value: 'cursor' },
12
- { label: 'Claude Code', value: 'claude' },
13
- { label: 'Windsurf', value: 'windsurf' },
14
- ]
15
-
16
- const MCP_URL = 'http://localhost:8001/mcp'
17
-
18
- function getSummary(agent: string): string {
19
- switch (agent) {
20
- case 'cursor':
21
- return 'Add MCP server to .cursor/mcp.json'
22
- case 'claude':
23
- return 'Add MCP server to .claude/mcp.json'
24
- case 'windsurf':
25
- return 'Add MCP server to windsurf.config.json'
26
- default:
27
- return ''
28
- }
29
- }
30
-
31
- function getConfigPath(agent: string, cwd: string): string {
32
- switch (agent) {
33
- case 'cursor':
34
- return path.join(cwd, '.cursor', 'mcp.json')
35
- case 'claude':
36
- return path.join(cwd, '.claude', 'mcp.json')
37
- case 'windsurf':
38
- return path.join(cwd, 'windsurf.config.json')
39
- default:
40
- return ''
41
- }
42
- }
43
-
44
- function getConfigPatch(agent: string): any {
45
- switch (agent) {
46
- case 'cursor':
47
- return {
48
- mcpServers: {
49
- 'integration-app-agent': {
50
- url: MCP_URL,
51
- },
52
- },
53
- }
54
- case 'claude':
55
- return {
56
- mcpServers: {
57
- 'integration-app-agent': {
58
- url: MCP_URL,
59
- },
60
- },
61
- }
62
- case 'windsurf':
63
- return {
64
- mcpServers: [
65
- {
66
- name: 'integration-app-agent',
67
- url: MCP_URL,
68
- },
69
- ],
70
- }
71
- default:
72
- return {}
73
- }
74
- }
75
-
76
- function mergeConfig(agent: string, existing: any, patch: any): any {
77
- if (agent === 'windsurf') {
78
- // Overwrite or append to mcpServers array
79
- const servers = Array.isArray(existing?.mcpServers) ? existing.mcpServers : []
80
- const filtered = servers.filter((s: any) => s.name !== 'integration-app-agent')
81
- return { ...existing, mcpServers: [...filtered, ...patch.mcpServers] }
82
- }
83
- // For Cursor/Claude, deep merge mcpServers
84
- return {
85
- ...existing,
86
- mcpServers: {
87
- ...existing?.mcpServers,
88
- ...patch.mcpServers,
89
- },
90
- }
91
- }
92
-
93
- interface AddMcpServerScreenProps {
94
- onExit?: () => void
95
- }
96
-
97
- export const AddMcpServerScreen = ({ onExit }: AddMcpServerScreenProps) => {
98
- const { exit } = useApp()
99
- const cwd = useCwd()
100
- const [step, setStep] = useState<'choose' | 'summary' | 'confirm' | 'done'>('choose')
101
- const [agent, setAgent] = useState<string>('')
102
- const [result, setResult] = useState<string>('')
103
- const [error, setError] = useState<string>('')
104
-
105
- useInput((input, key) => {
106
- if (step === 'summary') {
107
- if (input === 'y') setStep('confirm')
108
- if (input === 'n' || key.escape) {
109
- onExit?.()
110
- exit()
111
- }
112
- return
113
- }
114
- if (key.escape) {
115
- onExit?.()
116
- exit()
117
- }
118
- if (step === 'done' && (input === 'q' || key.return)) {
119
- onExit?.()
120
- exit()
121
- }
122
- })
123
-
124
- useEffect(() => {
125
- if (step === 'confirm') {
126
- try {
127
- const configPath = getConfigPath(agent, cwd)
128
- const patch = getConfigPatch(agent)
129
- let existing = {}
130
- if (fs.existsSync(configPath)) {
131
- try {
132
- existing = JSON.parse(fs.readFileSync(configPath, 'utf8'))
133
- } catch {
134
- existing = {}
135
- }
136
- } else {
137
- // Ensure directory exists for .cursor/.claude
138
- const dir = path.dirname(configPath)
139
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
140
- }
141
- const merged = mergeConfig(agent, existing, patch)
142
- fs.writeFileSync(configPath, JSON.stringify(merged, null, 2))
143
- setResult(`MCP server added to ${configPath}`)
144
- setStep('done')
145
- } catch (e: any) {
146
- setError(e?.message || 'Unknown error')
147
- setStep('done')
148
- }
149
- }
150
- }, [step, agent, cwd])
151
-
152
- const handleSelect = (item: { value: string }) => {
153
- setAgent(item.value)
154
- setStep('summary')
155
- }
156
-
157
- if (step === 'choose') {
158
- return (
159
- <Box flexDirection='column'>
160
- <Text>Select your coding agent to add the MCP server:</Text>
161
- <Box marginTop={1}>
162
- <SelectInput items={AGENTS} onSelect={handleSelect} />
163
- </Box>
164
- <Box marginTop={1}>
165
- <Text color='grey'>Press ESC to cancel</Text>
166
- </Box>
167
- </Box>
168
- )
169
- }
170
-
171
- if (step === 'summary') {
172
- return (
173
- <Box flexDirection='column'>
174
- <Text>{getSummary(agent)}</Text>
175
- <Box marginTop={1}>
176
- <Text>Proceed? (y/n)</Text>
177
- </Box>
178
- <Box marginTop={1}>
179
- <Text color='grey'>Press ESC to cancel</Text>
180
- </Box>
181
- <Box>
182
- <Text>
183
- <Text color='green'>[y]</Text> Yes{' '}
184
- <Text color='red'>[n]</Text> No
185
- </Text>
186
- </Box>
187
- </Box>
188
- )
189
- }
190
-
191
- if (step === 'confirm') {
192
- return (
193
- <Box>
194
- <Text>Adding MCP server...</Text>
195
- </Box>
196
- )
197
- }
198
-
199
- if (step === 'done') {
200
- return (
201
- <Box flexDirection='column'>
202
- {result && <Text color='green'>{result}</Text>}
203
- {error && <Text color='red'>Error: {error}</Text>}
204
- <Box marginTop={1}>
205
- <Text color='grey'>Press Q or Enter to exit</Text>
206
- </Box>
207
- </Box>
208
- )
209
- }
210
-
211
- // fallback
212
- return null
213
- }
214
-
215
- // To use this screen, render <AddMcpServerScreen /> from your main app or a menu.
@@ -1,15 +0,0 @@
1
- import { Box, Text } from 'ink'
2
-
3
- export const AgentStatus = () => {
4
- const width = Math.min(100, process.stdout.columns || 100)
5
- return (
6
- <Box flexDirection='column' paddingX={1} borderStyle='round' borderTop width={width}>
7
- {/* Header row with robot emoji and status */}
8
- <Box marginTop={-1} marginBottom={1}>
9
- <Text bold>
10
- 🤖 Membrane Code Agent — <Text color='green'>active</Text>
11
- </Text>
12
- </Box>
13
- </Box>
14
- )
15
- }
@@ -1,64 +0,0 @@
1
- import { exec } from 'node:child_process'
2
-
3
- import React, { useState } from 'react'
4
- import { Box, Text, useInput } from 'ink'
5
-
6
- import { AgentStatus } from './AgentStatus'
7
- import { SelectWorkspace } from './SelectWorkspace'
8
- import { Setup } from './Setup'
9
- import { WorkspaceSync } from './WorkspaceStatus'
10
- import { getWorkspaceId } from '../modules/api/workspace-api-client'
11
- import { useCwd } from '../modules/config/cwd-context'
12
- import { useStatus } from '../modules/status/useStatus'
13
-
14
- export const Main = () => {
15
- const { isSetupComplete, workspaceKey, config } = useStatus()
16
- const [showWorkspaceSelection, setShowWorkspaceSelection] = useState(false)
17
- const [showSetup, setShowSetup] = useState(false)
18
- const cwd = useCwd()
19
-
20
- useInput((input) => {
21
- // Only process hotkeys if main screen is visible
22
- if (!showWorkspaceSelection && !showSetup) {
23
- if (input === 'w') setShowWorkspaceSelection(true)
24
- if (input === 'o' && workspaceKey && config?.workspaceSecret) {
25
- // Fetch workspace ID and open console asynchronously
26
- void (async () => {
27
- try {
28
- const workspaceId = await getWorkspaceId(cwd)
29
- const url = `https://console.integration.app/w/${workspaceId}`
30
- // Open URL in default browser
31
- const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'
32
- exec(`${command} "${url}"`)
33
- } catch (error) {
34
- console.error('Failed to open workspace:', error)
35
- }
36
- })()
37
- }
38
- if (input === 's') {
39
- setShowSetup(true)
40
- }
41
- }
42
- })
43
-
44
- if (showWorkspaceSelection) {
45
- return <SelectWorkspace onExit={() => setShowWorkspaceSelection(false)} />
46
- }
47
-
48
- if (showSetup || !isSetupComplete) {
49
- // Show only Setup during initial setup or re-setup
50
- return <Setup key={Date.now()} onComplete={() => setShowSetup(false)} />
51
- }
52
-
53
- return (
54
- <Box flexDirection='column'>
55
- <Box flexGrow={1}>
56
- <AgentStatus />
57
- </Box>
58
- <WorkspaceSync />
59
- <Box paddingLeft={2}>
60
- <Text color='grey'>[s: (re-)setup]</Text>
61
- </Box>
62
- </Box>
63
- )
64
- }
@@ -1,24 +0,0 @@
1
- import React from 'react'
2
- import { Box } from 'ink'
3
-
4
- import type { BoxProps } from 'ink'
5
-
6
- type OverviewSectionProps = {
7
- children: React.ReactNode
8
- } & BoxProps
9
-
10
- export const OverviewSection = ({ children, ...props }: OverviewSectionProps) => {
11
- return (
12
- <Box
13
- borderStyle='round'
14
- borderColor='lime'
15
- paddingX={2}
16
- paddingY={1}
17
- alignSelf='flex-start'
18
- flexDirection='column'
19
- {...props}
20
- >
21
- {children}
22
- </Box>
23
- )
24
- }
@@ -1,56 +0,0 @@
1
- import React, { useState } from 'react'
2
- import { Box, Text } from 'ink'
3
- import Spinner from 'ink-spinner'
4
- import InkTextInput from 'ink-text-input'
5
-
6
- interface PersonalAccessTokenInputProps {
7
- currentPat?: string
8
- onSubmit: (pat: string) => Promise<void>
9
- }
10
-
11
- export const PersonalAccessTokenInput = ({ currentPat, onSubmit }: PersonalAccessTokenInputProps) => {
12
- const [patInput, setPatInput] = useState('')
13
- const [patLoading, setPatLoading] = useState(false)
14
- const [patError, setPatError] = useState<string | null>(null)
15
-
16
- const handleSubmit = async (input: string) => {
17
- setPatError(null)
18
- setPatLoading(true)
19
- try {
20
- await onSubmit(input)
21
- setPatInput('')
22
- } catch (err: any) {
23
- setPatError('Invalid token. Please try again.')
24
- } finally {
25
- setPatLoading(false)
26
- }
27
- }
28
-
29
- return (
30
- <Box flexDirection='column' borderStyle='round' borderTop width={70} paddingX={1}>
31
- <Box marginTop={-1} marginBottom={1}>
32
- <Text bold>🔑 Enter your Personal Access Token</Text>
33
- </Box>
34
- <Text>Please provide your Personal Access Token. You can find it here:</Text>
35
- <Box marginTop={1} marginBottom={1}>
36
- <Text color='yellow'>https://console.integration.app/w/0/manage-account/api-token</Text>
37
- </Box>
38
- {currentPat && <Text dimColor>Press Enter to keep your current token or type a new one.</Text>}
39
- <InkTextInput
40
- mask='*'
41
- placeholder={`${currentPat ? '******' : 'Enter your token here...'}`}
42
- value={patInput}
43
- onChange={setPatInput}
44
- onSubmit={handleSubmit}
45
- />
46
- {patLoading && (
47
- <Box marginTop={1}>
48
- <Text>
49
- <Spinner type='dots' /> Validating token...
50
- </Text>
51
- </Box>
52
- )}
53
- {patError && <Text color='red'>{patError}</Text>}
54
- </Box>
55
- )
56
- }
@@ -1,65 +0,0 @@
1
- import { Box, Text } from 'ink'
2
-
3
- import { useFileWatcher } from '../contexts/FileWatcherContext'
4
- import { FileChangeEventType } from '../modules/file-watcher/types'
5
-
6
- const getSymbolAndColor = (type: FileChangeEventType) => {
7
- switch (type) {
8
- case 'added':
9
- return { symbol: '+', color: 'green' as const }
10
- case 'changed':
11
- return { symbol: '*', color: 'yellow' as const }
12
- case 'deleted':
13
- return { symbol: '-', color: 'red' as const }
14
- default:
15
- return { symbol: '?', color: 'gray' as const }
16
- }
17
- }
18
-
19
- const getActionText = (type: FileChangeEventType) => {
20
- switch (type) {
21
- case 'added':
22
- return 'ADD'
23
- case 'changed':
24
- return 'UPD'
25
- case 'deleted':
26
- return 'DEL'
27
- default:
28
- return 'UNKNOWN'
29
- }
30
- }
31
-
32
- export function RecentChanges() {
33
- const { recentChanges } = useFileWatcher()
34
-
35
- const sortedRecentChanges = [...recentChanges].sort((a, b) => a.timestamp - b.timestamp)
36
-
37
- if (sortedRecentChanges.length === 0) {
38
- return (
39
- <Box paddingTop={1}>
40
- <Text color='gray'>No recent changes</Text>
41
- </Box>
42
- )
43
- }
44
-
45
- return (
46
- <Box flexDirection='column' paddingTop={1}>
47
- <Text color='gray'>Recent Changes:</Text>
48
- {sortedRecentChanges.map((change, _) => {
49
- const { symbol, color } = getSymbolAndColor(change.type)
50
- const actionText = getActionText(change.type)
51
-
52
- return (
53
- <Box key={`${change.filePath}-${change.timestamp}`} marginLeft={1}>
54
- <Text color={color}>
55
- {symbol} {actionText}:{' '}
56
- </Text>
57
- <Text>
58
- [{change.elementType}] <Text bold>{change.elementKey}</Text> ({change.relativePath})
59
- </Text>
60
- </Box>
61
- )
62
- })}
63
- </Box>
64
- )
65
- }
@@ -1,112 +0,0 @@
1
- import React, { useState } from 'react'
2
- import { Select, TextInput } from '@inkjs/ui'
3
- import { Box, Text, useInput } from 'ink'
4
- import Spinner from 'ink-spinner'
5
- import useSWRImmutable from 'swr/immutable'
6
-
7
- import { useProjectConfig } from '../modules/config/project'
8
-
9
- import type { OrgWorkspace } from '@integration-app/sdk'
10
-
11
- interface SelectWorkspaceProps {
12
- onExit?: () => void
13
- showEscOption?: boolean
14
- }
15
-
16
- export const SelectWorkspace = ({ onExit, showEscOption = true }: SelectWorkspaceProps) => {
17
- const [searchQuery, setSearchQuery] = useState('')
18
- const { data, error, isLoading: isLoadingWorkspaces } = useSWRImmutable('/account')
19
- const { updateConfig, isLoading: isLoadingConfig } = useProjectConfig()
20
-
21
- const workspaces = (data as any)?.workspaces as OrgWorkspace[]
22
- const isLoading = isLoadingWorkspaces || isLoadingConfig
23
-
24
- useInput((input, key) => {
25
- if (key.escape) {
26
- onExit?.()
27
- }
28
- })
29
-
30
- if (isLoading) {
31
- return (
32
- <Box>
33
- <Spinner />
34
- <Text> Fetching workspaces...</Text>
35
- </Box>
36
- )
37
- }
38
-
39
- if (error) {
40
- return (
41
- <Box flexDirection='column'>
42
- <Text color='red'>Error: {error.message}</Text>
43
- <Box marginTop={1}>
44
- <Text color='grey'>Press ESC to go back</Text>
45
- </Box>
46
- </Box>
47
- )
48
- }
49
-
50
- const filteredWorkspaces = workspaces?.filter((ws) => ws.name.toLowerCase().includes(searchQuery.toLowerCase())) ?? []
51
-
52
- const items = filteredWorkspaces.map((ws) => ({
53
- label: ws.name,
54
- value: ws.id,
55
- }))
56
-
57
- const showingWorkspaces = items.length
58
- const totalWorkspaces = workspaces?.length ?? 0
59
-
60
- const handleSelect = async (value: string) => {
61
- const selectedWorkspace = filteredWorkspaces.find((ws) => ws.id === value)
62
- if (!selectedWorkspace) {
63
- return
64
- }
65
-
66
- const { key, secret } = selectedWorkspace as any
67
- if (!key || !secret) {
68
- // TODO: handle error
69
- return
70
- }
71
-
72
- await updateConfig({
73
- workspaceKey: key,
74
- workspaceSecret: secret,
75
- })
76
-
77
- // Close the selection screen after successful workspace change
78
- onExit?.()
79
- }
80
-
81
- return (
82
- <Box flexDirection='column' borderStyle='round' borderTop width={70} paddingX={1}>
83
- <Box marginTop={-1}>
84
- <Text bold>📁 Select your workspace</Text>
85
- </Box>
86
- <Box marginTop={1}>
87
- <Text>Search: </Text>
88
- <TextInput placeholder='Enter a search query...' onChange={setSearchQuery} />
89
- </Box>
90
- {totalWorkspaces > 5 && (
91
- <Text>
92
- Showing {showingWorkspaces} of {totalWorkspaces} workspaces.
93
- </Text>
94
- )}
95
- <Box marginTop={1}>
96
- <Select
97
- options={items}
98
- onChange={(value) => {
99
- if (value) {
100
- void handleSelect(value)
101
- }
102
- }}
103
- />
104
- </Box>
105
- {showEscOption && (
106
- <Box marginTop={1}>
107
- <Text color='grey'>Press ESC to go back</Text>
108
- </Box>
109
- )}
110
- </Box>
111
- )
112
- }
@@ -1,121 +0,0 @@
1
- import React, { useState } from 'react'
2
- import { Box, Text, useInput } from 'ink'
3
- import Spinner from 'ink-spinner'
4
-
5
- import { PersonalAccessTokenInput } from './PersonalAccessTokenInput'
6
- import { SelectWorkspace } from './SelectWorkspace'
7
- import { AccountApiClient } from '../modules/api/account-api-client'
8
- import { setPat, getPat } from '../modules/config/system'
9
- import { Step, STEP_LABELS } from '../modules/setup/steps'
10
- import { useSetup } from '../modules/setup/useSetup'
11
-
12
- type StepStatus = 'pending' | 'current' | 'done'
13
-
14
- const StepDisplay = ({ status, label }: { status: StepStatus; label: string }) => {
15
- return (
16
- <Box>
17
- <Box width={2}>
18
- {status === 'current' && <Spinner type='dots' />}
19
- {status === 'done' && <Text>✅</Text>}
20
- </Box>
21
- <Text dimColor={status !== 'current'}>{label}</Text>
22
- </Box>
23
- )
24
- }
25
-
26
- const ALL_STEPS = [Step.Authenticate, Step.ConnectWorkspace]
27
-
28
- interface SetupProps {
29
- onComplete?: () => void
30
- }
31
-
32
- export const Setup = ({ onComplete }: SetupProps) => {
33
- const [completed, setCompleted] = useState(false)
34
- const [currentStepIndex, setCurrentStepIndex] = useState(0)
35
- const [patLoading, setPatLoading] = useState(false)
36
- const [patError, setPatError] = useState<string | null>(null)
37
- const [patInput, setPatInput] = useState('')
38
-
39
- const { isSetupComplete } = useSetup()
40
-
41
- const currentStep = ALL_STEPS[currentStepIndex]
42
- const stepIndex = currentStepIndex + 1
43
- const totalSteps = ALL_STEPS.length
44
-
45
- const steps = ALL_STEPS.map((step, idx) => {
46
- let status: StepStatus = 'pending'
47
- if (idx < currentStepIndex) {
48
- status = 'done'
49
- } else if (idx === currentStepIndex) {
50
- status = 'current'
51
- }
52
- return { id: step, label: STEP_LABELS[step], status }
53
- })
54
-
55
- const currentPat = getPat()
56
- const patMask = currentPat ? '*'.repeat(currentPat.length) : ''
57
-
58
- const handlePatSubmit = async (input: string) => {
59
- setPatError(null)
60
- setPatLoading(true)
61
- const patToTest = currentPat && input === '' ? currentPat : input
62
- const testClient = new AccountApiClient()
63
- try {
64
- await testClient.request('/account', { headers: { Authorization: `Bearer ${patToTest}` } })
65
- setPat(patToTest)
66
- setCurrentStepIndex((idx) => idx + 1)
67
- setPatInput('')
68
- } catch (err: any) {
69
- console.log(err)
70
- setPatError('Invalid token. Please try again.')
71
- } finally {
72
- setPatLoading(false)
73
- }
74
- }
75
-
76
- const handleWorkspaceSelected = () => {
77
- setCompleted(true)
78
- if (onComplete) onComplete()
79
- }
80
-
81
- useInput((input, key) => {
82
- if (isSetupComplete && key.escape && onComplete) {
83
- onComplete()
84
- }
85
- })
86
-
87
- return completed ? (
88
- <Box>
89
- <Text>✅ Setup complete. You are ready to go!</Text>
90
- </Box>
91
- ) : (
92
- <Box flexDirection='column' alignSelf='flex-start' gap={1}>
93
- {/* Setup block header */}
94
- <Box flexDirection='column' paddingX={1} borderStyle='round' borderTop width={70}>
95
- <Box marginTop={-1} marginBottom={1}>
96
- <Text bold>
97
- 🛠️ Setup —{' '}
98
- <Text color='cyan'>
99
- Step {stepIndex} of {totalSteps}
100
- </Text>
101
- {isSetupComplete && <Text color='grey'> [esc: go back]</Text>}
102
- </Text>
103
- </Box>
104
- <Box flexDirection='column' paddingLeft={2}>
105
- {steps.map((step) => (
106
- <StepDisplay key={step.id} status={step.status} label={step.label} />
107
- ))}
108
- </Box>
109
- </Box>
110
-
111
- {/* Step blocks with header */}
112
- {currentStep === Step.Authenticate && (
113
- <PersonalAccessTokenInput currentPat={currentPat} onSubmit={handlePatSubmit} />
114
- )}
115
-
116
- {currentStep === Step.ConnectWorkspace && (
117
- <SelectWorkspace onExit={handleWorkspaceSelected} showEscOption={false} />
118
- )}
119
- </Box>
120
- )
121
- }