@membranehq/cli 0.1.1

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 (80) hide show
  1. package/.turbo/turbo-build.log +9 -0
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +54 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +335 -0
  6. package/package.json +46 -0
  7. package/scripts/add-shebang.sh +6 -0
  8. package/scripts/prepare-package-json.ts +29 -0
  9. package/src/agent.tsx +50 -0
  10. package/src/cli.ts +72 -0
  11. package/src/commands/open.command.ts +51 -0
  12. package/src/commands/pull.command.ts +75 -0
  13. package/src/commands/push.command.ts +79 -0
  14. package/src/commands/test.command.ts +99 -0
  15. package/src/components/AddMcpServerScreen.tsx +215 -0
  16. package/src/components/AgentStatus.tsx +15 -0
  17. package/src/components/Main.tsx +64 -0
  18. package/src/components/OverviewSection.tsx +24 -0
  19. package/src/components/PersonalAccessTokenInput.tsx +56 -0
  20. package/src/components/RecentChanges.tsx +65 -0
  21. package/src/components/SelectWorkspace.tsx +112 -0
  22. package/src/components/Setup.tsx +121 -0
  23. package/src/components/WorkspaceStatus.tsx +61 -0
  24. package/src/contexts/FileWatcherContext.tsx +81 -0
  25. package/src/index.ts +27 -0
  26. package/src/legacy/commands/pullWorkspace.ts +70 -0
  27. package/src/legacy/commands/pushWorkspace.ts +246 -0
  28. package/src/legacy/integrationElements.ts +78 -0
  29. package/src/legacy/push/types.ts +17 -0
  30. package/src/legacy/reader/index.ts +113 -0
  31. package/src/legacy/types.ts +17 -0
  32. package/src/legacy/util.ts +149 -0
  33. package/src/legacy/workspace-elements/connectors.ts +397 -0
  34. package/src/legacy/workspace-elements/index.ts +27 -0
  35. package/src/legacy/workspace-tools/commands/pullWorkspace.ts +70 -0
  36. package/src/legacy/workspace-tools/integrationElements.ts +78 -0
  37. package/src/legacy/workspace-tools/util.ts +149 -0
  38. package/src/mcp/server-status.ts +27 -0
  39. package/src/mcp/server.ts +36 -0
  40. package/src/mcp/tools/getTestAccessToken.ts +32 -0
  41. package/src/modules/api/account-api-client.ts +89 -0
  42. package/src/modules/api/index.ts +3 -0
  43. package/src/modules/api/membrane-api-client.ts +116 -0
  44. package/src/modules/api/workspace-api-client.ts +11 -0
  45. package/src/modules/config/cwd-context.tsx +11 -0
  46. package/src/modules/config/project/getAgentVersion.ts +16 -0
  47. package/src/modules/config/project/index.ts +8 -0
  48. package/src/modules/config/project/paths.ts +25 -0
  49. package/src/modules/config/project/readProjectConfig.ts +27 -0
  50. package/src/modules/config/project/useProjectConfig.tsx +103 -0
  51. package/src/modules/config/system/index.ts +35 -0
  52. package/src/modules/file-watcher/index.ts +166 -0
  53. package/src/modules/file-watcher/types.ts +14 -0
  54. package/src/modules/setup/steps.ts +9 -0
  55. package/src/modules/setup/useSetup.ts +16 -0
  56. package/src/modules/status/useStatus.ts +16 -0
  57. package/src/modules/workspace-element-service/constants.ts +121 -0
  58. package/src/modules/workspace-element-service/getTypeAndKeyFromPath.ts +69 -0
  59. package/src/modules/workspace-element-service/index.ts +304 -0
  60. package/src/testing/environment.ts +172 -0
  61. package/src/testing/runners/base.runner.ts +27 -0
  62. package/src/testing/runners/test.runner.ts +123 -0
  63. package/src/testing/scripts/generate-test-report.ts +757 -0
  64. package/src/testing/test-suites/base.ts +92 -0
  65. package/src/testing/test-suites/data-collection.ts +128 -0
  66. package/src/testing/testers/base.ts +115 -0
  67. package/src/testing/testers/create.ts +273 -0
  68. package/src/testing/testers/delete.ts +155 -0
  69. package/src/testing/testers/find-by-id.ts +135 -0
  70. package/src/testing/testers/list.ts +110 -0
  71. package/src/testing/testers/match.ts +149 -0
  72. package/src/testing/testers/search.ts +148 -0
  73. package/src/testing/testers/spec.ts +30 -0
  74. package/src/testing/testers/update.ts +284 -0
  75. package/src/utils/auth.ts +19 -0
  76. package/src/utils/constants.ts +27 -0
  77. package/src/utils/fields.ts +83 -0
  78. package/src/utils/logger.ts +106 -0
  79. package/src/utils/templating.ts +50 -0
  80. package/tsconfig.json +21 -0
