@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
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@membranehq/cli",
3
+ "version": "0.1.1",
4
+ "description": "",
5
+ "source": "./src/index.ts",
6
+ "bin": {
7
+ "membrane": "./dist/index.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "main": "dist/index.js",
16
+ "keywords": [],
17
+ "author": "",
18
+ "type": "module",
19
+ "dependencies": {
20
+ "@anthropic-ai/sdk": "^0.30.1",
21
+ "@faker-js/faker": "^9.8.0",
22
+ "@inkjs/ui": "^2.0.0",
23
+ "@integration-app/sdk": "^1.14.0",
24
+ "@types/jsonwebtoken": "^9.0.10",
25
+ "archiver": "^7.0.1",
26
+ "chalk": "^4.1.2",
27
+ "chokidar": "^3.5.3",
28
+ "commander": "^12.0.0",
29
+ "conf": "^14.0.0",
30
+ "fastmcp": "^3.5.0",
31
+ "form-data": "^4.0.3",
32
+ "ink": "^5.2.1",
33
+ "ink-select-input": "^6.2.0",
34
+ "ink-spinner": "^5.0.0",
35
+ "ink-text-input": "^6.0.0",
36
+ "js-yaml": "^4.1.0",
37
+ "jsonwebtoken": "^9.0.2",
38
+ "lodash": "^4.17.21",
39
+ "minimist": "^1.2.8",
40
+ "open": "^8.4.2",
41
+ "react": "^18.3.1",
42
+ "react-dom": "^18.3.1",
43
+ "swr": "^2.3.4",
44
+ "unzipper": "^0.12.3"
45
+ }
46
+ }
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+ # Add shebang to the built index.js file for proper binary execution
3
+
4
+ echo '#!/usr/bin/env node' | cat - dist/index.js > dist/index.js.tmp
5
+ mv dist/index.js.tmp dist/index.js
6
+ chmod +x dist/index.js
@@ -0,0 +1,29 @@
1
+ /* eslint-disable no-console */
2
+ import { readFile, writeFile } from 'fs/promises'
3
+ import path from 'path'
4
+
5
+ const PKG = path.join(process.cwd(), './package.json')
6
+
7
+ const removeScripts = async () => {
8
+ const packageJson = JSON.parse(await readFile(PKG, 'utf-8'))
9
+ delete packageJson.scripts
10
+ console.log('Scripts removed')
11
+
12
+ delete packageJson.devDependencies
13
+ console.log('Dev dependencies removed')
14
+
15
+ await writeFile(PKG, JSON.stringify(packageJson, null, 2), 'utf-8')
16
+ console.log('Package.json updated')
17
+ }
18
+
19
+ const main = async () => {
20
+ try {
21
+ await removeScripts()
22
+ } catch (error) {
23
+ console.error('Failed to remove scripts', error)
24
+ }
25
+ }
26
+
27
+ main()
28
+ .then(() => process.exit(0))
29
+ .catch(() => process.exit(1))
package/src/agent.tsx ADDED
@@ -0,0 +1,50 @@
1
+ import { resolve } from 'node:path'
2
+
3
+ import { render } from 'ink'
4
+ import minimist from 'minimist'
5
+ import { SWRConfig } from 'swr'
6
+
7
+ import { Main } from './components/Main'
8
+ import { FileWatcherProvider } from './contexts/FileWatcherContext'
9
+ import { createAccountApiFetcher } from './modules/api/account-api-client'
10
+ import { CwdProvider } from './modules/config/cwd-context'
11
+ import { ProjectConfigProvider } from './modules/config/project'
12
+ import { FileWatcher } from './modules/file-watcher'
13
+ import { WorkspaceElementService } from './modules/workspace-element-service'
14
+ import { Logger } from './utils/logger'
15
+
16
+ // Parse CLI args
17
+ const argv = minimist(process.argv.slice(2))
18
+ const cwd = argv.cwd ? resolve(argv.cwd) : process.cwd()
19
+
20
+ async function startBackgroundServices() {
21
+ try {
22
+ await workspaceElementService.init()
23
+ // ToDo: enable when our workspaces sync functionality works in a basic version
24
+ //await fileWatcher.start()
25
+ } catch (error) {
26
+ Logger.error(error.toString())
27
+ process.exit(1)
28
+ }
29
+ }
30
+
31
+ const fileWatcher = new FileWatcher({ cwd })
32
+ const workspaceElementService = new WorkspaceElementService(fileWatcher)
33
+
34
+ const App = () => (
35
+ <CwdProvider cwd={cwd}>
36
+ <FileWatcherProvider fileWatcher={fileWatcher}>
37
+ <ProjectConfigProvider>
38
+ {/* TODO: Enhance this to use project config API URI when React version compatibility is resolved */}
39
+ <SWRConfig value={{ fetcher: createAccountApiFetcher() }}>
40
+ <Main />
41
+ </SWRConfig>
42
+ </ProjectConfigProvider>
43
+ </FileWatcherProvider>
44
+ </CwdProvider>
45
+ )
46
+
47
+ export function runAgent() {
48
+ void startBackgroundServices()
49
+ render(<App />)
50
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from 'node:fs'
4
+ import { dirname, join } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ import chalk from 'chalk'
8
+ import { Command } from 'commander'
9
+
10
+ import { setupOpenCommand } from './commands/open.command'
11
+ import { setupPullCommand } from './commands/pull.command'
12
+ import { setupPushCommand } from './commands/push.command'
13
+ import { setupTestCommand } from './commands/test.command'
14
+
15
+ const getPackageVersion = () => {
16
+ try {
17
+ const __filename = fileURLToPath(import.meta.url)
18
+ const __dirname = dirname(__filename)
19
+ const packagePath = join(__dirname, '..', 'package.json')
20
+ const packageJson = JSON.parse(readFileSync(packagePath, 'utf-8'))
21
+ return packageJson.version
22
+ } catch {
23
+ return '1.0.0'
24
+ }
25
+ }
26
+
27
+ const VERSION = getPackageVersion()
28
+
29
+ export function runCLI() {
30
+ const program = new Command()
31
+ .name('membrane')
32
+ .description('Command-line interface for Membrane Agent')
33
+ .version(VERSION, '-v, --version', 'Output the version number')
34
+
35
+ program.configureHelp({
36
+ sortSubcommands: true,
37
+ subcommandTerm: (cmd) => chalk.cyan(cmd.name()),
38
+ commandUsage: (cmd) => {
39
+ if (cmd.name() === 'membrane') {
40
+ return `${chalk.cyan(cmd.name())} ${chalk.gray('[options]')} ${chalk.cyan('[command]')}`
41
+ }
42
+ return cmd.usage()
43
+ },
44
+ optionTerm: (option) => {
45
+ const flag = option.flags
46
+ return `${chalk.gray('▸')} ${chalk.cyan(flag)}`
47
+ },
48
+ subcommandDescription: (cmd) => chalk.gray(cmd.description()),
49
+ optionDescription: (option) => chalk.gray(option.description),
50
+ commandDescription: (cmd) => chalk.gray(cmd.description()),
51
+ })
52
+
53
+ program.addHelpText(
54
+ 'beforeAll',
55
+ `
56
+ ${chalk.bold.cyan('Membrane Agent CLI')} ${chalk.gray(`v${VERSION}`)}
57
+
58
+ `,
59
+ )
60
+
61
+ setupPullCommand(program)
62
+ setupPushCommand(program)
63
+ setupTestCommand(program)
64
+ setupOpenCommand(program)
65
+
66
+ if (process.argv.length === 2) {
67
+ program.outputHelp()
68
+ process.exit(0)
69
+ }
70
+
71
+ program.parse()
72
+ }
@@ -0,0 +1,51 @@
1
+ import { Command } from 'commander'
2
+
3
+ import { getWorkspaceId } from '../modules/api/workspace-api-client'
4
+ import { readProjectConfig } from '../modules/config/project/readProjectConfig'
5
+ import { Logger } from '../utils/logger'
6
+
7
+ export function setupOpenCommand(program: Command): void {
8
+ program
9
+ .command('open')
10
+ .description('Open the workspace in the browser')
11
+ .addHelpText(
12
+ 'after',
13
+ ['', 'Examples:', ' membrane open # Open workspace in browser', ''].join('\n'),
14
+ )
15
+ .action(async () => {
16
+ try {
17
+ Logger.header('Opening Workspace in Browser')
18
+
19
+ Logger.info('Loading configuration...')
20
+ const workspaceConfig = readProjectConfig()
21
+
22
+ if (!workspaceConfig) {
23
+ throw new Error('No membrane.config.yml found. Please run `membrane init` first.')
24
+ }
25
+
26
+ if (!workspaceConfig.workspaceKey || !workspaceConfig.workspaceSecret) {
27
+ throw new Error('Missing workspace credentials')
28
+ }
29
+
30
+ Logger.info('Retrieving workspace ID...')
31
+ const workspaceId = await getWorkspaceId(process.cwd())
32
+
33
+ const consoleUrl = `https://console.integration.app/w/${workspaceId}`
34
+
35
+ Logger.info(`Opening ${consoleUrl}...`)
36
+
37
+ // Use dynamic import for 'open' package since it's an ESM module
38
+ const { default: open } = await import('open')
39
+ await open(consoleUrl)
40
+
41
+ Logger.success('Browser opened successfully')
42
+ } catch (error: unknown) {
43
+ if (error instanceof Error) {
44
+ Logger.error(error.message)
45
+ process.exit(1)
46
+ }
47
+ Logger.error('An unknown error occurred')
48
+ process.exit(1)
49
+ }
50
+ })
51
+ }
@@ -0,0 +1,75 @@
1
+ import { IntegrationAppClient } from '@integration-app/sdk'
2
+ import { Command } from 'commander'
3
+ import jwt from 'jsonwebtoken'
4
+
5
+ import { pullWorkspace } from '../legacy/commands/pullWorkspace'
6
+ import { readProjectConfig } from '../modules/config/project/readProjectConfig'
7
+ import { getPaths } from '../utils/constants'
8
+ import { Logger } from '../utils/logger'
9
+
10
+ const DEFAULT_WORKSPACE_ALIAS = 'default'
11
+
12
+ // Helper function to generate access token
13
+ async function generateAccessToken(key: string, secret: string): Promise<string> {
14
+ const payload = {
15
+ iss: key,
16
+ exp: Math.floor(Date.now() / 1000) + 3600, // 1 hour
17
+ isAdmin: true,
18
+ }
19
+ return jwt.sign(payload, secret)
20
+ }
21
+
22
+ export function setupPullCommand(program: Command): void {
23
+ program
24
+ .command('pull')
25
+ .description('Pull workspace data from specified workspace')
26
+ .option('-w, --workspace <workspace>', 'Workspace to pull from', DEFAULT_WORKSPACE_ALIAS)
27
+ .option('-a, --all-connectors', 'Include store connectors in export', false)
28
+ .addHelpText(
29
+ 'after',
30
+ [
31
+ '',
32
+ 'Examples:',
33
+ ' membrane pull # Pull from default workspace',
34
+ ' membrane pull -w prod # Pull from prod workspace',
35
+ ' membrane pull -w dev # Pull from dev workspace',
36
+ ' membrane pull -a # Pull with all connectors',
37
+ ' membrane pull -w dev -a # Pull from dev workspace with all connectors',
38
+ '',
39
+ ].join('\n'),
40
+ )
41
+ .action(async (options: { workspace: string; allConnectors: boolean }) => {
42
+ Logger.header('Pulling Workspace Data')
43
+ Logger.divider()
44
+
45
+ Logger.info('Loading configuration...')
46
+ const workspaceConfig = readProjectConfig()
47
+
48
+ if (!workspaceConfig) {
49
+ throw new Error('No membrane.config.yml found. Please run `membrane init` first.')
50
+ }
51
+
52
+ if (!workspaceConfig.workspaceKey || !workspaceConfig.workspaceSecret) {
53
+ throw new Error('Missing workspace credentials')
54
+ }
55
+
56
+ const paths = getPaths()
57
+
58
+ // Create client
59
+ const client = new IntegrationAppClient({
60
+ token: await generateAccessToken(workspaceConfig.workspaceKey, workspaceConfig.workspaceSecret),
61
+ apiUri: workspaceConfig.apiUri,
62
+ })
63
+
64
+ // Perform pull using the full legacy logic
65
+ Logger.info(`Pulling from workspace...`)
66
+ await pullWorkspace({
67
+ allConnectors: options.allConnectors,
68
+ outputPath: paths.membraneDirPath,
69
+ client,
70
+ })
71
+
72
+ Logger.divider()
73
+ Logger.success('Pull completed successfully')
74
+ })
75
+ }
@@ -0,0 +1,79 @@
1
+ import { IntegrationAppClient } from '@integration-app/sdk'
2
+ import { Command } from 'commander'
3
+ import jwt from 'jsonwebtoken'
4
+
5
+ import { pushWorkspace } from '../legacy/commands/pushWorkspace'
6
+ import { readProjectConfig } from '../modules/config/project/readProjectConfig'
7
+ import { getPaths } from '../utils/constants'
8
+ import { Logger } from '../utils/logger'
9
+
10
+ const DEFAULT_WORKSPACE_ALIAS = 'default'
11
+
12
+ // Helper function to generate access token
13
+ async function generateAccessToken(key: string, secret: string): Promise<string> {
14
+ const payload = {
15
+ iss: key,
16
+ exp: Math.floor(Date.now() / 1000) + 3600, // 1 hour
17
+ isAdmin: true,
18
+ }
19
+ return jwt.sign(payload, secret)
20
+ }
21
+
22
+ export function setupPushCommand(program: Command): void {
23
+ program
24
+ .command('push')
25
+ .description('Push workspace data to specified workspace')
26
+ .option('-w, --workspace <workspace>', 'Workspace to push to', DEFAULT_WORKSPACE_ALIAS)
27
+ .addHelpText(
28
+ 'after',
29
+ [
30
+ '',
31
+ 'Examples:',
32
+ ' membrane push # Push to default workspace',
33
+ ' membrane push -w prod # Push to prod workspace',
34
+ ' membrane push -w dev # Push to dev workspace',
35
+ '',
36
+ ].join('\n'),
37
+ )
38
+ .action(async (options: { workspace: string }) => {
39
+ try {
40
+ Logger.header('Pushing Workspace Data')
41
+
42
+ Logger.info('Loading configuration...')
43
+ const workspaceConfig = readProjectConfig()
44
+
45
+ if (!workspaceConfig) {
46
+ throw new Error('No membrane.config.yml found. Please run `membrane init` first.')
47
+ }
48
+
49
+ if (!workspaceConfig.workspaceKey || !workspaceConfig.workspaceSecret) {
50
+ throw new Error('Missing workspace credentials')
51
+ }
52
+
53
+ const currentWorkspace = options.workspace || DEFAULT_WORKSPACE_ALIAS
54
+
55
+ Logger.info('Preparing push configuration...')
56
+ const client = new IntegrationAppClient({
57
+ token: await generateAccessToken(workspaceConfig.workspaceKey, workspaceConfig.workspaceSecret),
58
+ apiUri: workspaceConfig.apiUri,
59
+ })
60
+
61
+ const paths = getPaths()
62
+
63
+ // Initial push
64
+ Logger.info(`Pushing to workspace ${currentWorkspace}...`)
65
+ await pushWorkspace({
66
+ basePath: paths.membraneDirPath,
67
+ client,
68
+ })
69
+ Logger.success(`Push to workspace ${currentWorkspace} completed successfully`)
70
+ } catch (error: unknown) {
71
+ if (error instanceof Error) {
72
+ Logger.error(error.message)
73
+ process.exit(1)
74
+ }
75
+ Logger.error('An unknown error occurred')
76
+ process.exit(1)
77
+ }
78
+ })
79
+ }
@@ -0,0 +1,99 @@
1
+ import chalk from 'chalk'
2
+ import { Command } from 'commander'
3
+
4
+ import { TestRunner } from '../testing/runners/test.runner'
5
+ import { Logger } from '../utils/logger'
6
+
7
+ // Helper function to create the experimental warning box
8
+ const createExperimentalWarning = (): string => {
9
+ return [
10
+ chalk.yellow('┌─────────────────────────────────────────────────────────────────┐'),
11
+ chalk.yellow('│ ⚠️ EXPERIMENTAL FEATURE WARNING │'),
12
+ chalk.yellow('├─────────────────────────────────────────────────────────────────┤'),
13
+ chalk.yellow('│ The test command is experimental and subject to rapid changes. │'),
14
+ chalk.yellow('│ Features, APIs, and file structures may change without notice. │'),
15
+ chalk.yellow('│ Use in production environments is not recommended. │'),
16
+ chalk.yellow('└─────────────────────────────────────────────────────────────────┘'),
17
+ '',
18
+ ].join('\n')
19
+ }
20
+
21
+ interface TestOptions {
22
+ testPath: string
23
+ fix?: boolean
24
+ path?: string
25
+ }
26
+
27
+ export function setupTestCommand(program: Command): void {
28
+ const testCommand = program
29
+ .command('test')
30
+ .description(
31
+ '⚠️ EXPERIMENTAL: Test management commands - This feature is experimental and will be changing rapidly. Use at your own risk.',
32
+ )
33
+ .addHelpText('after', createExperimentalWarning())
34
+ .action(async () => {
35
+ console.warn(createExperimentalWarning())
36
+ Logger.error('Please specify a subcommand. Use --help for available options.')
37
+ process.exit(1)
38
+ })
39
+
40
+ testCommand
41
+ .command('run')
42
+ .description('Run integration tests for various membrane components')
43
+ .argument(
44
+ '<testPath>',
45
+ 'Test path (e.g., "connectors/netsuite", "actions/create-contact", "connectors/hubspot/data/contacts", "connectors/netsuite/data/contacts/create")',
46
+ )
47
+ .option('-p, --path <path>', 'Additional path filter within the test scope')
48
+ .option('--fix', 'Enable auto-fix for test issues')
49
+ .addHelpText(
50
+ 'after',
51
+ [
52
+ '',
53
+ chalk.bold('Examples:'),
54
+ ` ${chalk.gray('▸')} ${chalk.cyan('membrane test run connectors/netsuite')} # Test all data collections for netsuite connector`,
55
+ ` ${chalk.gray('▸')} ${chalk.cyan('membrane test run connectors/netsuite/data/contacts')} # Test specific data collection`,
56
+ ` ${chalk.gray('▸')} ${chalk.cyan('membrane test run connectors/netsuite/data/contacts/create')} # Test specific method for data collection`,
57
+ ` ${chalk.gray('▸')} ${chalk.cyan('membrane test run connectors/netsuite/data/contacts/delete')} # Test delete method for data collection`,
58
+ ` ${chalk.gray('▸')} ${chalk.cyan('membrane test run actions/create-contact')} # Test specific action`,
59
+ ` ${chalk.gray('▸')} ${chalk.cyan('membrane test run connectors/hubspot/events')} # Test events for hubspot`,
60
+ ` ${chalk.gray('▸')} ${chalk.cyan('membrane test run connectors/salesforce --fix')} # Run tests with auto-fix enabled`,
61
+ '',
62
+ chalk.gray('For more information, visit:'),
63
+ chalk.blue(' https://docs.integration.app/cli'),
64
+ '',
65
+ createExperimentalWarning(),
66
+ ].join('\n'),
67
+ )
68
+ .action(async (testPath: string, options: TestOptions) => {
69
+ try {
70
+ // Show experimental warning
71
+ console.warn(createExperimentalWarning())
72
+
73
+ if (!testPath) {
74
+ Logger.error('Test path is required')
75
+ process.exit(1)
76
+ }
77
+
78
+ Logger.header(`Testing: ${testPath}`)
79
+
80
+ const runner = new TestRunner({
81
+ testPath,
82
+ path: options.path,
83
+ fix: options.fix,
84
+ })
85
+
86
+ await runner.initialize()
87
+ await runner.run()
88
+
89
+ Logger.success('Tests completed')
90
+ } catch (error: unknown) {
91
+ if (error instanceof Error) {
92
+ Logger.error(error.message)
93
+ process.exit(1)
94
+ }
95
+ Logger.error('An unknown error occurred')
96
+ process.exit(1)
97
+ }
98
+ })
99
+ }