@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.
- package/package.json +37 -0
- package/src/cli.ts +261 -0
- package/src/commands/auth.ts +405 -0
- package/src/commands/config.ts +31 -0
- package/src/commands/index.ts +6 -0
- package/src/commands/profiles.ts +143 -0
- package/src/commands/repl.ts +95 -0
- package/src/commands/test.ts +92 -0
- package/src/commands/tools.ts +57 -0
- package/src/lib/context.ts +192 -0
- package/src/lib/integration.test.ts +76 -0
- package/src/lib/integration.ts +134 -0
- package/src/lib/oauth.ts +136 -0
- package/src/lib/prompts.ts +159 -0
- package/src/lib/types.ts +10 -0
|
@@ -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,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
|
+
});
|