@orchid-labs/pluxx 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +100 -522
  2. package/dist/cli/agent.d.ts +7 -0
  3. package/dist/cli/agent.d.ts.map +1 -1
  4. package/dist/cli/doctor.d.ts +1 -0
  5. package/dist/cli/doctor.d.ts.map +1 -1
  6. package/dist/cli/eval.d.ts +22 -0
  7. package/dist/cli/eval.d.ts.map +1 -0
  8. package/dist/cli/index.d.ts +19 -2
  9. package/dist/cli/index.d.ts.map +1 -1
  10. package/dist/cli/init-from-mcp.d.ts +17 -2
  11. package/dist/cli/init-from-mcp.d.ts.map +1 -1
  12. package/dist/cli/install.d.ts +2 -0
  13. package/dist/cli/install.d.ts.map +1 -1
  14. package/dist/cli/lint.d.ts +5 -1
  15. package/dist/cli/lint.d.ts.map +1 -1
  16. package/dist/cli/mcp-proxy.d.ts +10 -0
  17. package/dist/cli/mcp-proxy.d.ts.map +1 -0
  18. package/dist/cli/migrate.d.ts.map +1 -1
  19. package/dist/cli/sync-from-mcp.d.ts.map +1 -1
  20. package/dist/cli/test.d.ts +2 -0
  21. package/dist/cli/test.d.ts.map +1 -1
  22. package/dist/generators/claude-code/index.d.ts +2 -0
  23. package/dist/generators/claude-code/index.d.ts.map +1 -1
  24. package/dist/generators/codex/index.d.ts +1 -0
  25. package/dist/generators/codex/index.d.ts.map +1 -1
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +99 -1
  29. package/dist/mcp/introspect.d.ts +43 -1
  30. package/dist/mcp/introspect.d.ts.map +1 -1
  31. package/dist/permissions.d.ts.map +1 -1
  32. package/dist/validation/platform-rules.d.ts +20 -0
  33. package/dist/validation/platform-rules.d.ts.map +1 -1
  34. package/package.json +2 -2
  35. package/src/cli/agent.ts +459 -34
  36. package/src/cli/doctor.ts +400 -1
  37. package/src/cli/eval.ts +470 -0
  38. package/src/cli/index.ts +633 -114
  39. package/src/cli/init-from-mcp.ts +545 -41
  40. package/src/cli/install.ts +166 -4
  41. package/src/cli/lint.ts +56 -26
  42. package/src/cli/mcp-proxy.ts +322 -0
  43. package/src/cli/migrate.ts +256 -3
  44. package/src/cli/sync-from-mcp.ts +23 -0
  45. package/src/cli/test.ts +10 -2
  46. package/src/generators/claude-code/index.ts +143 -0
  47. package/src/generators/codex/index.ts +23 -0
  48. package/src/index.ts +12 -1
  49. package/src/mcp/introspect.ts +297 -24
  50. package/src/permissions.ts +3 -1
  51. package/src/validation/platform-rules.ts +121 -0
package/src/cli/doctor.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { accessSync, constants, existsSync, readFileSync } from 'fs'
1
+ import { accessSync, constants, existsSync, lstatSync, readFileSync } from 'fs'
2
2
  import { resolve } from 'path'
3
3
  import { CONFIG_FILES, loadConfig } from '../config/load'
4
4
  import { listHookCommands } from './install'
@@ -42,6 +42,22 @@ const LOW_INFO_DESCRIPTION_PATTERNS = [
42
42
  /^description$/i,
43
43
  /^no description provided\.?$/i,
44
44
  ]
45
+ const MATERIALIZED_ENV_MARKER = 'materialized required config'
46
+
47
+ type ConsumerPlatform = 'claude-code' | 'cursor' | 'codex' | 'opencode'
48
+
49
+ interface ConsumerBundleLayout {
50
+ kind: 'installed-platform'
51
+ platform: ConsumerPlatform
52
+ manifestPath: string
53
+ mcpConfigPath?: string
54
+ }
55
+
56
+ type ConsumerLayoutDetection =
57
+ | ConsumerBundleLayout
58
+ | { kind: 'source-project' }
59
+ | { kind: 'multi-target-dist' }
60
+ | { kind: 'unknown' }
45
61
 
46
62
  function addCheck(checks: DoctorCheck[], check: DoctorCheck): void {
47
63
  checks.push(check)
@@ -468,6 +484,389 @@ function checkScaffoldMetadata(checks: DoctorCheck[], rootDir: string, config: P
468
484
  }
469
485
  }
