@slates/cli 1.0.0-rc.2 → 1.0.0-rc.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slates/cli",
3
- "version": "1.0.0-rc.2",
3
+ "version": "1.0.0-rc.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -25,8 +25,9 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@inquirer/prompts": "^7.4.0",
28
- "@slates/client": "1.0.0-rc.2",
29
- "@slates/profiles": "1.0.0-rc.2",
28
+ "@slates/client": "1.0.0-rc.7",
29
+ "@slates/oauth-microsoft": "1.0.0-rc.2",
30
+ "@slates/profiles": "1.0.0-rc.5",
30
31
  "sade": "^1.8.1"
31
32
  },
32
33
  "devDependencies": {
package/src/cli.ts CHANGED
@@ -59,203 +59,233 @@ if (isGlobalTestCommand) {
59
59
 
60
60
  cli.parse([process.argv[0] ?? 'bun', process.argv[1] ?? 'slates', ...argv]);
61
61
  } else {
62
+ cli
63
+ .command('profiles add')
64
+ .option('--name', 'Profile name')
65
+ .option('--entry', 'Local slate entry file')
66
+ .option('--export-name', 'Named export for the local slate provider')
67
+ .option('--default', 'Use this profile as the default')
68
+ .action(opts =>
69
+ printResult(() =>
70
+ addProfile({
71
+ integration: integration!,
72
+ name: opts.name,
73
+ entry: opts.entry,
74
+ exportName: opts.exportName,
75
+ useAsDefault: Boolean(opts.default)
76
+ })
77
+ )
78
+ );
62
79
 
63
- cli
64
- .command('profiles add')
65
- .option('--name', 'Profile name')
66
- .option('--entry', 'Local slate entry file')
67
- .option('--export-name', 'Named export for the local slate provider')
68
- .option('--default', 'Use this profile as the default')
69
- .action(opts =>
70
- printResult(() =>
71
- addProfile({
72
- integration: integration!,
73
- name: opts.name,
74
- entry: opts.entry,
75
- exportName: opts.exportName,
76
- useAsDefault: Boolean(opts.default)
77
- })
78
- )
79
- );
80
+ cli
81
+ .command('profiles list')
82
+ .action(() => printResult(() => listProfiles({ integration: integration! })));
80
83
 
81
- cli
82
- .command('profiles list')
83
- .action(() => printResult(() => listProfiles({ integration: integration! })));
84
+ cli
85
+ .command('profiles get [profile]')
86
+ .action((profile: string | undefined) =>
87
+ printResult(() => getProfile({ integration: integration!, profile }))
88
+ );
84
89
 
85
- cli
86
- .command('profiles get [profile]')
87
- .action((profile: string | undefined) =>
88
- printResult(() => getProfile({ integration: integration!, profile }))
89
- );
90
+ cli
91
+ .command('profiles use [profile]')
92
+ .action((profile: string | undefined) =>
93
+ printResult(() => useProfile({ integration: integration!, profile }))
94
+ );
90
95
 
91
- cli
92
- .command('profiles use [profile]')
93
- .action((profile: string | undefined) =>
94
- printResult(() => useProfile({ integration: integration!, profile }))
95
- );
96
+ cli
97
+ .command('profiles remove [profile]')
98
+ .action((profile: string | undefined) =>
99
+ printResult(() => removeProfile({ integration: integration!, profile }))
100
+ );
96
101
 
97
- cli
98
- .command('profiles remove [profile]')
99
- .action((profile: string | undefined) =>
100
- printResult(() => removeProfile({ integration: integration!, profile }))
101
- );
102
+ cli
103
+ .command('setup')
104
+ .option('--name', 'Profile name')
105
+ .option('--export-name', 'Named export for the local slate provider')
106
+ .action(opts =>
107
+ printResult(() =>
108
+ setupIntegration({
109
+ integration: integration!,
110
+ name: opts.name,
111
+ exportName: opts.exportName
112
+ })
113
+ )
114
+ );
102
115
 
103
- cli
104
- .command('setup')
105
- .option('--name', 'Profile name')
106
- .option('--export-name', 'Named export for the local slate provider')
107
- .action(opts =>
108
- printResult(() =>
109
- setupIntegration({
110
- integration: integration!,
111
- name: opts.name,
112
- exportName: opts.exportName
113
- })
114
- )
115
- );
116
+ cli
117
+ .command('tools list')
118
+ .option('--profile', 'Profile ID or name')
119
+ .action(opts =>
120
+ printResult(() =>
121
+ listTools({
122
+ integration: integration!,
123
+ profile: opts.profile
124
+ })
125
+ )
126
+ );
116
127
 
117
- cli
118
- .command('tools list')
119
- .option('--profile', 'Profile ID or name')
120
- .action(opts => printResult(() => listTools({ integration: integration!, profile: opts.profile })));
128
+ cli
129
+ .command('tools get [toolId]')
130
+ .option('--profile', 'Profile ID or name')
131
+ .action((toolId: string | undefined, opts) =>
132
+ printResult(() =>
133
+ getTool({
134
+ integration: integration!,
135
+ profile: opts.profile,
136
+ toolId
137
+ })
138
+ )
139
+ );
121
140
 
122
- cli
123
- .command('tools get [toolId]')
124
- .option('--profile', 'Profile ID or name')
125
- .action((toolId: string | undefined, opts) =>
126
- printResult(() => getTool({ integration: integration!, profile: opts.profile, toolId }))
127
- );
128
-
129
- cli
130
- .command('tools schema [toolId]')
131
- .option('--profile', 'Profile ID or name')
132
- .action((toolId: string | undefined, opts) =>
133
- printResult(async () => {
134
- let tool = await getTool({ integration: integration!, profile: opts.profile, toolId });
135
- return tool.inputSchema;
136
- })
137
- );
138
-
139
- cli
140
- .command('tools call [toolId]')
141
- .option('--profile', 'Profile ID or name')
142
- .option('--input', 'JSON input object')
143
- .option('--auth-method-id', 'Preferred auth method ID')
144
- .action((toolId: string | undefined, opts) =>
145
- printResult(() =>
146
- callTool({
147
- integration: integration!,
148
- profile: opts.profile,
149
- toolId,
150
- input: opts.input,
151
- authMethodId: opts.authMethodId
141
+ cli
142
+ .command('tools schema [toolId]')
143
+ .option('--profile', 'Profile ID or name')
144
+ .action((toolId: string | undefined, opts) =>
145
+ printResult(async () => {
146
+ let tool = await getTool({
147
+ integration: integration!,
148
+ profile: opts.profile,
149
+ toolId
150
+ });
151
+ return tool.inputSchema;
152
152
  })
153
- )
154
- );
153
+ );
155
154
 
156
- cli
157
- .command('auth list')
158
- .option('--profile', 'Profile ID or name')
159
- .action(opts => printResult(() => listAuth({ integration: integration!, profile: opts.profile })));
155
+ cli
156
+ .command('tools call [toolId]')
157
+ .option('--profile', 'Profile ID or name')
158
+ .option('--input', 'JSON input object')
159
+ .option('--auth-method-id', 'Preferred auth method ID')
160
+ .action((toolId: string | undefined, opts) =>
161
+ printResult(() =>
162
+ callTool({
163
+ integration: integration!,
164
+ profile: opts.profile,
165
+ toolId,
166
+ input: opts.input,
167
+ authMethodId: opts.authMethodId
168
+ })
169
+ )
170
+ );
160
171
 
161
- cli
162
- .command('auth get [authMethodId]')
163
- .option('--profile', 'Profile ID or name')
164
- .action((authMethodId: string | undefined, opts) =>
165
- printResult(() => getAuth({ integration: integration!, profile: opts.profile, authMethodId }))
166
- );
172
+ cli
173
+ .command('auth list')
174
+ .option('--profile', 'Profile ID or name')
175
+ .action(opts =>
176
+ printResult(() => listAuth({ integration: integration!, profile: opts.profile }))
177
+ );
167
178
 
168
- cli
169
- .command('auth setup [authMethodId]')
170
- .option('--profile', 'Profile ID or name')
171
- .option('--input', 'JSON auth input object')
172
- .option('--oauth-credential', 'OAuth credential ID or name')
173
- .option('--client-id', 'OAuth client ID')
174
- .option('--client-secret', 'OAuth client secret')
175
- .option('--scopes', 'Comma-separated OAuth scopes')
176
- .action((authMethodId: string | undefined, opts) =>
177
- printResult(() =>
178
- setupAuth({
179
- integration: integration!,
180
- profile: opts.profile,
181
- authMethodId,
182
- input: opts.input,
183
- oauthCredential: opts.oauthCredential,
184
- clientId: opts.clientId,
185
- clientSecret: opts.clientSecret,
186
- scopes: opts.scopes
187
- })
188
- )
189
- );
179
+ cli
180
+ .command('auth get [authMethodId]')
181
+ .option('--profile', 'Profile ID or name')
182
+ .action((authMethodId: string | undefined, opts) =>
183
+ printResult(() =>
184
+ getAuth({ integration: integration!, profile: opts.profile, authMethodId })
185
+ )
186
+ );
190
187
 
191
- cli
192
- .command('auth credentials list [authMethodId]')
193
- .action((authMethodId: string | undefined) =>
194
- printResult(() => listOAuthCredentials({ integration: integration!, authMethodId }))
195
- );
188
+ cli
189
+ .command('auth setup [authMethodId]')
190
+ .option('--profile', 'Profile ID or name')
191
+ .option('--input', 'JSON auth input object')
192
+ .option('--oauth-credential', 'OAuth credential ID or name')
193
+ .option('--client-id', 'OAuth client ID')
194
+ .option('--client-secret', 'OAuth client secret')
195
+ .option('--scopes', 'Comma-separated OAuth scopes')
196
+ .action((authMethodId: string | undefined, opts) =>
197
+ printResult(() =>
198
+ setupAuth({
199
+ integration: integration!,
200
+ profile: opts.profile,
201
+ authMethodId,
202
+ input: opts.input,
203
+ oauthCredential: opts.oauthCredential,
204
+ clientId: opts.clientId,
205
+ clientSecret: opts.clientSecret,
206
+ scopes: opts.scopes
207
+ })
208
+ )
209
+ );
196
210
 
197
- cli
198
- .command('auth credentials add [authMethodId]')
199
- .option('--name', 'Credential name')
200
- .option('--client-id', 'OAuth client ID')
201
- .option('--client-secret', 'OAuth client secret')
202
- .action((authMethodId: string | undefined, opts) =>
203
- printResult(() =>
204
- addOAuthCredentials({
205
- integration: integration!,
206
- authMethodId,
207
- name: opts.name,
208
- clientId: opts.clientId,
209
- clientSecret: opts.clientSecret
210
- })
211
- )
212
- );
211
+ cli
212
+ .command('auth credentials list [authMethodId]')
213
+ .action((authMethodId: string | undefined) =>
214
+ printResult(() => listOAuthCredentials({ integration: integration!, authMethodId }))
215
+ );
213
216
 
214
- cli
215
- .command('auth refresh [authMethodId]')
216
- .option('--profile', 'Profile ID or name')
217
- .action((authMethodId: string | undefined, opts) =>
218
- printResult(() => refreshAuth({ integration: integration!, profile: opts.profile, authMethodId }))
219
- );
217
+ cli
218
+ .command('auth credentials add [authMethodId]')
219
+ .option('--name', 'Credential name')
220
+ .option('--client-id', 'OAuth client ID')
221
+ .option('--client-secret', 'OAuth client secret')
222
+ .action((authMethodId: string | undefined, opts) =>
223
+ printResult(() =>
224
+ addOAuthCredentials({
225
+ integration: integration!,
226
+ authMethodId,
227
+ name: opts.name,
228
+ clientId: opts.clientId,
229
+ clientSecret: opts.clientSecret
230
+ })
231
+ )
232
+ );
220
233
 
221
- cli
222
- .command('config get')
223
- .option('--profile', 'Profile ID or name')
224
- .action(opts => printResult(() => getConfig({ integration: integration!, profile: opts.profile })));
234
+ cli
235
+ .command('auth refresh [authMethodId]')
236
+ .option('--profile', 'Profile ID or name')
237
+ .action((authMethodId: string | undefined, opts) =>
238
+ printResult(() =>
239
+ refreshAuth({ integration: integration!, profile: opts.profile, authMethodId })
240
+ )
241
+ );
225
242
 
226
- cli
227
- .command('config set')
228
- .option('--profile', 'Profile ID or name')
229
- .option('--input', 'JSON config object')
230
- .action(opts =>
231
- printResult(() => setConfig({ integration: integration!, profile: opts.profile, input: opts.input }))
232
- );
243
+ cli
244
+ .command('config get')
245
+ .option('--profile', 'Profile ID or name')
246
+ .action(opts =>
247
+ printResult(() => getConfig({ integration: integration!, profile: opts.profile }))
248
+ );
233
249
 
234
- cli
235
- .command('config schema')
236
- .option('--profile', 'Profile ID or name')
237
- .action(opts => printResult(() => getConfigSchema({ integration: integration!, profile: opts.profile })));
250
+ cli
251
+ .command('config set')
252
+ .option('--profile', 'Profile ID or name')
253
+ .option('--input', 'JSON config object')
254
+ .action(opts =>
255
+ printResult(() =>
256
+ setConfig({ integration: integration!, profile: opts.profile, input: opts.input })
257
+ )
258
+ );
238
259
 
239
- cli
240
- .command('test')
241
- .option('--profile', 'Profile ID or name')
242
- .action(opts =>
243
- printResult(async () => {
244
- let separatorIndex = process.argv.indexOf('--');
245
- await runVitestWithProfile({
246
- integration: integration!,
247
- profile: opts.profile,
248
- vitestArgs: separatorIndex === -1 ? [] : process.argv.slice(separatorIndex + 1)
249
- });
260
+ cli
261
+ .command('config schema')
262
+ .option('--profile', 'Profile ID or name')
263
+ .action(opts =>
264
+ printResult(() => getConfigSchema({ integration: integration!, profile: opts.profile }))
265
+ );
250
266
 
251
- return { success: true };
252
- })
253
- );
267
+ cli
268
+ .command('test')
269
+ .option('--profile', 'Profile ID or name')
270
+ .action(opts =>
271
+ printResult(async () => {
272
+ let separatorIndex = process.argv.indexOf('--');
273
+ await runVitestWithProfile({
274
+ integration: integration!,
275
+ profile: opts.profile,
276
+ vitestArgs: separatorIndex === -1 ? [] : process.argv.slice(separatorIndex + 1)
277
+ });
278
+
279
+ return { success: true };
280
+ })
281
+ );
254
282
 
255
- cli
256
- .command('repl')
257
- .option('--profile', 'Profile ID or name')
258
- .action(opts => printResult(() => startRepl({ integration: integration!, profile: opts.profile })));
283
+ cli
284
+ .command('repl')
285
+ .option('--profile', 'Profile ID or name')
286
+ .action(opts =>
287
+ printResult(() => startRepl({ integration: integration!, profile: opts.profile }))
288
+ );
259
289
 
260
290
  cli.parse([process.argv[0] ?? 'bun', process.argv[1] ?? 'slates', ...argv.slice(1)]);
261
291
  }
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { normalizeCallbackRedirectUriForIntegration } from './auth';
3
+
4
+ describe('normalizeCallbackRedirectUriForIntegration', () => {
5
+ it('normalizes Notion loopback redirects to localhost', () => {
6
+ expect(
7
+ normalizeCallbackRedirectUriForIntegration('notion', 'http://127.0.0.1:45873/callback')
8
+ ).toBe('http://localhost:45873/callback');
9
+ });
10
+
11
+ it('leaves unrelated integration redirects unchanged', () => {
12
+ expect(
13
+ normalizeCallbackRedirectUriForIntegration('attio', 'http://127.0.0.1:45873/callback')
14
+ ).toBe('http://127.0.0.1:45873/callback');
15
+ });
16
+
17
+ it('normalizes HubSpot developer platform OAuth redirects to localhost', () => {
18
+ expect(
19
+ normalizeCallbackRedirectUriForIntegration(
20
+ 'hubspot',
21
+ 'http://127.0.0.1:45873/callback',
22
+ 'developer_platform_oauth'
23
+ )
24
+ ).toBe('http://localhost:45873/callback');
25
+ });
26
+
27
+ it('leaves HubSpot legacy OAuth redirects unchanged', () => {
28
+ expect(
29
+ normalizeCallbackRedirectUriForIntegration(
30
+ 'hubspot',
31
+ 'http://127.0.0.1:45873/callback',
32
+ 'oauth'
33
+ )
34
+ ).toBe('http://127.0.0.1:45873/callback');
35
+ });
36
+ });
@@ -1,4 +1,8 @@
1
1
  import { confirm, select } from '@inquirer/prompts';
2
+ import {
3
+ normalizeMicrosoftRedirectUri,
4
+ normalizeMicrosoftRedirectUriForIntegration
5
+ } from '@slates/oauth-microsoft';
2
6
  import { SlatesOAuthCredentialRecord, SlatesStoredAuth } from '@slates/profiles';
3
7
  import {
4
8
  chooseAuthMethod,
@@ -6,7 +10,7 @@ import {
6
10
  createIntegrationClientContext,
7
11
  openIntegrationStore
8
12
  } from '../lib/context';
9
- import { chooseScopes, createOAuthCallbackListener, openBrowser } from '../lib/oauth';
13
+ import { chooseScopes, createOAuthCallbackListener, printBrowserUrl } from '../lib/oauth';
10
14
  import {
11
15
  parseJsonObject,
12
16
  parseList,
@@ -16,6 +20,14 @@ import {
16
20
  import { JsonInput, WithProfile } from '../lib/types';
17
21
 
18
22
  type JsonObject = Record<string, any>;
23
+ let NOTION_INTEGRATION_KEY = 'notion';
24
+ let SALESFORCE_INTEGRATION_KEY = 'salesforce';
25
+ let HUBSPOT_INTEGRATION_KEY = 'hubspot';
26
+ let HUBSPOT_DEVELOPER_PLATFORM_OAUTH_METHOD_ID = 'developer_platform_oauth';
27
+ let LOOPBACK_REDIRECT_NORMALIZED_INTEGRATIONS = new Set([
28
+ NOTION_INTEGRATION_KEY,
29
+ SALESFORCE_INTEGRATION_KEY
30
+ ]);
19
31
 
20
32
  type AuthSetupOptions = WithProfile &
21
33
  JsonInput & {
@@ -26,6 +38,22 @@ type AuthSetupOptions = WithProfile &
26
38
  scopes?: string;
27
39
  };
28
40
 
41
+ export let normalizeCallbackRedirectUriForIntegration = (
42
+ integration: string,
43
+ redirectUri: string,
44
+ authMethodId?: string
45
+ ) => {
46
+ if (
47
+ LOOPBACK_REDIRECT_NORMALIZED_INTEGRATIONS.has(integration) ||
48
+ (integration === HUBSPOT_INTEGRATION_KEY &&
49
+ authMethodId === HUBSPOT_DEVELOPER_PLATFORM_OAUTH_METHOD_ID)
50
+ ) {
51
+ return normalizeMicrosoftRedirectUri(redirectUri);
52
+ }
53
+
54
+ return normalizeMicrosoftRedirectUriForIntegration(integration, redirectUri);
55
+ };
56
+
29
57
  export let listAuth = async (opts: WithProfile) => {
30
58
  let { store, profile } = await createClientContext(opts);
31
59
  return store.listAuth(profile.id);
@@ -231,7 +259,11 @@ let chooseOAuthCredentialsForSetup = async (opts: {
231
259
  };
232
260
 
233
261
  let runAuthSetup = async (opts: AuthSetupOptions): Promise<SlatesStoredAuth> => {
234
- let { store, profile, client } = await createClientContext(opts);
262
+ let { store, profile, client } = await createClientContext({
263
+ ...opts,
264
+ autoRefresh: false
265
+ });
266
+ client.clearAuth();
235
267
  let authMethod = await chooseAuthMethod({
236
268
  client,
237
269
  authMethodId: opts.authMethodId,
@@ -263,7 +295,12 @@ let runAuthSetup = async (opts: AuthSetupOptions): Promise<SlatesStoredAuth> =>
263
295
 
264
296
  if (authMethod.type === 'auth.oauth') {
265
297
  let callback = await createOAuthCallbackListener();
266
- console.log(`OAuth redirect URL: ${callback.redirectUri}`);
298
+ let redirectUri = normalizeCallbackRedirectUriForIntegration(
299
+ opts.integration,
300
+ callback.redirectUri,
301
+ authMethod.id
302
+ );
303
+ console.log(`OAuth redirect URL: ${redirectUri}`);
267
304
 
268
305
  let resolvedOAuthCredentials = await chooseOAuthCredentialsForSetup({
269
306
  store,
@@ -282,7 +319,7 @@ let runAuthSetup = async (opts: AuthSetupOptions): Promise<SlatesStoredAuth> =>
282
319
 
283
320
  let authorizationUrl = await client.getAuthorizationUrl({
284
321
  authenticationMethodId: authMethod.id,
285
- redirectUri: callback.redirectUri,
322
+ redirectUri,
286
323
  state: callback.state,
287
324
  input: authInput,
288
325
  clientId,
@@ -293,7 +330,7 @@ let runAuthSetup = async (opts: AuthSetupOptions): Promise<SlatesStoredAuth> =>
293
330
  callbackState = authorizationUrl.callbackState ?? null;
294
331
  finalInput = authorizationUrl.input ?? authInput;
295
332
 
296
- await openBrowser(authorizationUrl.authorizationUrl);
333
+ printBrowserUrl(authorizationUrl.authorizationUrl);
297
334
  let callbackResult = await callback.wait();
298
335
  if (callbackResult.state !== callback.state) {
299
336
  throw new Error('OAuth state mismatch.');
@@ -303,7 +340,7 @@ let runAuthSetup = async (opts: AuthSetupOptions): Promise<SlatesStoredAuth> =>
303
340
  authenticationMethodId: authMethod.id,
304
341
  code: callbackResult.code,
305
342
  state: callbackResult.state,
306
- redirectUri: callback.redirectUri,
343
+ redirectUri,
307
344
  input: finalInput,
308
345
  clientId,
309
346
  clientSecret,
@@ -47,6 +47,7 @@ export let runVitestWithProfile = async (opts: WithProfile & { vitestArgs: strin
47
47
  {
48
48
  integration: integration.relativeDir,
49
49
  profileId: profile.id,
50
+ rootDir: store.rootDir,
50
51
  storePath: store.storePath,
51
52
  cliDir: store.dirPath
52
53
  },
@@ -64,6 +65,7 @@ export let runVitestWithProfile = async (opts: WithProfile & { vitestArgs: strin
64
65
  SLATES_INTEGRATION: integration.relativeDir,
65
66
  SLATES_PROFILE_ID: profile.id,
66
67
  SLATES_CLI_DIR: store.dirPath,
68
+ SLATES_STORE_ROOT_DIR: store.rootDir,
67
69
  SLATES_STORE_PATH: store.storePath,
68
70
  SLATES_TEST_CONTEXT_PATH: contextPath
69
71
  }
@@ -71,9 +71,16 @@ export let chooseProfile = async (d: {
71
71
  };
72
72
  };
73
73
 
74
- export let createClientContext = async (opts: { integration: string; profile?: string }) => {
74
+ export let createClientContext = async (opts: {
75
+ integration: string;
76
+ profile?: string;
77
+ autoRefresh?: boolean;
78
+ }) => {
75
79
  let { integration, store, profile } = await chooseProfile(opts);
76
- let client = await createSlatesClientFromProfile(profile, { store });
80
+ let client = await createSlatesClientFromProfile(profile, {
81
+ store,
82
+ autoRefresh: opts.autoRefresh
83
+ });
77
84
  return { integration, store, profile, client };
78
85
  };
79
86
 
@@ -36,17 +36,24 @@ let isWithinRoot = (rootDir: string, targetPath: string) => {
36
36
 
37
37
  let resolveIntegrationDir = async (input: string, cwd: string) => {
38
38
  let rootDir = resolveSlatesCliRoot(cwd);
39
- let namedPath = path.join(rootDir, 'integrations', input);
39
+ let integrationRoots = [
40
+ path.join(rootDir, 'integrations'),
41
+ path.join(rootDir, 'test-integrations')
42
+ ];
43
+
40
44
  if (!input.includes(path.sep) && !input.includes('/')) {
41
- if (await pathExists(path.join(namedPath, 'package.json'))) {
42
- return { rootDir, dirPath: namedPath };
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
+ }
43
50
  }
44
51
  }
45
52
 
46
53
  let candidate = path.resolve(cwd, input);
47
54
  if (!(await pathExists(path.join(candidate, 'package.json')))) {
48
55
  throw new Error(
49
- `Could not resolve integration "${input}". Pass an integration name from \`integrations/\` or a relative path to an integration directory.`
56
+ `Could not resolve integration "${input}". Pass an integration name from \`integrations/\` or \`test-integrations/\`, or a relative path to an integration directory.`
50
57
  );
51
58
  }
52
59
 
@@ -103,32 +110,41 @@ export let resolveIntegration = async (
103
110
  export let listWorkspaceIntegrations = async (opts: { cwd?: string } = {}) => {
104
111
  let cwd = opts.cwd ?? process.cwd();
105
112
  let rootDir = resolveSlatesCliRoot(cwd);
106
- let integrationsDir = path.join(rootDir, 'integrations');
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
+ }
107
124
 
108
- if (!(await pathExists(integrationsDir))) {
109
- return [];
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((integration): integration is WorkspaceIntegrationSummary => integration !== null)
146
+ );
110
147
  }
111
148
 
112
- let entries = await readdir(integrationsDir, { withFileTypes: true });
113
- let integrations = await Promise.all(
114
- entries
115
- .filter(entry => entry.isDirectory())
116
- .map(async entry => {
117
- let dirPath = path.join(integrationsDir, entry.name);
118
- if (!(await pathExists(path.join(dirPath, 'package.json')))) {
119
- return null;
120
- }
121
-
122
- return {
123
- rootDir,
124
- dirPath,
125
- relativeDir: toPosixPath(path.relative(rootDir, dirPath)),
126
- name: entry.name
127
- } satisfies WorkspaceIntegrationSummary;
128
- })
129
- );
130
-
131
- return integrations
132
- .filter((integration): integration is WorkspaceIntegrationSummary => integration !== null)
133
- .sort((a, b) => a.name.localeCompare(b.name));
149
+ return integrations.sort((a, b) => a.relativeDir.localeCompare(b.relativeDir));
134
150
  };
package/src/lib/oauth.ts CHANGED
@@ -1,10 +1,7 @@
1
1
  import { checkbox } from '@inquirer/prompts';
2
- import { execFile } from 'child_process';
3
2
  import { randomUUID } from 'crypto';
4
3
  import { createServer } from 'http';
5
- import { promisify } from 'util';
6
4
 
7
- let execFileAsync = promisify(execFile);
8
5
  let DEFAULT_OAUTH_CALLBACK_PORT = 45873;
9
6
 
10
7
  export let chooseScopes = async (
@@ -21,27 +18,16 @@ export let chooseScopes = async (
21
18
  choices: authMethod.scopes.map((scope: any) => ({
22
19
  name: `${scope.title} (${scope.id})`,
23
20
  value: scope.id,
24
- checked: initialScopes.length > 0 ? initialScopes.includes(scope.id) : true
21
+ checked:
22
+ initialScopes.length > 0
23
+ ? initialScopes.includes(scope.id)
24
+ : (scope.defaultChecked ?? true)
25
25
  }))
26
26
  })) as string[];
27
27
  };
28
28
 
29
- export let openBrowser = async (url: string) => {
30
- try {
31
- if (process.platform === 'darwin') {
32
- await execFileAsync('open', [url]);
33
- return;
34
- }
35
-
36
- if (process.platform === 'win32') {
37
- await execFileAsync('cmd', ['/c', 'start', '', url]);
38
- return;
39
- }
40
-
41
- await execFileAsync('xdg-open', [url]);
42
- } catch {
43
- console.log(`Open this URL in your browser:\n${url}`);
44
- }
29
+ export let printBrowserUrl = (url: string) => {
30
+ console.log(`Open this URL in your browser:\n${url}`);
45
31
  };
46
32
 
47
33
  export let createOAuthCallbackListener = async () => {
@@ -59,10 +45,34 @@ export let createOAuthCallbackListener = async () => {
59
45
  let url = new URL(req.url ?? '/', 'http://127.0.0.1');
60
46
  let code = url.searchParams.get('code');
61
47
  let state = url.searchParams.get('state');
48
+ let oauthError = url.searchParams.get('error');
49
+ let oauthErrorDescription = url.searchParams.get('error_description');
50
+ let oauthErrorUri = url.searchParams.get('error_uri');
51
+
52
+ if (oauthError) {
53
+ let description = oauthErrorDescription ?? 'No error description was provided.';
54
+ let errorMessage = `OAuth callback returned "${oauthError}": ${description}${
55
+ oauthErrorUri ? ` (${oauthErrorUri})` : ''
56
+ }`;
57
+
58
+ res.statusCode = 400;
59
+ res.end(errorMessage);
60
+ server.close();
61
+ settled = true;
62
+ waiter.reject(new Error(errorMessage));
63
+ return;
64
+ }
62
65
 
63
66
  if (!code || !state) {
64
67
  res.statusCode = 400;
65
68
  res.end('Missing code or state.');
69
+ server.close();
70
+ settled = true;
71
+ waiter.reject(
72
+ new Error(
73
+ `OAuth callback did not include the required query parameters. Received path: ${url.pathname}${url.search}`
74
+ )
75
+ );
66
76
  return;
67
77
  }
68
78