@appilots/cli 0.1.0 → 0.4.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/dist/index.d.ts CHANGED
@@ -519,6 +519,326 @@ declare class FormAnalyzer {
519
519
  private isFieldRequired;
520
520
  }
521
521
 
522
+ /**
523
+ * Options that shape how a `PlatformAnalyzer` walks a project. These mirror
524
+ * the subset of `MCPGeneratorConfig` that today's React Native analyzer
525
+ * set already accepts — kept as a separate options bag (instead of the
526
+ * generator's whole config) so a future non-RN analyzer only needs to
527
+ * implement what's relevant to it.
528
+ */
529
+ interface PlatformAnalyzerOptions {
530
+ /** §1: Only include files with registerScreen() or matching screen patterns. */
531
+ strictScreens?: boolean;
532
+ /** §1: Custom glob patterns for screen file detection. */
533
+ screenPatterns?: string[];
534
+ /** §5: Additional navigation file patterns (added to defaults). */
535
+ navigationInclude?: string[];
536
+ /** §5: Exclude patterns for navigation analysis. */
537
+ navigationExclude?: string[];
538
+ }
539
+ interface PlatformAnalyzerResult {
540
+ screens: ScreenDescriptor[];
541
+ navigation: NavigationGraph;
542
+ /** Number of source files matched by glob (before screen filtering). */
543
+ analyzedFiles: number;
544
+ /** Number of files excluded by strict screen filtering, when applicable. */
545
+ screensFilteredOut?: number;
546
+ }
547
+ /**
548
+ * Seam `MCPGenerator` consumes to turn a project's source tree into
549
+ * screens + a navigation graph. The React Native implementation
550
+ * (`ReactNativePlatformAnalyzer`) wraps the existing Babel/JSX analyzer
551
+ * pipeline (`ScreenAnalyzer` / `NavigationAnalyzer` / `ComponentAnalyzer` /
552
+ * `FormAnalyzer`) unchanged; a platform with no source-level analyzer yet
553
+ * (or a project with no JS/TSX at all) can use `GenericPlatformAnalyzer`,
554
+ * which is a safe no-op — screens then come entirely from the declared
555
+ * manifest (`appilots.manifest.json`, see `../manifest/loadManifest.ts`).
556
+ *
557
+ * `MCPGenerator` selects an implementation based on `.appilotsrc`'s
558
+ * `platform` field (default `'react-native'`) and merges its output with
559
+ * any declared manifest — the manifest wins on a screen-name conflict.
560
+ */
561
+ interface PlatformAnalyzer {
562
+ /** Platform identifier this analyzer implements, e.g. `'react-native'`. */
563
+ readonly platform: string;
564
+ analyze(config: AnalyzerConfig, options: PlatformAnalyzerOptions): Promise<PlatformAnalyzerResult>;
565
+ }
566
+
567
+ /**
568
+ * Default `PlatformAnalyzer` implementation — the React Native Babel/JSX
569
+ * analyzer pipeline that existed inline in `MCPGenerator` before the
570
+ * platform-analyzer seam was introduced. This is a straight move: the
571
+ * orchestration (run `ScreenAnalyzer` + `NavigationAnalyzer`, glob every
572
+ * source file, run `ComponentAnalyzer` + `FormAnalyzer` per file, merge
573
+ * their output into each screen's forms/components) is unchanged, so
574
+ * output for existing React Native projects is byte-for-byte identical.
575
+ */
576
+ declare class ReactNativePlatformAnalyzer implements PlatformAnalyzer {
577
+ readonly platform = "react-native";
578
+ analyze(config: AnalyzerConfig, options: PlatformAnalyzerOptions): Promise<PlatformAnalyzerResult>;
579
+ private mergeForm;
580
+ private findEquivalentField;
581
+ private mergeField;
582
+ private namedSubmitAction;
583
+ private isWeakInferredFieldName;
584
+ private findFormWithSharedFields;
585
+ }
586
+
587
+ /**
588
+ * `PlatformAnalyzer` for a platform with no source-level analyzer yet
589
+ * (anything other than `'react-native'` — Android/Kotlin, iOS/Swift, a
590
+ * web SPA, ...). It does zero filesystem work — no glob, no parsing — so
591
+ * it's safe to run on a project that has no JS/TSX at all. Screens for
592
+ * these platforms come entirely from the declared manifest
593
+ * (`appilots.manifest.json`); `MCPGenerator` merges the manifest on top
594
+ * of whatever this analyzer returns (nothing).
595
+ *
596
+ * This keeps the manifest-only integration path (deliverable §2) fully
597
+ * decoupled from `react-native`-specific tooling: a new platform can
598
+ * ship its own generator days before anyone writes a real AST analyzer
599
+ * for it.
600
+ */
601
+ declare class GenericPlatformAnalyzer implements PlatformAnalyzer {
602
+ readonly platform: string;
603
+ constructor(platform: string);
604
+ analyze(_config: AnalyzerConfig, _options: PlatformAnalyzerOptions): Promise<PlatformAnalyzerResult>;
605
+ }
606
+
607
+ /**
608
+ * `PlatformAnalyzer` for React web apps (`.appilotsrc` `platform:
609
+ * "web"`) — source-level inference instead of the manifest-only path
610
+ * `GenericPlatformAnalyzer` provides. Orchestrates:
611
+ *
612
+ * - `WebScreenAnalyzer` — screens, forms, actions, collections from
613
+ * DOM-vocabulary JSX (`onClick`, `data-testid`, `type=`, ...)
614
+ * - `WebNavigationAnalyzer` — a navigation graph from React Router
615
+ * configuration (`createBrowserRouter`, `<Route path element>`)
616
+ *
617
+ * The screen analyzer records navigation targets as ROUTE PATHS
618
+ * (`/vehicles/new`); this orchestrator owns the route table and
619
+ * resolves them to screen names, leaving unresolved paths as-is (still
620
+ * useful to the agent, and valid against the wire schema).
621
+ *
622
+ * Strict-screen filtering (on by default, like RN): a file counts as a
623
+ * screen when it calls `registerScreen()`, matches a screen glob
624
+ * (web defaults: `pages/`, `routes/`, `views/`, `app/`, `*Page`/
625
+ * `*Screen`/`*View`), or its component is rendered by a detected route
626
+ * — that last rule is what "the detected route patterns" adds over
627
+ * static globs.
628
+ *
629
+ * A declared `appilots.manifest.json` still merges ON TOP of this
630
+ * analyzer's output in `MCPGenerator` (manifest wins on a screen-name
631
+ * conflict) — same precedence as every other platform.
632
+ */
633
+ declare class ReactWebPlatformAnalyzer implements PlatformAnalyzer {
634
+ readonly platform = "web";
635
+ analyze(config: AnalyzerConfig, options: PlatformAnalyzerOptions): Promise<PlatformAnalyzerResult>;
636
+ private isScreen;
637
+ /** Replace route-path references with screen names where the route table resolves them. */
638
+ private resolveRoutePaths;
639
+ }
640
+
641
+ /**
642
+ * One analyzed source file, before strict-screen filtering. The
643
+ * orchestrator (`ReactWebPlatformAnalyzer`) decides inclusion — it also
644
+ * knows the route table, so a file can qualify as a screen by being a
645
+ * route component even when it matches no glob pattern.
646
+ */
647
+ interface WebScreenCandidate {
648
+ descriptor: ScreenDescriptor;
649
+ hasRegisterScreen: boolean;
650
+ matchesScreenPattern: boolean;
651
+ }
652
+ interface WebScreenAnalysis {
653
+ candidates: WebScreenCandidate[];
654
+ /** Files matched by the include globs (before screen filtering). */
655
+ analyzedFiles: number;
656
+ }
657
+ /**
658
+ * Default glob patterns that identify screen files in a web project —
659
+ * the RN defaults (`*Screen.tsx`, `screens/**`) don't match how web
660
+ * apps are laid out, so the web dialect looks at the conventional
661
+ * page/route/view directories instead.
662
+ */
663
+ declare const DEFAULT_WEB_SCREEN_PATTERNS: string[];
664
+ /**
665
+ * Web (DOM/JSX) sibling of `ScreenAnalyzer`. Same output contract
666
+ * (`ScreenDescriptor`), web-native vocabulary:
667
+ *
668
+ * - actions come from `onClick`/`onSubmit` (not `onPress`)
669
+ * - locators come from `data-testid`, `id`, `name`, `aria-label`, and
670
+ * `<label htmlFor>` association (plus `appilotsId`, shared with RN)
671
+ * - field types come from `type=` / `inputMode=` (not `keyboardType`)
672
+ * - navigation targets come from React Router (`useNavigate`'s
673
+ * `navigate('/x')`, `<Link to>`, `<Navigate to>`, internal `href`s);
674
+ * they are recorded as ROUTE PATHS here and resolved to screen names
675
+ * by the orchestrator, which owns the route table
676
+ */
677
+ declare class WebScreenAnalyzer {
678
+ private config;
679
+ private verbose;
680
+ private screenPatterns;
681
+ constructor(config: AnalyzerConfig, options?: {
682
+ screenPatterns?: string[];
683
+ });
684
+ analyze(): Promise<WebScreenAnalysis>;
685
+ analyzeFile(filePath: string): Promise<WebScreenCandidate | null>;
686
+ private detectRegisterScreenCall;
687
+ private extractRegisterScreenMetadata;
688
+ /** Default-exported component name, else the first exported capitalized function. */
689
+ private extractComponentName;
690
+ private extractComponents;
691
+ private collectHtmlForLabels;
692
+ private extractForms;
693
+ private isSubmitElement;
694
+ private extractField;
695
+ private inferInputType;
696
+ private extractSelectOptions;
697
+ private mergeForms;
698
+ private extractActions;
699
+ private extractActionFromElement;
700
+ private mergeActionMetadata;
701
+ private enrichActionsFromHandlers;
702
+ /**
703
+ * Handler behavior via the shared, platform-neutral analyzer
704
+ * (async/await, `.then`, state setters, toasts, destructive verbs)
705
+ * plus the web-only signals: React Router navigation targets and
706
+ * `window.confirm(...)` as the native confirmation dialog.
707
+ */
708
+ private collectWebHandlerBehaviors;
709
+ private handlerNameFromAttr;
710
+ private extractNavigationTargets;
711
+ private extractCollections;
712
+ }
713
+
714
+ /**
715
+ * A single resolved React Router route: its full URL path and the screen
716
+ * (component) it renders. This is the web analog of a React Navigation
717
+ * `<Stack.Screen name=...>` entry.
718
+ */
719
+ interface WebRoute {
720
+ /** Full path with parent segments joined, e.g. `/vehicles/:id`. */
721
+ path: string;
722
+ /** Screen name — the route component's name, or derived from the path. */
723
+ screenName: string;
724
+ /** Path params extracted from `:segment` placeholders. */
725
+ params?: ParamDescriptor[];
726
+ /** True for `<Route index>` / `{ index: true }` routes. */
727
+ index?: boolean;
728
+ /**
729
+ * True when this route has child routes — it renders a layout wrapper
730
+ * around them, not a page of its own. A layout route loses to its own
731
+ * `index` child when both resolve to the same path, since the index
732
+ * child is what the user actually lands on.
733
+ */
734
+ layout?: boolean;
735
+ }
736
+ interface WebNavigationResult {
737
+ graph: NavigationGraph;
738
+ routes: WebRoute[];
739
+ }
740
+ /**
741
+ * Analyzes React Router route configuration to build a navigation graph
742
+ * — the sibling of `NavigationAnalyzer` (which is 100% react-navigation
743
+ * and never runs for `platform: 'web'`). Understands both declaration
744
+ * styles:
745
+ *
746
+ * - JSX: `<Routes><Route path="/x" element={<X/>}/></Routes>` (nested
747
+ * `<Route>` children join their parent's path)
748
+ * - Object config: `createBrowserRouter([{ path, element, children }])`
749
+ * (also `createHashRouter`, `createMemoryRouter`, `useRoutes`)
750
+ */
751
+ declare class WebNavigationAnalyzer {
752
+ private config;
753
+ private navigationInclude;
754
+ private navigationExclude;
755
+ constructor(config: AnalyzerConfig, options?: {
756
+ navigationInclude?: string[];
757
+ navigationExclude?: string[];
758
+ });
759
+ analyze(): Promise<WebNavigationResult>;
760
+ /** Files likely to contain route configuration. */
761
+ private findRouteFiles;
762
+ private extractJsxRoutes;
763
+ /** `element={<VehicleList/>}` or `Component={VehicleList}`. */
764
+ private componentNameFromElementAttr;
765
+ private extractObjectRoutes;
766
+ private visitRouteObjects;
767
+ private joinPaths;
768
+ private normalizePath;
769
+ private buildRoute;
770
+ private paramsFromPath;
771
+ /**
772
+ * One entry per path. When several declarations resolve to the same
773
+ * path, keep the one that best describes what the user lands on: a
774
+ * page beats a layout wrapper (an `index` child and its parent layout
775
+ * share a path), and a resolved component name beats a name derived
776
+ * from the path.
777
+ */
778
+ private dedupeRoutes;
779
+ private buildGraph;
780
+ }
781
+ /** `/vehicles/:id/edit` → `VehiclesIdEdit`; `/` → `Home`. */
782
+ declare function screenNameFromPath(routePath: string): string;
783
+ /**
784
+ * Match a navigated-to concrete path (e.g. `/vehicles/123`) against the
785
+ * route table, honoring `:param` segments. Returns the matched screen
786
+ * name, or undefined.
787
+ */
788
+ declare function resolvePathToScreen(routes: WebRoute[], target: string): string | undefined;
789
+
790
+ /**
791
+ * Default manifest filename, resolved relative to the project's
792
+ * `rootDir` unless `.appilotsrc`'s `manifestPath` overrides it.
793
+ */
794
+ declare const DEFAULT_MANIFEST_FILENAME = "appilots.manifest.json";
795
+ /**
796
+ * A declared, hand-authored (or externally generated) application
797
+ * structure map — the manifest-mode integration path for any platform
798
+ * that doesn't have a source-level `PlatformAnalyzer` yet. Screens here
799
+ * use the exact same `ScreenDescriptor` shape the AST analyzers produce
800
+ * (validated against `@appilots/shared`'s `screenDescriptorSchema`, the
801
+ * same schema the formal `MCPDocument` wire contract uses), so a
802
+ * manifest and an AST-derived screen merge without any shape
803
+ * translation.
804
+ */
805
+ interface AppilotsManifest {
806
+ /** Informational; not the MCPDocument's own `version` field. */
807
+ version?: string;
808
+ screens: ScreenDescriptor[];
809
+ /** Optional — merged into the analyzer-derived navigation graph. */
810
+ navigation?: Partial<NavigationGraph>;
811
+ }
812
+ interface LoadManifestResult {
813
+ /** `null` when no manifest file exists at the resolved path — not an error. */
814
+ manifest: AppilotsManifest | null;
815
+ /** Absolute path the manifest was (or would be) read from. */
816
+ resolvedPath: string;
817
+ }
818
+ /**
819
+ * Resolve and load `appilots.manifest.json` (or `manifestPath` from
820
+ * `.appilotsrc`) from `rootDir`. Returns `{ manifest: null }` when the
821
+ * file doesn't exist — manifest mode is opt-in, not required — but
822
+ * throws a descriptive error when the file exists and is unparseable
823
+ * JSON or fails `screenDescriptorSchema`/`navigationGraphSchema`
824
+ * validation, so a typo'd manifest fails loudly instead of silently
825
+ * contributing zero screens.
826
+ */
827
+ declare function loadManifest(rootDir: string, manifestPath?: string): Promise<LoadManifestResult>;
828
+ /**
829
+ * Merge a declared manifest's screens on top of analyzer-derived screens.
830
+ * The manifest wins on a screen-name conflict (it's the developer's
831
+ * explicit, authoritative statement); screens only one side knows about
832
+ * pass through unchanged.
833
+ */
834
+ declare function mergeManifestScreens(analyzerScreens: ScreenDescriptor[], manifestScreens: ScreenDescriptor[] | undefined): ScreenDescriptor[];
835
+ /**
836
+ * Merge a declared manifest's (partial) navigation graph on top of the
837
+ * analyzer-derived one. Manifest entries win per-key; arrays are
838
+ * concatenated (manifest navigators appended after analyzer ones).
839
+ */
840
+ declare function mergeManifestNavigation(analyzerNavigation: NavigationGraph, manifestNavigation: Partial<NavigationGraph> | undefined): NavigationGraph;
841
+
522
842
  interface MCPGeneratorOptions {
523
843
  /** Output format (only 'json' supported currently) */
524
844
  format: 'json';
@@ -596,12 +916,44 @@ interface MCPGeneratorConfig {
596
916
  include?: string[];
597
917
  /** Source exclude patterns */
598
918
  exclude?: string[];
919
+ /**
920
+ * Which `PlatformAnalyzer` to run against the source tree. Defaults to
921
+ * `'react-native'` (the existing Babel/JSX analyzer pipeline). `'web'`
922
+ * runs `ReactWebPlatformAnalyzer` (DOM-vocabulary JSX + React Router).
923
+ * Any other value runs `GenericPlatformAnalyzer` — a safe no-op that
924
+ * does no filesystem/AST work — so a platform with no source analyzer
925
+ * yet still produces a valid document from a declared manifest alone.
926
+ */
927
+ platform?: string;
928
+ /**
929
+ * Optional custom analyzer instance, mainly for tests. When set,
930
+ * `platform` is ignored for analyzer selection (but still recorded).
931
+ */
932
+ platformAnalyzer?: PlatformAnalyzer;
933
+ /**
934
+ * Path to a declared `appilots.manifest.json`, relative to `rootDir`
935
+ * unless absolute. Defaults to `appilots.manifest.json` at the project
936
+ * root. Manifest screens merge on top of analyzer-derived screens,
937
+ * winning on a screen-name conflict. Missing file is not an error —
938
+ * manifest mode is opt-in.
939
+ */
940
+ manifestPath?: string;
599
941
  }
600
942
  declare class MCPGenerator {
601
943
  private analyzerConfig;
602
944
  private options;
603
945
  private generatorConfig;
946
+ private platformAnalyzer;
604
947
  constructor(config: MCPGeneratorConfig);
948
+ /**
949
+ * Select the `PlatformAnalyzer` implementation for a `.appilotsrc`
950
+ * `platform` value. `'react-native'` (or unset — the existing default)
951
+ * gets the RN Babel/JSX pipeline; `'web'` gets the React web (DOM +
952
+ * React Router) pipeline; anything else gets the no-op generic
953
+ * analyzer, relying entirely on a declared manifest. The manifest
954
+ * still merges on top of every analyzer's output either way.
955
+ */
956
+ static createPlatformAnalyzer(platform?: string): PlatformAnalyzer;
605
957
  /** Generate MCP documents from the project */
606
958
  generate(): Promise<MCPOutput>;
607
959
  /**
@@ -617,12 +969,6 @@ declare class MCPGenerator {
617
969
  * Calculate SHA-256 checksum of content
618
970
  */
619
971
  private calculateChecksum;
620
- private mergeForm;
621
- private findEquivalentField;
622
- private mergeField;
623
- private namedSubmitAction;
624
- private isWeakInferredFieldName;
625
- private findFormWithSharedFields;
626
972
  /**
627
973
  * Get project name and version from package.json in rootDir
628
974
  */
@@ -672,6 +1018,28 @@ interface AppilotsConfig {
672
1018
  * Glob patterns to exclude from navigation analysis (§5).
673
1019
  */
674
1020
  navigationExclude?: string[];
1021
+ /**
1022
+ * Target platform for source analysis (multi-platform prep). Selects
1023
+ * which `PlatformAnalyzer` the generator runs and its default globs.
1024
+ * `'react-native'` (the default when unset) runs the existing
1025
+ * Babel/JSX analyzer pipeline. `'web'` runs the React web analyzer
1026
+ * (DOM-vocabulary JSX + React Router route discovery). Any other
1027
+ * value (`'android'`, `'ios'`, a custom platform id, ...) runs a
1028
+ * no-op analyzer that does no source parsing — screens for that
1029
+ * platform come entirely from a declared manifest (see
1030
+ * `manifestPath`). Forward-compatible: unknown values degrade
1031
+ * gracefully instead of erroring.
1032
+ */
1033
+ platform?: string;
1034
+ /**
1035
+ * Path to a declared `appilots.manifest.json`, relative to the
1036
+ * project root unless absolute. Screens declared here (in the same
1037
+ * `ScreenDescriptor` shape the analyzers produce) merge on top of
1038
+ * analyzer-derived screens, winning on a name conflict. Default:
1039
+ * `appilots.manifest.json` at the project root. A missing file is not
1040
+ * an error — manifest mode is opt-in.
1041
+ */
1042
+ manifestPath?: string;
675
1043
  /** `appilots eval` settings — see docs: Testing your agent between releases. */
676
1044
  eval?: EvalConfig;
677
1045
  }
@@ -888,4 +1256,4 @@ declare class AppilotsAPIClient {
888
1256
  health(): Promise<boolean>;
889
1257
  }
890
1258
 
891
- export { type ActionDescriptor, type AnalyzerConfig, AppilotsAPIClient, type AppilotsConfig, ComponentAnalyzer, type ComponentDescriptor, type EnvOverrides, type FlowDescriptor, type FlowStepDescriptor, FormAnalyzer, type FormDescriptor, type FormFieldDescriptor, type LocatorDescriptor, type MCPDocument, MCPGenerator, type MCPGeneratorConfig, type MCPGeneratorOptions, type MCPOutput, type MetadataLintWarning, NavigationAnalyzer, type NavigationGraph, type NavigationNode, type NavigatorDescriptor, type ParamDescriptor, type ScreenAgentHints, ScreenAnalyzer, type ScreenDescriptor, type SignalDescriptor, type StatusResult, type SyncResult, type TargetDescriptor, type WaitPolicyDescriptor, formatMetadataWarnings, getConfigPath, getEnvOverrides, lintActionMetadata, loadConfig, saveConfig, validateConfig };
1259
+ export { type ActionDescriptor, type AnalyzerConfig, AppilotsAPIClient, type AppilotsConfig, type AppilotsManifest, ComponentAnalyzer, type ComponentDescriptor, DEFAULT_MANIFEST_FILENAME, DEFAULT_WEB_SCREEN_PATTERNS, type EnvOverrides, type FlowDescriptor, type FlowStepDescriptor, FormAnalyzer, type FormDescriptor, type FormFieldDescriptor, GenericPlatformAnalyzer, type LoadManifestResult, type LocatorDescriptor, type MCPDocument, MCPGenerator, type MCPGeneratorConfig, type MCPGeneratorOptions, type MCPOutput, type MetadataLintWarning, NavigationAnalyzer, type NavigationGraph, type NavigationNode, type NavigatorDescriptor, type ParamDescriptor, type PlatformAnalyzer, type PlatformAnalyzerOptions, type PlatformAnalyzerResult, ReactNativePlatformAnalyzer, ReactWebPlatformAnalyzer, type ScreenAgentHints, ScreenAnalyzer, type ScreenDescriptor, type SignalDescriptor, type StatusResult, type SyncResult, type TargetDescriptor, type WaitPolicyDescriptor, WebNavigationAnalyzer, type WebNavigationResult, type WebRoute, WebScreenAnalyzer, formatMetadataWarnings, getConfigPath, getEnvOverrides, lintActionMetadata, loadConfig, loadManifest, mergeManifestNavigation, mergeManifestScreens, resolvePathToScreen, saveConfig, screenNameFromPath, validateConfig };