470
486
 
487
+ function detectConsumerLayout(rootDir: string): ConsumerLayoutDetection {
488
+ if (existsSync(resolve(rootDir, '.claude-plugin/plugin.json'))) {
489
+ return {
490
+ kind: 'installed-platform',
491
+ platform: 'claude-code',
492
+ manifestPath: '.claude-plugin/plugin.json',
493
+ mcpConfigPath: '.mcp.json',
494
+ }
495
+ }
496
+
497
+ if (existsSync(resolve(rootDir, '.cursor-plugin/plugin.json'))) {
498
+ return {
499
+ kind: 'installed-platform',
500
+ platform: 'cursor',
501
+ manifestPath: '.cursor-plugin/plugin.json',
502
+ mcpConfigPath: 'mcp.json',
503
+ }
504
+ }
505
+
506
+ if (existsSync(resolve(rootDir, '.codex-plugin/plugin.json'))) {
507
+ return {
508
+ kind: 'installed-platform',
509
+ platform: 'codex',
510
+ manifestPath: '.codex-plugin/plugin.json',
511
+ mcpConfigPath: '.mcp.json',
512
+ }
513
+ }
514
+
515
+ const packagePath = resolve(rootDir, 'package.json')
516
+ const indexPath = resolve(rootDir, 'index.ts')
517
+ if (existsSync(packagePath) && existsSync(indexPath)) {
518
+ try {
519
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf-8')) as {
520
+ peerDependencies?: Record<string, string>
521
+ keywords?: string[]
522
+ }
523
+ if (pkg.peerDependencies?.['@opencode-ai/plugin'] || pkg.keywords?.includes('opencode-plugin')) {
524
+ return {
525
+ kind: 'installed-platform',
526
+ platform: 'opencode',
527
+ manifestPath: 'package.json',
528
+ }
529
+ }
530
+ } catch {
531
+ return {
532
+ kind: 'installed-platform',
533
+ platform: 'opencode',
534
+ manifestPath: 'package.json',
535
+ }
536
+ }
537
+ }
538
+
539
+ if (CONFIG_FILES.some((filename) => existsSync(resolve(rootDir, filename)))) {
540
+ return { kind: 'source-project' }
541
+ }
542
+
543
+ if (['claude-code', 'cursor', 'codex', 'opencode'].some((dir) => existsSync(resolve(rootDir, dir)))) {
544
+ return { kind: 'multi-target-dist' }
545
+ }
546
+
547
+ return { kind: 'unknown' }
548
+ }
549
+
550
+ function readJsonFile<T>(rootDir: string, relativePath: string): T {
551
+ return JSON.parse(readFileSync(resolve(rootDir, relativePath), 'utf-8')) as T
552
+ }
553
+
554
+ function checkConsumerBundlePath(checks: DoctorCheck[], rootDir: string): void {
555
+ try {
556
+ accessSync(rootDir, constants.R_OK)
557
+ const details = lstatSync(rootDir)
558
+ addCheck(checks, {
559
+ level: 'success',
560
+ code: 'consumer-path-readable',
561
+ title: 'Consumer bundle path readable',
562
+ detail: `${rootDir} is present and readable${details.isSymbolicLink() ? ' (symlinked install)' : ''}.`,
563
+ fix: 'No action needed.',
564
+ path: rootDir,
565
+ })
566
+ } catch {
567
+ addCheck(checks, {
568
+ level: 'error',
569
+ code: 'consumer-path-unreadable',
570
+ title: 'Consumer bundle path unreadable',
571
+ detail: `The installed plugin path is not readable: ${rootDir}`,
572
+ fix: 'Fix the path or permissions and rerun pluxx doctor --consumer.',
573
+ path: rootDir,
574
+ })
575
+ }
576
+ }
577
+
578
+ function checkConsumerManifest(checks: DoctorCheck[], rootDir: string, layout: ConsumerBundleLayout): void {
579
+ try {
580
+ const manifest = readJsonFile<Record<string, unknown>>(rootDir, layout.manifestPath)
581
+ const name = typeof manifest.name === 'string' && manifest.name.trim() !== ''
582
+ ? manifest.name
583
+ : layout.platform
584
+ const version = typeof manifest.version === 'string' && manifest.version.trim() !== ''
585
+ ? manifest.version
586
+ : 'unknown'
587
+
588
+ addCheck(checks, {
589
+ level: 'success',
590
+ code: 'consumer-manifest-valid',
591
+ title: 'Installed plugin manifest parsed successfully',
592
+ detail: `Detected ${name}@${version} for ${layout.platform}.`,
593
+ fix: 'No action needed.',
594
+ path: layout.manifestPath,
595
+ })
596
+ } catch (error) {
597
+ addCheck(checks, {
598
+ level: 'error',
599
+ code: 'consumer-manifest-invalid',
600
+ title: 'Installed plugin manifest is not parseable',
601
+ detail: error instanceof Error ? error.message : String(error),
602
+ fix: 'Rebuild or reinstall this plugin bundle and rerun pluxx doctor --consumer.',
603
+ path: layout.manifestPath,
604
+ })
605
+ }
606
+ }
607
+
608
+ function checkInstalledUserConfig(checks: DoctorCheck[], rootDir: string): void {
609
+ const userConfigPath = '.pluxx-user.json'
610
+ const resolvedPath = resolve(rootDir, userConfigPath)
611
+ if (!existsSync(resolvedPath)) {
612
+ addCheck(checks, {
613
+ level: 'info',
614
+ code: 'consumer-user-config-missing',
615
+ title: 'No local install config materialized',
616
+ detail: 'This bundle does not include a .pluxx-user.json file.',
617
+ fix: 'If tools require secrets or install-time config, reinstall the plugin and provide the requested values.',
618
+ path: userConfigPath,
619
+ })
620
+ return
621
+ }
622
+
623
+ try {
624
+ const payload = JSON.parse(readFileSync(resolvedPath, 'utf-8')) as {
625
+ values?: Record<string, unknown>
626
+ env?: Record<string, string>
627
+ }
628
+ const valueCount = Object.keys(payload.values ?? {}).length
629
+ const envCount = Object.keys(payload.env ?? {}).length
630
+ addCheck(checks, {
631
+ level: 'success',
632
+ code: 'consumer-user-config-valid',
633
+ title: 'Local install config parsed successfully',
634
+ detail: `.pluxx-user.json contains ${valueCount} saved value entr${valueCount === 1 ? 'y' : 'ies'} and ${envCount} env binding${envCount === 1 ? '' : 's'}.`,
635
+ fix: 'No action needed.',
636
+ path: userConfigPath,
637
+ })
638
+ } catch (error) {
639
+ addCheck(checks, {
640
+ level: 'error',
641
+ code: 'consumer-user-config-invalid',
642
+ title: 'Local install config is not parseable',
643
+ detail: error instanceof Error ? error.message : String(error),
644
+ fix: 'Delete or repair .pluxx-user.json, then reinstall the plugin if needed.',
645
+ path: userConfigPath,
646
+ })
647
+ }
648
+ }
649
+
650
+ function checkInstalledEnvValidation(checks: DoctorCheck[], rootDir: string): void {
651
+ const envScriptPath = 'scripts/check-env.sh'
652
+ const resolvedPath = resolve(rootDir, envScriptPath)
653
+ if (!existsSync(resolvedPath)) {
654
+ addCheck(checks, {
655
+ level: 'info',
656
+ code: 'consumer-env-script-missing',
657
+ title: 'No install-time env validation script found',
658
+ detail: 'This bundle does not ship a scripts/check-env.sh file.',
659
+ fix: 'No action needed unless this plugin is expected to validate runtime secrets on install.',
660
+ path: envScriptPath,
661
+ })
662
+ return
663
+ }
664
+
665
+ const content = readFileSync(resolvedPath, 'utf-8')
666
+ if (content.includes(MATERIALIZED_ENV_MARKER)) {
667
+ addCheck(checks, {
668
+ level: 'success',
669
+ code: 'consumer-env-script-materialized',
670
+ title: 'Install-time env validation was disabled after materialization',
671
+ detail: 'This local install already materialized required config, so the env validation hook is bypassed.',
672
+ fix: 'No action needed.',
673
+ path: envScriptPath,
674
+ })
675
+ return
676
+ }
677
+
678
+ addCheck(checks, {
679
+ level: 'warning',
680
+ code: 'consumer-env-script-active',
681
+ title: 'Install-time env validation is still active',
682
+ detail: 'This bundle still runs scripts/check-env.sh, which usually means required config was not materialized into the installed plugin.',
683
+ fix: 'If authenticated tools fail, reinstall the plugin and provide the requested userConfig values or required env vars.',
684
+ path: envScriptPath,
685
+ })
686
+ }
687
+
688
+ function checkInstalledMcpConfig(checks: DoctorCheck[], rootDir: string, layout: ConsumerBundleLayout): void {
689
+ if (!layout.mcpConfigPath) {
690
+ addCheck(checks, {
691
+ level: 'info',
692
+ code: 'consumer-mcp-config-not-applicable',
693
+ title: 'No static MCP config file for this platform',
694
+ detail: `${layout.platform} builds runtime MCP wiring inside the plugin wrapper rather than a standalone JSON file.`,
695
+ fix: 'No action needed.',
696
+ path: layout.manifestPath,
697
+ })
698
+ return
699
+ }
700
+
701
+ const resolvedPath = resolve(rootDir, layout.mcpConfigPath)
702
+ if (!existsSync(resolvedPath)) {
703
+ addCheck(checks, {
704
+ level: 'info',
705
+ code: 'consumer-mcp-config-missing',
706
+ title: 'No MCP config file emitted in this bundle',
707
+ detail: `This ${layout.platform} bundle does not include ${layout.mcpConfigPath}.`,
708
+ fix: 'No action needed unless this plugin should expose MCP servers on this platform.',
709
+ path: layout.mcpConfigPath,
710
+ })
711
+ return
712
+ }
713
+
714
+ try {
715
+ const payload = readJsonFile<{ mcpServers?: Record<string, Record<string, unknown>> }>(rootDir, layout.mcpConfigPath)
716
+ const servers = Object.values(payload.mcpServers ?? {})
717
+ addCheck(checks, {
718
+ level: 'success',
719
+ code: 'consumer-mcp-config-valid',
720
+ title: 'Installed MCP config parsed successfully',
721
+ detail: `${layout.mcpConfigPath} defines ${servers.length} MCP server${servers.length === 1 ? '' : 's'}.`,
722
+ fix: 'No action needed.',
723
+ path: layout.mcpConfigPath,
724
+ })
725
+
726
+ if (servers.length === 0) {
727
+ return
728
+ }
729
+
730
+ const remoteEntries = servers.filter((server) => 'url' in server)
731
+ const stdioEntries = servers.filter((server) => 'command' in server)
732
+ const inlineHeaderEntries = servers.filter((server) => {
733
+ if ('headers' in server && server.headers && typeof server.headers === 'object') return true
734
+ if ('http_headers' in server && server.http_headers && typeof server.http_headers === 'object') return true
735
+ return false
736
+ })
737
+
738
+ if (stdioEntries.length > 0) {
739
+ addCheck(checks, {
740
+ level: 'info',
741
+ code: 'consumer-mcp-stdio',
742
+ title: 'Local MCP servers configured',
743
+ detail: `${stdioEntries.length} MCP server${stdioEntries.length === 1 ? '' : 's'} run via local stdio commands in this bundle.`,
744
+ fix: 'If tools fail, verify the bundled command or its runtime dependencies on this machine.',
745
+ path: layout.mcpConfigPath,
746
+ })
747
+ }
748
+
749
+ if (remoteEntries.length > 0 && inlineHeaderEntries.length > 0) {
750
+ addCheck(checks, {
751
+ level: 'success',
752
+ code: 'consumer-mcp-inline-auth',
753
+ title: 'Remote MCP auth was materialized into this install',
754
+ detail: `${inlineHeaderEntries.length} remote MCP server${inlineHeaderEntries.length === 1 ? '' : 's'} include inline auth headers in the installed bundle.`,
755
+ fix: 'No action needed.',
756
+ path: layout.mcpConfigPath,
757
+ })
758
+ return
759
+ }
760
+
761
+ if (remoteEntries.length > 0) {
762
+ const fix = layout.platform === 'claude-code' || layout.platform === 'cursor'
763
+ ? 'If authenticated tools fail, complete the platform auth flow in the host or reinstall with any required userConfig values.'
764
+ : 'If authenticated tools fail, reinstall the plugin and provide any required userConfig or runtime env vars.'
765
+ addCheck(checks, {
766
+ level: 'info',
767
+ code: 'consumer-mcp-remote-auth-runtime',
768
+ title: 'Remote MCP auth is expected at runtime',
769
+ detail: `${remoteEntries.length} remote MCP server${remoteEntries.length === 1 ? '' : 's'} are configured without inline auth headers in this installed bundle.`,
770
+ fix,
771
+ path: layout.mcpConfigPath,
772
+ })
773
+ }
774
+ } catch (error) {
775
+ addCheck(checks, {
776
+ level: 'error',
777
+ code: 'consumer-mcp-config-invalid',
778
+ title: 'Installed MCP config is not parseable',
779
+ detail: error instanceof Error ? error.message : String(error),
780
+ fix: 'Rebuild or reinstall this platform bundle and rerun pluxx doctor --consumer.',
781
+ path: layout.mcpConfigPath,
782
+ })
783
+ }
784
+ }
785
+
786
+ export async function doctorConsumer(rootDir: string = process.cwd()): Promise<DoctorReport> {
787
+ const checks: DoctorCheck[] = []
788
+ const bunVersion = process.versions.bun
789
+ const bunMajor = parseMajorVersion(bunVersion)
790
+
791
+ if (!bunVersion) {
792
+ addCheck(checks, {
793
+ level: 'error',
794
+ code: 'bun-missing',
795
+ title: 'Bun runtime not detected',
796
+ detail: 'pluxx currently requires Bun at runtime.',
797
+ fix: 'Install Bun from https://bun.sh and rerun pluxx doctor --consumer.',
798
+ })
799
+ } else if (bunMajor === null || bunMajor < 1) {
800
+ addCheck(checks, {
801
+ level: 'error',
802
+ code: 'bun-version-unsupported',
803
+ title: 'Unsupported Bun version',
804
+ detail: `Detected Bun ${bunVersion}. pluxx requires Bun >= 1.0.`,
805
+ fix: 'Upgrade Bun to a supported version and rerun pluxx doctor --consumer.',
806
+ })
807
+ } else {
808
+ addCheck(checks, {
809
+ level: 'success',
810
+ code: 'bun-version',
811
+ title: 'Supported Bun runtime detected',
812
+ detail: `Bun ${bunVersion} is available.`,
813
+ fix: 'No action needed.',
814
+ })
815
+ }
816
+
817
+ checkConsumerBundlePath(checks, rootDir)
818
+ const layout = detectConsumerLayout(rootDir)
819
+
820
+ if (layout.kind === 'source-project') {
821
+ addCheck(checks, {
822
+ level: 'error',
823
+ code: 'consumer-source-project',
824
+ title: 'Consumer doctor expects an installed or built platform bundle',
825
+ detail: `Found a pluxx source project at ${rootDir}, not a built platform directory.`,
826
+ fix: 'Run `pluxx doctor` in the source project, or run `pluxx doctor --consumer <dist/platform>` against an installed or built bundle.',
827
+ })
828
+ return summarizeChecks(checks)
829
+ }
830
+
831
+ if (layout.kind === 'multi-target-dist') {
832
+ addCheck(checks, {
833
+ level: 'error',
834
+ code: 'consumer-dist-root',
835
+ title: 'Consumer doctor expects one platform directory at a time',
836
+ detail: `Found a multi-target dist root at ${rootDir}.`,
837
+ fix: 'Point --consumer at one built platform directory such as dist/cursor or an installed plugin path.',
838
+ })
839
+ return summarizeChecks(checks)
840
+ }
841
+
842
+ if (layout.kind === 'unknown') {
843
+ addCheck(checks, {
844
+ level: 'error',
845
+ code: 'consumer-platform-unknown',
846
+ title: 'Could not detect an installed plugin layout',
847
+ detail: `No known installed plugin markers were found in ${rootDir}.`,
848
+ fix: 'Pass the root of a built platform bundle or installed plugin directory to pluxx doctor --consumer.',
849
+ })
850
+ return summarizeChecks(checks)
851
+ }
852
+
853
+ addCheck(checks, {
854
+ level: 'success',
855
+ code: 'consumer-platform-detected',
856
+ title: 'Installed platform bundle detected',
857
+ detail: `Detected a ${layout.platform} plugin bundle.`,
858
+ fix: 'No action needed.',
859
+ path: layout.manifestPath,
860
+ })
861
+
862
+ checkConsumerManifest(checks, rootDir, layout)
863
+ checkInstalledUserConfig(checks, rootDir)
864
+ checkInstalledEnvValidation(checks, rootDir)
865
+ checkInstalledMcpConfig(checks, rootDir, layout)
866
+
867
+ return summarizeChecks(checks)
868
+ }
869
+
471
870
  export async function doctorProject(rootDir: string = process.cwd()): Promise<DoctorReport> {
472
871
  const checks: DoctorCheck[] = []
473
872
  const bunVersion = process.versions.bun