effect-start 0.9.0
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/LICENSE +21 -0
- package/README.md +109 -0
- package/package.json +57 -0
- package/src/Bundle.ts +167 -0
- package/src/BundleFiles.ts +174 -0
- package/src/BundleHttp.test.ts +160 -0
- package/src/BundleHttp.ts +259 -0
- package/src/Commander.test.ts +1378 -0
- package/src/Commander.ts +672 -0
- package/src/Datastar.test.ts +267 -0
- package/src/Datastar.ts +68 -0
- package/src/Effect_HttpRouter.test.ts +570 -0
- package/src/EncryptedCookies.test.ts +427 -0
- package/src/EncryptedCookies.ts +451 -0
- package/src/FileHttpRouter.test.ts +207 -0
- package/src/FileHttpRouter.ts +122 -0
- package/src/FileRouter.ts +405 -0
- package/src/FileRouterCodegen.test.ts +598 -0
- package/src/FileRouterCodegen.ts +251 -0
- package/src/FileRouter_files.test.ts +64 -0
- package/src/FileRouter_path.test.ts +132 -0
- package/src/FileRouter_tree.test.ts +126 -0
- package/src/FileSystemExtra.ts +102 -0
- package/src/HttpAppExtra.ts +127 -0
- package/src/Hyper.ts +194 -0
- package/src/HyperHtml.test.ts +90 -0
- package/src/HyperHtml.ts +139 -0
- package/src/HyperNode.ts +37 -0
- package/src/JsModule.test.ts +14 -0
- package/src/JsModule.ts +116 -0
- package/src/PublicDirectory.test.ts +280 -0
- package/src/PublicDirectory.ts +108 -0
- package/src/Route.test.ts +873 -0
- package/src/Route.ts +992 -0
- package/src/Router.ts +80 -0
- package/src/SseHttpResponse.ts +55 -0
- package/src/Start.ts +133 -0
- package/src/StartApp.ts +43 -0
- package/src/StartHttp.ts +42 -0
- package/src/StreamExtra.ts +146 -0
- package/src/TestHttpClient.test.ts +54 -0
- package/src/TestHttpClient.ts +100 -0
- package/src/bun/BunBundle.test.ts +277 -0
- package/src/bun/BunBundle.ts +309 -0
- package/src/bun/BunBundle_imports.test.ts +50 -0
- package/src/bun/BunFullstackServer.ts +45 -0
- package/src/bun/BunFullstackServer_httpServer.ts +541 -0
- package/src/bun/BunImportTrackerPlugin.test.ts +77 -0
- package/src/bun/BunImportTrackerPlugin.ts +97 -0
- package/src/bun/BunTailwindPlugin.test.ts +335 -0
- package/src/bun/BunTailwindPlugin.ts +322 -0
- package/src/bun/BunVirtualFilesPlugin.ts +59 -0
- package/src/bun/index.ts +4 -0
- package/src/client/Overlay.ts +34 -0
- package/src/client/ScrollState.ts +120 -0
- package/src/client/index.ts +101 -0
- package/src/index.ts +24 -0
- package/src/jsx-datastar.d.ts +63 -0
- package/src/jsx-runtime.ts +23 -0
- package/src/jsx.d.ts +4402 -0
- package/src/testing.ts +55 -0
- package/src/x/cloudflare/CloudflareTunnel.ts +110 -0
- package/src/x/cloudflare/index.ts +1 -0
- package/src/x/datastar/Datastar.test.ts +267 -0
- package/src/x/datastar/Datastar.ts +68 -0
- package/src/x/datastar/index.ts +4 -0
- package/src/x/datastar/jsx-datastar.d.ts +63 -0
package/src/Commander.ts
ADDED
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
import * as Data from "effect/Data"
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import * as Pipeable from "effect/Pipeable"
|
|
4
|
+
import * as Predicate from "effect/Predicate"
|
|
5
|
+
import * as Schema from "effect/Schema"
|
|
6
|
+
|
|
7
|
+
export class CommanderError extends Data.TaggedError("CommanderError")<{
|
|
8
|
+
message: string
|
|
9
|
+
cause?: unknown
|
|
10
|
+
}> {}
|
|
11
|
+
|
|
12
|
+
const TypeId: unique symbol = Symbol.for("effect-start/Commander")
|
|
13
|
+
|
|
14
|
+
type KebabToCamel<S extends string> = S extends `${infer First}-${infer Rest}`
|
|
15
|
+
? `${First}${KebabToCamel<Capitalize<Rest>>}`
|
|
16
|
+
: S
|
|
17
|
+
|
|
18
|
+
type StripPrefix<S extends string> = S extends `--${infer Name}` ? Name
|
|
19
|
+
: S extends `-${infer Name}` ? Name
|
|
20
|
+
: S
|
|
21
|
+
|
|
22
|
+
type OptionNameToCamelCase<S extends string> = KebabToCamel<StripPrefix<S>>
|
|
23
|
+
|
|
24
|
+
export interface OptionBuilder<A = any, Name extends string = string> {
|
|
25
|
+
readonly _tag: "OptionBuilder"
|
|
26
|
+
readonly name: Name
|
|
27
|
+
readonly long: Name
|
|
28
|
+
readonly short?: string
|
|
29
|
+
readonly description: string
|
|
30
|
+
readonly schema?: Schema.Schema<A, any>
|
|
31
|
+
readonly defaultValue?: A
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type OptionBuilderWithSchema<A, Name extends string> =
|
|
35
|
+
& Omit<
|
|
36
|
+
OptionBuilder<A, Name>,
|
|
37
|
+
"schema" | "defaultValue"
|
|
38
|
+
>
|
|
39
|
+
& {
|
|
40
|
+
readonly schema: Schema.Schema<A, any>
|
|
41
|
+
readonly defaultValue?: A
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const option = <
|
|
45
|
+
const Long extends `--${string}`,
|
|
46
|
+
const Short extends `-${string}` | undefined = undefined,
|
|
47
|
+
>(
|
|
48
|
+
long: Long,
|
|
49
|
+
short?: Short,
|
|
50
|
+
): {
|
|
51
|
+
// Using exact `string` would reject schemas with literal encoded types,
|
|
52
|
+
// like in case of Schema.BooleanFromString.
|
|
53
|
+
schema<A, I extends string = string>(
|
|
54
|
+
schema: Schema.Schema<A, I>,
|
|
55
|
+
): OptionBuilderWithSchema<A, Long>
|
|
56
|
+
|
|
57
|
+
description(
|
|
58
|
+
desc: string,
|
|
59
|
+
): {
|
|
60
|
+
schema<A, I extends string = string>(
|
|
61
|
+
schema: Schema.Schema<A, I>,
|
|
62
|
+
): OptionBuilderWithSchema<A, Long>
|
|
63
|
+
|
|
64
|
+
default<A>(
|
|
65
|
+
value: A,
|
|
66
|
+
): {
|
|
67
|
+
schema<A2 extends A, I extends string = string>(
|
|
68
|
+
schema: Schema.Schema<A2, I>,
|
|
69
|
+
): OptionBuilderWithSchema<A2, Long>
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
default<A>(
|
|
74
|
+
value: A,
|
|
75
|
+
): {
|
|
76
|
+
schema<A2 extends A, I extends string = string>(
|
|
77
|
+
schema: Schema.Schema<A2, I>,
|
|
78
|
+
): OptionBuilderWithSchema<A2, Long>
|
|
79
|
+
|
|
80
|
+
description(
|
|
81
|
+
desc: string,
|
|
82
|
+
): {
|
|
83
|
+
schema<A2 extends A, I extends string = string>(
|
|
84
|
+
schema: Schema.Schema<A2, I>,
|
|
85
|
+
): OptionBuilderWithSchema<A2, Long>
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} => {
|
|
89
|
+
const longName = long
|
|
90
|
+
const shortName = short ? short.slice(1) : undefined
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
schema: <A, I extends string = string>(schema: Schema.Schema<A, I>) => ({
|
|
94
|
+
_tag: "OptionBuilder" as const,
|
|
95
|
+
name: longName,
|
|
96
|
+
long: longName,
|
|
97
|
+
short: shortName,
|
|
98
|
+
description: "",
|
|
99
|
+
schema,
|
|
100
|
+
}),
|
|
101
|
+
|
|
102
|
+
description: (desc) => ({
|
|
103
|
+
schema: <A, I extends string = string>(schema: Schema.Schema<A, I>) => ({
|
|
104
|
+
_tag: "OptionBuilder" as const,
|
|
105
|
+
name: longName,
|
|
106
|
+
long: longName,
|
|
107
|
+
short: shortName,
|
|
108
|
+
description: desc,
|
|
109
|
+
schema,
|
|
110
|
+
}),
|
|
111
|
+
|
|
112
|
+
default: <A>(value: A) => ({
|
|
113
|
+
schema: <A2 extends A, I extends string = string>(
|
|
114
|
+
schema: Schema.Schema<A2, I>,
|
|
115
|
+
) => ({
|
|
116
|
+
_tag: "OptionBuilder" as const,
|
|
117
|
+
name: longName,
|
|
118
|
+
long: longName,
|
|
119
|
+
short: shortName,
|
|
120
|
+
description: desc,
|
|
121
|
+
schema,
|
|
122
|
+
defaultValue: value as A2,
|
|
123
|
+
}),
|
|
124
|
+
}),
|
|
125
|
+
}),
|
|
126
|
+
|
|
127
|
+
default: <A>(value: A) => ({
|
|
128
|
+
schema: <A2 extends A, I extends string = string>(
|
|
129
|
+
schema: Schema.Schema<A2, I>,
|
|
130
|
+
) => ({
|
|
131
|
+
_tag: "OptionBuilder" as const,
|
|
132
|
+
name: longName,
|
|
133
|
+
long: longName,
|
|
134
|
+
short: shortName,
|
|
135
|
+
description: "",
|
|
136
|
+
schema,
|
|
137
|
+
defaultValue: value as A2,
|
|
138
|
+
}),
|
|
139
|
+
|
|
140
|
+
description: (desc) => ({
|
|
141
|
+
schema: <A2 extends A, I extends string = string>(
|
|
142
|
+
schema: Schema.Schema<A2, I>,
|
|
143
|
+
) => ({
|
|
144
|
+
_tag: "OptionBuilder" as const,
|
|
145
|
+
name: longName,
|
|
146
|
+
long: longName,
|
|
147
|
+
short: shortName,
|
|
148
|
+
description: desc,
|
|
149
|
+
schema,
|
|
150
|
+
defaultValue: value as A2,
|
|
151
|
+
}),
|
|
152
|
+
}),
|
|
153
|
+
}),
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export type OptionsMap = Record<string, OptionBuilder<any, any>>
|
|
158
|
+
|
|
159
|
+
type ExtractOptionsFromBuilders<Opts extends OptionsMap> = {
|
|
160
|
+
[
|
|
161
|
+
K in keyof Opts as Opts[K] extends OptionBuilder<any, infer Name>
|
|
162
|
+
? OptionNameToCamelCase<Name>
|
|
163
|
+
: never
|
|
164
|
+
]: Opts[K] extends OptionBuilder<infer A, any> ? A : never
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export type ExtractOptionValues<Opts extends OptionsMap> =
|
|
168
|
+
ExtractOptionsFromBuilders<Opts>
|
|
169
|
+
|
|
170
|
+
export interface SubcommandDef<Handled extends boolean = boolean> {
|
|
171
|
+
readonly _tag: "SubcommandDef"
|
|
172
|
+
readonly command: CommanderSet<any, any, Handled>
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
type CommanderBuilder = {
|
|
176
|
+
option: typeof optionMethod
|
|
177
|
+
optionHelp: typeof optionHelp
|
|
178
|
+
optionVersion: typeof optionVersion
|
|
179
|
+
subcommand: typeof subcommand
|
|
180
|
+
handle: typeof handle
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export type CommanderSet<
|
|
184
|
+
Opts extends OptionsMap = {},
|
|
185
|
+
Subcommands extends ReadonlyArray<SubcommandDef> = [],
|
|
186
|
+
Handled extends boolean = false,
|
|
187
|
+
> =
|
|
188
|
+
& Pipeable.Pipeable
|
|
189
|
+
& CommanderSet.Instance<Opts, Subcommands, Handled>
|
|
190
|
+
& {
|
|
191
|
+
[TypeId]: typeof TypeId
|
|
192
|
+
}
|
|
193
|
+
& CommanderBuilder
|
|
194
|
+
|
|
195
|
+
export namespace CommanderSet {
|
|
196
|
+
export type Instance<
|
|
197
|
+
Opts extends OptionsMap = {},
|
|
198
|
+
Subcommands extends ReadonlyArray<SubcommandDef> = [],
|
|
199
|
+
Handled extends boolean = false,
|
|
200
|
+
> = {
|
|
201
|
+
readonly name: string
|
|
202
|
+
readonly description?: string
|
|
203
|
+
readonly version?: string
|
|
204
|
+
readonly options: Opts
|
|
205
|
+
readonly subcommands: Subcommands
|
|
206
|
+
readonly handler?: Handled extends true
|
|
207
|
+
? (args: ExtractOptionValues<Opts>) => Effect.Effect<void>
|
|
208
|
+
: never
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export type Default = CommanderSet<{}, [], false>
|
|
212
|
+
|
|
213
|
+
export type Proto = {
|
|
214
|
+
[TypeId]: typeof TypeId
|
|
215
|
+
pipe(): any
|
|
216
|
+
} & CommanderBuilder
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const optionMethod = function<
|
|
220
|
+
S,
|
|
221
|
+
Opt extends OptionBuilder<any, any>,
|
|
222
|
+
>(
|
|
223
|
+
this: S,
|
|
224
|
+
opt: Opt,
|
|
225
|
+
): S extends CommanderSet<infer Opts, infer Subs, infer _H> ? CommanderSet<
|
|
226
|
+
& Opts
|
|
227
|
+
& {
|
|
228
|
+
[K in Opt["name"] as OptionNameToCamelCase<K>]: Opt
|
|
229
|
+
},
|
|
230
|
+
Subs,
|
|
231
|
+
false
|
|
232
|
+
>
|
|
233
|
+
: CommanderSet<
|
|
234
|
+
{
|
|
235
|
+
[K in Opt["name"] as OptionNameToCamelCase<K>]: Opt
|
|
236
|
+
},
|
|
237
|
+
[],
|
|
238
|
+
false
|
|
239
|
+
>
|
|
240
|
+
{
|
|
241
|
+
const base = this && typeof this === "object" ? this as any : {}
|
|
242
|
+
const baseName = base.name || ""
|
|
243
|
+
const baseOptions: OptionsMap = base.options || {}
|
|
244
|
+
const baseSubcommands = base.subcommands || []
|
|
245
|
+
const baseDescription = base.description
|
|
246
|
+
const baseVersion = base.version
|
|
247
|
+
|
|
248
|
+
const camelKey = kebabToCamel(stripPrefix(opt.long))
|
|
249
|
+
|
|
250
|
+
return makeSet({
|
|
251
|
+
name: baseName,
|
|
252
|
+
description: baseDescription,
|
|
253
|
+
version: baseVersion,
|
|
254
|
+
options: { ...baseOptions, [camelKey]: opt },
|
|
255
|
+
subcommands: baseSubcommands,
|
|
256
|
+
}) as any
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export const optionHelp = function<S>(
|
|
260
|
+
this: S,
|
|
261
|
+
): S extends CommanderSet<infer Opts, infer Subs, infer _H> ? CommanderSet<
|
|
262
|
+
& Opts
|
|
263
|
+
& {
|
|
264
|
+
"help": OptionBuilder<boolean, "--help">
|
|
265
|
+
},
|
|
266
|
+
Subs,
|
|
267
|
+
false
|
|
268
|
+
>
|
|
269
|
+
: CommanderSet<
|
|
270
|
+
{
|
|
271
|
+
"help": OptionBuilder<boolean, "--help">
|
|
272
|
+
},
|
|
273
|
+
[],
|
|
274
|
+
false
|
|
275
|
+
>
|
|
276
|
+
{
|
|
277
|
+
const base = this && typeof this === "object" ? this as any : {}
|
|
278
|
+
const baseName = base.name || ""
|
|
279
|
+
const baseOptions: OptionsMap = base.options || {}
|
|
280
|
+
const baseSubcommands = base.subcommands || []
|
|
281
|
+
const baseDescription = base.description
|
|
282
|
+
const baseVersion = base.version
|
|
283
|
+
|
|
284
|
+
const helpOption: OptionBuilder<boolean, "--help"> = {
|
|
285
|
+
_tag: "OptionBuilder",
|
|
286
|
+
name: "--help",
|
|
287
|
+
long: "--help",
|
|
288
|
+
short: "h",
|
|
289
|
+
description: "Show help information",
|
|
290
|
+
defaultValue: false,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return makeSet({
|
|
294
|
+
name: baseName,
|
|
295
|
+
description: baseDescription,
|
|
296
|
+
version: baseVersion,
|
|
297
|
+
options: { ...baseOptions, help: helpOption },
|
|
298
|
+
subcommands: baseSubcommands,
|
|
299
|
+
}) as any
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export const optionVersion = function<S>(
|
|
303
|
+
this: S,
|
|
304
|
+
): S extends CommanderSet<infer Opts, infer Subs, infer _H> ? CommanderSet<
|
|
305
|
+
& Opts
|
|
306
|
+
& {
|
|
307
|
+
"version": OptionBuilder<boolean, "--version">
|
|
308
|
+
},
|
|
309
|
+
Subs,
|
|
310
|
+
false
|
|
311
|
+
>
|
|
312
|
+
: CommanderSet<
|
|
313
|
+
{
|
|
314
|
+
"version": OptionBuilder<boolean, "--version">
|
|
315
|
+
},
|
|
316
|
+
[],
|
|
317
|
+
false
|
|
318
|
+
>
|
|
319
|
+
{
|
|
320
|
+
const base = this && typeof this === "object" ? this as any : {}
|
|
321
|
+
const baseName = base.name || ""
|
|
322
|
+
const baseOptions: OptionsMap = base.options || {}
|
|
323
|
+
const baseSubcommands = base.subcommands || []
|
|
324
|
+
const baseDescription = base.description
|
|
325
|
+
const baseVersion = base.version
|
|
326
|
+
|
|
327
|
+
const versionOption: OptionBuilder<boolean, "--version"> = {
|
|
328
|
+
_tag: "OptionBuilder",
|
|
329
|
+
name: "--version",
|
|
330
|
+
long: "--version",
|
|
331
|
+
short: "V",
|
|
332
|
+
description: "Show version information",
|
|
333
|
+
defaultValue: false,
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return makeSet({
|
|
337
|
+
name: baseName,
|
|
338
|
+
description: baseDescription,
|
|
339
|
+
version: baseVersion,
|
|
340
|
+
options: { ...baseOptions, version: versionOption },
|
|
341
|
+
subcommands: baseSubcommands,
|
|
342
|
+
}) as any
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export const subcommand = function<
|
|
346
|
+
S,
|
|
347
|
+
SubOpts extends OptionsMap,
|
|
348
|
+
SubSubs extends ReadonlyArray<SubcommandDef>,
|
|
349
|
+
SubHandled extends boolean,
|
|
350
|
+
>(
|
|
351
|
+
this: S,
|
|
352
|
+
cmd: CommanderSet<SubOpts, SubSubs, SubHandled>,
|
|
353
|
+
): S extends CommanderSet<infer Opts, infer Subs, infer _H> ? CommanderSet<
|
|
354
|
+
Opts,
|
|
355
|
+
[...Subs, SubcommandDef<SubHandled>],
|
|
356
|
+
false
|
|
357
|
+
>
|
|
358
|
+
: CommanderSet<
|
|
359
|
+
{},
|
|
360
|
+
[SubcommandDef<SubHandled>],
|
|
361
|
+
false
|
|
362
|
+
>
|
|
363
|
+
{
|
|
364
|
+
const base = this && typeof this === "object" ? this as any : {}
|
|
365
|
+
const baseName = base.name || ""
|
|
366
|
+
const baseOptions = base.options || {}
|
|
367
|
+
const baseSubcommands = base.subcommands || []
|
|
368
|
+
const baseDescription = base.description
|
|
369
|
+
const baseVersion = base.version
|
|
370
|
+
|
|
371
|
+
const subDef: SubcommandDef = {
|
|
372
|
+
_tag: "SubcommandDef",
|
|
373
|
+
command: cmd as any,
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return makeSet({
|
|
377
|
+
name: baseName,
|
|
378
|
+
description: baseDescription,
|
|
379
|
+
version: baseVersion,
|
|
380
|
+
options: baseOptions,
|
|
381
|
+
subcommands: [...baseSubcommands, subDef] as any,
|
|
382
|
+
}) as any
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export const handle = function<
|
|
386
|
+
Opts extends OptionsMap,
|
|
387
|
+
Subs extends ReadonlyArray<SubcommandDef>,
|
|
388
|
+
>(
|
|
389
|
+
this: CommanderSet<Opts, Subs, false>,
|
|
390
|
+
handler: (args: ExtractOptionValues<Opts>) => Effect.Effect<void>,
|
|
391
|
+
): CommanderSet<Opts, Subs, true> {
|
|
392
|
+
const base = this && typeof this === "object" ? this as any : {}
|
|
393
|
+
|
|
394
|
+
return makeSet({
|
|
395
|
+
name: base.name || "",
|
|
396
|
+
description: base.description,
|
|
397
|
+
version: base.version,
|
|
398
|
+
options: base.options || {},
|
|
399
|
+
subcommands: base.subcommands || [],
|
|
400
|
+
handler: handler as any,
|
|
401
|
+
}) as any
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export const make = <const Name extends string>(config: {
|
|
405
|
+
readonly name: Name
|
|
406
|
+
readonly description?: string
|
|
407
|
+
readonly version?: string
|
|
408
|
+
}): CommanderSet<{}, [], false> =>
|
|
409
|
+
makeSet({
|
|
410
|
+
name: config.name,
|
|
411
|
+
description: config.description,
|
|
412
|
+
version: config.version,
|
|
413
|
+
options: {},
|
|
414
|
+
subcommands: [],
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
const CommanderProto = {
|
|
418
|
+
[TypeId]: TypeId,
|
|
419
|
+
|
|
420
|
+
option: optionMethod,
|
|
421
|
+
optionHelp,
|
|
422
|
+
optionVersion,
|
|
423
|
+
subcommand,
|
|
424
|
+
handle,
|
|
425
|
+
|
|
426
|
+
pipe() {
|
|
427
|
+
return Pipeable.pipeArguments(this, arguments)
|
|
428
|
+
},
|
|
429
|
+
} satisfies CommanderSet.Proto
|
|
430
|
+
|
|
431
|
+
function makeSet<
|
|
432
|
+
Opts extends OptionsMap,
|
|
433
|
+
Subs extends ReadonlyArray<SubcommandDef>,
|
|
434
|
+
Handled extends boolean = false,
|
|
435
|
+
>(config: {
|
|
436
|
+
readonly name: string
|
|
437
|
+
readonly description?: string
|
|
438
|
+
readonly version?: string
|
|
439
|
+
readonly options: Opts
|
|
440
|
+
readonly subcommands: Subs
|
|
441
|
+
readonly handler?: (args: ExtractOptionValues<Opts>) => Effect.Effect<void>
|
|
442
|
+
}): CommanderSet<Opts, Subs, Handled> {
|
|
443
|
+
return Object.assign(
|
|
444
|
+
Object.create(CommanderProto),
|
|
445
|
+
config,
|
|
446
|
+
) as CommanderSet<Opts, Subs, Handled>
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function isCommanderSet(
|
|
450
|
+
input: unknown,
|
|
451
|
+
): input is CommanderSet<any, any, any> {
|
|
452
|
+
return Predicate.hasProperty(input, TypeId)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const kebabToCamel = (str: string): string => {
|
|
456
|
+
return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const stripPrefix = (str: string): string => {
|
|
460
|
+
if (str.startsWith("--")) return str.slice(2)
|
|
461
|
+
if (str.startsWith("-")) return str.slice(1)
|
|
462
|
+
return str
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
interface ParsedArgs {
|
|
466
|
+
readonly flags: Record<string, boolean>
|
|
467
|
+
readonly options: Record<string, string>
|
|
468
|
+
readonly positional: ReadonlyArray<string>
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const parseRawArgs = (
|
|
472
|
+
args: ReadonlyArray<string>,
|
|
473
|
+
): Effect.Effect<ParsedArgs, CommanderError> =>
|
|
474
|
+
Effect.gen(function*() {
|
|
475
|
+
const flags: Record<string, boolean> = {}
|
|
476
|
+
const options: Record<string, string> = {}
|
|
477
|
+
const positional: Array<string> = []
|
|
478
|
+
|
|
479
|
+
let i = 0
|
|
480
|
+
while (i < args.length) {
|
|
481
|
+
const arg = args[i]!
|
|
482
|
+
|
|
483
|
+
if (arg === "--") {
|
|
484
|
+
positional.push(...args.slice(i + 1))
|
|
485
|
+
break
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (arg.startsWith("--")) {
|
|
489
|
+
const equalIndex = arg.indexOf("=")
|
|
490
|
+
if (equalIndex !== -1) {
|
|
491
|
+
const key = arg.slice(2, equalIndex)
|
|
492
|
+
const value = arg.slice(equalIndex + 1)
|
|
493
|
+
options[key] = value
|
|
494
|
+
} else {
|
|
495
|
+
const key = arg.slice(2)
|
|
496
|
+
if (i + 1 < args.length && !args[i + 1]!.startsWith("-")) {
|
|
497
|
+
options[key] = args[i + 1]!
|
|
498
|
+
i++
|
|
499
|
+
} else {
|
|
500
|
+
flags[key] = true
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
} else if (arg.startsWith("-") && arg.length > 1) {
|
|
504
|
+
const chars = arg.slice(1)
|
|
505
|
+
for (let j = 0; j < chars.length; j++) {
|
|
506
|
+
const char = chars[j]!
|
|
507
|
+
if (
|
|
508
|
+
j === chars.length - 1 && i + 1 < args.length && !args[i + 1]!
|
|
509
|
+
.startsWith("-")
|
|
510
|
+
) {
|
|
511
|
+
options[char] = args[i + 1]!
|
|
512
|
+
i++
|
|
513
|
+
} else {
|
|
514
|
+
flags[char] = true
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
positional.push(arg)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
i++
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return { flags, options, positional }
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
export const parse = <
|
|
528
|
+
Opts extends OptionsMap,
|
|
529
|
+
Subs extends ReadonlyArray<SubcommandDef>,
|
|
530
|
+
Handled extends boolean,
|
|
531
|
+
>(
|
|
532
|
+
cmd: CommanderSet<Opts, Subs, Handled>,
|
|
533
|
+
args: ReadonlyArray<string>,
|
|
534
|
+
): Effect.Effect<ExtractOptionValues<Opts>, CommanderError> =>
|
|
535
|
+
Effect.gen(function*() {
|
|
536
|
+
const parsed = yield* parseRawArgs(args)
|
|
537
|
+
|
|
538
|
+
const result: Record<string, any> = {}
|
|
539
|
+
|
|
540
|
+
for (const optBuilder of Object.values(cmd.options)) {
|
|
541
|
+
const longName = stripPrefix(optBuilder.long)
|
|
542
|
+
const shortName = optBuilder.short
|
|
543
|
+
|
|
544
|
+
const longMatch = parsed.options[longName] || parsed.flags[longName]
|
|
545
|
+
const shortMatch = shortName
|
|
546
|
+
? (parsed.options[shortName] || parsed.flags[shortName])
|
|
547
|
+
: undefined
|
|
548
|
+
|
|
549
|
+
const rawValue = longMatch ?? shortMatch
|
|
550
|
+
|
|
551
|
+
const camelKey = kebabToCamel(stripPrefix(optBuilder.long))
|
|
552
|
+
|
|
553
|
+
if (rawValue !== undefined) {
|
|
554
|
+
if (typeof rawValue === "boolean") {
|
|
555
|
+
result[camelKey] = rawValue
|
|
556
|
+
} else if (optBuilder.schema) {
|
|
557
|
+
const decoded = yield* Schema
|
|
558
|
+
.decode(optBuilder.schema)(rawValue)
|
|
559
|
+
.pipe(
|
|
560
|
+
Effect.mapError(
|
|
561
|
+
(error) =>
|
|
562
|
+
new CommanderError({
|
|
563
|
+
message:
|
|
564
|
+
`Invalid value for option ${optBuilder.long}: ${error.message}`,
|
|
565
|
+
cause: error,
|
|
566
|
+
}),
|
|
567
|
+
),
|
|
568
|
+
)
|
|
569
|
+
result[camelKey] = decoded
|
|
570
|
+
} else {
|
|
571
|
+
result[camelKey] = rawValue
|
|
572
|
+
}
|
|
573
|
+
} else if (optBuilder.defaultValue !== undefined) {
|
|
574
|
+
result[camelKey] = optBuilder.defaultValue
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return result as ExtractOptionValues<Opts>
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
export const runMain = <
|
|
582
|
+
Opts extends OptionsMap,
|
|
583
|
+
Subs extends ReadonlyArray<SubcommandDef>,
|
|
584
|
+
>(
|
|
585
|
+
cmd: CommanderSet<Opts, Subs, true>,
|
|
586
|
+
): Effect.Effect<void, CommanderError> =>
|
|
587
|
+
Effect.gen(function*() {
|
|
588
|
+
const args = typeof process !== "undefined" ? process.argv.slice(2) : []
|
|
589
|
+
|
|
590
|
+
const parsedOptions = yield* parse(cmd, args)
|
|
591
|
+
|
|
592
|
+
if ((parsedOptions as any).help) {
|
|
593
|
+
console.log(generateHelp(cmd))
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if ((parsedOptions as any).version && cmd.version) {
|
|
598
|
+
console.log(`${cmd.name} v${cmd.version}`)
|
|
599
|
+
return
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (cmd.handler) {
|
|
603
|
+
yield* cmd.handler(parsedOptions)
|
|
604
|
+
}
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
const generateHelp = <
|
|
608
|
+
Opts extends OptionsMap,
|
|
609
|
+
Subs extends ReadonlyArray<SubcommandDef>,
|
|
610
|
+
Handled extends boolean,
|
|
611
|
+
>(cmd: CommanderSet<Opts, Subs, Handled>): string => {
|
|
612
|
+
const lines: Array<string> = []
|
|
613
|
+
|
|
614
|
+
if (cmd.description) {
|
|
615
|
+
lines.push(cmd.description)
|
|
616
|
+
lines.push("")
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
lines.push(`Usage: ${cmd.name} [options]`)
|
|
620
|
+
lines.push("")
|
|
621
|
+
|
|
622
|
+
const optionsArray = Object.values(cmd.options)
|
|
623
|
+
if (optionsArray.length > 0) {
|
|
624
|
+
lines.push("Options:")
|
|
625
|
+
|
|
626
|
+
for (const opt of optionsArray) {
|
|
627
|
+
const short = opt.short ? `-${opt.short}, ` : " "
|
|
628
|
+
const long = opt.long
|
|
629
|
+
const hasValue = opt.schema !== undefined
|
|
630
|
+
const name = hasValue ? `${long} <value>` : long
|
|
631
|
+
lines.push(` ${short}${name.padEnd(20)} ${opt.description}`)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
lines.push("")
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (cmd.subcommands.length > 0) {
|
|
638
|
+
lines.push("Commands:")
|
|
639
|
+
for (const sub of cmd.subcommands) {
|
|
640
|
+
const subCmd = sub.command
|
|
641
|
+
lines.push(` ${subCmd.name.padEnd(20)} ${subCmd.description || ""}`)
|
|
642
|
+
}
|
|
643
|
+
lines.push("")
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return lines.join("\n")
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export const help = <
|
|
650
|
+
Opts extends OptionsMap,
|
|
651
|
+
Subs extends ReadonlyArray<SubcommandDef>,
|
|
652
|
+
Handled extends boolean,
|
|
653
|
+
>(cmd: CommanderSet<Opts, Subs, Handled>): string => generateHelp(cmd)
|
|
654
|
+
|
|
655
|
+
export const NumberFromString = Schema.NumberFromString
|
|
656
|
+
|
|
657
|
+
export const choice = <const Choices extends ReadonlyArray<string>>(
|
|
658
|
+
choices: Choices,
|
|
659
|
+
): Schema.Schema<Choices[number], string> => Schema.Literal(...choices)
|
|
660
|
+
|
|
661
|
+
export const repeatable = <A>(
|
|
662
|
+
schema: Schema.Schema<A, string>,
|
|
663
|
+
): Schema.Schema<ReadonlyArray<A>, string> =>
|
|
664
|
+
Schema
|
|
665
|
+
.transform(Schema.String, Schema.Array(Schema.String), {
|
|
666
|
+
strict: true,
|
|
667
|
+
decode: (s) => s.split(",").map((part) => part.trim()),
|
|
668
|
+
encode: (arr) => arr.join(","),
|
|
669
|
+
})
|
|
670
|
+
.pipe(
|
|
671
|
+
Schema.compose(Schema.Array(schema)),
|
|
672
|
+
) as any
|