cloak22 2.2.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/src/cli.ts ADDED
@@ -0,0 +1,815 @@
1
+ import yargs, { type Argv, type ArgumentsCamelCase } from "yargs"
2
+ import { formatHeading } from "./output.js"
3
+
4
+ type HelpMode = {
5
+ mode: "help"
6
+ text: string
7
+ }
8
+
9
+ type VersionMode = {
10
+ mode: "version"
11
+ }
12
+
13
+ export type RunModeConfig = {
14
+ mode: "run"
15
+ headless: boolean
16
+ daemon: boolean
17
+ persistCookies: boolean
18
+ consent: boolean
19
+ profile?: string
20
+ cookieUrls: string[]
21
+ cookieFile?: string
22
+ }
23
+
24
+ type DaemonStatusMode = {
25
+ mode: "daemon-status"
26
+ }
27
+
28
+ type DaemonStopMode = {
29
+ mode: "daemon-stop"
30
+ }
31
+
32
+ type DaemonRestartMode = {
33
+ mode: "daemon-restart"
34
+ }
35
+
36
+ type DaemonLogsMode = {
37
+ mode: "daemon-logs"
38
+ }
39
+
40
+ type ProfileListMode = {
41
+ mode: "profile-list"
42
+ }
43
+
44
+ type ProfileShowMode = {
45
+ mode: "profile-show"
46
+ }
47
+
48
+ type ProfileUseMode = {
49
+ mode: "profile-use"
50
+ profile: string
51
+ consent: boolean
52
+ }
53
+
54
+ type SitesListMode = {
55
+ mode: "sites-list"
56
+ limit: number
57
+ noPager: boolean
58
+ consent: boolean
59
+ }
60
+
61
+ type StorageShowMode = {
62
+ mode: "storage-show"
63
+ }
64
+
65
+ type StorageDestroyMode = {
66
+ mode: "storage-destroy"
67
+ }
68
+
69
+ type CliConfig =
70
+ | HelpMode
71
+ | VersionMode
72
+ | RunModeConfig
73
+ | DaemonStatusMode
74
+ | DaemonStopMode
75
+ | DaemonRestartMode
76
+ | DaemonLogsMode
77
+ | ProfileListMode
78
+ | ProfileShowMode
79
+ | ProfileUseMode
80
+ | SitesListMode
81
+ | StorageShowMode
82
+ | StorageDestroyMode
83
+
84
+ function parseUrl(value: string): string {
85
+ try {
86
+ const parsed = new URL(value)
87
+
88
+ if (!["http:", "https:"].includes(parsed.protocol)) {
89
+ throw new Error(`invalid site URL: ${value}`)
90
+ }
91
+
92
+ return parsed.toString()
93
+ } catch {
94
+ throw new Error(`invalid URL: ${value}`)
95
+ }
96
+ }
97
+
98
+ function normalizeUrls(values: string | string[] | undefined): string[] {
99
+ if (!values) {
100
+ return []
101
+ }
102
+
103
+ const urls = Array.isArray(values) ? values : [values]
104
+ const normalized = urls.map(parseUrl)
105
+
106
+ return [...new Set(normalized)]
107
+ }
108
+
109
+ function normalizeFilePath(value: string | undefined): string | undefined {
110
+ const normalized = value?.trim()
111
+
112
+ if (!normalized) {
113
+ return undefined
114
+ }
115
+
116
+ return normalized
117
+ }
118
+
119
+ function parseLimit(value: number): number {
120
+ if (!Number.isInteger(value) || value <= 0) {
121
+ throw new Error("--limit must be a positive integer")
122
+ }
123
+
124
+ return value
125
+ }
126
+
127
+ function createParser(args: string[], scriptName: string): Argv {
128
+ return yargs(args)
129
+ .scriptName(scriptName)
130
+ .exitProcess(false)
131
+ .help(false)
132
+ .version(false)
133
+ .showHelpOnFail(false)
134
+ .strict()
135
+ .strictCommands()
136
+ .strictOptions()
137
+ .wrap(100)
138
+ .fail((message: string | undefined, error: Error | undefined) => {
139
+ if (error) {
140
+ throw error
141
+ }
142
+
143
+ throw new Error(message)
144
+ })
145
+ }
146
+
147
+ function isHelpFlag(value: string | undefined): boolean {
148
+ return value === "-h" || value === "--help"
149
+ }
150
+
151
+ function rootEpilog(): string {
152
+ return `${formatHeading("Examples:")}
153
+ cloak profile list
154
+ cloak profile use "Profile 7"
155
+ cloak profile show
156
+ cloak sites list
157
+ cloak run
158
+ cloak daemon start --profile "Profile 7" --persist-cookies --consent --cookie-url https://x.com
159
+ cloak daemon status
160
+ cloak daemon logs
161
+ cloak storage show
162
+ cloak storage destroy
163
+ cloak version
164
+
165
+ ${formatHeading("Storage:")}
166
+ cloak stores state in ~/.config/cloak/state.sqlite
167
+ cloak stores daemon logs and the pinned OpenCLI extension under ~/.cache/cloak/
168
+ `
169
+ }
170
+
171
+ function buildRootProgram(args: string[] = []): Argv {
172
+ return createParser(args, "cloak")
173
+ .usage("$0 <command>")
174
+ .command("run", "launch Patchright headless by default")
175
+ .command("daemon", "start, inspect, stop, restart, or print the managed daemon log")
176
+ .command("profile", "list, show, or select a Chrome profile")
177
+ .command("sites", "list cookie-bearing site URLs for the active profile")
178
+ .command("storage", "inspect or destroy cloak storage")
179
+ .command("version", "print the installed cloak version")
180
+ .epilog(rootEpilog())
181
+ }
182
+
183
+ function rootHelpText(): string {
184
+ return `Usage: cloak <command>
185
+
186
+ Commands:
187
+ cloak run launch Patchright headless by default
188
+ cloak daemon start start the managed daemon
189
+ cloak daemon status inspect the managed daemon
190
+ cloak daemon stop stop the managed daemon
191
+ cloak daemon restart restart the managed daemon
192
+ cloak daemon logs print the managed daemon log
193
+ cloak profile list list available Chrome profiles
194
+ cloak profile show show the saved default Chrome profile
195
+ cloak profile use <name> save the default Chrome profile
196
+ cloak sites list list Chrome cookie URLs for the active profile
197
+ cloak storage show show the cloak storage paths
198
+ cloak storage destroy destroy cloak storage after confirmation
199
+ cloak version print the installed cloak version
200
+
201
+ Options:
202
+ -h, --help Show help
203
+ -v, --version Show version
204
+
205
+ ${rootEpilog()}`
206
+ }
207
+
208
+ function runEpilog(): string {
209
+ return `${formatHeading("Examples:")}
210
+ cloak run
211
+ cloak run --window
212
+ cloak run --cookie-file ./cookies.json
213
+ cloak run --cookie-url https://x.com --profile "Profile 7"
214
+ cloak run --persist-cookies --consent --cookie-url https://x.com
215
+
216
+ ${formatHeading("Related:")}
217
+ cloak daemon start [same options as run]
218
+
219
+ ${formatHeading("Storage:")}
220
+ --persist-cookies remembers cookie URLs for the chosen profile
221
+ `
222
+ }
223
+
224
+ function buildRunProgram(args: string[] = []): Argv {
225
+ return createParser(args, "cloak run")
226
+ .usage("$0 [options]")
227
+ .option("window", {
228
+ alias: "w",
229
+ type: "boolean",
230
+ description: "open a visible browser window",
231
+ })
232
+ .option("profile", {
233
+ type: "string",
234
+ description: "Chrome profile directory name",
235
+ })
236
+ .option("cookie-url", {
237
+ type: "string",
238
+ array: true,
239
+ description: "HTTP(S) site URL to import cookies from",
240
+ coerce: normalizeUrls,
241
+ })
242
+ .option("cookie-file", {
243
+ type: "string",
244
+ description: "JSON cookie export or Playwright storage-state file",
245
+ coerce: normalizeFilePath,
246
+ })
247
+ .option("persist-cookies", {
248
+ type: "boolean",
249
+ description: "remember the provided --cookie-url values for this profile",
250
+ })
251
+ .option("consent", {
252
+ type: "boolean",
253
+ description: "create ~/.config/cloak without prompting",
254
+ })
255
+ .check((options: ArgumentsCamelCase<Record<string, unknown>>) => {
256
+ const cookieUrls = Array.isArray(options.cookieUrl)
257
+ ? (options.cookieUrl as string[])
258
+ : []
259
+
260
+ if (options.persistCookies && cookieUrls.length === 0) {
261
+ throw new Error("--persist-cookies requires at least one --cookie-url")
262
+ }
263
+
264
+ return true
265
+ })
266
+ .epilog(runEpilog())
267
+ }
268
+
269
+ function runHelpText(): string {
270
+ return `Usage: cloak run [options]
271
+
272
+ Options:
273
+ -h, --help Show help
274
+ -w, --window open a visible browser window
275
+ --profile <profile> Chrome profile directory name
276
+ --cookie-url <url> HTTP(S) site URL to import cookies from
277
+ --cookie-file <path> JSON cookie export or Playwright storage-state file
278
+ --persist-cookies remember the provided --cookie-url values
279
+ --consent create ~/.config/cloak without prompting
280
+
281
+ ${runEpilog()}`
282
+ }
283
+
284
+ function buildDaemonProgram(args: string[] = []): Argv {
285
+ return createParser(args, "cloak daemon")
286
+ .usage("$0 <command>")
287
+ .command("start", "start the managed daemon")
288
+ .command("status", "inspect the managed daemon")
289
+ .command("stop", "stop the managed daemon")
290
+ .command("restart", "restart the managed daemon")
291
+ .command("logs", "print the managed daemon log")
292
+ }
293
+
294
+ function daemonHelpText(): string {
295
+ return `Usage: cloak daemon <command>
296
+
297
+ Commands:
298
+ cloak daemon start start the managed daemon
299
+ cloak daemon status inspect the managed daemon
300
+ cloak daemon stop stop the managed daemon
301
+ cloak daemon restart restart the managed daemon
302
+ cloak daemon logs print the managed daemon log
303
+
304
+ Options:
305
+ -h, --help Show help
306
+ `
307
+ }
308
+
309
+ function buildDaemonStartProgram(args: string[] = []): Argv {
310
+ return createParser(args, "cloak daemon start")
311
+ .usage("$0 [options]")
312
+ .option("window", {
313
+ alias: "w",
314
+ type: "boolean",
315
+ description: "open a visible browser window",
316
+ })
317
+ .option("profile", {
318
+ type: "string",
319
+ description: "Chrome profile directory name",
320
+ })
321
+ .option("cookie-url", {
322
+ type: "string",
323
+ array: true,
324
+ description: "HTTP(S) site URL to import cookies from",
325
+ coerce: normalizeUrls,
326
+ })
327
+ .option("cookie-file", {
328
+ type: "string",
329
+ description: "JSON cookie export or Playwright storage-state file",
330
+ coerce: normalizeFilePath,
331
+ })
332
+ .option("persist-cookies", {
333
+ type: "boolean",
334
+ description: "remember the provided --cookie-url values for this profile",
335
+ })
336
+ .option("consent", {
337
+ type: "boolean",
338
+ description: "create ~/.config/cloak without prompting",
339
+ })
340
+ .check((options: ArgumentsCamelCase<Record<string, unknown>>) => {
341
+ const cookieUrls = Array.isArray(options.cookieUrl)
342
+ ? (options.cookieUrl as string[])
343
+ : []
344
+
345
+ if (options.persistCookies && cookieUrls.length === 0) {
346
+ throw new Error("--persist-cookies requires at least one --cookie-url")
347
+ }
348
+
349
+ return true
350
+ })
351
+ }
352
+
353
+ function daemonStartHelpText(): string {
354
+ return `Usage: cloak daemon start [options]
355
+
356
+ Options:
357
+ -h, --help Show help
358
+ -w, --window open a visible browser window
359
+ --profile <profile> Chrome profile directory name
360
+ --cookie-url <url> HTTP(S) site URL to import cookies from
361
+ --cookie-file <path> JSON cookie export or Playwright storage-state file
362
+ --persist-cookies remember the provided --cookie-url values
363
+ --consent create ~/.config/cloak without prompting
364
+ `
365
+ }
366
+
367
+ function buildZeroArgProgram(args: string[] = [], scriptName: string): Argv {
368
+ return createParser(args, scriptName).usage("$0")
369
+ }
370
+
371
+ function zeroArgHelpText(usage: string): string {
372
+ return `Usage: ${usage}
373
+
374
+ Options:
375
+ -h, --help Show help
376
+ `
377
+ }
378
+
379
+ function buildProfileProgram(args: string[] = []): Argv {
380
+ return createParser(args, "cloak profile")
381
+ .usage("$0 <command>")
382
+ .command("list", "list available Chrome profiles")
383
+ .command("show", "show the saved default Chrome profile")
384
+ .command("use <profile>", "save the default Chrome profile")
385
+ }
386
+
387
+ function profileHelpText(): string {
388
+ return `Usage: cloak profile <command>
389
+
390
+ Commands:
391
+ cloak profile list list available Chrome profiles
392
+ cloak profile show show the saved default Chrome profile
393
+ cloak profile use <name> save the default Chrome profile
394
+
395
+ Options:
396
+ -h, --help Show help
397
+ `
398
+ }
399
+
400
+ function buildProfileUseProgram(args: string[] = []): Argv {
401
+ return createParser(args, "cloak profile use")
402
+ .usage("$0 <profile> [options]")
403
+ .option("consent", {
404
+ type: "boolean",
405
+ description: "create ~/.config/cloak without prompting",
406
+ })
407
+ }
408
+
409
+ function profileUseHelpText(): string {
410
+ return `Usage: cloak profile use <profile> [options]
411
+
412
+ Options:
413
+ -h, --help Show help
414
+ --consent create ~/.config/cloak without prompting
415
+ `
416
+ }
417
+
418
+ function buildSitesProgram(args: string[] = []): Argv {
419
+ return createParser(args, "cloak sites")
420
+ .usage("$0 <command>")
421
+ .command("list", "list Chrome cookie URLs for the active profile")
422
+ }
423
+
424
+ function sitesHelpText(): string {
425
+ return `Usage: cloak sites <command>
426
+
427
+ Commands:
428
+ cloak sites list list Chrome cookie URLs for the active profile
429
+
430
+ Options:
431
+ -h, --help Show help
432
+ `
433
+ }
434
+
435
+ function buildSitesListProgram(args: string[] = []): Argv {
436
+ return createParser(args, "cloak sites list")
437
+ .usage("$0 [options]")
438
+ .option("pager", {
439
+ type: "boolean",
440
+ default: true,
441
+ description: "prompt for interactive selection after printing the list",
442
+ })
443
+ .option("limit", {
444
+ alias: "l",
445
+ type: "number",
446
+ default: 100,
447
+ description: "maximum number of URLs to print",
448
+ coerce: parseLimit,
449
+ })
450
+ .option("consent", {
451
+ type: "boolean",
452
+ description: "create ~/.config/cloak without prompting",
453
+ })
454
+ }
455
+
456
+ function sitesListHelpText(): string {
457
+ return `Usage: cloak sites list [options]
458
+
459
+ Options:
460
+ -h, --help Show help
461
+ -n, --no-pager print without prompting for selection
462
+ -l, --limit <count> maximum number of URLs to print (default 100)
463
+ --consent create ~/.config/cloak without prompting
464
+ `
465
+ }
466
+
467
+ function buildStorageProgram(args: string[] = []): Argv {
468
+ return createParser(args, "cloak storage")
469
+ .usage("$0 <command>")
470
+ .command("show", "show the cloak storage paths")
471
+ .command("destroy", "destroy cloak storage after confirmation")
472
+ }
473
+
474
+ function storageHelpText(): string {
475
+ return `Usage: cloak storage <command>
476
+
477
+ Commands:
478
+ cloak storage show show the cloak storage paths
479
+ cloak storage destroy destroy cloak storage after confirmation
480
+
481
+ Options:
482
+ -h, --help Show help
483
+ `
484
+ }
485
+
486
+ function parseRunMode(args: string[]): CliConfig {
487
+ const parser = buildRunProgram(args)
488
+ const options = parser.parseSync() as {
489
+ help?: boolean
490
+ window?: boolean
491
+ profile?: string
492
+ cookieUrl?: string[]
493
+ cookieFile?: string
494
+ persistCookies?: boolean
495
+ consent?: boolean
496
+ }
497
+
498
+ if (options.help) {
499
+ return {
500
+ mode: "help",
501
+ text: runHelpText(),
502
+ }
503
+ }
504
+
505
+ return {
506
+ mode: "run",
507
+ headless: !Boolean(options.window),
508
+ daemon: false,
509
+ persistCookies: Boolean(options.persistCookies),
510
+ consent: Boolean(options.consent),
511
+ profile: options.profile,
512
+ cookieUrls: options.cookieUrl ?? [],
513
+ cookieFile: options.cookieFile,
514
+ }
515
+ }
516
+
517
+ function parseDaemonStartMode(args: string[]): CliConfig {
518
+ const parser = buildDaemonStartProgram(args)
519
+ const options = parser.parseSync() as {
520
+ help?: boolean
521
+ window?: boolean
522
+ profile?: string
523
+ cookieUrl?: string[]
524
+ cookieFile?: string
525
+ persistCookies?: boolean
526
+ consent?: boolean
527
+ }
528
+
529
+ if (options.help) {
530
+ return {
531
+ mode: "help",
532
+ text: daemonStartHelpText(),
533
+ }
534
+ }
535
+
536
+ return {
537
+ mode: "run",
538
+ headless: !Boolean(options.window),
539
+ daemon: true,
540
+ persistCookies: Boolean(options.persistCookies),
541
+ consent: Boolean(options.consent),
542
+ profile: options.profile,
543
+ cookieUrls: options.cookieUrl ?? [],
544
+ cookieFile: options.cookieFile,
545
+ }
546
+ }
547
+
548
+ function parseProfileUseMode(args: string[]): CliConfig {
549
+ const profile = String(args[0] ?? "").trim()
550
+
551
+ if (!profile) {
552
+ throw new Error("profile is required")
553
+ }
554
+
555
+ const parser = buildProfileUseProgram(args.slice(1))
556
+ const options = parser.parseSync() as {
557
+ help?: boolean
558
+ consent?: boolean
559
+ }
560
+
561
+ if (options.help) {
562
+ return {
563
+ mode: "help",
564
+ text: profileUseHelpText(),
565
+ }
566
+ }
567
+
568
+ return {
569
+ mode: "profile-use",
570
+ profile,
571
+ consent: Boolean(options.consent),
572
+ }
573
+ }
574
+
575
+ function parseSitesListMode(args: string[]): CliConfig {
576
+ const normalizedArgs = args.map((arg) => (arg === "-n" ? "--no-pager" : arg))
577
+ const parser = buildSitesListProgram(normalizedArgs)
578
+ const options = parser.parseSync() as {
579
+ help?: boolean
580
+ pager?: boolean
581
+ limit?: number
582
+ consent?: boolean
583
+ }
584
+
585
+ if (options.help) {
586
+ return {
587
+ mode: "help",
588
+ text: sitesListHelpText(),
589
+ }
590
+ }
591
+
592
+ return {
593
+ mode: "sites-list",
594
+ limit: options.limit ?? 100,
595
+ noPager: options.pager === false,
596
+ consent: Boolean(options.consent),
597
+ }
598
+ }
599
+
600
+ function parseRootMode(args: string[]): never {
601
+ buildRootProgram(args).parseSync()
602
+ throw new Error("root mode parsing should not return")
603
+ }
604
+
605
+ export function parseCli(argv: string[]): CliConfig {
606
+ const args = argv.slice(2)
607
+
608
+ if (args.length === 0 || isHelpFlag(args[0])) {
609
+ return {
610
+ mode: "help",
611
+ text: rootHelpText(),
612
+ }
613
+ }
614
+
615
+ if (args[0] === "-v" || args[0] === "--version" || args[0] === "version") {
616
+ return {
617
+ mode: "version",
618
+ }
619
+ }
620
+
621
+ if (args[0] === "run") {
622
+ if (args.length === 2 && isHelpFlag(args[1])) {
623
+ return {
624
+ mode: "help",
625
+ text: runHelpText(),
626
+ }
627
+ }
628
+
629
+ return parseRunMode(args.slice(1))
630
+ }
631
+
632
+ if (args[0] === "daemon") {
633
+ if (args.length === 1 || isHelpFlag(args[1])) {
634
+ return {
635
+ mode: "help",
636
+ text: daemonHelpText(),
637
+ }
638
+ }
639
+
640
+ if (args[1] === "start") {
641
+ if (args.length === 3 && isHelpFlag(args[2])) {
642
+ return {
643
+ mode: "help",
644
+ text: daemonStartHelpText(),
645
+ }
646
+ }
647
+
648
+ return parseDaemonStartMode(args.slice(2))
649
+ }
650
+
651
+ if (args[1] === "status") {
652
+ if (args.length === 3 && isHelpFlag(args[2])) {
653
+ return {
654
+ mode: "help",
655
+ text: zeroArgHelpText("cloak daemon status"),
656
+ }
657
+ }
658
+
659
+ buildZeroArgProgram(args.slice(2), "cloak daemon status").parseSync()
660
+ return { mode: "daemon-status" }
661
+ }
662
+
663
+ if (args[1] === "stop") {
664
+ if (args.length === 3 && isHelpFlag(args[2])) {
665
+ return {
666
+ mode: "help",
667
+ text: zeroArgHelpText("cloak daemon stop"),
668
+ }
669
+ }
670
+
671
+ buildZeroArgProgram(args.slice(2), "cloak daemon stop").parseSync()
672
+ return { mode: "daemon-stop" }
673
+ }
674
+
675
+ if (args[1] === "restart") {
676
+ if (args.length === 3 && isHelpFlag(args[2])) {
677
+ return {
678
+ mode: "help",
679
+ text: zeroArgHelpText("cloak daemon restart"),
680
+ }
681
+ }
682
+
683
+ buildZeroArgProgram(args.slice(2), "cloak daemon restart").parseSync()
684
+ return { mode: "daemon-restart" }
685
+ }
686
+
687
+ if (args[1] === "logs") {
688
+ if (args.length === 3 && isHelpFlag(args[2])) {
689
+ return {
690
+ mode: "help",
691
+ text: zeroArgHelpText("cloak daemon logs"),
692
+ }
693
+ }
694
+
695
+ buildZeroArgProgram(args.slice(2), "cloak daemon logs").parseSync()
696
+ return { mode: "daemon-logs" }
697
+ }
698
+
699
+ buildDaemonProgram(args.slice(1)).parseSync()
700
+ throw new Error("daemon parsing should not return")
701
+ }
702
+
703
+ if (args[0] === "profile") {
704
+ if (args.length === 1 || isHelpFlag(args[1])) {
705
+ return {
706
+ mode: "help",
707
+ text: profileHelpText(),
708
+ }
709
+ }
710
+
711
+ if (args[1] === "list") {
712
+ if (args.length === 3 && isHelpFlag(args[2])) {
713
+ return {
714
+ mode: "help",
715
+ text: zeroArgHelpText("cloak profile list"),
716
+ }
717
+ }
718
+
719
+ buildZeroArgProgram(args.slice(2), "cloak profile list").parseSync()
720
+ return { mode: "profile-list" }
721
+ }
722
+
723
+ if (args[1] === "show") {
724
+ if (args.length === 3 && isHelpFlag(args[2])) {
725
+ return {
726
+ mode: "help",
727
+ text: zeroArgHelpText("cloak profile show"),
728
+ }
729
+ }
730
+
731
+ buildZeroArgProgram(args.slice(2), "cloak profile show").parseSync()
732
+ return { mode: "profile-show" }
733
+ }
734
+
735
+ if (args[1] === "use") {
736
+ if (args.length === 3 && isHelpFlag(args[2])) {
737
+ return {
738
+ mode: "help",
739
+ text: profileUseHelpText(),
740
+ }
741
+ }
742
+
743
+ return parseProfileUseMode(args.slice(2))
744
+ }
745
+
746
+ buildProfileProgram(args.slice(1)).parseSync()
747
+ throw new Error("profile parsing should not return")
748
+ }
749
+
750
+ if (args[0] === "sites") {
751
+ if (args.length === 1 || isHelpFlag(args[1])) {
752
+ return {
753
+ mode: "help",
754
+ text: sitesHelpText(),
755
+ }
756
+ }
757
+
758
+ if (args[1] === "list") {
759
+ if (args.length === 3 && isHelpFlag(args[2])) {
760
+ return {
761
+ mode: "help",
762
+ text: sitesListHelpText(),
763
+ }
764
+ }
765
+
766
+ return parseSitesListMode(args.slice(2))
767
+ }
768
+
769
+ buildSitesProgram(args.slice(1)).parseSync()
770
+ throw new Error("sites parsing should not return")
771
+ }
772
+
773
+ if (args[0] === "storage") {
774
+ if (args.length === 1 || isHelpFlag(args[1])) {
775
+ return {
776
+ mode: "help",
777
+ text: storageHelpText(),
778
+ }
779
+ }
780
+
781
+ if (args[1] === "show") {
782
+ if (args.length === 3 && isHelpFlag(args[2])) {
783
+ return {
784
+ mode: "help",
785
+ text: zeroArgHelpText("cloak storage show"),
786
+ }
787
+ }
788
+
789
+ buildZeroArgProgram(args.slice(2), "cloak storage show").parseSync()
790
+ return { mode: "storage-show" }
791
+ }
792
+
793
+ if (args[1] === "destroy") {
794
+ if (args.length === 3 && isHelpFlag(args[2])) {
795
+ return {
796
+ mode: "help",
797
+ text: zeroArgHelpText("cloak storage destroy"),
798
+ }
799
+ }
800
+
801
+ buildZeroArgProgram(args.slice(2), "cloak storage destroy").parseSync()
802
+ return { mode: "storage-destroy" }
803
+ }
804
+
805
+ buildStorageProgram(args.slice(1)).parseSync()
806
+ throw new Error("storage parsing should not return")
807
+ }
808
+
809
+ return parseRootMode(args)
810
+ }
811
+
812
+ export function isHeadlessEnabled(argv: string[]): boolean {
813
+ const parsed = parseCli(argv)
814
+ return parsed.mode === "run" ? parsed.headless : false
815
+ }