@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,103 @@
1
+ import * as fs from 'node:fs'
2
+ import * as path from 'node:path'
3
+
4
+ import { createContext, useCallback, useContext, useEffect, useState } from 'react'
5
+ import type { PropsWithChildren } from 'react'
6
+ import YAML from 'js-yaml'
7
+ import { z } from 'zod'
8
+
9
+ import { useCwd } from '../cwd-context'
10
+ import { readProjectConfig } from './readProjectConfig'
11
+
12
+ const CONFIG_FILE_NAME = 'membrane.config.yml'
13
+
14
+ export const configSchema = z.object({
15
+ workspaceKey: z.string().optional(),
16
+ workspaceSecret: z.string().optional(),
17
+ apiUri: z.string().optional(),
18
+ testCustomerId: z.string().optional(),
19
+ anthropicApiKey: z.string().optional(),
20
+ })
21
+
22
+ export type ProjectConfig = z.infer<typeof configSchema>
23
+
24
+ export const commandSchema = z.enum(['pull'])
25
+ export type Command = z.infer<typeof commandSchema>
26
+
27
+ interface IProjectConfigContext {
28
+ readonly config: ProjectConfig | null
29
+ readonly isLoading: boolean
30
+ readonly updateConfig: (newConfig: Partial<ProjectConfig>) => boolean
31
+ readonly validateConfig: () => boolean
32
+ }
33
+
34
+ const ProjectConfigContext = createContext<IProjectConfigContext | undefined>(undefined)
35
+
36
+ export function ProjectConfigProvider({ children }: PropsWithChildren) {
37
+ const cwd = useCwd()
38
+ const [config, setConfig] = useState<ProjectConfig | null>(null)
39
+ const [isLoading, setIsLoading] = useState(true)
40
+
41
+ useEffect(() => {
42
+ const config = readProjectConfig(cwd)
43
+ setConfig(config)
44
+ setIsLoading(false)
45
+ }, [cwd])
46
+
47
+ const updateConfig = useCallback(
48
+ (newConfig: Partial<ProjectConfig>): boolean => {
49
+ const configPath = path.join(cwd, CONFIG_FILE_NAME)
50
+ const currentConfig = readProjectConfig(cwd) ?? {}
51
+ const newConfigData = {
52
+ ...currentConfig,
53
+ ...newConfig,
54
+ }
55
+
56
+ const validationResult = configSchema.safeParse(newConfigData)
57
+ if (!validationResult.success) {
58
+ return false
59
+ }
60
+
61
+ fs.writeFileSync(configPath, YAML.dump(validationResult.data))
62
+ setConfig(validationResult.data)
63
+ return true
64
+ },
65
+ [cwd],
66
+ )
67
+
68
+ const validateConfig = useCallback((): boolean => {
69
+ return !!(config?.workspaceKey && config?.workspaceSecret)
70
+ }, [config])
71
+
72
+ const value: IProjectConfigContext = {
73
+ config,
74
+ isLoading,
75
+ updateConfig,
76
+ validateConfig,
77
+ }
78
+
79
+ return <ProjectConfigContext.Provider value={value}>{children}</ProjectConfigContext.Provider>
80
+ }
81
+
82
+ export function useProjectConfig(): IProjectConfigContext {
83
+ const context = useContext(ProjectConfigContext)
84
+ if (context === undefined) {
85
+ throw new Error('useProjectConfig must be used within a ProjectConfigProvider')
86
+ }
87
+ return context
88
+ }
89
+
90
+ export function clearWorkspaceConfig(): void {
91
+ const cwd = process.cwd()
92
+ const configPath = path.join(cwd, CONFIG_FILE_NAME)
93
+ const currentConfig = readProjectConfig(cwd) ?? {}
94
+ const newConfigData = {
95
+ ...currentConfig,
96
+ workspaceKey: undefined,
97
+ workspaceSecret: undefined,
98
+ }
99
+ // Remove keys if present
100
+ delete newConfigData.workspaceKey
101
+ delete newConfigData.workspaceSecret
102
+ fs.writeFileSync(configPath, YAML.dump(newConfigData))
103
+ }
@@ -0,0 +1,35 @@
1
+ import os from 'os'
2
+ import path from 'path'
3
+
4
+ import Conf from 'conf'
5
+
6
+ const schema = {
7
+ pat: {
8
+ type: 'string',
9
+ },
10
+ workspace: {
11
+ type: 'object',
12
+ },
13
+ }
14
+
15
+ const config = new Conf({
16
+ schema,
17
+ configName: 'config',
18
+ cwd: path.join(os.homedir(), '.membrane'),
19
+ })
20
+
21
+ export const setPat = (token: string): void => {
22
+ config.set('pat', token)
23
+ }
24
+
25
+ export const getPat = (): string | undefined => {
26
+ return config.get('pat') as string | undefined
27
+ }
28
+
29
+ export const hasPat = (): boolean => {
30
+ return !!config.get('pat')
31
+ }
32
+
33
+ export const clearPat = (): void => {
34
+ config.delete('pat')
35
+ }
@@ -0,0 +1,166 @@
1
+ import * as crypto from 'node:crypto'
2
+ import { EventEmitter } from 'node:events'
3
+ import * as fs from 'node:fs'
4
+ import * as path from 'node:path'
5
+
6
+ import chokidar from 'chokidar'
7
+ import yaml from 'yaml'
8
+
9
+ import { FileChangeEventType, FileChangeEvent, FileChangeEventMap } from './types'
10
+ import { getMembraneDir, IGNORED_FILES } from '../config/project/paths'
11
+
12
+ import type { FSWatcher } from 'chokidar'
13
+
14
+ type WatchOptions = Parameters<typeof chokidar.watch>[1]
15
+
16
+ const DEFAULT_WATCHER_CONFIG: WatchOptions = {
17
+ ignored: IGNORED_FILES,
18
+ persistent: true,
19
+ ignoreInitial: true,
20
+ followSymlinks: false,
21
+ depth: 10,
22
+ awaitWriteFinish: {
23
+ stabilityThreshold: 500,
24
+ pollInterval: 100,
25
+ },
26
+ ignorePermissionErrors: true,
27
+ atomic: true,
28
+ usePolling: false,
29
+ alwaysStat: false,
30
+ interval: 1000,
31
+ binaryInterval: 300,
32
+ }
33
+
34
+ export class FileWatcher extends EventEmitter<FileChangeEventMap> {
35
+ private isWatching = false
36
+ private watcher?: FSWatcher
37
+ private readonly membraneDir: string
38
+ private readonly contentCache: Record<string, string> = {}
39
+
40
+ constructor(private readonly options: { cwd: string }) {
41
+ super()
42
+ this.membraneDir = getMembraneDir(options.cwd)
43
+ }
44
+
45
+ /**
46
+ * Starts the file watcher
47
+ */
48
+ async start(): Promise<void> {
49
+ if (this.isWatching) {
50
+ return
51
+ }
52
+ if (!fs.existsSync(this.membraneDir)) {
53
+ fs.mkdirSync(this.membraneDir, { recursive: true })
54
+ }
55
+
56
+ this.watcher = chokidar.watch(this.membraneDir, DEFAULT_WATCHER_CONFIG)
57
+ this.watcher
58
+ .on('add', (filePath) => this.handleFileSystemEvent('added', filePath))
59
+ .on('change', (filePath) => this.handleFileSystemEvent('changed', filePath))
60
+ .on('unlink', (filePath) => this.handleFileSystemEvent('deleted', filePath))
61
+ .on('ready', () => (this.isWatching = true))
62
+ }
63
+
64
+ /**
65
+ * Stops the file watcher
66
+ */
67
+ async stop(): Promise<void> {
68
+ if (!this.isWatching || !this.watcher) {
69
+ return
70
+ }
71
+
72
+ await this.watcher.close()
73
+ this.isWatching = false
74
+ this.watcher = undefined
75
+ this.clearCache()
76
+ this.emit('stopped')
77
+ }
78
+
79
+ getCwd(): string {
80
+ return this.options.cwd
81
+ }
82
+
83
+ /**
84
+ * Handles file system events and only emits when content actually changes
85
+ */
86
+ private handleFileSystemEvent(eventType: FileChangeEventType, filePath: string): void {
87
+ const relativePath = path.relative(this.membraneDir, filePath)
88
+
89
+ if (eventType === 'deleted') {
90
+ this.removeFromCache(relativePath)
91
+ const event: FileChangeEvent = {
92
+ filePath,
93
+ relativePath,
94
+ data: undefined,
95
+ }
96
+ this.emit(eventType, event)
97
+ return
98
+ }
99
+
100
+ const fileContent = this.readFileContent(filePath)
101
+
102
+ const hasContentChanged = this.hasContentChanged(relativePath, fileContent, eventType)
103
+ if (!hasContentChanged) {
104
+ return
105
+ }
106
+
107
+ const event = this.processFileEvent(filePath, fileContent)
108
+ this.updateCache(relativePath, fileContent)
109
+ this.emit(eventType, event)
110
+ }
111
+
112
+ private readFileContent(filePath: string): string {
113
+ return fs.readFileSync(filePath, 'utf8')
114
+ }
115
+
116
+ private processFileEvent(filePath: string, fileContent: string): FileChangeEvent | null {
117
+ const relativePath = path.relative(this.membraneDir, filePath)
118
+
119
+ let data: any
120
+ try {
121
+ data = fileContent ? yaml.parse(fileContent) : undefined
122
+ } catch (_error) {
123
+ data = undefined
124
+ }
125
+
126
+ return { filePath, relativePath, data }
127
+ }
128
+
129
+ private hasContentChanged(relativePath: string, fileContent: string, eventType: FileChangeEventType): boolean {
130
+ if (eventType === 'added') {
131
+ return true
132
+ }
133
+
134
+ if (!fileContent) {
135
+ return this.contentCache[relativePath] !== undefined
136
+ }
137
+
138
+ const contentHash = this.getContentHash(fileContent)
139
+ const cachedHash = this.contentCache[relativePath]
140
+
141
+ return cachedHash !== contentHash
142
+ }
143
+
144
+ private getContentHash(fileContent: string): string {
145
+ return crypto.createHash('sha256').update(fileContent).digest('hex')
146
+ }
147
+
148
+ private updateCache(relativePath: string, fileContent: string | null): void {
149
+ if (!fileContent) {
150
+ delete this.contentCache[relativePath]
151
+ return
152
+ }
153
+
154
+ this.contentCache[relativePath] = this.getContentHash(fileContent)
155
+ }
156
+
157
+ private removeFromCache(relativePath: string): void {
158
+ delete this.contentCache[relativePath]
159
+ }
160
+
161
+ private clearCache(): void {
162
+ Object.keys(this.contentCache).forEach((key) => {
163
+ delete this.contentCache[key]
164
+ })
165
+ }
166
+ }
@@ -0,0 +1,14 @@
1
+ export interface FileChangeEvent {
2
+ filePath: string
3
+ relativePath: string
4
+ data?: any
5
+ }
6
+
7
+ export type FileChangeEventMap = {
8
+ added: [FileChangeEvent]
9
+ changed: [FileChangeEvent]
10
+ deleted: [FileChangeEvent]
11
+ stopped: []
12
+ }
13
+
14
+ export type FileChangeEventType = keyof FileChangeEventMap
@@ -0,0 +1,9 @@
1
+ export enum Step {
2
+ Authenticate,
3
+ ConnectWorkspace,
4
+ }
5
+
6
+ export const STEP_LABELS: Record<Step, string> = {
7
+ [Step.Authenticate]: 'Authenticate in Membrane',
8
+ [Step.ConnectWorkspace]: 'Connect a Membrane Workspace',
9
+ }
@@ -0,0 +1,16 @@
1
+ import { useProjectConfig } from '../config/project'
2
+ import { hasPat } from '../config/system'
3
+
4
+ export const useSetup = () => {
5
+ const { config } = useProjectConfig()
6
+
7
+ const isAuthenticated = hasPat()
8
+ const workspaceIsConfigured = !!(config?.workspaceKey && config?.workspaceSecret)
9
+ const isSetupComplete = isAuthenticated && workspaceIsConfigured
10
+
11
+ return {
12
+ isAuthenticated,
13
+ workspaceIsConfigured,
14
+ isSetupComplete,
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ import { useCwd } from '../config/cwd-context'
2
+ import { useProjectConfig } from '../config/project'
3
+ import { useSetup } from '../setup/useSetup'
4
+
5
+ export const useStatus = () => {
6
+ const { isSetupComplete } = useSetup()
7
+ const { config } = useProjectConfig()
8
+ const cwd = useCwd()
9
+
10
+ return {
11
+ isSetupComplete,
12
+ workspaceKey: config?.workspaceKey,
13
+ config,
14
+ cwd,
15
+ }
16
+ }
@@ -0,0 +1,121 @@
1
+ import { WorkspaceElementType } from '@integration-app/sdk'
2
+
3
+ export interface ElementConfig {
4
+ element: string
5
+ elements: string
6
+ exportable?: boolean
7
+ integrationSpecific?: boolean
8
+ }
9
+
10
+ export const INTEGRATION_ELEMENTS: Record<WorkspaceElementType, ElementConfig> = {
11
+ [WorkspaceElementType.Integration]: {
12
+ element: 'integration',
13
+ elements: 'integrations',
14
+ exportable: false,
15
+ },
16
+ [WorkspaceElementType.Connector]: {
17
+ element: 'connector',
18
+ elements: 'connectors',
19
+ exportable: false,
20
+ },
21
+ [WorkspaceElementType.Action]: {
22
+ element: 'action',
23
+ elements: 'actions',
24
+ integrationSpecific: true,
25
+ },
26
+ [WorkspaceElementType.AppDataSchema]: {
27
+ element: 'appDataSchema',
28
+ elements: 'appDataSchemas',
29
+ },
30
+ [WorkspaceElementType.AppEventType]: {
31
+ element: 'appEventType',
32
+ elements: 'appEventTypes',
33
+ },
34
+ [WorkspaceElementType.DataLinkTable]: {
35
+ element: 'dataLinkTable',
36
+ elements: 'dataLinkTables',
37
+ },
38
+ [WorkspaceElementType.DataSource]: {
39
+ element: 'dataSource',
40
+ elements: 'dataSources',
41
+ integrationSpecific: true,
42
+ },
43
+ [WorkspaceElementType.FieldMapping]: {
44
+ element: 'fieldMapping',
45
+ elements: 'fieldMappings',
46
+ integrationSpecific: true,
47
+ },
48
+ [WorkspaceElementType.Flow]: {
49
+ element: 'flow',
50
+ elements: 'flows',
51
+ integrationSpecific: true,
52
+ },
53
+ [WorkspaceElementType.Customer]: {
54
+ element: 'customer',
55
+ elements: 'customers',
56
+ },
57
+ [WorkspaceElementType.FlowInstance]: {
58
+ element: 'flowInstance',
59
+ elements: 'flowInstances',
60
+ },
61
+ [WorkspaceElementType.FlowRun]: {
62
+ element: 'flowRun',
63
+ elements: 'flowRuns',
64
+ },
65
+ [WorkspaceElementType.Scenario]: {
66
+ element: 'scenario',
67
+ elements: 'scenarios',
68
+ },
69
+ [WorkspaceElementType.ActionInstance]: {
70
+ element: 'actionInstance',
71
+ elements: 'actionInstances',
72
+ },
73
+ [WorkspaceElementType.Connection]: {
74
+ element: 'connection',
75
+ elements: 'connections',
76
+ },
77
+ [WorkspaceElementType.FieldMappingInstance]: {
78
+ element: 'fieldMappingInstance',
79
+ elements: 'fieldMappingInstances',
80
+ },
81
+ [WorkspaceElementType.DataSourceInstance]: {
82
+ element: 'dataSourceInstance',
83
+ elements: 'dataSourceInstances',
84
+ },
85
+ [WorkspaceElementType.DataLinkTableInstance]: {
86
+ element: 'dataLinkTableInstance',
87
+ elements: 'dataLinkTableInstances',
88
+ },
89
+ [WorkspaceElementType.AppEventSubscription]: {
90
+ element: 'appEventSubscription',
91
+ elements: 'appEventSubscriptions',
92
+ },
93
+ [WorkspaceElementType.AppDataSchemaInstance]: {
94
+ element: 'appDataSchemaInstance',
95
+ elements: 'appDataSchemaInstances',
96
+ },
97
+ [WorkspaceElementType.ExternalEventSubscription]: {
98
+ element: 'externalEventSubscription',
99
+ elements: 'externalEventSubscriptions',
100
+ },
101
+ [WorkspaceElementType.ExternalEventLogRecord]: {
102
+ element: 'externalEventLogRecord',
103
+ elements: 'externalEventLogRecords',
104
+ },
105
+ [WorkspaceElementType.ExternalEventPull]: {
106
+ element: 'externalEventPull',
107
+ elements: 'externalEventPulls',
108
+ },
109
+ [WorkspaceElementType.DataCollection]: {
110
+ element: 'dataCollection',
111
+ elements: 'dataCollections',
112
+ },
113
+ [WorkspaceElementType.Screen]: {
114
+ element: 'screen',
115
+ elements: 'screens',
116
+ },
117
+ [WorkspaceElementType.ActionRunLogRecord]: {
118
+ element: 'actionRunLogRecord',
119
+ elements: 'actionRunLogRecords',
120
+ },
121
+ }
@@ -0,0 +1,69 @@
1
+ import * as path from 'node:path'
2
+
3
+ import { WorkspaceElementSpecs, WorkspaceElementType } from '@integration-app/sdk'
4
+
5
+ export interface WorkspaceElementKeyReference {
6
+ readonly type: WorkspaceElementType
7
+ readonly key: string
8
+ }
9
+
10
+ export function getTypeAndKeyFromPath(relativePath: string): WorkspaceElementKeyReference | undefined {
11
+ // NOTE: skip connector files for now
12
+ if (isConnectorFile(relativePath)) {
13
+ return undefined
14
+ }
15
+
16
+ if (isWorkspaceElementFile(relativePath)) {
17
+ return extractWorkspaceElementKeyReference(relativePath)
18
+ }
19
+ return undefined
20
+ }
21
+
22
+ function extractWorkspaceElementKeyReference(relativePath: string): WorkspaceElementKeyReference | undefined {
23
+ const elementTypes = Object.values(WorkspaceElementSpecs).map((spec) => spec.apiPath)
24
+
25
+ // <element-type>/<element-key>/spec.yml or .yaml
26
+ const universalElementPattern = new RegExp(
27
+ `^(?<elementTypePlural>${elementTypes.join('|')})\/(?<elementKey>[^\/]+)\/spec\.y[a]*ml$`,
28
+ )
29
+ const universalMatch = relativePath.match(universalElementPattern)
30
+ if (universalMatch && universalMatch.groups) {
31
+ const { elementTypePlural, elementKey } = universalMatch.groups
32
+ const type = getWorkspaceElementTypeFromFolderName(elementTypePlural)
33
+ if (type) {
34
+ return { key: elementKey, type }
35
+ }
36
+ }
37
+
38
+ // integrations/<integration-key>/<element-type>/<element-key>/spec.yml or .yaml
39
+ const integratedElementPattern = new RegExp(
40
+ `^integrations\/(?<integrationKey>[^\/]+)\/(?<elementTypePlural>${elementTypes.join('|')})\/(?<elementKey>[^\/]+)\/spec\.y[a]*ml$`,
41
+ )
42
+ const integratedMatch = relativePath.match(integratedElementPattern)
43
+ if (integratedMatch && integratedMatch.groups) {
44
+ const { elementTypePlural, elementKey } = integratedMatch.groups
45
+ const type = getWorkspaceElementTypeFromFolderName(elementTypePlural)
46
+ if (type) {
47
+ return { key: elementKey, type }
48
+ }
49
+ }
50
+
51
+ return undefined
52
+ }
53
+
54
+ function isWorkspaceElementFile(relativePath: string): boolean {
55
+ return relativePath.endsWith('.yml') || relativePath.endsWith('.yaml')
56
+ }
57
+
58
+ function isConnectorFile(relativePath: string): boolean {
59
+ const [rootDir] = relativePath.split(path.sep)
60
+ return rootDir === WorkspaceElementSpecs[WorkspaceElementType.Connector].apiPath
61
+ }
62
+
63
+ function getWorkspaceElementTypeFromFolderName(folderName: string): WorkspaceElementType | undefined {
64
+ return Object.values(WorkspaceElementType).find((type) => getWorkspaceElementTypeFolderName(type) === folderName)
65
+ }
66
+
67
+ function getWorkspaceElementTypeFolderName(type: WorkspaceElementType): string {
68
+ return WorkspaceElementSpecs[type].apiPath
69
+ }