@slates/cli 1.0.0-rc.10

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,95 @@
1
+ import { input } from '@inquirer/prompts';
2
+ import { print } from '../lib/prompts';
3
+ import type { 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,94 @@
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 type { 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
+ rootDir: store.rootDir,
51
+ storePath: store.storePath,
52
+ cliDir: store.dirPath
53
+ },
54
+ null,
55
+ 2
56
+ )}\n`,
57
+ 'utf-8'
58
+ );
59
+
60
+ await runVitest({
61
+ cwd: integration.dirPath,
62
+ vitestArgs: opts.vitestArgs,
63
+ env: {
64
+ ...process.env,
65
+ SLATES_INTEGRATION: integration.relativeDir,
66
+ SLATES_PROFILE_ID: profile.id,
67
+ SLATES_CLI_DIR: store.dirPath,
68
+ SLATES_STORE_ROOT_DIR: store.rootDir,
69
+ SLATES_STORE_PATH: store.storePath,
70
+ SLATES_TEST_CONTEXT_PATH: contextPath
71
+ }
72
+ });
73
+ };
74
+
75
+ export let runAllIntegrationTests = async (opts: { cwd?: string; vitestArgs: string[] }) => {
76
+ let integrations = await listWorkspaceIntegrations({ cwd: opts.cwd });
77
+ if (integrations.length === 0) {
78
+ throw new Error('No integrations directory was found in the current repository.');
79
+ }
80
+
81
+ for (let integration of integrations) {
82
+ await runVitest({
83
+ cwd: integration.dirPath,
84
+ vitestArgs: opts.vitestArgs,
85
+ env: process.env,
86
+ label: integration.relativeDir
87
+ });
88
+ }
89
+
90
+ return {
91
+ success: true,
92
+ count: integrations.length
93
+ };
94
+ };
@@ -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 type { 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,199 @@
1
+ import { select } from '@inquirer/prompts';
2
+ import type { SlatesProtocolClient } from '@slates/client';
3
+ import {
4
+ createSlatesClientFromProfile,
5
+ openSlatesCliStore,
6
+ type SlatesCliStore,
7
+ type SlatesProfileRecord
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: {
75
+ integration: string;
76
+ profile?: string;
77
+ autoRefresh?: boolean;
78
+ }) => {
79
+ let { integration, store, profile } = await chooseProfile(opts);
80
+ let client = await createSlatesClientFromProfile(profile, {
81
+ store,
82
+ autoRefresh: opts.autoRefresh
83
+ });
84
+ return { integration, store, profile, client };
85
+ };
86
+
87
+ export let createIntegrationClientContext = async (opts: { integration: string }) => {
88
+ let { integration, store } = await openIntegrationStore(opts.integration);
89
+ let client = await createSlatesClientFromProfile(
90
+ {
91
+ id: `integration-${integration.name}`,
92
+ name: integration.name,
93
+ target: {
94
+ type: 'local',
95
+ entry: integration.entry,
96
+ exportName: 'provider'
97
+ },
98
+ config: null,
99
+ auth: {},
100
+ session: null,
101
+ metadata: {
102
+ provider: null,
103
+ actions: null
104
+ },
105
+ createdAt: new Date(0).toISOString(),
106
+ updatedAt: new Date(0).toISOString()
107
+ },
108
+ { store }
109
+ );
110
+
111
+ return { integration, store, client };
112
+ };
113
+
114
+ export let syncProfileMetadata = async (d: {
115
+ store: SlatesCliStore;
116
+ profile: SlatesProfileRecord;
117
+ client: SlatesProtocolClient;
118
+ }) => {
119
+ let [provider, actions] = await Promise.all([d.client.identify(), d.client.listActions()]);
120
+ d.store.setProfileMetadata(d.profile.id, {
121
+ provider: provider.provider,
122
+ actions: actions.actions
123
+ });
124
+ await d.store.save();
125
+ };
126
+
127
+ export let ensureProfileConfig = async (d: {
128
+ store: SlatesCliStore;
129
+ profile: SlatesProfileRecord;
130
+ client: SlatesProtocolClient;
131
+ }) => {
132
+ if (d.profile.config) {
133
+ d.client.setConfig(d.profile.config);
134
+ return d.profile.config;
135
+ }
136
+
137
+ let defaultConfig = (await d.client.getDefaultConfig()).config ?? {};
138
+ let schema = (await d.client.getConfigSchema()).schema;
139
+ let config = await promptForObjectSchema(schema, defaultConfig);
140
+ d.store.setProfileConfig(d.profile.id, config);
141
+ await d.store.save();
142
+ d.client.setConfig(config);
143
+ return config;
144
+ };
145
+
146
+ export let chooseTool = async (d: { client: SlatesProtocolClient; toolId?: string }) => {
147
+ if (d.toolId) {
148
+ return d.client.getTool(d.toolId);
149
+ }
150
+
151
+ let tools = await d.client.listTools();
152
+ if (tools.length === 0) {
153
+ throw new Error('This slate does not expose any tools.');
154
+ }
155
+
156
+ let toolId = await select({
157
+ message: 'Choose a tool',
158
+ choices: tools.map(tool => ({
159
+ name: `${tool.name} (${tool.id})`,
160
+ value: tool.id
161
+ }))
162
+ });
163
+
164
+ return d.client.getTool(toolId);
165
+ };
166
+
167
+ export let chooseAuthMethod = async (d: {
168
+ client: SlatesProtocolClient;
169
+ authMethodId?: string;
170
+ forcePrompt?: boolean;
171
+ }) => {
172
+ let methods = (await d.client.listAuthMethods()).authenticationMethods;
173
+ if (methods.length === 0) {
174
+ throw new Error('This slate does not expose any authentication methods.');
175
+ }
176
+
177
+ if (d.authMethodId) {
178
+ let method = methods.find(item => item.id === d.authMethodId);
179
+ if (!method) {
180
+ throw new Error(`Unknown auth method: ${d.authMethodId}`);
181
+ }
182
+
183
+ return method;
184
+ }
185
+
186
+ if (methods.length === 1 && !d.forcePrompt) {
187
+ return methods[0]!;
188
+ }
189
+
190
+ let methodId = await select({
191
+ message: 'Choose an authentication method',
192
+ choices: methods.map(method => ({
193
+ name: `${method.name} (${method.type})`,
194
+ value: method.id
195
+ }))
196
+ });
197
+
198
+ return methods.find(method => method.id === methodId)!;
199
+ };
@@ -0,0 +1,84 @@
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(
30
+ path.join(integrationDir, 'src', 'index.ts'),
31
+ 'export let provider = {};\n',
32
+ 'utf-8'
33
+ );
34
+
35
+ let resolved = await resolveIntegration('demo', { cwd });
36
+
37
+ expect(resolved.name).toBe('demo');
38
+ expect(resolved.relativeDir).toBe('integrations/demo');
39
+ expect(resolved.entry).toBe('integrations/demo/src/index.ts');
40
+ });
41
+
42
+ it('resolves an integration from a relative path', async () => {
43
+ let cwd = await createTempDir();
44
+ let integrationDir = path.join(cwd, 'custom', 'demo');
45
+ await mkdir(path.join(integrationDir, 'src'), { recursive: true });
46
+ await writeFile(
47
+ path.join(integrationDir, 'package.json'),
48
+ JSON.stringify({ source: 'src/index.ts' }, null, 2),
49
+ 'utf-8'
50
+ );
51
+ await writeFile(
52
+ path.join(integrationDir, 'src', 'index.ts'),
53
+ 'export let provider = {};\n',
54
+ 'utf-8'
55
+ );
56
+
57
+ let resolved = await resolveIntegration('./custom/demo', { cwd });
58
+
59
+ expect(resolved.name).toBe('demo');
60
+ expect(resolved.relativeDir).toBe('custom/demo');
61
+ expect(resolved.entry).toBe('custom/demo/src/index.ts');
62
+ });
63
+
64
+ it('lists workspace integrations from the integrations directory', async () => {
65
+ let cwd = await createTempDir();
66
+ for (let name of ['beta', 'alpha']) {
67
+ let integrationDir = path.join(cwd, 'integrations', name);
68
+ await mkdir(integrationDir, { recursive: true });
69
+ await writeFile(
70
+ path.join(integrationDir, 'package.json'),
71
+ JSON.stringify({ main: 'src/index.ts' }, null, 2),
72
+ 'utf-8'
73
+ );
74
+ }
75
+
76
+ let integrations = await listWorkspaceIntegrations({ cwd });
77
+
78
+ expect(integrations.map(integration => integration.name)).toEqual(['alpha', 'beta']);
79
+ expect(integrations.map(integration => integration.relativeDir)).toEqual([
80
+ 'integrations/alpha',
81
+ 'integrations/beta'
82
+ ]);
83
+ });
84
+ });
@@ -0,0 +1,152 @@
1
+ import { resolveSlatesCliRoot } from '@slates/profiles';
2
+ import { access, readdir, readFile } from 'fs/promises';
3
+ import path from 'path';
4
+
5
+ export interface ResolvedIntegration {
6
+ input: string;
7
+ rootDir: string;
8
+ dirPath: string;
9
+ relativeDir: string;
10
+ name: string;
11
+ entry: string;
12
+ }
13
+
14
+ export interface WorkspaceIntegrationSummary {
15
+ rootDir: string;
16
+ dirPath: string;
17
+ relativeDir: string;
18
+ name: string;
19
+ }
20
+
21
+ let toPosixPath = (value: string) => value.replace(/\\/g, '/');
22
+
23
+ let pathExists = async (targetPath: string) => {
24
+ try {
25
+ await access(targetPath);
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ };
31
+
32
+ let isWithinRoot = (rootDir: string, targetPath: string) => {
33
+ let relative = path.relative(rootDir, targetPath);
34
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
35
+ };
36
+
37
+ let resolveIntegrationDir = async (input: string, cwd: string) => {
38
+ let rootDir = resolveSlatesCliRoot(cwd);
39
+ let integrationRoots = [
40
+ path.join(rootDir, 'integrations'),
41
+ path.join(rootDir, 'test-integrations')
42
+ ];
43
+
44
+ if (!input.includes(path.sep) && !input.includes('/')) {
45
+ for (let root of integrationRoots) {
46
+ let namedPath = path.join(root, input);
47
+ if (await pathExists(path.join(namedPath, 'package.json'))) {
48
+ return { rootDir, dirPath: namedPath };
49
+ }
50
+ }
51
+ }
52
+
53
+ let candidate = path.resolve(cwd, input);
54
+ if (!(await pathExists(path.join(candidate, 'package.json')))) {
55
+ throw new Error(
56
+ `Could not resolve integration "${input}". Pass an integration name from \`integrations/\` or \`test-integrations/\`, or a relative path to an integration directory.`
57
+ );
58
+ }
59
+
60
+ if (!isWithinRoot(rootDir, candidate)) {
61
+ throw new Error(`Integration "${input}" must be inside the current repository.`);
62
+ }
63
+
64
+ return { rootDir, dirPath: candidate };
65
+ };
66
+
67
+ let resolveDefaultEntry = async (dirPath: string) => {
68
+ let packageJsonPath = path.join(dirPath, 'package.json');
69
+ let manifest = JSON.parse(await readFile(packageJsonPath, 'utf-8')) as {
70
+ main?: string;
71
+ source?: string;
72
+ };
73
+
74
+ let candidates = [
75
+ manifest.source,
76
+ manifest.main,
77
+ 'src/index.ts',
78
+ 'src/index.js',
79
+ 'index.ts',
80
+ 'index.js'
81
+ ].filter((candidate): candidate is string => Boolean(candidate));
82
+
83
+ for (let candidate of candidates) {
84
+ if (await pathExists(path.join(dirPath, candidate))) {
85
+ return candidate;
86
+ }
87
+ }
88
+
89
+ throw new Error(`Could not determine a default entry file for integration at ${dirPath}.`);
90
+ };
91
+
92
+ export let resolveIntegration = async (
93
+ input: string,
94
+ opts: { cwd?: string } = {}
95
+ ): Promise<ResolvedIntegration> => {
96
+ let cwd = opts.cwd ?? process.cwd();
97
+ let { rootDir, dirPath } = await resolveIntegrationDir(input, cwd);
98
+ let relativeDir = path.relative(rootDir, dirPath);
99
+
100
+ return {
101
+ input,
102
+ rootDir,
103
+ dirPath,
104
+ relativeDir: toPosixPath(relativeDir),
105
+ name: path.basename(dirPath),
106
+ entry: toPosixPath(path.join(relativeDir, await resolveDefaultEntry(dirPath)))
107
+ };
108
+ };
109
+
110
+ export let listWorkspaceIntegrations = async (opts: { cwd?: string } = {}) => {
111
+ let cwd = opts.cwd ?? process.cwd();
112
+ let rootDir = resolveSlatesCliRoot(cwd);
113
+ let integrationRoots = [
114
+ path.join(rootDir, 'integrations'),
115
+ path.join(rootDir, 'test-integrations')
116
+ ];
117
+
118
+ let integrations: WorkspaceIntegrationSummary[] = [];
119
+
120
+ for (let integrationsDir of integrationRoots) {
121
+ if (!(await pathExists(integrationsDir))) {
122
+ continue;
123
+ }
124
+
125
+ let entries = await readdir(integrationsDir, { withFileTypes: true });
126
+ let chunk = await Promise.all(
127
+ entries
128
+ .filter(entry => entry.isDirectory())
129
+ .map(async entry => {
130
+ let dirPath = path.join(integrationsDir, entry.name);
131
+ if (!(await pathExists(path.join(dirPath, 'package.json')))) {
132
+ return null;
133
+ }
134
+
135
+ return {
136
+ rootDir,
137
+ dirPath,
138
+ relativeDir: toPosixPath(path.relative(rootDir, dirPath)),
139
+ name: entry.name
140
+ } satisfies WorkspaceIntegrationSummary;
141
+ })
142
+ );
143
+
144
+ integrations.push(
145
+ ...chunk.filter(
146
+ (integration): integration is WorkspaceIntegrationSummary => integration !== null
147
+ )
148
+ );
149
+ }
150
+
151
+ return integrations.sort((a, b) => a.relativeDir.localeCompare(b.relativeDir));
152
+ };