@@ -0,0 +1,61 @@
1
+ import { Box, Text } from 'ink'
2
+ import useSWRImmutable from 'swr/immutable'
3
+
4
+ import { RecentChanges } from './RecentChanges'
5
+ import { useStatus } from '../modules/status/useStatus'
6
+
7
+ import type { OrgWorkspace } from '@integration-app/sdk'
8
+
9
+ function truncateWorkspaceName(name: string, maxLength: number = 30): string {
10
+ if (name.length <= maxLength) {
11
+ return name
12
+ }
13
+ return name.slice(0, maxLength - 3) + '...'
14
+ }
15
+
16
+ export const WorkspaceSync = () => {
17
+ const { workspaceKey, cwd } = useStatus()
18
+ const width = Math.min(100, process.stdout.columns || 100)
19
+
20
+ // Fetch workspace data to get the workspace name
21
+ const { data } = useSWRImmutable('/account')
22
+ const workspaces = (data as any)?.workspaces as OrgWorkspace[]
23
+
24
+ // Find the current workspace by key to get its name
25
+ const currentWorkspace = workspaces?.find((ws: any) => ws.key === workspaceKey)
26
+ const workspaceName = currentWorkspace?.name
27
+ const displayName = workspaceName ? truncateWorkspaceName(workspaceName) : workspaceKey
28
+
29
+ return (
30
+ <Box flexDirection='column' paddingX={1} borderStyle='round' borderTop width={width}>
31
+ {/* Header row with sync emoji and status */}
32
+ <Box marginTop={-1} marginBottom={1}>
33
+ <Text bold>
34
+ 🔄 Workspaces — <Text color='green'>in sync</Text>
35
+ </Text>
36
+ </Box>
37
+ {/* Local row */}
38
+ <Box>
39
+ <Box width={12}>
40
+ <Text color='grey'>Local:</Text>
41
+ </Box>
42
+ <Text color='grey'>{cwd}</Text>
43
+ </Box>
44
+ {/* Remote row (always shown) */}
45
+ <Box>
46
+ <Box width={12}>
47
+ <Text color='grey'>Remote:</Text>
48
+ </Box>
49
+ {workspaceKey ? (
50
+ <Text color='grey'>{displayName} [o: open in console] [w: change]</Text>
51
+ ) : (
52
+ <Text>
53
+ <Text color='yellow'>not selected</Text> [w: select]
54
+ </Text>
55
+ )}
56
+ </Box>
57
+
58
+ <RecentChanges />
59
+ </Box>
60
+ )
61
+ }
@@ -0,0 +1,81 @@
1
+ import React, { createContext, useContext, useEffect, useState } from 'react'
2
+ import { WorkspaceElementType } from '@integration-app/sdk'
3
+
4
+ import { FileChangeEventType } from '../modules/file-watcher/types'
5
+ import { getTypeAndKeyFromPath } from '../modules/workspace-element-service/getTypeAndKeyFromPath'
6
+
7
+ import type { FileWatcher } from '../modules/file-watcher'
8
+ import type { FileChangeEvent } from '../modules/file-watcher/types'
9
+
10
+ interface FileChangeWithType extends FileChangeEvent {
11
+ readonly type: FileChangeEventType
12
+ readonly elementKey: string
13
+ readonly elementType: WorkspaceElementType
14
+ readonly timestamp: number
15
+ }
16
+
17
+ interface FileWatcherContextValue {
18
+ readonly fileWatcher: FileWatcher | null
19
+ readonly recentChanges: readonly FileChangeWithType[]
20
+ }
21
+
22
+ const FileWatcherContext = createContext<FileWatcherContextValue | null>(null)
23
+
24
+ interface FileWatcherProviderProps {
25
+ readonly children: React.ReactNode
26
+ readonly fileWatcher: FileWatcher
27
+ }
28
+
29
+ export function FileWatcherProvider({ children, fileWatcher }: FileWatcherProviderProps) {
30
+ const [recentChanges, setRecentChanges] = useState<FileChangeWithType[]>([])
31
+
32
+ useEffect(() => {
33
+ const handleFileChange = (type: FileChangeEventType) => (event: FileChangeEvent) => {
34
+ const elementRef = getTypeAndKeyFromPath(event.relativePath)
35
+ if (!elementRef) {
36
+ return
37
+ }
38
+
39
+ const changeWithType: FileChangeWithType = {
40
+ ...event,
41
+ elementType: elementRef.type,
42
+ elementKey: elementRef.key,
43
+ type,
44
+ timestamp: Date.now(),
45
+ }
46
+
47
+ setRecentChanges((prev) => [changeWithType, ...prev.slice(0, 2)]) // keep only last 3 changes
48
+ }
49
+
50
+ const handleAdded = handleFileChange('added')
51
+ const handleChanged = handleFileChange('changed')
52
+ const handleDeleted = handleFileChange('deleted')
53
+
54
+ fileWatcher.on('added', handleAdded)
55
+ fileWatcher.on('changed', handleChanged)
56
+ fileWatcher.on('deleted', handleDeleted)
57
+
58
+ return () => {
59
+ fileWatcher.off('added', handleAdded)
60
+ fileWatcher.off('changed', handleChanged)
61
+ fileWatcher.off('deleted', handleDeleted)
62
+ }
63
+ }, [fileWatcher])
64
+
65
+ const value: FileWatcherContextValue = {
66
+ fileWatcher,
67
+ recentChanges,
68
+ }
69
+
70
+ return <FileWatcherContext.Provider value={value}>{children}</FileWatcherContext.Provider>
71
+ }
72
+
73
+ export function useFileWatcher(): FileWatcherContextValue {
74
+ const context = useContext(FileWatcherContext)
75
+
76
+ if (!context) {
77
+ throw new Error('useFileWatcher must be used within a FileWatcherProvider')
78
+ }
79
+
80
+ return context
81
+ }
package/src/index.ts ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runAgent } from './agent'
4
+ import { runCLI } from './cli'
5
+
6
+ const AGENT_ARGUMENTS = ['--cwd']
7
+
8
+ function hasCLIArguments(): boolean {
9
+ const args = process.argv.slice(2)
10
+ return args.some((arg, index) => {
11
+ // Skip the value of an agent argument
12
+ if (index > 0 && AGENT_ARGUMENTS.includes(args[index - 1])) {
13
+ return false
14
+ }
15
+ // Check if current argument is not an agent argument
16
+ return !AGENT_ARGUMENTS.includes(arg)
17
+ })
18
+ }
19
+
20
+ // Check if there are any arguments that are not agent arguments
21
+ if (1 || hasCLIArguments()) {
22
+ // Run single-command CLI mode
23
+ runCLI()
24
+ } else {
25
+ // Run in agent mode (interactive)
26
+ runAgent()
27
+ }
@@ -0,0 +1,70 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+
4
+ import { IntegrationAppClient, WorkspaceElementType } from '@integration-app/sdk'
5
+ import YAML from 'js-yaml'
6
+
7
+ import { getWorkspaceData, coloredLog } from '../util'
8
+ import { getWorkspaceElementTypePath } from '../workspace-elements'
9
+ import { pullConnectors } from '../workspace-elements/connectors'
10
+
11
+ interface ExportPackageOptions {
12
+ outputPath?: string
13
+ allConnectors?: boolean
14
+ client?: IntegrationAppClient
15
+ }
16
+
17
+ export async function pullWorkspace(options: ExportPackageOptions): Promise<void> {
18
+ const { outputPath, allConnectors, client } = options
19
+
20
+ await pullConnectors({
21
+ client,
22
+ basePath: outputPath,
23
+ allConnectors,
24
+ })
25
+
26
+ const workspaceData = await getWorkspaceData(client)
27
+
28
+ for (const elementType of Object.keys(workspaceData)) {
29
+ if (workspaceData[elementType].length > 0) {
30
+ try {
31
+ fs.readdirSync(outputPath)
32
+ } catch (_err) {
33
+ fs.mkdirSync(outputPath, { recursive: true })
34
+ }
35
+
36
+ const elementTypePath = getWorkspaceElementTypePath(elementType as WorkspaceElementType)
37
+
38
+ for (const element of workspaceData[elementType]) {
39
+ try {
40
+ fs.readdirSync(path.join(outputPath, elementTypePath, `${element.key}`))
41
+ } catch (_err) {
42
+ fs.mkdirSync(path.join(outputPath, elementTypePath, `${element.key}`), {
43
+ recursive: true,
44
+ })
45
+ }
46
+ if (element.integration || element.integrationKey) {
47
+ const integrationKey = element.integration ? element.integration.key : element.integrationKey
48
+ try {
49
+ fs.readdirSync(path.join(outputPath, elementTypePath, `${element.key}`, `${integrationKey}`))
50
+ } catch (_err) {
51
+ fs.mkdirSync(path.join(outputPath, elementTypePath, `${element.key}`, `${integrationKey}`), {
52
+ recursive: true,
53
+ })
54
+ }
55
+ fs.writeFileSync(
56
+ path.join(outputPath, elementTypePath, `${element.key}`, `${integrationKey}`, `${integrationKey}.yaml`),
57
+ YAML.dump(element),
58
+ )
59
+ } else {
60
+ fs.writeFileSync(
61
+ path.join(outputPath, elementTypePath, element.key, `${element.key}.yaml`),
62
+ YAML.dump(element),
63
+ )
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ coloredLog(`Data written to ${outputPath} successfully.`, 'BgGreen')
70
+ }
@@ -0,0 +1,246 @@
1
+ import { WorkspaceElementSpecs } from '@integration-app/sdk'
2
+ import { toCamelCase } from 'js-convert-case'
3
+
4
+ import { Logger } from '../../utils/logger'
5
+ import { INTEGRATION_ELEMENTS } from '../integrationElements'
6
+ import { PushContext, WorkspaceData } from '../push/types'
7
+ import { readIntegrationLevelElements, readTopLevelElements } from '../reader/index'
8
+ import { getWorkspaceData, hasParent, splitWorkspaceData, coloredLog } from '../util'
9
+ import { pushConnectors } from '../workspace-elements/connectors'
10
+ import { mergeWorkspaceData } from '../workspace-elements/index'
11
+
12
+ export async function pushWorkspace({ basePath, client }) {
13
+ const response = await client.get('org-workspace-id')
14
+ const workspaceId = response.id
15
+
16
+ const workspaceData = await getWorkspaceData(client, 'minified')
17
+
18
+ const data: WorkspaceData = {}
19
+ Logger.info(`Loading local workspace data`)
20
+
21
+ const topLevelElements = await readTopLevelElements()
22
+ mergeWorkspaceData(data, topLevelElements)
23
+
24
+ const integrationLevelElements = await readIntegrationLevelElements()
25
+ mergeWorkspaceData(data, integrationLevelElements)
26
+
27
+ const { universalElements, integrationSpecificElements } = splitWorkspaceData(data)
28
+
29
+ const pushContext: PushContext = {
30
+ connectorsMapping: {},
31
+ }
32
+
33
+ await syncIntegrations({
34
+ basePath,
35
+ sourceData: data,
36
+ workspaceData,
37
+ pushContext,
38
+ iApp: client,
39
+ workspaceId,
40
+ })
41
+
42
+ // Re-fetch latest integrations
43
+ workspaceData.integration = await client.integrations.findAll()
44
+
45
+ coloredLog(`Syncing Universal Elements`, 'BgBlue')
46
+ Logger.group('Syncing Universal Elements')
47
+ await syncElements(universalElements, workspaceData, client)
48
+ Logger.groupEnd()
49
+
50
+ coloredLog(`Syncing Integration Specific Elements`, 'BgBlue')
51
+ Logger.group('Syncing Integration Specific Elements')
52
+
53
+ await syncElements(integrationSpecificElements, workspaceData, client)
54
+ Logger.groupEnd()
55
+
56
+ Logger.success(`Data written successfully.`)
57
+
58
+ return pushContext
59
+ }
60
+
61
+ async function syncIntegrations({
62
+ basePath,
63
+ sourceData,
64
+ workspaceData,
65
+ pushContext,
66
+ iApp,
67
+ workspaceId,
68
+ }: {
69
+ basePath: string
70
+ sourceData: any
71
+ workspaceData: WorkspaceData
72
+ pushContext: PushContext
73
+ iApp: any
74
+ workspaceId: string
75
+ }) {
76
+ coloredLog('Matching imported integrations and connectors with existing ones', 'BgBlue')
77
+
78
+ await pushConnectors({
79
+ client: iApp,
80
+ basePath,
81
+ workspaceData,
82
+ pushContext,
83
+ workspaceId,
84
+ })
85
+
86
+ const integrationMissmatchErrors = []
87
+ for (const integration of sourceData.integration) {
88
+ const matchedIntegration = workspaceData.integration.find((item) => item.key == integration.key)
89
+
90
+ const integrationPayload = {
91
+ connectorId: integration.connectorId,
92
+ connectorVersion: integration.connectorVersion,
93
+ name: integration.name,
94
+ key: integration.key,
95
+ }
96
+
97
+ if (pushContext.connectorsMapping[integration.connectorId]) {
98
+ integrationPayload.connectorId = pushContext.connectorsMapping[integration.connectorId]
99
+ }
100
+
101
+ if (!matchedIntegration) {
102
+ console.debug(`[Push] Creating integration ${integration.key}`)
103
+ await iApp.integrations.create(integrationPayload)
104
+ } else {
105
+ console.debug(`[Push] Updating integration ${integration.key}`)
106
+ await iApp.integration((matchedIntegration as any).id).patch(integrationPayload)
107
+ }
108
+
109
+ coloredLog(`Imported integration ${integration.name} (${integration.key})`, 'Green')
110
+ }
111
+ if (integrationMissmatchErrors.length > 0) {
112
+ Logger.table(
113
+ integrationMissmatchErrors.map((item) => ({
114
+ key: item.key,
115
+ name: item.name,
116
+ })),
117
+ )
118
+ coloredLog(
119
+ 'Integration missmatch errors. Make sure you have those applications in your destination workspace',
120
+ 'BgRed',
121
+ )
122
+ throw new Error('Integration missmatch errors')
123
+ }
124
+ }
125
+
126
+ async function syncElements(data, workspaceData, iApp) {
127
+ for (const elementType of Object.keys(data)) {
128
+ const isExportable = INTEGRATION_ELEMENTS[elementType]?.exportable
129
+
130
+ if (isExportable === false) continue
131
+
132
+ for (const element of data[elementType]) {
133
+ // Cleanup
134
+ delete element.integrationId
135
+
136
+ const destinationElement = matchElement(element, elementType, workspaceData)
137
+ const singleElementAccessorKey = toCamelCase(WorkspaceElementSpecs[elementType].name)
138
+ const pluralElementAccessorKey = toCamelCase(WorkspaceElementSpecs[elementType].namePlural)
139
+
140
+ if (destinationElement) {
141
+ if (!hasParent(element) || (elementIsCustomized(element) && hasParent(element))) {
142
+ // Update the element without parent OR the integration-specific element that should be customized
143
+ await iApp[singleElementAccessorKey](destinationElement.id).put(element)
144
+ coloredLog(
145
+ `Updated ${element.integrationKey || 'universal'} ${INTEGRATION_ELEMENTS[elementType].element} ${element.key}`,
146
+ 'Cyan',
147
+ )
148
+ } else if (hasParent(element)) {
149
+ // Reset Integration-specific element if it has universal parent
150
+ try {
151
+ await iApp[singleElementAccessorKey](destinationElement.id).reset()
152
+ } catch (error) {
153
+ console.debug(destinationElement, element)
154
+ throw error
155
+ }
156
+ coloredLog(
157
+ `Customization reset ${element.integrationKey || 'universal'} ${INTEGRATION_ELEMENTS[elementType].element} ${element.key}`,
158
+ 'Magenta',
159
+ )
160
+ } else {
161
+ coloredLog(
162
+ `Corrupted. Migrate Manually ${element.integrationKey || 'universal'} ${INTEGRATION_ELEMENTS[elementType].element} ${element.key}`,
163
+ 'Red',
164
+ )
165
+ }
166
+ } else {
167
+ // Create the element
168
+
169
+ if (hasParent(element)) {
170
+ try {
171
+ await iApp[singleElementAccessorKey]({
172
+ key: element.key,
173
+ }).apply([element.integrationKey])
174
+ } catch (_error) {
175
+ try {
176
+ await iApp[singleElementAccessorKey]({
177
+ key: element.key,
178
+ integrationKey: element.integrationKey,
179
+ }).put(element)
180
+ } catch (_error) {
181
+ await iApp[pluralElementAccessorKey].create({
182
+ ...element,
183
+ integrationId: workspaceData.integration.find((item) => item.key == element.integrationKey).id,
184
+ })
185
+ }
186
+ }
187
+ if (elementIsCustomized(element)) {
188
+ try {
189
+ await iApp[singleElementAccessorKey]({
190
+ key: element.key,
191
+ integrationKey: element.integrationKey,
192
+ }).put(element)
193
+ } catch (_error) {
194
+ console.debug(destinationElement, element)
195
+ throw _error
196
+ }
197
+ coloredLog(
198
+ `Applied & Customized ${element.integrationKey || 'universal'} ${INTEGRATION_ELEMENTS[elementType].element} ${element.key}`,
199
+ 'Green',
200
+ )
201
+ } else {
202
+ coloredLog(
203
+ `Applied universal ${INTEGRATION_ELEMENTS[elementType].element} ${element.key} to ${element.integrationKey} `,
204
+ 'Green',
205
+ )
206
+ }
207
+ } else {
208
+ try {
209
+ if (element.integrationKey) {
210
+ delete element.integration
211
+ element.integrationId = workspaceData.integration.find((item) => item.key == element.integrationKey).id
212
+ }
213
+ await iApp[pluralElementAccessorKey].create(element)
214
+ } catch (_error) {
215
+ console.debug(`Error creating element ${elementType}`, hasParent(element), element.integrationKey, element)
216
+ throw _error
217
+ }
218
+ coloredLog(
219
+ `Created ${element.integrationKey || 'universal'} ${INTEGRATION_ELEMENTS[elementType].element} ${element.key}`,
220
+ 'Green',
221
+ )
222
+ }
223
+ }
224
+ //console.debug(elementType, element.key, element.integrationKey || "universal")
225
+ }
226
+ }
227
+ }
228
+
229
+ function elementIsCustomized(element) {
230
+ return element.integrationKey && (element.customized || element.isCustomized)
231
+ }
232
+
233
+ function matchElement(element, elementType, workspaceData) {
234
+ const matchedElements = workspaceData[elementType]?.filter(
235
+ (item) => item.key == element.key && item.integrationKey == element.integrationKey && !item.archivedAt,
236
+ )
237
+ if (matchedElements.length > 1) {
238
+ throw new Error(
239
+ `More than one ${element.integrationKey || 'universal'} ${elementType} with key ${element.key} found in the workspace`,
240
+ )
241
+ }
242
+ if (matchedElements.length == 1) {
243
+ return matchedElements[0]
244
+ }
245
+ return
246
+ }
@@ -0,0 +1,78 @@
1
+ import { WorkspaceElementType } from '@integration-app/sdk'
2
+
3
+ export const INTEGRATION_ELEMENTS = {
4
+ [WorkspaceElementType.Integration]: {
5
+ element: 'integration',
6
+ elements: 'integrations',
7
+ exportable: false,
8
+ exportCleanup: (el) => {
9
+ return {
10
+ id: el.id,
11
+ key: el.key,
12
+ name: el.name,
13
+ connectorId: el.connectorId,
14
+ baseUri: el.baseUri,
15
+ connectorVersion: el.connectorVersion,
16
+ }
17
+ },
18
+ },
19
+ [WorkspaceElementType.Connector]: {
20
+ element: 'connector',
21
+ elements: 'connectors',
22
+ exportable: false,
23
+ },
24
+ [WorkspaceElementType.Action]: {
25
+ element: 'action',
26
+ elements: 'actions',
27
+ integrationSpecific: true,
28
+ exportCleanup: (el) => {
29
+ delete el.integration
30
+ return el
31
+ },
32
+ },
33
+ [WorkspaceElementType.AppDataSchema]: {
34
+ element: 'appDataSchema',
35
+ elements: 'appDataSchemas',
36
+ },
37
+ [WorkspaceElementType.AppEventType]: {
38
+ element: 'appEventType',
39
+ elements: 'appEventTypes',
40
+ },
41
+ [WorkspaceElementType.DataLinkTable]: {
42
+ element: 'dataLinkTable',
43
+ elements: 'dataLinkTables',
44
+ },
45
+ [WorkspaceElementType.DataSource]: {
46
+ element: 'dataSource',
47
+ elements: 'dataSources',
48
+ integrationSpecific: true,
49
+ },
50
+ [WorkspaceElementType.FieldMapping]: {
51
+ element: 'fieldMapping',
52
+ elements: 'fieldMappings',
53
+ integrationSpecific: true,
54
+ exportCleanup: (fieldMapping) => {
55
+ delete fieldMapping.dataSourceId
56
+ return fieldMapping
57
+ },
58
+ },
59
+ [WorkspaceElementType.Flow]: {
60
+ element: 'flow',
61
+ elements: 'flows',
62
+ integrationSpecific: true,
63
+ },
64
+ }
65
+
66
+ export function baseExportCleanup(element) {
67
+ delete element.workspaceId
68
+ delete element.createdAt
69
+ delete element.updatedAt
70
+ delete element.revision
71
+ delete element.parentRevision
72
+ Object.keys(element).map((key) => {
73
+ if (key.match(/universal.*Revision/g)) {
74
+ delete element[key]
75
+ }
76
+ })
77
+ return element
78
+ }
@@ -0,0 +1,17 @@
1
+ import { WorkspaceElementType } from '@integration-app/sdk'
2
+
3
+ export type ConnectorsMapping = Record<string, string>
4
+
5
+ export interface PushContext {
6
+ connectorsMapping: ConnectorsMapping
7
+ }
8
+
9
+ interface WorkspaceElement {
10
+ key: string
11
+ integration?: {
12
+ key: string
13
+ }
14
+ integrationKey?: string
15
+ }
16
+
17
+ export type WorkspaceData = Partial<Record<WorkspaceElementType, WorkspaceElement[]>>
@@ -0,0 +1,113 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+
4
+ import { WorkspaceElementType, WorkspaceElement, WorkspaceElementSpecs } from '@integration-app/sdk'
5
+ import { toCamelCase } from 'js-convert-case'
6
+ import yaml from 'js-yaml'
7
+
8
+ import { getPaths } from '../../utils/constants'
9
+ import { Logger } from '../../utils/logger'
10
+ import { WorkspaceData } from '../push/types'
11
+ import { getWorkspaceElementTypePath, PUSHABLE_ELEMENT_TYPES } from '../workspace-elements'
12
+
13
+ export async function readTopLevelElements(): Promise<WorkspaceData> {
14
+ const result: WorkspaceData = {}
15
+
16
+ for (const elementType of PUSHABLE_ELEMENT_TYPES) {
17
+ Logger.info(`Loading ${WorkspaceElementSpecs[elementType].namePlural}`)
18
+
19
+ const elements: WorkspaceElement[] = []
20
+
21
+ const paths = getPaths()
22
+ const basePath = paths.membraneDirPath
23
+
24
+ const elementsDirs = [path.join(basePath, getWorkspaceElementTypePath(elementType))]
25
+
26
+ const legacyDir = path.join(basePath, toCamelCase(getWorkspaceElementTypePath(elementType))) // Legacy: we used `dataSources` instead of `data-sources`.
27
+
28
+ if (!elementsDirs.includes(legacyDir)) {
29
+ elementsDirs.push(legacyDir)
30
+ }
31
+
32
+ for (const elementsDir of elementsDirs) {
33
+ if (!fs.existsSync(elementsDir)) {
34
+ continue
35
+ }
36
+
37
+ elements.push(...(await readWorkspaceElementsFromDir(elementsDir)))
38
+ }
39
+
40
+ result[elementType] = [...(result[elementType] || []), ...elements]
41
+ }
42
+
43
+ return result
44
+ }
45
+
46
+ export async function readIntegrationLevelElements(): Promise<WorkspaceData> {
47
+ const result: WorkspaceData = {}
48
+ const paths = getPaths()
49
+ const basePath = paths.membraneDirPath
50
+ const integrationsPath = path.join(basePath, getWorkspaceElementTypePath(WorkspaceElementType.Integration))
51
+
52
+ const integrationItems = fs.readdirSync(integrationsPath)
53
+
54
+ for (const integrationPath of integrationItems) {
55
+ if (fs.statSync(path.join(integrationsPath, integrationPath)).isDirectory()) {
56
+ const integration = await readWorkspaceElementFromDirItem(integrationsPath, integrationPath)
57
+
58
+ for (const elementType of PUSHABLE_ELEMENT_TYPES) {
59
+ const elementTypeDir = path.join(integrationsPath, integrationPath, getWorkspaceElementTypePath(elementType))
60
+ const elements = await readWorkspaceElementsFromDir(elementTypeDir)
61
+ elements.forEach((element) => {
62
+ ;(element as any).integrationKey = integration?.key
63
+ })
64
+ result[elementType] = [...(result[elementType] || []), ...elements]
65
+ }
66
+ }
67
+ }
68
+
69
+ return result
70
+ }
71
+
72
+ export async function readWorkspaceElementsFromDir(dirPath: string): Promise<WorkspaceElement[]> {
73
+ const elements: WorkspaceElement[] = []
74
+
75
+ if (!fs.existsSync(dirPath)) {
76
+ return elements
77
+ }
78
+
79
+ Logger.info(`Reading ${dirPath}`)
80
+
81
+ const elementItems = fs.readdirSync(dirPath)
82
+
83
+ for (const elementPath of elementItems) {
84
+ const element = await readWorkspaceElementFromDirItem(dirPath, elementPath)
85
+
86
+ if (element) {
87
+ elements.push(element)
88
+
89
+ Logger.success(`Loaded ${elementPath}`)
90
+ }
91
+ }
92
+
93
+ return elements
94
+ }
95
+
96
+ export async function readWorkspaceElementFromDirItem(
97
+ dirPath: string,
98
+ elementPath: string,
99
+ ): Promise<WorkspaceElement | undefined> {
100
+ const fullElementPath = path.join(dirPath, elementPath)
101
+
102
+ const elementPathIsDirectory = fs.statSync(fullElementPath).isDirectory()
103
+
104
+ const elementSpecPath = elementPathIsDirectory ? path.join(fullElementPath, `${elementPath}.yaml`) : fullElementPath
105
+
106
+ if (fs.existsSync(elementSpecPath)) {
107
+ const elementSpec = yaml.load(fs.readFileSync(elementSpecPath, 'utf8'))
108
+
109
+ return elementSpec as WorkspaceElement
110
+ }
111
+
112
+ return undefined
113
+ }