@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 +4 -3
- package/src/cli.ts +206 -176
- package/src/commands/auth.test.ts +36 -0
- package/src/commands/auth.ts +43 -6
- package/src/commands/test.ts +2 -0
- package/src/lib/context.ts +9 -2
- package/src/lib/integration.ts +45 -29
- package/src/lib/oauth.ts +30 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slates/cli",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
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.
|
|
29
|
-
"@slates/
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
+
cli
|
|
85
|
+
.command('profiles get [profile]')
|
|
86
|
+
.action((profile: string | undefined) =>
|
|
87
|
+
printResult(() => getProfile({ integration: integration!, profile }))
|
|
88
|
+
);
|
|
84
89
|
|
|
85
|
-
cli
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
+
});
|
package/src/commands/auth.ts
CHANGED
|
@@ -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,
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
343
|
+
redirectUri,
|
|
307
344
|
input: finalInput,
|
|
308
345
|
clientId,
|
|
309
346
|
clientSecret,
|
package/src/commands/test.ts
CHANGED
|
@@ -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
|
}
|
package/src/lib/context.ts
CHANGED
|
@@ -71,9 +71,16 @@ export let chooseProfile = async (d: {
|
|
|
71
71
|
};
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
-
export let createClientContext = async (opts: {
|
|
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, {
|
|
80
|
+
let client = await createSlatesClientFromProfile(profile, {
|
|
81
|
+
store,
|
|
82
|
+
autoRefresh: opts.autoRefresh
|
|
83
|
+
});
|
|
77
84
|
return { integration, store, profile, client };
|
|
78
85
|
};
|
|
79
86
|
|
package/src/lib/integration.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
42
|
-
|
|
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
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
30
|
-
|
|
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
|
|