libretto 0.5.3-experimental.5 → 0.5.3

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.
Files changed (126) hide show
  1. package/README.md +114 -37
  2. package/README.template.md +160 -0
  3. package/dist/cli/cli.js +22 -97
  4. package/dist/cli/commands/browser.js +86 -59
  5. package/dist/cli/commands/deploy.js +148 -0
  6. package/dist/cli/commands/execution.js +218 -96
  7. package/dist/cli/commands/init.js +34 -29
  8. package/dist/cli/commands/logs.js +4 -5
  9. package/dist/cli/commands/shared.js +30 -29
  10. package/dist/cli/commands/snapshot.js +26 -39
  11. package/dist/cli/core/ai-config.js +21 -4
  12. package/dist/cli/core/api-snapshot-analyzer.js +15 -5
  13. package/dist/cli/core/browser.js +207 -37
  14. package/dist/cli/core/context.js +4 -1
  15. package/dist/cli/core/deploy-artifact.js +687 -0
  16. package/dist/cli/core/session-telemetry.js +434 -174
  17. package/dist/cli/core/session.js +21 -8
  18. package/dist/cli/core/snapshot-analyzer.js +14 -31
  19. package/dist/cli/core/snapshot-api-config.js +2 -6
  20. package/dist/cli/core/telemetry.js +20 -4
  21. package/dist/cli/framework/simple-cli.js +144 -43
  22. package/dist/cli/router.js +16 -21
  23. package/dist/cli/workers/run-integration-runtime.js +25 -45
  24. package/dist/cli/workers/run-integration-worker-protocol.js +3 -2
  25. package/dist/cli/workers/run-integration-worker.js +1 -4
  26. package/dist/index.d.ts +1 -2
  27. package/dist/index.js +13 -10
  28. package/dist/runtime/download/download.js +5 -1
  29. package/dist/runtime/extract/extract.js +11 -2
  30. package/dist/runtime/network/network.js +8 -1
  31. package/dist/runtime/recovery/agent.js +6 -2
  32. package/dist/runtime/recovery/errors.js +3 -1
  33. package/dist/runtime/recovery/recovery.js +3 -1
  34. package/dist/shared/condense-dom/condense-dom.js +17 -69
  35. package/dist/shared/config/config.d.ts +1 -9
  36. package/dist/shared/config/config.js +0 -18
  37. package/dist/shared/config/index.d.ts +2 -1
  38. package/dist/shared/config/index.js +0 -10
  39. package/dist/shared/debug/pause.js +9 -3
  40. package/dist/shared/dom-semantics.d.ts +8 -0
  41. package/dist/shared/dom-semantics.js +69 -0
  42. package/dist/shared/instrumentation/instrument.js +101 -5
  43. package/dist/shared/llm/ai-sdk-adapter.js +3 -1
  44. package/dist/shared/llm/client.js +3 -1
  45. package/dist/shared/logger/index.js +4 -1
  46. package/dist/shared/run/api.js +3 -1
  47. package/dist/shared/run/browser.js +47 -3
  48. package/dist/shared/state/session-state.d.ts +2 -1
  49. package/dist/shared/state/session-state.js +5 -2
  50. package/dist/shared/visualization/ghost-cursor.js +36 -14
  51. package/dist/shared/visualization/highlight.js +9 -6
  52. package/dist/shared/workflow/workflow.d.ts +18 -10
  53. package/dist/shared/workflow/workflow.js +50 -5
  54. package/package.json +14 -6
  55. package/scripts/generate-changelog.ts +132 -0
  56. package/scripts/postinstall.mjs +4 -3
  57. package/scripts/skills-libretto.mjs +2 -88
  58. package/scripts/summarize-evals.mjs +32 -10
  59. package/skills/libretto/SKILL.md +132 -62
  60. package/skills/libretto/references/action-logs.md +101 -0
  61. package/skills/libretto/references/auth-profiles.md +1 -2
  62. package/skills/libretto/references/code-generation-rules.md +176 -0
  63. package/skills/libretto/references/configuration-file-reference.md +53 -0
  64. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  65. package/skills/libretto/references/site-security-review.md +143 -0
  66. package/src/cli/cli.ts +23 -110
  67. package/src/cli/commands/browser.ts +94 -70
  68. package/src/cli/commands/deploy.ts +198 -0
  69. package/src/cli/commands/execution.ts +251 -111
  70. package/src/cli/commands/init.ts +37 -33
  71. package/src/cli/commands/logs.ts +7 -7
  72. package/src/cli/commands/shared.ts +36 -37
  73. package/src/cli/commands/snapshot.ts +44 -59
  74. package/src/cli/core/ai-config.ts +24 -4
  75. package/src/cli/core/api-snapshot-analyzer.ts +17 -6
  76. package/src/cli/core/browser.ts +260 -49
  77. package/src/cli/core/context.ts +7 -2
  78. package/src/cli/core/deploy-artifact.ts +938 -0
  79. package/src/cli/core/session-telemetry.ts +449 -197
  80. package/src/cli/core/session.ts +21 -7
  81. package/src/cli/core/snapshot-analyzer.ts +26 -46
  82. package/src/cli/core/snapshot-api-config.ts +170 -175
  83. package/src/cli/core/telemetry.ts +39 -4
  84. package/src/cli/framework/simple-cli.ts +281 -98
  85. package/src/cli/router.ts +15 -21
  86. package/src/cli/workers/run-integration-runtime.ts +35 -57
  87. package/src/cli/workers/run-integration-worker-protocol.ts +2 -1
  88. package/src/cli/workers/run-integration-worker.ts +1 -4
  89. package/src/index.ts +77 -67
  90. package/src/runtime/download/download.ts +62 -58
  91. package/src/runtime/download/index.ts +5 -5
  92. package/src/runtime/extract/extract.ts +71 -61
  93. package/src/runtime/network/index.ts +3 -3
  94. package/src/runtime/network/network.ts +99 -93
  95. package/src/runtime/recovery/agent.ts +217 -212
  96. package/src/runtime/recovery/errors.ts +107 -104
  97. package/src/runtime/recovery/index.ts +3 -3
  98. package/src/runtime/recovery/recovery.ts +38 -35
  99. package/src/shared/condense-dom/condense-dom.ts +27 -82
  100. package/src/shared/config/config.ts +0 -19
  101. package/src/shared/config/index.ts +0 -5
  102. package/src/shared/debug/pause.ts +57 -51
  103. package/src/shared/dom-semantics.ts +68 -0
  104. package/src/shared/instrumentation/errors.ts +64 -62
  105. package/src/shared/instrumentation/index.ts +5 -5
  106. package/src/shared/instrumentation/instrument.ts +339 -209
  107. package/src/shared/llm/ai-sdk-adapter.ts +58 -55
  108. package/src/shared/llm/client.ts +181 -174
  109. package/src/shared/llm/types.ts +39 -39
  110. package/src/shared/logger/index.ts +11 -4
  111. package/src/shared/logger/logger.ts +312 -306
  112. package/src/shared/logger/sinks.ts +118 -114
  113. package/src/shared/paths/paths.ts +50 -49
  114. package/src/shared/paths/repo-root.ts +17 -17
  115. package/src/shared/run/api.ts +5 -1
  116. package/src/shared/run/browser.ts +65 -3
  117. package/src/shared/state/index.ts +9 -9
  118. package/src/shared/state/session-state.ts +46 -43
  119. package/src/shared/visualization/ghost-cursor.ts +180 -149
  120. package/src/shared/visualization/highlight.ts +89 -86
  121. package/src/shared/visualization/index.ts +13 -13
  122. package/src/shared/workflow/workflow.ts +107 -30
  123. package/scripts/check-skills-sync.mjs +0 -23
  124. package/scripts/prepare-release.sh +0 -97
  125. package/skills/libretto/references/reverse-engineering-network-requests.md +0 -75
  126. package/skills/libretto/references/user-action-log.md +0 -31
