@slates/cli 1.0.0-rc.2

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.
@@ -0,0 +1,31 @@
1
+ import { createClientContext } from '../lib/context';
2
+ import { parseJsonObject, promptForObjectSchema } from '../lib/prompts';
3
+ import { JsonInput, WithProfile } from '../lib/types';
4
+
5
+ export let getConfig = async (opts: WithProfile) => {
6
+ let { profile } = await createClientContext(opts);
7
+ return profile.config;
8
+ };
9
+
10
+ export let getConfigSchema = async (opts: WithProfile) => {
11
+ let { client } = await createClientContext(opts);
12
+ return client.getConfigSchema();
13
+ };
14
+
15
+ export let setConfig = async (opts: WithProfile & JsonInput) => {
16
+ let { store, profile, client } = await createClientContext(opts);
17
+ let previousConfig = profile.config;
18
+ let schema = (await client.getConfigSchema()).schema;
19
+ let defaultConfig = (await client.getDefaultConfig()).config ?? {};
20
+ let desiredConfig =
21
+ parseJsonObject(opts.input, 'config input') ??
22
+ (await promptForObjectSchema(schema, previousConfig ?? defaultConfig));
23
+ let result = await client.updateConfig(previousConfig, desiredConfig);
24
+ let finalConfig = result.config ?? desiredConfig;
25
+ store.setProfileConfig(profile.id, finalConfig);
26
+ await store.save();
27
+ return {
28
+ ...result,
29
+ config: finalConfig
30
+ };
31
+ };
@@ -0,0 +1,6 @@
1
+ export * from './auth';
2
+ export * from './config';
3
+ export * from './profiles';
4
+ export * from './repl';
5
+ export * from './test';
6
+ export * from './tools';
@@ -0,0 +1,143 @@
1
+ import { input } from '@inquirer/prompts';
2
+ import { createSlatesClientFromProfile, openSlatesCliStore } from '@slates/profiles';
3
+ import path from 'path';
4
+ import { chooseProfile, openIntegrationStore, syncProfileMetadata } from '../lib/context';
5
+ import { WithProfile } from '../lib/types';
6
+
7
+ let normalizeEntry = (rootDir: string, entry: string) => {
8
+ let absolute = path.isAbsolute(entry) ? entry : path.resolve(process.cwd(), entry);
9
+ let relative = path.relative(rootDir, absolute);
10
+ return relative && !relative.startsWith('..') && !path.isAbsolute(relative)
11
+ ? relative
12
+ : absolute;
13
+ };
14
+
15
+ let getNextSetupProfileName = async (store: Awaited<ReturnType<typeof openSlatesCliStore>>) => {
16
+ let names = new Set(store.listProfiles().map(profile => profile.name));
17
+ if (!names.has('default')) {
18
+ return 'default';
19
+ }
20
+
21
+ let suffix = 2;
22
+ while (names.has(`default-${suffix}`)) {
23
+ suffix += 1;
24
+ }
25
+
26
+ return `default-${suffix}`;
27
+ };
28
+
29
+ let createProfile = async (
30
+ opts: WithProfile & {
31
+ name?: string;
32
+ entry?: string;
33
+ exportName?: string;
34
+ useAsDefault?: boolean;
35
+ initializeConfig?: boolean;
36
+ interactive?: boolean;
37
+ }
38
+ ) => {
39
+ let { integration, store } = await openIntegrationStore(opts.integration);
40
+ let interactive = opts.interactive ?? true;
41
+
42
+ let defaultName =
43
+ opts.name ??
44
+ (opts.initializeConfig ? await getNextSetupProfileName(store) : `profile-${store.listProfiles().length + 1}`);
45
+ let name =
46
+ opts.name ??
47
+ (interactive ? await input({ message: 'Profile name', default: defaultName }) : defaultName);
48
+ let defaultEntry = opts.entry ?? integration.entry;
49
+ let entry =
50
+ opts.entry ??
51
+ (interactive
52
+ ? await input({
53
+ message: 'Local slate entry file',
54
+ default: defaultEntry
55
+ })
56
+ : defaultEntry);
57
+ let exportName =
58
+ opts.exportName ??
59
+ (interactive ? await input({ message: 'Export name (optional)', default: 'provider' }) : 'provider');
60
+
61
+ let profile = store.upsertProfile({
62
+ name,
63
+ target: {
64
+ type: 'local',
65
+ entry: normalizeEntry(store.rootDir, entry),
66
+ exportName: exportName.trim() ? exportName.trim() : undefined
67
+ }
68
+ });
69
+
70
+ let client = await createSlatesClientFromProfile(profile);
71
+ await syncProfileMetadata({ store, profile, client });
72
+
73
+ if (opts.initializeConfig) {
74
+ let defaultConfig = (await client.getDefaultConfig()).config ?? {};
75
+ let result = await client.updateConfig(null, defaultConfig);
76
+ store.setProfileConfig(profile.id, result.config ?? defaultConfig);
77
+ }
78
+
79
+ if (opts.useAsDefault ?? store.listProfiles().length === 1) {
80
+ store.setCurrentProfile(profile.id);
81
+ }
82
+
83
+ await store.save();
84
+
85
+ return profile;
86
+ };
87
+
88
+ export let addProfile = async (
89
+ opts: WithProfile & {
90
+ name?: string;
91
+ entry?: string;
92
+ exportName?: string;
93
+ useAsDefault?: boolean;
94
+ }
95
+ ) =>
96
+ createProfile({
97
+ ...opts,
98
+ interactive: true
99
+ });
100
+
101
+ export let setupIntegration = async (
102
+ opts: WithProfile & {
103
+ name?: string;
104
+ exportName?: string;
105
+ }
106
+ ) =>
107
+ createProfile({
108
+ ...opts,
109
+ useAsDefault: true,
110
+ initializeConfig: true,
111
+ interactive: false
112
+ });
113
+
114
+ export let listProfiles = async (opts: Pick<WithProfile, 'integration'>) => {
115
+ let { store } = await openIntegrationStore(opts.integration);
116
+ let current = store.getProfile();
117
+ return store.listProfiles().map(profile => ({
118
+ name: profile.name,
119
+ id: profile.id,
120
+ current: profile.id === current?.id,
121
+ entry: profile.target.type === 'local' ? profile.target.entry : null,
122
+ authMethods: Object.keys(profile.auth)
123
+ }));
124
+ };
125
+
126
+ export let getProfile = async (opts: WithProfile) => {
127
+ let { store } = await openIntegrationStore(opts.integration);
128
+ return store.requireProfile(opts.profile);
129
+ };
130
+
131
+ export let useProfile = async (opts: WithProfile) => {
132
+ let { store, profile } = await chooseProfile(opts);
133
+ store.setCurrentProfile(profile.id);
134
+ await store.save();
135
+ return profile;
136
+ };
137
+
138
+ export let removeProfile = async (opts: WithProfile) => {
139
+ let { store, profile } = await chooseProfile(opts);
140
+ store.removeProfile(profile.id);
141
+ await store.save();
142
+ return profile;
143
+ };
@@ -0,0 +1,95 @@
1
+ import { input } from '@inquirer/prompts';
2
+ import { print } from '../lib/prompts';
3
+ import { WithProfile } from '../lib/types';
4
+ import { listAuth, setupAuth } from './auth';
5
+ import { getConfig, setConfig } from './config';
6
+ import { getProfile } from './profiles';
7
+ import { callTool, getTool, listTools } from './tools';
8
+
9
+ let printHelp = () => {
10
+ console.log(
11
+ [
12
+ 'Available commands:',
13
+ ' help',
14
+ ' tools',
15
+ ' tool <toolId>',
16
+ ' call <toolId>',
17
+ ' auth list',
18
+ ' auth setup [authMethodId]',
19
+ ' config get',
20
+ ' config set',
21
+ ' profile',
22
+ ' quit'
23
+ ].join('\n')
24
+ );
25
+ };
26
+
27
+ export let startRepl = async (opts: WithProfile) => {
28
+ let keepRunning = true;
29
+ printHelp();
30
+
31
+ while (keepRunning) {
32
+ let commandLine = await input({
33
+ message: 'slates>'
34
+ });
35
+ let [command, ...rest] = commandLine.trim().split(/\s+/).filter(Boolean);
36
+
37
+ if (!command) continue;
38
+
39
+ switch (command) {
40
+ case 'exit':
41
+ case 'quit':
42
+ keepRunning = false;
43
+ break;
44
+
45
+ case 'help':
46
+ printHelp();
47
+ break;
48
+
49
+ case 'tools':
50
+ print(await listTools(opts));
51
+ break;
52
+
53
+ case 'tool':
54
+ print(await getTool({ ...opts, toolId: rest[0] }));
55
+ break;
56
+
57
+ case 'call':
58
+ print(await callTool({ ...opts, toolId: rest[0] }));
59
+ break;
60
+
61
+ case 'auth':
62
+ if (rest[0] === 'list') {
63
+ print(await listAuth(opts));
64
+ break;
65
+ }
66
+
67
+ if (rest[0] === 'setup') {
68
+ print(await setupAuth({ ...opts, authMethodId: rest[1] }));
69
+ break;
70
+ }
71
+
72
+ throw new Error(`Unsupported auth command: ${rest.join(' ')}`);
73
+
74
+ case 'config':
75
+ if (rest[0] === 'get') {
76
+ print(await getConfig(opts));
77
+ break;
78
+ }
79
+
80
+ if (rest[0] === 'set') {
81
+ print(await setConfig(opts));
82
+ break;
83
+ }
84
+
85
+ throw new Error(`Unsupported config command: ${rest.join(' ')}`);
86
+
87
+ case 'profile':
88
+ print(await getProfile(opts));
89
+ break;
90
+
91
+ default:
92
+ throw new Error(`Unknown REPL command: ${command}`);
93
+ }
94
+ }
95
+ };
@@ -0,0 +1,92 @@
1
+ import { spawn } from 'child_process';
2
+ import { writeFile } from 'fs/promises';
3
+ import path from 'path';
4
+ import { chooseProfile } from '../lib/context';
5
+ import { listWorkspaceIntegrations } from '../lib/integration';
6
+ import { WithProfile } from '../lib/types';
7
+
8
+ let runVitest = async (opts: {
9
+ cwd: string;
10
+ env: NodeJS.ProcessEnv;
11
+ vitestArgs: string[];
12
+ label?: string;
13
+ }) => {
14
+ let command = process.execPath;
15
+ let args = ['x', 'vitest', 'run', '--passWithNoTests', ...opts.vitestArgs];
16
+
17
+ if (opts.label) {
18
+ console.log(`\n==> ${opts.label}`);
19
+ }
20
+
21
+ await new Promise<void>((resolve, reject) => {
22
+ let child = spawn(command, args, {
23
+ cwd: opts.cwd,
24
+ stdio: 'inherit',
25
+ env: opts.env
26
+ });
27
+
28
+ child.on('error', reject);
29
+ child.on('exit', code => {
30
+ if (code === 0) {
31
+ resolve();
32
+ return;
33
+ }
34
+
35
+ reject(new Error(`Vitest exited with code ${code ?? 1}.`));
36
+ });
37
+ });
38
+ };
39
+
40
+ export let runVitestWithProfile = async (opts: WithProfile & { vitestArgs: string[] }) => {
41
+ let { integration, store, profile } = await chooseProfile(opts);
42
+ let contextPath = path.join(store.dirPath, 'test-runtime.json');
43
+
44
+ await writeFile(
45
+ contextPath,
46
+ JSON.stringify(
47
+ {
48
+ integration: integration.relativeDir,
49
+ profileId: profile.id,
50
+ storePath: store.storePath,
51
+ cliDir: store.dirPath
52
+ },
53
+ null,
54
+ 2
55
+ ) + '\n',
56
+ 'utf-8'
57
+ );
58
+
59
+ await runVitest({
60
+ cwd: integration.dirPath,
61
+ vitestArgs: opts.vitestArgs,
62
+ env: {
63
+ ...process.env,
64
+ SLATES_INTEGRATION: integration.relativeDir,
65
+ SLATES_PROFILE_ID: profile.id,
66
+ SLATES_CLI_DIR: store.dirPath,
67
+ SLATES_STORE_PATH: store.storePath,
68
+ SLATES_TEST_CONTEXT_PATH: contextPath
69
+ }
70
+ });
71
+ };
72
+
73
+ export let runAllIntegrationTests = async (opts: { cwd?: string; vitestArgs: string[] }) => {
74
+ let integrations = await listWorkspaceIntegrations({ cwd: opts.cwd });
75
+ if (integrations.length === 0) {
76
+ throw new Error('No integrations directory was found in the current repository.');
77
+ }
78
+
79
+ for (let integration of integrations) {
80
+ await runVitest({
81
+ cwd: integration.dirPath,
82
+ vitestArgs: opts.vitestArgs,
83
+ env: process.env,
84
+ label: integration.relativeDir
85
+ });
86
+ }
87
+
88
+ return {
89
+ success: true,
90
+ count: integrations.length
91
+ };
92
+ };
@@ -0,0 +1,57 @@
1
+ import {
2
+ chooseTool,
3
+ createClientContext,
4
+ ensureProfileConfig,
5
+ syncProfileMetadata
6
+ } from '../lib/context';
7
+ import { parseJsonObject, promptForObjectSchema } from '../lib/prompts';
8
+ import { JsonInput, WithProfile } from '../lib/types';
9
+
10
+ export let listTools = async (opts: WithProfile) => {
11
+ let { store, profile, client } = await createClientContext(opts);
12
+ let tools = await client.listTools();
13
+ await syncProfileMetadata({ store, profile, client });
14
+ return tools.map(tool => `${tool.name} (${tool.id})`);
15
+ };
16
+
17
+ export let getTool = async (opts: WithProfile & { toolId?: string }) => {
18
+ let { client } = await createClientContext(opts);
19
+ return chooseTool({ client, toolId: opts.toolId });
20
+ };
21
+
22
+ export let callTool = async (
23
+ opts: WithProfile &
24
+ JsonInput & {
25
+ toolId?: string;
26
+ authMethodId?: string;
27
+ }
28
+ ) => {
29
+ let { store, profile, client } = await createClientContext(opts);
30
+ let tool = await chooseTool({ client, toolId: opts.toolId });
31
+
32
+ await ensureProfileConfig({ store, profile, client });
33
+
34
+ let authMethods = (await client.listAuthMethods()).authenticationMethods;
35
+ let storedAuth = store.getAuth(profile.id, opts.authMethodId);
36
+ if (!storedAuth && authMethods.length > 0) {
37
+ throw new Error(
38
+ `No stored authentication found for this profile. Run \`slates ${opts.integration} auth setup\` first.`
39
+ );
40
+ }
41
+
42
+ if (storedAuth) {
43
+ client.setAuth({
44
+ authenticationMethodId: storedAuth.authMethodId,
45
+ output: storedAuth.output
46
+ });
47
+ }
48
+
49
+ let toolInput =
50
+ parseJsonObject(opts.input, 'tool input') ??
51
+ (await promptForObjectSchema(tool.inputSchema, {}));
52
+
53
+ let result = await client.invokeTool(tool.id, toolInput);
54
+ store.setProfileSession(profile.id, client.state.session);
55
+ await store.save();
56
+ return result;
57
+ };
@@ -0,0 +1,192 @@
1
+ import { select } from '@inquirer/prompts';
2
+ import { SlatesProtocolClient } from '@slates/client';
3
+ import {
4
+ createSlatesClientFromProfile,
5
+ openSlatesCliStore,
6
+ SlatesProfileRecord,
7
+ type SlatesCliStore
8
+ } from '@slates/profiles';
9
+ import { resolveIntegration } from './integration';
10
+ import { promptForObjectSchema } from './prompts';
11
+
12
+ export let openIntegrationStore = async (integration: string) => {
13
+ let resolved = await resolveIntegration(integration);
14
+ let store = await openSlatesCliStore({
15
+ cwd: resolved.rootDir,
16
+ scope: {
17
+ key: resolved.relativeDir,
18
+ name: resolved.name
19
+ }
20
+ });
21
+
22
+ return {
23
+ integration: resolved,
24
+ store
25
+ };
26
+ };
27
+
28
+ export let chooseProfile = async (d: {
29
+ integration: string;
30
+ profile?: string;
31
+ message?: string;
32
+ }) => {
33
+ let { integration, store } = await openIntegrationStore(d.integration);
34
+ if (d.profile) {
35
+ return {
36
+ integration,
37
+ store,
38
+ profile: store.requireProfile(d.profile)
39
+ };
40
+ }
41
+
42
+ let profiles = store.listProfiles();
43
+ if (profiles.length === 0) {
44
+ throw new Error(
45
+ `No Slates profiles found for ${integration.name}. Create one with \`slates ${d.integration} setup\`.`
46
+ );
47
+ }
48
+
49
+ let current = store.getProfile();
50
+ if (profiles.length === 1) {
51
+ return {
52
+ integration,
53
+ store,
54
+ profile: profiles[0]!
55
+ };
56
+ }
57
+
58
+ let profileId = await select({
59
+ message: d.message ?? 'Choose a profile',
60
+ default: current?.id,
61
+ choices: profiles.map(profile => ({
62
+ name: `${profile.name} (${profile.id})`,
63
+ value: profile.id
64
+ }))
65
+ });
66
+
67
+ return {
68
+ integration,
69
+ store,
70
+ profile: store.requireProfile(profileId)
71
+ };
72
+ };
73
+
74
+ export let createClientContext = async (opts: { integration: string; profile?: string }) => {
75
+ let { integration, store, profile } = await chooseProfile(opts);
76
+ let client = await createSlatesClientFromProfile(profile, { store });
77
+ return { integration, store, profile, client };
78
+ };
79
+
80
+ export let createIntegrationClientContext = async (opts: { integration: string }) => {
81
+ let { integration, store } = await openIntegrationStore(opts.integration);
82
+ let client = await createSlatesClientFromProfile(
83
+ {
84
+ id: `integration-${integration.name}`,
85
+ name: integration.name,
86
+ target: {
87
+ type: 'local',
88
+ entry: integration.entry,
89
+ exportName: 'provider'
90
+ },
91
+ config: null,
92
+ auth: {},
93
+ session: null,
94
+ metadata: {
95
+ provider: null,
96
+ actions: null
97
+ },
98
+ createdAt: new Date(0).toISOString(),
99
+ updatedAt: new Date(0).toISOString()
100
+ },
101
+ { store }
102
+ );
103
+
104
+ return { integration, store, client };
105
+ };
106
+
107
+ export let syncProfileMetadata = async (d: {
108
+ store: SlatesCliStore;
109
+ profile: SlatesProfileRecord;
110
+ client: SlatesProtocolClient;
111
+ }) => {
112
+ let [provider, actions] = await Promise.all([d.client.identify(), d.client.listActions()]);
113
+ d.store.setProfileMetadata(d.profile.id, {
114
+ provider: provider.provider,
115
+ actions: actions.actions
116
+ });
117
+ await d.store.save();
118
+ };
119
+
120
+ export let ensureProfileConfig = async (d: {
121
+ store: SlatesCliStore;
122
+ profile: SlatesProfileRecord;
123
+ client: SlatesProtocolClient;
124
+ }) => {
125
+ if (d.profile.config) {
126
+ d.client.setConfig(d.profile.config);
127
+ return d.profile.config;
128
+ }
129
+
130
+ let defaultConfig = (await d.client.getDefaultConfig()).config ?? {};
131
+ let schema = (await d.client.getConfigSchema()).schema;
132
+ let config = await promptForObjectSchema(schema, defaultConfig);
133
+ d.store.setProfileConfig(d.profile.id, config);
134
+ await d.store.save();
135
+ d.client.setConfig(config);
136
+ return config;
137
+ };
138
+
139
+ export let chooseTool = async (d: { client: SlatesProtocolClient; toolId?: string }) => {
140
+ if (d.toolId) {
141
+ return d.client.getTool(d.toolId);
142
+ }
143
+
144
+ let tools = await d.client.listTools();
145
+ if (tools.length === 0) {
146
+ throw new Error('This slate does not expose any tools.');
147
+ }
148
+
149
+ let toolId = await select({
150
+ message: 'Choose a tool',
151
+ choices: tools.map(tool => ({
152
+ name: `${tool.name} (${tool.id})`,
153
+ value: tool.id
154
+ }))
155
+ });
156
+
157
+ return d.client.getTool(toolId);
158
+ };
159
+
160
+ export let chooseAuthMethod = async (d: {
161
+ client: SlatesProtocolClient;
162
+ authMethodId?: string;
163
+ forcePrompt?: boolean;
164
+ }) => {
165
+ let methods = (await d.client.listAuthMethods()).authenticationMethods;
166
+ if (methods.length === 0) {
167
+ throw new Error('This slate does not expose any authentication methods.');
168
+ }
169
+
170
+ if (d.authMethodId) {
171
+ let method = methods.find(item => item.id === d.authMethodId);
172
+ if (!method) {
173
+ throw new Error(`Unknown auth method: ${d.authMethodId}`);
174
+ }
175
+
176
+ return method;
177
+ }
178
+
179
+ if (methods.length === 1 && !d.forcePrompt) {
180
+ return methods[0]!;
181
+ }
182
+
183
+ let methodId = await select({
184
+ message: 'Choose an authentication method',
185
+ choices: methods.map(method => ({
186
+ name: `${method.name} (${method.type})`,
187
+ value: method.id
188
+ }))
189
+ });
190
+
191
+ return methods.find(method => method.id === methodId)!;
192
+ };
@@ -0,0 +1,76 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises';
2
+ import { tmpdir } from 'os';
3
+ import path from 'path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import { listWorkspaceIntegrations, resolveIntegration } from './integration';
6
+
7
+ let tempDirs: string[] = [];
8
+
9
+ let createTempDir = async () => {
10
+ let dir = await mkdtemp(path.join(tmpdir(), 'slates-cli-'));
11
+ tempDirs.push(dir);
12
+ return dir;
13
+ };
14
+
15
+ afterEach(async () => {
16
+ await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })));
17
+ });
18
+
19
+ describe('resolveIntegration', () => {
20
+ it('resolves an integration by name from the integrations directory', async () => {
21
+ let cwd = await createTempDir();
22
+ let integrationDir = path.join(cwd, 'integrations', 'demo');
23
+ await mkdir(path.join(integrationDir, 'src'), { recursive: true });
24
+ await writeFile(
25
+ path.join(integrationDir, 'package.json'),
26
+ JSON.stringify({ main: 'src/index.ts' }, null, 2),
27
+ 'utf-8'
28
+ );
29
+ await writeFile(path.join(integrationDir, 'src', 'index.ts'), 'export let provider = {};\n', 'utf-8');
30
+
31
+ let resolved = await resolveIntegration('demo', { cwd });
32
+
33
+ expect(resolved.name).toBe('demo');
34
+ expect(resolved.relativeDir).toBe('integrations/demo');
35
+ expect(resolved.entry).toBe('integrations/demo/src/index.ts');
36
+ });
37
+
38
+ it('resolves an integration from a relative path', async () => {
39
+ let cwd = await createTempDir();
40
+ let integrationDir = path.join(cwd, 'custom', 'demo');
41
+ await mkdir(path.join(integrationDir, 'src'), { recursive: true });
42
+ await writeFile(
43
+ path.join(integrationDir, 'package.json'),
44
+ JSON.stringify({ source: 'src/index.ts' }, null, 2),
45
+ 'utf-8'
46
+ );
47
+ await writeFile(path.join(integrationDir, 'src', 'index.ts'), 'export let provider = {};\n', 'utf-8');
48
+
49
+ let resolved = await resolveIntegration('./custom/demo', { cwd });
50
+
51
+ expect(resolved.name).toBe('demo');
52
+ expect(resolved.relativeDir).toBe('custom/demo');
53
+ expect(resolved.entry).toBe('custom/demo/src/index.ts');
54
+ });
55
+
56
+ it('lists workspace integrations from the integrations directory', async () => {
57
+ let cwd = await createTempDir();
58
+ for (let name of ['beta', 'alpha']) {
59
+ let integrationDir = path.join(cwd, 'integrations', name);
60
+ await mkdir(integrationDir, { recursive: true });
61
+ await writeFile(
62
+ path.join(integrationDir, 'package.json'),
63
+ JSON.stringify({ main: 'src/index.ts' }, null, 2),
64
+ 'utf-8'
65
+ );
66
+ }
67
+
68
+ let integrations = await listWorkspaceIntegrations({ cwd });
69
+
70
+ expect(integrations.map(integration => integration.name)).toEqual(['alpha', 'beta']);
71
+ expect(integrations.map(integration => integration.relativeDir)).toEqual([
72
+ 'integrations/alpha',
73
+ 'integrations/beta'
74
+ ]);
75
+ });
76
+ });