instant-cli 1.0.23 → 1.0.24-branch-codex-cli-args-combinators.25405829034.1
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/.turbo/turbo-build.log +1 -1
- package/__tests__/args.test.ts +358 -0
- package/__tests__/authClientAddApple.test.ts +9 -10
- package/__tests__/authClientAddClerk.test.ts +6 -5
- package/__tests__/authClientAddGithub.test.ts +6 -5
- package/__tests__/authClientAddGoogle.test.ts +5 -4
- package/__tests__/authClientAddLinkedin.test.ts +6 -5
- package/__tests__/oauthMock.ts +8 -9
- package/dist/commands/auth/client/add.d.ts.map +1 -1
- package/dist/commands/auth/client/add.js +53 -167
- package/dist/commands/auth/client/add.js.map +1 -1
- package/dist/commands/auth/client/shared.d.ts +0 -4
- package/dist/commands/auth/client/shared.d.ts.map +1 -1
- package/dist/commands/auth/client/shared.js +0 -4
- package/dist/commands/auth/client/shared.js.map +1 -1
- package/dist/commands/auth/client/update.d.ts +1 -1
- package/dist/commands/auth/client/update.d.ts.map +1 -1
- package/dist/commands/auth/client/update.js +25 -112
- package/dist/commands/auth/client/update.js.map +1 -1
- package/dist/commands/auth/origin/add.d.ts.map +1 -1
- package/dist/commands/auth/origin/add.js +37 -59
- package/dist/commands/auth/origin/add.js.map +1 -1
- package/dist/lib/args.d.ts +212 -0
- package/dist/lib/args.d.ts.map +1 -0
- package/dist/lib/args.js +313 -0
- package/dist/lib/args.js.map +1 -0
- package/dist/lib/oauth.d.ts +2 -2
- package/dist/lib/oauth.d.ts.map +1 -1
- package/dist/lib/oauth.js +13 -17
- package/dist/lib/oauth.js.map +1 -1
- package/dist/lib/ui.d.ts +0 -18
- package/dist/lib/ui.d.ts.map +1 -1
- package/dist/lib/ui.js +0 -76
- package/dist/lib/ui.js.map +1 -1
- package/package.json +4 -4
- package/src/commands/auth/client/add.ts +114 -180
- package/src/commands/auth/client/shared.ts +0 -12
- package/src/commands/auth/client/update.ts +85 -139
- package/src/commands/auth/origin/add.ts +25 -36
- package/src/lib/args.ts +453 -0
- package/src/lib/oauth.ts +8 -14
- package/src/lib/ui.ts +0 -127
- package/__tests__/optOrPrompt.test.ts +0 -229
package/src/lib/args.ts
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Args lets commands read CLI flags as a small pipeline:
|
|
3
|
+
*
|
|
4
|
+
* 1. Parse a value from opts
|
|
5
|
+
* 2. Gate it: when is this flag available?
|
|
6
|
+
* 3. Prompt for a missing value, when interactive
|
|
7
|
+
* 4. Validate and finish as required or optional
|
|
8
|
+
*
|
|
9
|
+
* This keeps the "can this flag be used here?" logic next to the value it
|
|
10
|
+
* controls. For example, a Google client secret is only meaningful when using
|
|
11
|
+
* custom web credentials:
|
|
12
|
+
*
|
|
13
|
+
* const clientSecret = yield* Args.text(opts, 'client-secret').pipe(
|
|
14
|
+
* Args.availableWhen(usesCustomWebCredentials),
|
|
15
|
+
* Args.prompt(clientSecretPrompt({ providerUrl })),
|
|
16
|
+
* Args.required(),
|
|
17
|
+
* );
|
|
18
|
+
*
|
|
19
|
+
* The key argument to text/bool/has is the exact opts lookup key. If the
|
|
20
|
+
* user-facing flag name differs, pass simpleName for errors:
|
|
21
|
+
*
|
|
22
|
+
* Args.text(opts, 'customRedirectUri', { simpleName: '--custom-redirect-uri' })
|
|
23
|
+
*/
|
|
24
|
+
import { Effect } from 'effect';
|
|
25
|
+
import { pipeArguments, type Pipeable } from 'effect/Pipeable';
|
|
26
|
+
import { BadArgsError } from '../errors.ts';
|
|
27
|
+
import { GlobalOpts } from '../context/globalOpts.ts';
|
|
28
|
+
import { UI } from '../ui/index.ts';
|
|
29
|
+
import { runUIEffect } from './ui.ts';
|
|
30
|
+
|
|
31
|
+
type ActiveArg<A, E, R> = {
|
|
32
|
+
readonly _tag: 'Active';
|
|
33
|
+
readonly provided: boolean;
|
|
34
|
+
readonly value: Effect.Effect<A | undefined, E, R>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type InactiveArg = {
|
|
38
|
+
readonly _tag: 'Inactive';
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type ArgState<A, E, R, CanBeInactive extends boolean> =
|
|
42
|
+
| ActiveArg<A, E, R>
|
|
43
|
+
| (CanBeInactive extends true ? InactiveArg : never);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A parsed CLI arg that can be composed with Args helpers.
|
|
47
|
+
*
|
|
48
|
+
* Commands usually create one with text() or bool(), then finish with
|
|
49
|
+
* required() or optional().
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* const name = yield* Args.text(opts, 'name').pipe(
|
|
53
|
+
* Args.prompt({ prompt: 'Client Name:' }),
|
|
54
|
+
* Args.required(),
|
|
55
|
+
* );
|
|
56
|
+
*/
|
|
57
|
+
export interface Arg<
|
|
58
|
+
A,
|
|
59
|
+
E = never,
|
|
60
|
+
R = never,
|
|
61
|
+
CanBeInactive extends boolean = false,
|
|
62
|
+
> extends Pipeable {
|
|
63
|
+
readonly flag: string;
|
|
64
|
+
readonly state: Effect.Effect<ArgState<A, E, R, CanBeInactive>, E, R>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type ArgOptions = {
|
|
68
|
+
/**
|
|
69
|
+
* Use when the opts key is not the user-facing flag name.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* Args.text(opts, 'customRedirectUri', {
|
|
73
|
+
* simpleName: '--custom-redirect-uri',
|
|
74
|
+
* })
|
|
75
|
+
*/
|
|
76
|
+
simpleName?: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const makeArg = <A, E, R, CanBeInactive extends boolean>(
|
|
80
|
+
flag: string,
|
|
81
|
+
state: Effect.Effect<ArgState<A, E, R, CanBeInactive>, E, R>,
|
|
82
|
+
): Arg<A, E, R, CanBeInactive> => {
|
|
83
|
+
const arg: Arg<A, E, R, CanBeInactive> = {
|
|
84
|
+
flag,
|
|
85
|
+
state,
|
|
86
|
+
pipe() {
|
|
87
|
+
return pipeArguments(this, arguments);
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
return arg;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const active = <A, E, R>(
|
|
94
|
+
value: Effect.Effect<A | undefined, E, R>,
|
|
95
|
+
provided: boolean,
|
|
96
|
+
): ActiveArg<A, E, R> => ({ _tag: 'Active', provided, value });
|
|
97
|
+
|
|
98
|
+
const inactive: InactiveArg = { _tag: 'Inactive' };
|
|
99
|
+
|
|
100
|
+
const missingMessage = (flag: string) => `Missing required value for ${flag}`;
|
|
101
|
+
|
|
102
|
+
type MissingOptions = {
|
|
103
|
+
message?: string;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
type UnavailableOptions = {
|
|
107
|
+
message?: string;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @example
|
|
112
|
+
* if (Args.has(opts, 'custom-redirect-uri')) {
|
|
113
|
+
* // The user explicitly passed --custom-redirect-uri.
|
|
114
|
+
* }
|
|
115
|
+
*/
|
|
116
|
+
function has(opts: Record<string, unknown>, key: string) {
|
|
117
|
+
return Object.prototype.hasOwnProperty.call(opts, key);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @example
|
|
122
|
+
* const configureWeb = Args.hasAny(opts, [
|
|
123
|
+
* 'team-id',
|
|
124
|
+
* 'key-id',
|
|
125
|
+
* 'private-key-file',
|
|
126
|
+
* ]);
|
|
127
|
+
*/
|
|
128
|
+
function hasAny(opts: Record<string, unknown>, keys: string[]) {
|
|
129
|
+
return keys.some((key) => has(opts, key));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Returns true only for boolean true or the string "true".
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* const useDevCredentials = Args.isTrue(opts, 'dev-credentials');
|
|
137
|
+
*/
|
|
138
|
+
function isTrue(opts: Record<string, unknown>, key: string) {
|
|
139
|
+
const value = opts[key];
|
|
140
|
+
return value === true || value === 'true';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Starts a string arg pipeline. Strings are trimmed, numbers are stringified,
|
|
145
|
+
* empty values become missing values, and other types produce BadArgsError.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* const clientId = yield* Args.text(opts, 'client-id').pipe(
|
|
149
|
+
* Args.prompt(clientIdPrompt({ providerUrl })),
|
|
150
|
+
* Args.required(),
|
|
151
|
+
* );
|
|
152
|
+
*/
|
|
153
|
+
function text(
|
|
154
|
+
opts: Record<string, unknown>,
|
|
155
|
+
key: string,
|
|
156
|
+
options?: ArgOptions,
|
|
157
|
+
): Arg<string, BadArgsError> {
|
|
158
|
+
const flag = options?.simpleName ?? `--${key}`;
|
|
159
|
+
const provided = Object.prototype.hasOwnProperty.call(opts, key);
|
|
160
|
+
|
|
161
|
+
return makeArg(
|
|
162
|
+
flag,
|
|
163
|
+
Effect.succeed(active(readTextValue(opts[key], flag), provided)),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function readTextValue(value: unknown, flag: string) {
|
|
168
|
+
return Effect.gen(function* readTextValue() {
|
|
169
|
+
if (value === undefined || value === null) return undefined;
|
|
170
|
+
|
|
171
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
172
|
+
const trimmed = String(value).trim();
|
|
173
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return yield* BadArgsError.make({
|
|
177
|
+
message: `Invalid value for ${flag}`,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Starts a boolean arg pipeline. Accepts booleans and the strings "true" or
|
|
184
|
+
* "false"; other supplied values produce BadArgsError.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* const configureWeb = yield* Args.bool(opts, 'configure-web').pipe(
|
|
188
|
+
* Args.confirm({ promptText: 'Configure web redirect flow?' }),
|
|
189
|
+
* Args.required(),
|
|
190
|
+
* );
|
|
191
|
+
*/
|
|
192
|
+
function bool(
|
|
193
|
+
opts: Record<string, unknown>,
|
|
194
|
+
key: string,
|
|
195
|
+
options?: ArgOptions,
|
|
196
|
+
): Arg<boolean, BadArgsError> {
|
|
197
|
+
const flag = options?.simpleName ?? `--${key}`;
|
|
198
|
+
const provided = Object.prototype.hasOwnProperty.call(opts, key);
|
|
199
|
+
|
|
200
|
+
return makeArg(
|
|
201
|
+
flag,
|
|
202
|
+
Effect.succeed(active(readBooleanValue(opts[key], flag), provided)),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function readBooleanValue(value: unknown, flag: string) {
|
|
207
|
+
return Effect.gen(function* readBooleanValue() {
|
|
208
|
+
if (value === undefined || value === null) return undefined;
|
|
209
|
+
if (value === true || value === 'true') return true;
|
|
210
|
+
if (value === false || value === 'false') return false;
|
|
211
|
+
|
|
212
|
+
return yield* BadArgsError.make({
|
|
213
|
+
message: `Invalid value for ${flag}`,
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function uiErrorToBadArgs(e: { message: string }) {
|
|
219
|
+
return BadArgsError.make({ message: `UI error: ${e.message}` });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function mapActive<A, B, E, R, E2, R2, CanBeInactive extends boolean>(
|
|
223
|
+
wrapper: Arg<A, E, R, CanBeInactive>,
|
|
224
|
+
mapValue: (
|
|
225
|
+
value: Effect.Effect<A | undefined, E, R>,
|
|
226
|
+
) => Effect.Effect<B | undefined, E | E2, R | R2>,
|
|
227
|
+
): Arg<B, E | E2, R | R2, CanBeInactive> {
|
|
228
|
+
return makeArg(
|
|
229
|
+
wrapper.flag,
|
|
230
|
+
Effect.gen(function* mapArgState() {
|
|
231
|
+
const state = yield* wrapper.state;
|
|
232
|
+
if (state._tag === 'Inactive') return inactive;
|
|
233
|
+
return active(mapValue(state.value), state.provided);
|
|
234
|
+
}) as Effect.Effect<
|
|
235
|
+
ArgState<B, E | E2, R | R2, CanBeInactive>,
|
|
236
|
+
E | E2,
|
|
237
|
+
R | R2
|
|
238
|
+
>,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Gates an arg behind a mode or earlier choice.
|
|
244
|
+
*
|
|
245
|
+
* If the condition is false and the user supplied the flag, this returns an
|
|
246
|
+
* error.
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* const clientSecret = yield* Args.text(opts, 'client-secret').pipe(
|
|
250
|
+
* Args.availableWhen(appType === 'web'),
|
|
251
|
+
* Args.prompt(clientSecretPrompt({ providerUrl })),
|
|
252
|
+
* Args.required(),
|
|
253
|
+
* );
|
|
254
|
+
*/
|
|
255
|
+
function availableWhen(condition: boolean, options?: UnavailableOptions) {
|
|
256
|
+
return function availableWhenArg<A, E, R, CanBeInactive extends boolean>(
|
|
257
|
+
arg: Arg<A, E, R, CanBeInactive>,
|
|
258
|
+
) {
|
|
259
|
+
return makeArg<A, E | BadArgsError, R, true>(
|
|
260
|
+
arg.flag,
|
|
261
|
+
Effect.gen(function* gateArgAvailability() {
|
|
262
|
+
const state = yield* arg.state;
|
|
263
|
+
if (condition || state._tag === 'Inactive') return state;
|
|
264
|
+
if (state.provided) {
|
|
265
|
+
return yield* BadArgsError.make({
|
|
266
|
+
message:
|
|
267
|
+
options?.message ??
|
|
268
|
+
`${arg.flag} is not compatible with other options`,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return inactive;
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Prompts for a missing text value.
|
|
279
|
+
*
|
|
280
|
+
* With --yes, the prompt is skipped. If inputProps.defaultValue is set, that
|
|
281
|
+
* value is used. Otherwise the value stays missing.
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* const clientName = yield* Args.text(opts, 'name').pipe(
|
|
285
|
+
* Args.prompt({
|
|
286
|
+
* prompt: 'Client Name:',
|
|
287
|
+
* defaultValue: suggestedClientName,
|
|
288
|
+
* }),
|
|
289
|
+
* Args.required(),
|
|
290
|
+
* );
|
|
291
|
+
*/
|
|
292
|
+
function prompt(inputProps: UI.TextInputProps) {
|
|
293
|
+
return function promptArg<E, R, CanBeInactive extends boolean>(
|
|
294
|
+
arg: Arg<string, E, R, CanBeInactive>,
|
|
295
|
+
): Arg<string, E | BadArgsError, R | GlobalOpts, CanBeInactive> {
|
|
296
|
+
return mapActive(arg, (valueEffect) =>
|
|
297
|
+
Effect.gen(function* promptWhenMissing() {
|
|
298
|
+
const value = yield* valueEffect;
|
|
299
|
+
if (value !== undefined) return value;
|
|
300
|
+
|
|
301
|
+
const { yes } = yield* GlobalOpts;
|
|
302
|
+
const result = yes
|
|
303
|
+
? inputProps.defaultValue
|
|
304
|
+
: yield* runUIEffect(new UI.TextInput(inputProps)).pipe(
|
|
305
|
+
Effect.catchTag('UIError', uiErrorToBadArgs),
|
|
306
|
+
);
|
|
307
|
+
if (result === undefined) return undefined;
|
|
308
|
+
|
|
309
|
+
const trimmed = result.trim();
|
|
310
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
311
|
+
}),
|
|
312
|
+
);
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Prompts for a missing boolean value.
|
|
318
|
+
*
|
|
319
|
+
* With --yes, the prompt is skipped and confirmationProps.defaultValue is used.
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* const configureWeb = yield* Args.bool(opts, 'configure-web').pipe(
|
|
323
|
+
* Args.confirm({
|
|
324
|
+
* promptText: 'Configure web redirect flow?',
|
|
325
|
+
* defaultValue: false,
|
|
326
|
+
* }),
|
|
327
|
+
* Args.required(),
|
|
328
|
+
* );
|
|
329
|
+
*/
|
|
330
|
+
function confirm(confirmationProps: UI.ConfirmationProps) {
|
|
331
|
+
return function confirmArg<E, R, CanBeInactive extends boolean>(
|
|
332
|
+
arg: Arg<boolean, E, R, CanBeInactive>,
|
|
333
|
+
): Arg<boolean, E | BadArgsError, R | GlobalOpts, CanBeInactive> {
|
|
334
|
+
return mapActive(arg, (valueEffect) =>
|
|
335
|
+
Effect.gen(function* confirmWhenMissing() {
|
|
336
|
+
const value = yield* valueEffect;
|
|
337
|
+
if (value !== undefined) return value;
|
|
338
|
+
|
|
339
|
+
const { yes } = yield* GlobalOpts;
|
|
340
|
+
if (yes) return confirmationProps.defaultValue;
|
|
341
|
+
|
|
342
|
+
return yield* runUIEffect(new UI.Confirmation(confirmationProps)).pipe(
|
|
343
|
+
Effect.catchTag('UIError', uiErrorToBadArgs),
|
|
344
|
+
);
|
|
345
|
+
}),
|
|
346
|
+
);
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Validates a present value. Return an error message to fail, or undefined to
|
|
352
|
+
* accept the value. Missing and inactive args pass through unchanged.
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* const projectId = yield* Args.text(opts, 'project-id').pipe(
|
|
356
|
+
* Args.prompt(firebaseProjectIdPrompt({})),
|
|
357
|
+
* Args.validate(validateFirebaseProjectId),
|
|
358
|
+
* Args.required(),
|
|
359
|
+
* );
|
|
360
|
+
*/
|
|
361
|
+
function validate<A>(validator: (value: A) => string | undefined) {
|
|
362
|
+
return function validateArg<E, R, CanBeInactive extends boolean>(
|
|
363
|
+
arg: Arg<A, E, R, CanBeInactive>,
|
|
364
|
+
): Arg<A, E | BadArgsError, R, CanBeInactive> {
|
|
365
|
+
return mapActive(arg, (valueEffect) =>
|
|
366
|
+
Effect.gen(function* validateValue() {
|
|
367
|
+
const value = yield* valueEffect;
|
|
368
|
+
if (value === undefined) return undefined;
|
|
369
|
+
|
|
370
|
+
const message = validator(value);
|
|
371
|
+
if (!message) return value;
|
|
372
|
+
|
|
373
|
+
return yield* BadArgsError.make({ message });
|
|
374
|
+
}),
|
|
375
|
+
);
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Finishes a pipeline with a required value.
|
|
381
|
+
*
|
|
382
|
+
* Missing active args produce BadArgsError. Inactive args return undefined, so
|
|
383
|
+
* callers can skip downstream work for unavailable flags.
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* const clientId = yield* Args.text(opts, 'client-id').pipe(
|
|
387
|
+
* Args.prompt(clientIdPrompt({ providerUrl })),
|
|
388
|
+
* Args.required(),
|
|
389
|
+
* );
|
|
390
|
+
*/
|
|
391
|
+
function required(options?: MissingOptions) {
|
|
392
|
+
return function requiredArg<A, E, R, CanBeInactive extends boolean>(
|
|
393
|
+
arg: Arg<A, E, R, CanBeInactive>,
|
|
394
|
+
): Effect.Effect<
|
|
395
|
+
CanBeInactive extends true ? A | undefined : A,
|
|
396
|
+
E | BadArgsError,
|
|
397
|
+
R
|
|
398
|
+
> {
|
|
399
|
+
return Effect.gen(function* requireValue() {
|
|
400
|
+
const state = yield* arg.state;
|
|
401
|
+
if (state._tag === 'Inactive') return undefined;
|
|
402
|
+
|
|
403
|
+
const value = yield* state.value;
|
|
404
|
+
if (value !== undefined) return value;
|
|
405
|
+
|
|
406
|
+
return yield* BadArgsError.make({
|
|
407
|
+
message: options?.message ?? missingMessage(arg.flag),
|
|
408
|
+
});
|
|
409
|
+
}) as Effect.Effect<
|
|
410
|
+
CanBeInactive extends true ? A | undefined : A,
|
|
411
|
+
E | BadArgsError,
|
|
412
|
+
R
|
|
413
|
+
>;
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Finishes a pipeline with an optional value.
|
|
419
|
+
*
|
|
420
|
+
* Missing and inactive args return undefined.
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* const customRedirectUri = yield* Args.text(opts, 'custom-redirect-uri').pipe(
|
|
424
|
+
* Args.prompt(optionalRedirectPrompt),
|
|
425
|
+
* Args.optional(),
|
|
426
|
+
* );
|
|
427
|
+
*/
|
|
428
|
+
function optional() {
|
|
429
|
+
return function optionalArg<A, E, R, CanBeInactive extends boolean>(
|
|
430
|
+
arg: Arg<A, E, R, CanBeInactive>,
|
|
431
|
+
) {
|
|
432
|
+
return Effect.gen(function* optionalValue() {
|
|
433
|
+
const state = yield* arg.state;
|
|
434
|
+
if (state._tag === 'Inactive') return undefined;
|
|
435
|
+
|
|
436
|
+
return yield* state.value;
|
|
437
|
+
});
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export const Args = {
|
|
442
|
+
text,
|
|
443
|
+
bool,
|
|
444
|
+
has,
|
|
445
|
+
hasAny,
|
|
446
|
+
isTrue,
|
|
447
|
+
availableWhen,
|
|
448
|
+
prompt,
|
|
449
|
+
confirm,
|
|
450
|
+
validate,
|
|
451
|
+
required,
|
|
452
|
+
optional,
|
|
453
|
+
};
|
package/src/lib/oauth.ts
CHANGED
|
@@ -3,16 +3,12 @@ import { Effect, Schema } from 'effect';
|
|
|
3
3
|
import { CurrentApp } from '../context/currentApp.ts';
|
|
4
4
|
import { InstantHttpAuthed, withCommand } from './http.ts';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
-
import {
|
|
7
|
-
optOrPrompt,
|
|
8
|
-
runUIEffect,
|
|
9
|
-
stripFirstBlankLine,
|
|
10
|
-
validateRequired,
|
|
11
|
-
} from './ui.ts';
|
|
6
|
+
import { runUIEffect, stripFirstBlankLine, validateRequired } from './ui.ts';
|
|
12
7
|
import { UI } from '../ui/index.ts';
|
|
13
8
|
import { BadArgsError } from '../errors.ts';
|
|
14
9
|
import { link } from '../logging.ts';
|
|
15
10
|
import type { ClientTypeSchema } from '../commands/auth/client/add.ts';
|
|
11
|
+
import { Args } from './args.ts';
|
|
16
12
|
|
|
17
13
|
export const AuthorizedOriginService = Schema.Literal(
|
|
18
14
|
'generic',
|
|
@@ -263,11 +259,8 @@ export const getClientNameAndProvider = Effect.fn(function* (
|
|
|
263
259
|
);
|
|
264
260
|
const suggestedClientName = findName(providerType, usedClientNames);
|
|
265
261
|
|
|
266
|
-
const clientName = yield*
|
|
267
|
-
|
|
268
|
-
required: true,
|
|
269
|
-
skipIf: false,
|
|
270
|
-
prompt: {
|
|
262
|
+
const clientName = yield* Args.text(opts, 'name').pipe(
|
|
263
|
+
Args.prompt({
|
|
271
264
|
prompt: 'Client Name:',
|
|
272
265
|
defaultValue: suggestedClientName,
|
|
273
266
|
placeholder: suggestedClientName,
|
|
@@ -276,10 +269,11 @@ export const getClientNameAndProvider = Effect.fn(function* (
|
|
|
276
269
|
UI.modifiers.topPadding,
|
|
277
270
|
UI.modifiers.dimOnComplete,
|
|
278
271
|
]),
|
|
279
|
-
},
|
|
280
|
-
|
|
272
|
+
}),
|
|
273
|
+
Args.required(),
|
|
274
|
+
);
|
|
281
275
|
|
|
282
|
-
if (usedClientNames.has(clientName
|
|
276
|
+
if (usedClientNames.has(clientName)) {
|
|
283
277
|
return yield* BadArgsError.make({
|
|
284
278
|
message: `The unique name '${clientName}' is already in use.`,
|
|
285
279
|
});
|
package/src/lib/ui.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Data, Effect } from 'effect';
|
|
2
|
-
import { BadArgsError } from '../errors.ts';
|
|
3
2
|
import { GlobalOpts } from '../context/globalOpts.ts';
|
|
4
3
|
import { Prompt, renderUnwrap } from '../ui/lib.ts';
|
|
5
4
|
import { UI } from '../ui/index.ts';
|
|
@@ -37,25 +36,6 @@ export const runUIEffect = <P>(prompt: Prompt<P>) =>
|
|
|
37
36
|
}),
|
|
38
37
|
});
|
|
39
38
|
|
|
40
|
-
export const invalidFlagError = (flag: string, message: string) =>
|
|
41
|
-
BadArgsError.make({ message: `Invalid ${flag}: ${message}` });
|
|
42
|
-
|
|
43
|
-
export const getOptionalStringFlag = Effect.fn(function* (
|
|
44
|
-
value: unknown,
|
|
45
|
-
flag: string,
|
|
46
|
-
) {
|
|
47
|
-
if (value === undefined || value === null || value === false) {
|
|
48
|
-
return undefined;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (typeof value !== 'string') {
|
|
52
|
-
return yield* invalidFlagError(flag, 'expected a string value');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const trimmed = value.trim();
|
|
56
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
57
|
-
});
|
|
58
|
-
|
|
59
39
|
export const stripFirstBlankLine = (str: string): string => {
|
|
60
40
|
const lines = str.split('\n');
|
|
61
41
|
const firstBlankIndex = lines.findIndex((line) => line.trim() === '');
|
|
@@ -64,112 +44,5 @@ export const stripFirstBlankLine = (str: string): string => {
|
|
|
64
44
|
return lines.join('\n');
|
|
65
45
|
};
|
|
66
46
|
|
|
67
|
-
const coerceValue = (value: unknown, simpleName: string) =>
|
|
68
|
-
Effect.gen(function* () {
|
|
69
|
-
if (value === undefined || value === null) return undefined;
|
|
70
|
-
if (typeof value === 'string' || typeof value === 'number') {
|
|
71
|
-
return String(value).trim();
|
|
72
|
-
}
|
|
73
|
-
return yield* BadArgsError.make({
|
|
74
|
-
message: `Invalid value for ${simpleName}`,
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const resolveOrPrompt = (prompt: UI.TextInputProps, yes: boolean) =>
|
|
79
|
-
Effect.gen(function* () {
|
|
80
|
-
if (yes) return undefined;
|
|
81
|
-
const result = yield* runUIEffect(new UI.TextInput(prompt));
|
|
82
|
-
return result.trim() || undefined;
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const requireOrReturn = (
|
|
86
|
-
resolved: string | undefined,
|
|
87
|
-
params: {
|
|
88
|
-
required: boolean;
|
|
89
|
-
simpleName: string;
|
|
90
|
-
customMissingMessage?: string;
|
|
91
|
-
},
|
|
92
|
-
) =>
|
|
93
|
-
Effect.gen(function* () {
|
|
94
|
-
if (resolved) return resolved;
|
|
95
|
-
if (params.required) {
|
|
96
|
-
return yield* BadArgsError.make({
|
|
97
|
-
message:
|
|
98
|
-
params.customMissingMessage ??
|
|
99
|
-
`Missing required value for ${params.simpleName}`,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
return undefined;
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
export const optOrPrompt = (
|
|
106
|
-
value: unknown,
|
|
107
|
-
params: {
|
|
108
|
-
required: boolean;
|
|
109
|
-
skipMessage?: string;
|
|
110
|
-
customMissingMessage?: string;
|
|
111
|
-
simpleName: string;
|
|
112
|
-
skipIf: boolean;
|
|
113
|
-
prompt: UI.TextInputProps;
|
|
114
|
-
},
|
|
115
|
-
) =>
|
|
116
|
-
Effect.gen(function* () {
|
|
117
|
-
if (params.skipIf) {
|
|
118
|
-
if (value) {
|
|
119
|
-
return yield* BadArgsError.make({
|
|
120
|
-
message:
|
|
121
|
-
params.skipMessage ??
|
|
122
|
-
`${params.simpleName} is not compatible with other options`,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
return undefined;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const { yes } = yield* GlobalOpts;
|
|
129
|
-
const coerced = yield* coerceValue(value, params.simpleName);
|
|
130
|
-
const resolved = coerced ?? (yield* resolveOrPrompt(params.prompt, yes));
|
|
131
|
-
return yield* requireOrReturn(resolved, params);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
47
|
export const validateRequired = (input: string) =>
|
|
135
48
|
input.trim().length > 0 ? undefined : 'Value is required';
|
|
136
|
-
|
|
137
|
-
export const optOrPromptBoolean = (
|
|
138
|
-
value: unknown,
|
|
139
|
-
params: {
|
|
140
|
-
prompt: ConstructorParameters<typeof UI.Confirmation>[0];
|
|
141
|
-
required: boolean;
|
|
142
|
-
skipMessage?: string;
|
|
143
|
-
simpleName: string;
|
|
144
|
-
skipIf: boolean;
|
|
145
|
-
},
|
|
146
|
-
) =>
|
|
147
|
-
Effect.gen(function* () {
|
|
148
|
-
if (params.skipIf && value !== undefined) {
|
|
149
|
-
return yield* BadArgsError.make({
|
|
150
|
-
message:
|
|
151
|
-
params.skipMessage ??
|
|
152
|
-
`${params.simpleName} is not compatible with other options`,
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
if (params.skipIf) {
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
const { yes } = yield* GlobalOpts;
|
|
159
|
-
|
|
160
|
-
if (yes) {
|
|
161
|
-
return Boolean(value);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (value === true) {
|
|
165
|
-
return value;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const response = yield* runUIEffect(
|
|
169
|
-
new UI.Confirmation({
|
|
170
|
-
...params.prompt,
|
|
171
|
-
}),
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
return response;
|
|
175
|
-
});
|