@@ -4,6 +4,7 @@ type RecordUnknown = Record<string, unknown>;
4
4
 
5
5
  export type SimpleCLICommandConfig = {
6
6
  description: string;
7
+ experimental?: boolean;
7
8
  };
8
9
 
9
10
  export type SimpleCLIInputRaw = {
@@ -49,7 +50,10 @@ type SimpleCLIPositionalsDefinition = readonly SimpleCLIPositionalDefinition<
49
50
  ZodTypeAny
50
51
  >[];
51
52
 
52
- type SimpleCLINamedDefinition = Record<string, SimpleCLINamedArgDefinition<ZodTypeAny>>;
53
+ type SimpleCLINamedDefinition = Record<
54
+ string,
55
+ SimpleCLINamedArgDefinition<ZodTypeAny>
56
+ >;
53
57
 
54
58
  type SimpleCLIInputDefinition = {
55
59
  positionals: SimpleCLIPositionalsDefinition;
@@ -97,7 +101,8 @@ type NormalizedCommandDefinition<
97
101
 
98
102
  type SimpleCLIRouteTree<TContext extends SimpleCLIContext = {}> = Record<
99
103
  string,
100
- SimpleCLIGroup<TContext, any> | SimpleCLICommandBuilder<any, TContext, any, any>
104
+ | SimpleCLIGroup<TContext, any>
105
+ | SimpleCLICommandBuilder<any, TContext, any, any>
101
106
  >;
102
107
 
103
108
  export type SimpleCLIResolvedCommand = {
@@ -107,6 +112,7 @@ export type SimpleCLIResolvedCommand = {
107
112
  };
108
113
 
109
114
  type InternalResolvedCommand = SimpleCLIResolvedCommand & {
115
+ experimental?: boolean;
110
116
  input?: SimpleCLIInput<unknown>;
111
117
  middlewares: AnySimpleCLIMiddleware[];
112
118
  handler: SimpleCLIHandler<unknown, SimpleCLIContext, unknown>;
@@ -123,6 +129,11 @@ type InternalResolvedRouteEntry = {
123
129
  path: readonly string[];
124
130
  };
125
131
 
132
+ type CollectedRouteTreeResult = {
133
+ commands: InternalResolvedCommand[];
134
+ groupDescriptions: Map<string, string | undefined>;
135
+ };
136
+
126
137
  type ResolveRouteTreeResult = {
127
138
  commands: InternalResolvedCommand[];
128
139
  groups: InternalResolvedGroup[];
@@ -144,6 +155,9 @@ type ExtractedGlobalArgs = {
144
155
  named: Readonly<Record<string, unknown>>;
145
156
  };
146
157
 
158
+ const EXPERIMENTAL_COMMAND_PREFIX = "experimental";
159
+ const EXPERIMENTAL_GROUP_DESCRIPTION = "Experimental commands";
160
+
147
161
  function toCamelCase(input: string): string {
148
162
  return input.replace(/-([a-zA-Z0-9])/g, (_match, letter: string) =>
149
163
  letter.toUpperCase(),
@@ -295,13 +309,23 @@ export class SimpleCLICommandBuilder<
295
309
  TResult,
296
310
  > {
297
311
  constructor(
298
- private readonly definition: NormalizedCommandDefinition<TInput, TContextIn, TContext, TResult>,
312
+ private readonly definition: NormalizedCommandDefinition<
313
+ TInput,
314
+ TContextIn,
315
+ TContext,
316
+ TResult
317
+ >,
299
318
  ) {}
300
319
 
301
320
  input<TNextInput>(
302
321
  input: SimpleCLIInput<TNextInput>,
303
322
  ): SimpleCLICommandBuilder<TNextInput, TContextIn, TContext, TResult> {
304
- return new SimpleCLICommandBuilder<TNextInput, TContextIn, TContext, TResult>({
323
+ return new SimpleCLICommandBuilder<
324
+ TNextInput,
325
+ TContextIn,
326
+ TContext,
327
+ TResult
328
+ >({
305
329
  config: this.definition.config,
306
330
  input,
307
331
  middlewares: this.definition.middlewares,
@@ -332,7 +356,12 @@ export class SimpleCLICommandBuilder<
332
356
  handle<TNextResult>(
333
357
  handler: SimpleCLIHandler<TInput, TContext, TNextResult>,
334
358
  ): SimpleCLICommandBuilder<TInput, TContextIn, TContext, TNextResult> {
335
- return new SimpleCLICommandBuilder<TInput, TContextIn, TContext, TNextResult>({
359
+ return new SimpleCLICommandBuilder<
360
+ TInput,
361
+ TContextIn,
362
+ TContext,
363
+ TNextResult
364
+ >({
336
365
  config: this.definition.config,
337
366
  input: this.definition.input,
338
367
  middlewares: this.definition.middlewares,
@@ -340,7 +369,12 @@ export class SimpleCLICommandBuilder<
340
369
  });
341
370
  }
342
371
 
343
- getDefinition(): NormalizedCommandDefinition<TInput, TContextIn, TContext, TResult> {
372
+ getDefinition(): NormalizedCommandDefinition<
373
+ TInput,
374
+ TContextIn,
375
+ TContext,
376
+ TResult
377
+ > {
344
378
  return this.definition;
345
379
  }
346
380
  }
@@ -358,7 +392,10 @@ export type SimpleCLIGroup<
358
392
  };
359
393
 
360
394
  export class SimpleCLIApp {
361
- private readonly resolvedCommands = new Map<string, InternalResolvedCommand>();
395
+ private readonly resolvedCommands = new Map<
396
+ string,
397
+ InternalResolvedCommand
398
+ >();
362
399
  private readonly resolvedGroups = new Map<string, InternalResolvedGroup>();
363
400
  private readonly routeEntries: InternalResolvedRouteEntry[];
364
401
  private readonly globalNamed: SimpleCLINamedDefinition;
@@ -485,7 +522,9 @@ export class SimpleCLIApp {
485
522
  return [];
486
523
  }
487
524
 
488
- const helpFlagIndex = argsBeforePassthrough.findIndex((arg) => isHelpFlag(arg));
525
+ const helpFlagIndex = argsBeforePassthrough.findIndex((arg) =>
526
+ isHelpFlag(arg),
527
+ );
489
528
  if (helpFlagIndex >= 0) {
490
529
  return argsBeforePassthrough.slice(0, helpFlagIndex);
491
530
  }
@@ -503,7 +542,10 @@ export class SimpleCLIApp {
503
542
  throw new Error(`Unknown command: ${args.join(" ")}`);
504
543
  }
505
544
 
506
- const rawInput = this.parseCommandInput(command, args.slice(command.path.length));
545
+ const rawInput = this.parseCommandInput(
546
+ command,
547
+ args.slice(command.path.length),
548
+ );
507
549
  return {
508
550
  routeKey: command.routeKey,
509
551
  rawInput,
@@ -517,7 +559,9 @@ export class SimpleCLIApp {
517
559
  const inputDefinition = command.input?.getDefinition();
518
560
  if (!inputDefinition) {
519
561
  if (args.length > 0) {
520
- throw new Error(`Unexpected arguments for ${this.name} ${command.path.join(" ")}.`);
562
+ throw new Error(
563
+ `Unexpected arguments for ${this.name} ${command.path.join(" ")}.`,
564
+ );
521
565
  }
522
566
  return {
523
567
  positionals: [],
@@ -537,12 +581,19 @@ export class SimpleCLIApp {
537
581
 
538
582
  if (arg === "--") {
539
583
  if (!passthroughEntry) {
540
- throw new Error(`Unexpected "--" for ${this.name} ${command.path.join(" ")}.`);
584
+ throw new Error(
585
+ `Unexpected "--" for ${this.name} ${command.path.join(" ")}.`,
586
+ );
541
587
  }
542
588
  named["--"] = args.slice(index + 1);
543
589
  break;
544
590
  }
545
591
 
592
+ if (arg === "-") {
593
+ positionals.push(arg);
594
+ continue;
595
+ }
596
+
546
597
  if (arg.startsWith("--")) {
547
598
  const [rawName, inlineValue] = splitNamedArg(arg.slice(2));
548
599
  const namedEntry = namedSpecs.get(rawName);
@@ -592,7 +643,11 @@ export class SimpleCLIApp {
592
643
  positionals.push(arg);
593
644
  }
594
645
 
595
- validateParsedPositionals(command, inputDefinition.positionals, positionals);
646
+ validateParsedPositionals(
647
+ command,
648
+ inputDefinition.positionals,
649
+ positionals,
650
+ );
596
651
  validateRequiredNamedArgs(inputDefinition.named, named);
597
652
 
598
653
  return {
@@ -685,7 +740,9 @@ export class SimpleCLIApp {
685
740
  return rawInput;
686
741
  }
687
742
 
688
- const inputDefinition = this.resolvedCommands.get(routeKey)?.input?.getDefinition();
743
+ const inputDefinition = this.resolvedCommands
744
+ .get(routeKey)
745
+ ?.input?.getDefinition();
689
746
  if (!inputDefinition) {
690
747
  return rawInput;
691
748
  }
@@ -712,7 +769,7 @@ export class SimpleCLIApp {
712
769
 
713
770
  private renderRootHelp(): string {
714
771
  const lines = [`Usage: ${this.name} <command>`, "", "Commands:"];
715
- for (const entry of this.getImmediateRouteEntries([])) {
772
+ for (const entry of this.getRootHelpEntries()) {
716
773
  lines.push(formatListEntry(entry.label, entry.description));
717
774
  }
718
775
  return lines.join("\n");
@@ -756,8 +813,9 @@ export class SimpleCLIApp {
756
813
  lines.push(...argumentLines);
757
814
  }
758
815
 
759
- const optionLines = Object.entries(inputDefinition.named).map(([key, spec]) =>
760
- formatListEntry(buildNamedArgHelpLabel(key, spec), spec.help),
816
+ const optionLines = Object.entries(inputDefinition.named).map(
817
+ ([key, spec]) =>
818
+ formatListEntry(buildNamedArgHelpLabel(key, spec), spec.help),
761
819
  );
762
820
 
763
821
  if (optionLines.length > 0) {
@@ -819,7 +877,52 @@ export class SimpleCLIApp {
819
877
  return entries;
820
878
  }
821
879
 
822
- private findBestMatchingCommand(args: readonly string[]): InternalResolvedCommand | null {
880
+ private getRootHelpEntries(): Array<{
881
+ label: string;
882
+ description?: string;
883
+ }> {
884
+ return this.getImmediateRouteEntries([]).filter((entry) => {
885
+ const token = entry.label.replace(/\s+<subcommand>$/, "");
886
+ const group = this.findGroupByPath([token]);
887
+ if (!group) {
888
+ return true;
889
+ }
890
+ if (token === EXPERIMENTAL_COMMAND_PREFIX) {
891
+ return this.groupHasExperimentalCommand(group.path);
892
+ }
893
+ return this.groupHasVisibleNonExperimentalCommand(group.path);
894
+ });
895
+ }
896
+
897
+ private groupHasVisibleNonExperimentalCommand(path: readonly string[]): boolean {
898
+ for (const routeEntry of this.routeEntries) {
899
+ if (routeEntry.kind !== "command") continue;
900
+ if (!pathStartsWith(routeEntry.path, path)) continue;
901
+ const command = this.findCommandByPath(routeEntry.path);
902
+ if (command && !command.experimental) {
903
+ return true;
904
+ }
905
+ }
906
+
907
+ return false;
908
+ }
909
+
910
+ private groupHasExperimentalCommand(path: readonly string[]): boolean {
911
+ for (const routeEntry of this.routeEntries) {
912
+ if (routeEntry.kind !== "command") continue;
913
+ if (!pathStartsWith(routeEntry.path, path)) continue;
914
+ const command = this.findCommandByPath(routeEntry.path);
915
+ if (command?.experimental) {
916
+ return true;
917
+ }
918
+ }
919
+
920
+ return false;
921
+ }
922
+
923
+ private findBestMatchingCommand(
924
+ args: readonly string[],
925
+ ): InternalResolvedCommand | null {
823
926
  let bestMatch: InternalResolvedCommand | null = null;
824
927
 
825
928
  for (const command of this.resolvedCommands.values()) {
@@ -833,12 +936,16 @@ export class SimpleCLIApp {
833
936
  return bestMatch;
834
937
  }
835
938
 
836
- private findCommandByPath(path: readonly string[]): InternalResolvedCommand | null {
939
+ private findCommandByPath(
940
+ path: readonly string[],
941
+ ): InternalResolvedCommand | null {
837
942
  const routeKey = pathToRouteKey(path);
838
943
  return this.resolvedCommands.get(routeKey) ?? null;
839
944
  }
840
945
 
841
- private findGroupByPath(path: readonly string[]): InternalResolvedGroup | null {
946
+ private findGroupByPath(
947
+ path: readonly string[],
948
+ ): InternalResolvedGroup | null {
842
949
  const routeKey = pathToRouteKey(path);
843
950
  return this.resolvedGroups.get(routeKey) ?? null;
844
951
  }
@@ -847,10 +954,7 @@ export class SimpleCLIApp {
847
954
  function splitNamedArg(arg: string): [string, string | undefined] {
848
955
  const separatorIndex = arg.indexOf("=");
849
956
  if (separatorIndex < 0) return [arg, undefined];
850
- return [
851
- arg.slice(0, separatorIndex),
852
- arg.slice(separatorIndex + 1),
853
- ];
957
+ return [arg.slice(0, separatorIndex), arg.slice(separatorIndex + 1)];
854
958
  }
855
959
 
856
960
  function readNamedArgValue(
@@ -860,7 +964,10 @@ function readNamedArgValue(
860
964
  displayName: string,
861
965
  spec: SimpleCLINamedArgDefinition<ZodTypeAny>,
862
966
  inlineValue: string | undefined,
863
- namedSpecs: ReadonlyMap<string, { key: string; spec: SimpleCLINamedArgDefinition<ZodTypeAny> }>,
967
+ namedSpecs: ReadonlyMap<
968
+ string,
969
+ { key: string; spec: SimpleCLINamedArgDefinition<ZodTypeAny> }
970
+ >,
864
971
  ): unknown {
865
972
  if (spec.kind === "flag") {
866
973
  return inlineValue === undefined
@@ -874,9 +981,9 @@ function readNamedArgValue(
874
981
 
875
982
  const nextValue = args[index + 1];
876
983
  if (
877
- nextValue === undefined
878
- || nextValue === "--"
879
- || isRecognizedNamedArgToken(nextValue, namedSpecs)
984
+ nextValue === undefined ||
985
+ nextValue === "--" ||
986
+ isRecognizedNamedArgToken(nextValue, namedSpecs)
880
987
  ) {
881
988
  throw new Error(`Missing value for ${displayName}.`);
882
989
  }
@@ -886,7 +993,10 @@ function readNamedArgValue(
886
993
 
887
994
  function isRecognizedNamedArgToken(
888
995
  token: string,
889
- namedSpecs: ReadonlyMap<string, { key: string; spec: SimpleCLINamedArgDefinition<ZodTypeAny> }>,
996
+ namedSpecs: ReadonlyMap<
997
+ string,
998
+ { key: string; spec: SimpleCLINamedArgDefinition<ZodTypeAny> }
999
+ >,
890
1000
  ): boolean {
891
1001
  if (token === "-" || !token.startsWith("-")) {
892
1002
  return false;
@@ -899,11 +1009,13 @@ function isRecognizedNamedArgToken(
899
1009
  return namedSpecs.has(rawName);
900
1010
  }
901
1011
 
902
- function buildNamedArgLookup(namedDefinition: SimpleCLINamedDefinition): Map<
903
- string,
904
- { key: string; spec: SimpleCLINamedArgDefinition<ZodTypeAny> }
905
- > {
906
- const lookup = new Map<string, { key: string; spec: SimpleCLINamedArgDefinition<ZodTypeAny> }>();
1012
+ function buildNamedArgLookup(
1013
+ namedDefinition: SimpleCLINamedDefinition,
1014
+ ): Map<string, { key: string; spec: SimpleCLINamedArgDefinition<ZodTypeAny> }> {
1015
+ const lookup = new Map<
1016
+ string,
1017
+ { key: string; spec: SimpleCLINamedArgDefinition<ZodTypeAny> }
1018
+ >();
907
1019
 
908
1020
  for (const [key, spec] of Object.entries(namedDefinition)) {
909
1021
  if (spec.source === "--") continue;
@@ -926,7 +1038,9 @@ function validateParsedPositionals(
926
1038
  definitions: SimpleCLIPositionalsDefinition,
927
1039
  positionals: readonly string[],
928
1040
  ): void {
929
- const variadicDefinition = definitions.find((definition) => definition.variadic);
1041
+ const variadicDefinition = definitions.find(
1042
+ (definition) => definition.variadic,
1043
+ );
930
1044
  if (!variadicDefinition && positionals.length > definitions.length) {
931
1045
  throw new Error(`Unexpected arguments for ${command.path.join(" ")}.`);
932
1046
  }
@@ -935,17 +1049,22 @@ function validateParsedPositionals(
935
1049
  const value = definition.variadic
936
1050
  ? positionals.slice(index)
937
1051
  : positionals[index];
938
- if (value !== undefined && (!Array.isArray(value) || value.length > 0)) return;
1052
+ if (value !== undefined && (!Array.isArray(value) || value.length > 0))
1053
+ return;
939
1054
  if (schemaAcceptsUndefined(definition.schema)) return;
940
1055
  throw new Error(`Missing required argument <${definition.key}>.`);
941
1056
  });
942
1057
  }
943
1058
 
944
1059
  function validateInputDefinition(definition: SimpleCLIInputDefinition): void {
945
- const variadicIndex = definition.positionals.findIndex((positional) => positional.variadic);
1060
+ const variadicIndex = definition.positionals.findIndex(
1061
+ (positional) => positional.variadic,
1062
+ );
946
1063
  if (variadicIndex < 0) return;
947
1064
  if (variadicIndex !== definition.positionals.length - 1) {
948
- throw new Error("Variadic positional arguments must be the last positional.");
1065
+ throw new Error(
1066
+ "Variadic positional arguments must be the last positional.",
1067
+ );
949
1068
  }
950
1069
  }
951
1070
 
@@ -955,7 +1074,8 @@ function validateRequiredNamedArgs(
955
1074
  ): void {
956
1075
  for (const [key, spec] of Object.entries(definitions)) {
957
1076
  if (schemaAcceptsUndefined(spec.schema)) continue;
958
- const flagName = spec.source === "--" ? "--" : buildNamedArgFlagName(key, spec);
1077
+ const flagName =
1078
+ spec.source === "--" ? "--" : buildNamedArgFlagName(key, spec);
959
1079
  if (Object.prototype.hasOwnProperty.call(named, flagName)) continue;
960
1080
  if (spec.source === "--") {
961
1081
  throw new Error(`Missing required passthrough arguments after --.`);
@@ -964,66 +1084,128 @@ function validateRequiredNamedArgs(
964
1084
  }
965
1085
  }
966
1086
 
967
- function resolveRouteTree(
1087
+ function resolveRouteTree(routes: SimpleCLIRouteTree<any>): ResolveRouteTreeResult {
1088
+ const collected = collectRouteTree(routes);
1089
+ const { groups, routeEntries } = buildResolvedRouteEntries(
1090
+ collected.commands,
1091
+ collected.groupDescriptions,
1092
+ );
1093
+
1094
+ return {
1095
+ commands: collected.commands,
1096
+ groups,
1097
+ routeEntries,
1098
+ };
1099
+ }
1100
+
1101
+ function collectRouteTree(
968
1102
  routes: SimpleCLIRouteTree<any>,
969
1103
  parentPath: readonly string[] = [],
970
1104
  parentMiddlewares: readonly AnySimpleCLIMiddleware[] = [],
971
- ): ResolveRouteTreeResult {
972
- const resolved: ResolveRouteTreeResult = {
1105
+ ): CollectedRouteTreeResult {
1106
+ const resolved: CollectedRouteTreeResult = {
973
1107
  commands: [],
974
- groups: [],
975
- routeEntries: [],
1108
+ groupDescriptions: new Map<string, string | undefined>(),
976
1109
  };
977
1110
 
978
1111
  for (const [token, routeValue] of Object.entries(routes)) {
979
1112
  if (isGroup(routeValue)) {
980
1113
  const groupPath = [...parentPath, token];
981
- resolved.groups.push({
982
- routeKey: pathToRouteKey(groupPath),
983
- path: groupPath,
984
- description: routeValue.description,
985
- });
986
- resolved.routeEntries.push({
987
- kind: "group",
988
- path: groupPath,
989
- });
990
-
991
- const nested = resolveRouteTree(
992
- routeValue.routes,
993
- groupPath,
994
- [...parentMiddlewares, ...routeValue.middlewares],
1114
+ resolved.groupDescriptions.set(
1115
+ pathToRouteKey(groupPath),
1116
+ routeValue.description,
995
1117
  );
1118
+
1119
+ const nested = collectRouteTree(routeValue.routes, groupPath, [
1120
+ ...parentMiddlewares,
1121
+ ...routeValue.middlewares,
1122
+ ]);
996
1123
  resolved.commands.push(...nested.commands);
997
- resolved.groups.push(...nested.groups);
998
- resolved.routeEntries.push(...nested.routeEntries);
1124
+ for (const [routeKey, description] of nested.groupDescriptions) {
1125
+ resolved.groupDescriptions.set(routeKey, description);
1126
+ }
999
1127
  continue;
1000
1128
  }
1001
1129
 
1002
1130
  const command = routeValue.getDefinition();
1003
1131
  if (!command.handler) {
1004
- throw new Error(`Command "${[...parentPath, token].join(" ")}" is missing a handler.`);
1132
+ throw new Error(
1133
+ `Command "${[...parentPath, token].join(" ")}" is missing a handler.`,
1134
+ );
1005
1135
  }
1006
1136
 
1007
- const path = [...parentPath, token];
1137
+ const rawPath = [...parentPath, token];
1138
+ const path = command.config.experimental
1139
+ ? [EXPERIMENTAL_COMMAND_PREFIX, ...rawPath]
1140
+ : rawPath;
1008
1141
  resolved.commands.push({
1009
1142
  routeKey: pathToRouteKey(path),
1010
1143
  path,
1011
1144
  description: command.config.description,
1145
+ experimental: command.config.experimental,
1012
1146
  input: command.input,
1013
- middlewares: mergeInheritedMiddlewares(parentMiddlewares, command.middlewares),
1147
+ middlewares: mergeInheritedMiddlewares(
1148
+ parentMiddlewares,
1149
+ command.middlewares,
1150
+ ),
1014
1151
  handler: command.handler as unknown as SimpleCLIHandler<
1015
1152
  unknown,
1016
1153
  SimpleCLIContext,
1017
1154
  unknown
1018
1155
  >,
1019
1156
  });
1020
- resolved.routeEntries.push({
1157
+ }
1158
+
1159
+ return resolved;
1160
+ }
1161
+
1162
+ function buildResolvedRouteEntries(
1163
+ commands: readonly InternalResolvedCommand[],
1164
+ groupDescriptions: ReadonlyMap<string, string | undefined>,
1165
+ ): Pick<ResolveRouteTreeResult, "groups" | "routeEntries"> {
1166
+ const groups = new Map<string, InternalResolvedGroup>();
1167
+ const routeEntries: InternalResolvedRouteEntry[] = [];
1168
+
1169
+ for (const command of commands) {
1170
+ for (let depth = 1; depth < command.path.length; depth += 1) {
1171
+ const path = command.path.slice(0, depth);
1172
+ const routeKey = pathToRouteKey(path);
1173
+ if (groups.has(routeKey)) continue;
1174
+
1175
+ groups.set(routeKey, {
1176
+ routeKey,
1177
+ path,
1178
+ description: resolveGroupDescription(path, groupDescriptions),
1179
+ });
1180
+ routeEntries.push({
1181
+ kind: "group",
1182
+ path,
1183
+ });
1184
+ }
1185
+
1186
+ routeEntries.push({
1021
1187
  kind: "command",
1022
- path,
1188
+ path: command.path,
1023
1189
  });
1024
1190
  }
1025
1191
 
1026
- return resolved;
1192
+ return {
1193
+ groups: [...groups.values()],
1194
+ routeEntries,
1195
+ };
1196
+ }
1197
+
1198
+ function resolveGroupDescription(
1199
+ path: readonly string[],
1200
+ groupDescriptions: ReadonlyMap<string, string | undefined>,
1201
+ ): string | undefined {
1202
+ if (path.length === 1 && path[0] === EXPERIMENTAL_COMMAND_PREFIX) {
1203
+ return EXPERIMENTAL_GROUP_DESCRIPTION;
1204
+ }
1205
+
1206
+ const originalPath =
1207
+ path[0] === EXPERIMENTAL_COMMAND_PREFIX ? path.slice(1) : path;
1208
+ return groupDescriptions.get(pathToRouteKey(originalPath));
1027
1209
  }
1028
1210
 
1029
1211
  function mergeInheritedMiddlewares(
@@ -1035,8 +1217,10 @@ function mergeInheritedMiddlewares(
1035
1217
  }
1036
1218
 
1037
1219
  if (
1038
- commandMiddlewares.length >= parentMiddlewares.length
1039
- && parentMiddlewares.every((middleware, index) => commandMiddlewares[index] === middleware)
1220
+ commandMiddlewares.length >= parentMiddlewares.length &&
1221
+ parentMiddlewares.every(
1222
+ (middleware, index) => commandMiddlewares[index] === middleware,
1223
+ )
1040
1224
  ) {
1041
1225
  return [...commandMiddlewares];
1042
1226
  }
@@ -1053,12 +1237,10 @@ function isGroup(
1053
1237
  function buildInputNormalizer<
1054
1238
  TPositionals extends SimpleCLIPositionalsDefinition,
1055
1239
  TNamed extends SimpleCLINamedDefinition,
1056
- >(
1057
- definition: {
1058
- positionals: TPositionals;
1059
- named: TNamed;
1060
- },
1061
- ): (raw: SimpleCLIInputRaw) => InputObjectFor<TPositionals, TNamed> {
1240
+ >(definition: {
1241
+ positionals: TPositionals;
1242
+ named: TNamed;
1243
+ }): (raw: SimpleCLIInputRaw) => InputObjectFor<TPositionals, TNamed> {
1062
1244
  return (raw) => {
1063
1245
  const output: RecordUnknown = {};
1064
1246
  const positionals = raw.positionals ?? [];
@@ -1077,10 +1259,7 @@ function buildInputNormalizer<
1077
1259
  spec.name ? toCamelCase(spec.name) : "",
1078
1260
  ...(spec.aliases ?? []).flatMap((alias) => {
1079
1261
  const normalizedAlias = normalizeNamedArgToken(alias);
1080
- return [
1081
- normalizedAlias,
1082
- toCamelCase(normalizedAlias),
1083
- ];
1262
+ return [normalizedAlias, toCamelCase(normalizedAlias)];
1084
1263
  }),
1085
1264
  toKebabCase(key),
1086
1265
  key,
@@ -1103,12 +1282,10 @@ function buildInputNormalizer<
1103
1282
  function buildInputSchema<
1104
1283
  TPositionals extends SimpleCLIPositionalsDefinition,
1105
1284
  TNamed extends SimpleCLINamedDefinition,
1106
- >(
1107
- definition: {
1108
- positionals: TPositionals;
1109
- named: TNamed;
1110
- },
1111
- ): z.ZodType<InputObjectFor<TPositionals, TNamed>, unknown> {
1285
+ >(definition: {
1286
+ positionals: TPositionals;
1287
+ named: TNamed;
1288
+ }): z.ZodType<InputObjectFor<TPositionals, TNamed>, unknown> {
1112
1289
  const shape: Record<string, ZodTypeAny> = {};
1113
1290
 
1114
1291
  for (const positional of definition.positionals) {
@@ -1140,7 +1317,12 @@ function positional<TKey extends string, TSchema extends ZodTypeAny>(
1140
1317
 
1141
1318
  function option<TSchema extends ZodTypeAny>(
1142
1319
  schema: TSchema,
1143
- options?: { help?: string; name?: string; aliases?: readonly string[]; source?: "--" },
1320
+ options?: {
1321
+ help?: string;
1322
+ name?: string;
1323
+ aliases?: readonly string[];
1324
+ source?: "--";
1325
+ },
1144
1326
  ): SimpleCLINamedArgDefinition<TSchema> {
1145
1327
  return {
1146
1328
  kind: "option",
@@ -1152,9 +1334,11 @@ function option<TSchema extends ZodTypeAny>(
1152
1334
  };
1153
1335
  }
1154
1336
 
1155
- function flag(
1156
- options?: { help?: string; name?: string; aliases?: readonly string[] },
1157
- ): SimpleCLINamedArgDefinition<z.ZodDefault<z.ZodBoolean>> {
1337
+ function flag(options?: {
1338
+ help?: string;
1339
+ name?: string;
1340
+ aliases?: readonly string[];
1341
+ }): SimpleCLINamedArgDefinition<z.ZodDefault<z.ZodBoolean>> {
1158
1342
  return {
1159
1343
  kind: "flag",
1160
1344
  schema: z.boolean().default(false),
@@ -1184,9 +1368,11 @@ type SimpleCLIScope<
1184
1368
  TContext extends SimpleCLIContext,
1185
1369
  > = {
1186
1370
  use<TContextOut extends SimpleCLIContext>(
1187
- middleware: SimpleCLIMiddleware<unknown, TContext, TContextOut>
1371
+ middleware: SimpleCLIMiddleware<unknown, TContext, TContextOut>,
1188
1372
  ): SimpleCLIScope<TParentContext, TContextOut>;
1189
- group(config: SimpleCLIGroupConfig<TContext>): SimpleCLIGroup<TParentContext, TContext>;
1373
+ group(
1374
+ config: SimpleCLIGroupConfig<TContext>,
1375
+ ): SimpleCLIGroup<TParentContext, TContext>;
1190
1376
  command(
1191
1377
  config: SimpleCLICommandConfig,
1192
1378
  ): SimpleCLICommandBuilder<unknown, TParentContext, TContext, unknown>;
@@ -1201,9 +1387,7 @@ function command(
1201
1387
  });
1202
1388
  }
1203
1389
 
1204
- function group(
1205
- config: SimpleCLIGroupConfig<{}>,
1206
- ): SimpleCLIGroup<{}, {}> {
1390
+ function group(config: SimpleCLIGroupConfig<{}>): SimpleCLIGroup<{}, {}> {
1207
1391
  return createScope<{}, {}>([]).group(config);
1208
1392
  }
1209
1393
 
@@ -1222,7 +1406,9 @@ function createScope<
1222
1406
  middleware,
1223
1407
  ]);
1224
1408
  },
1225
- group(config: SimpleCLIGroupConfig<TContext>): SimpleCLIGroup<TParentContext, TContext> {
1409
+ group(
1410
+ config: SimpleCLIGroupConfig<TContext>,
1411
+ ): SimpleCLIGroup<TParentContext, TContext> {
1226
1412
  return {
1227
1413
  kind: "group",
1228
1414
  description: config.description,
@@ -1255,11 +1441,8 @@ function define(
1255
1441
  return new SimpleCLIApp(name, routes, config);
1256
1442
  }
1257
1443
 
1258
- export type InferInput<TInput extends SimpleCLIInput<unknown>> = TInput extends SimpleCLIInput<
1259
- infer TOutput
1260
- >
1261
- ? TOutput
1262
- : never;
1444
+ export type InferInput<TInput extends SimpleCLIInput<unknown>> =
1445
+ TInput extends SimpleCLIInput<infer TOutput> ? TOutput : never;
1263
1446
 
1264
1447
  export const SimpleCLI = {
1265
1448
  define,