@cometchat/skills 4.4.2 → 5.0.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/.claude-plugin/marketplace.json +55 -0
- package/.claude-plugin/plugin.json +14 -0
- package/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +41 -163
- package/RULES.md +114 -0
- package/bin/cometchat-skills.mjs +1180 -0
- package/bin/legacy-skills.json +95 -0
- package/catalogs/android-v6.json +1646 -0
- package/catalogs/flutter-v6.json +651 -0
- package/catalogs/ios-v5.json +1383 -0
- package/catalogs/rn-calls-v5.json +134 -0
- package/catalogs/rn-calls-v5.symbols.txt +58 -0
- package/catalogs/rn-sdk-v4.json +672 -0
- package/catalogs/rn-v5.json +449 -0
- package/catalogs/rn-v5.symbols.txt +216 -0
- package/catalogs/sdk-android-v5.json +514 -0
- package/contracts.android-calls-v5.json +122 -0
- package/contracts.android-v6.json +135 -0
- package/contracts.angular-v5.json +694 -0
- package/contracts.flutter-calls-v5.json +77 -0
- package/contracts.flutter-v6.json +126 -0
- package/contracts.ios-calls-v5.json +67 -0
- package/contracts.ios-v5.json +436 -0
- package/contracts.json +82 -0
- package/contracts.rn-calls-v5.json +77 -0
- package/contracts.rn-v5.json +727 -0
- package/contracts.sdk-android-v5.json +15 -0
- package/contracts.web-calls-v5.json +45 -0
- package/features.android-calls-v5.json +246 -0
- package/features.android-v6.json +65 -0
- package/features.angular-v5.json +521 -0
- package/features.flutter-calls-v5.json +286 -0
- package/features.flutter-v6.json +484 -0
- package/features.ios-calls-v5.json +236 -0
- package/features.ios-v5.json +573 -0
- package/features.json +61 -0
- package/features.rn-calls-v5.json +187 -0
- package/features.rn-v5.json +614 -0
- package/features.sdk-android-v5.json +40 -0
- package/features.sdk-jsv4.json +24 -0
- package/features.web-calls-v5.json +108 -0
- package/package.json +151 -74
- package/peers.yaml +312 -0
- package/registry/README.md +31 -0
- package/registry/android-calls-v5.json +54 -0
- package/registry/android-v6.json +137 -0
- package/registry/angular-v5.json +114 -0
- package/registry/flutter-calls-v5.json +54 -0
- package/registry/flutter-v6.json +119 -0
- package/registry/ios-calls-v5.json +59 -0
- package/registry/ios-v5.json +97 -0
- package/registry/rn-calls-v5.json +59 -0
- package/registry/rn-v5.json +129 -0
- package/registry/sdk-android-v5.json +53 -0
- package/registry/sdk-jsv4.json +53 -0
- package/registry/web-calls-v5.json +54 -0
- package/registry/web-v7.json +99 -0
- package/skills/cometchat/SKILL.md +42 -2096
- package/skills/cometchat-android-v5-calls-sdk/SKILL.md +206 -0
- package/skills/cometchat-android-v5-calls-sdk/references/build-truth.md +63 -0
- package/skills/cometchat-android-v5-calls-sdk/references/docs-map.md +119 -0
- package/skills/cometchat-android-v5-calls-sdk/references/method-map.md +53 -0
- package/skills/cometchat-android-v5-calls-sdk/references/pitfalls.md +72 -0
- package/skills/cometchat-android-v5-calls-sdk/references/ringing.md +92 -0
- package/skills/cometchat-android-v5-calls-sdk/references/user-switch.md +90 -0
- package/skills/cometchat-android-v5-sdk/SKILL.md +127 -0
- package/skills/cometchat-android-v5-sdk/references/app-surface.md +88 -0
- package/skills/cometchat-android-v5-sdk/references/docs-map.md +91 -0
- package/skills/cometchat-android-v6-builder-settings/SKILL.md +51 -137
- package/skills/cometchat-android-v6-calls/SKILL.md +128 -413
- package/skills/cometchat-android-v6-compose-components/SKILL.md +84 -194
- package/skills/cometchat-android-v6-compose-customization/SKILL.md +66 -411
- package/skills/cometchat-android-v6-compose-placement/SKILL.md +154 -237
- package/skills/cometchat-android-v6-core/SKILL.md +236 -672
- package/skills/cometchat-android-v6-core/references/anti-patterns.md +31 -0
- package/skills/cometchat-android-v6-core/references/component-props.md +168 -0
- package/skills/cometchat-android-v6-core/references/docs-map.md +153 -0
- package/skills/cometchat-android-v6-core/references/layout.md +112 -0
- package/skills/cometchat-android-v6-core/references/lifecycle.md +75 -0
- package/skills/cometchat-android-v6-core/references/setup-credentials.md +105 -0
- package/skills/cometchat-android-v6-core/references/troubleshooting.md +36 -0
- package/skills/cometchat-android-v6-events/SKILL.md +46 -184
- package/skills/cometchat-android-v6-extensions/SKILL.md +39 -179
- package/skills/cometchat-android-v6-features/SKILL.md +74 -143
- package/skills/cometchat-android-v6-kotlin-components/SKILL.md +75 -193
- package/skills/cometchat-android-v6-kotlin-customization/SKILL.md +67 -419
- package/skills/cometchat-android-v6-kotlin-placement/SKILL.md +77 -223
- package/skills/cometchat-android-v6-migration/SKILL.md +67 -378
- package/skills/cometchat-android-v6-production/SKILL.md +40 -179
- package/skills/cometchat-android-v6-push/SKILL.md +39 -289
- package/skills/cometchat-android-v6-testing/SKILL.md +50 -265
- package/skills/cometchat-android-v6-troubleshooting/SKILL.md +70 -254
- package/skills/cometchat-angular-v5-calls/SKILL.md +193 -0
- package/skills/cometchat-angular-v5-components/SKILL.md +131 -0
- package/skills/cometchat-angular-v5-components/references/catalog.md +111 -0
- package/skills/cometchat-angular-v5-components/references/host-composed.md +46 -0
- package/skills/cometchat-angular-v5-core/SKILL.md +300 -0
- package/skills/cometchat-angular-v5-core/references/anti-patterns.md +49 -0
- package/skills/cometchat-angular-v5-core/references/component-props.md +55 -0
- package/skills/cometchat-angular-v5-core/references/dependencies.md +66 -0
- package/skills/cometchat-angular-v5-core/references/docs-map.md +132 -0
- package/skills/cometchat-angular-v5-core/references/layout.md +64 -0
- package/skills/cometchat-angular-v5-core/references/lifecycle.md +202 -0
- package/skills/cometchat-angular-v5-core/references/setup-credentials.md +137 -0
- package/skills/cometchat-angular-v5-core/references/theming.md +70 -0
- package/skills/cometchat-angular-v5-core/references/troubleshooting.md +27 -0
- package/skills/cometchat-angular-v5-customization/SKILL.md +94 -0
- package/skills/cometchat-angular-v5-features/SKILL.md +107 -0
- package/skills/cometchat-angular-v5-features/references/custom-messages.md +133 -0
- package/skills/cometchat-angular-v5-features/references/formatters.md +28 -0
- package/skills/cometchat-angular-v5-features/references/host-composed-extensions.md +85 -0
- package/skills/cometchat-angular-v5-migration/SKILL.md +40 -0
- package/skills/cometchat-angular-v5-migration/references/migration-guide.md +165 -0
- package/skills/cometchat-angular-v5-patterns/SKILL.md +107 -0
- package/skills/cometchat-angular-v5-placement/SKILL.md +133 -0
- package/skills/cometchat-angular-v5-placement/references/combined-app.md +174 -0
- package/skills/cometchat-angular-v5-placement/references/core-surface.md +199 -0
- package/skills/cometchat-angular-v5-production/SKILL.md +101 -0
- package/skills/cometchat-angular-v5-push/SKILL.md +61 -0
- package/skills/cometchat-angular-v5-testing/SKILL.md +112 -0
- package/skills/cometchat-angular-v5-troubleshooting/SKILL.md +96 -0
- package/skills/cometchat-flutter-v5-sdk/SKILL.md +188 -0
- package/skills/cometchat-flutter-v5-sdk/references/doc-corrections.md +55 -0
- package/skills/cometchat-flutter-v5-sdk/references/docs-map.md +211 -0
- package/skills/cometchat-flutter-v5-sdk/references/platform-notes.md +48 -0
- package/skills/cometchat-flutter-v5-sdk/references/transcription.md +87 -0
- package/skills/cometchat-flutter-v6-calls/SKILL.md +75 -389
- package/skills/cometchat-flutter-v6-components/SKILL.md +88 -361
- package/skills/cometchat-flutter-v6-core/SKILL.md +154 -593
- package/skills/cometchat-flutter-v6-core/references/anti-patterns.md +55 -0
- package/skills/cometchat-flutter-v6-core/references/docs-map.md +143 -0
- package/skills/cometchat-flutter-v6-core/references/layout.md +45 -0
- package/skills/cometchat-flutter-v6-core/references/lifecycle.md +87 -0
- package/skills/cometchat-flutter-v6-core/references/setup-credentials.md +84 -0
- package/skills/cometchat-flutter-v6-core/references/troubleshooting.md +26 -0
- package/skills/cometchat-flutter-v6-customization/SKILL.md +70 -712
- package/skills/cometchat-flutter-v6-events/SKILL.md +79 -174
- package/skills/cometchat-flutter-v6-features/SKILL.md +55 -394
- package/skills/cometchat-flutter-v6-migration/SKILL.md +68 -367
- package/skills/cometchat-flutter-v6-migration/references/migration-guide.md +131 -0
- package/skills/cometchat-flutter-v6-patterns/SKILL.md +61 -0
- package/skills/cometchat-flutter-v6-placement/SKILL.md +94 -381
- package/skills/cometchat-flutter-v6-production/SKILL.md +58 -615
- package/skills/cometchat-flutter-v6-push/SKILL.md +46 -455
- package/skills/cometchat-flutter-v6-testing/SKILL.md +68 -299
- package/skills/cometchat-flutter-v6-troubleshooting/SKILL.md +54 -878
- package/skills/cometchat-ios-calls/SKILL.md +112 -563
- package/skills/cometchat-ios-components/SKILL.md +44 -834
- package/skills/cometchat-ios-core/SKILL.md +181 -1068
- package/skills/cometchat-ios-core/references/anti-patterns.md +80 -0
- package/skills/cometchat-ios-core/references/docs-map.md +123 -0
- package/skills/cometchat-ios-core/references/install.md +23 -0
- package/skills/cometchat-ios-core/references/layout.md +186 -0
- package/skills/cometchat-ios-core/references/setup-credentials.md +197 -0
- package/skills/cometchat-ios-core/references/swiftui.md +91 -0
- package/skills/cometchat-ios-core/references/troubleshooting.md +100 -0
- package/skills/cometchat-ios-customization/SKILL.md +33 -968
- package/skills/cometchat-ios-features/SKILL.md +29 -738
- package/skills/cometchat-ios-migration/SKILL.md +39 -0
- package/skills/cometchat-ios-placement/SKILL.md +51 -729
- package/skills/cometchat-ios-push/SKILL.md +24 -602
- package/skills/cometchat-ios-v5-sdk/SKILL.md +161 -0
- package/skills/cometchat-ios-v5-sdk/references/docs-map.md +326 -0
- package/skills/cometchat-js-v5-sdk/SKILL.md +95 -0
- package/skills/cometchat-js-v5-sdk/references/docs-map.md +114 -0
- package/skills/cometchat-onboarding/SKILL.md +89 -0
- package/skills/cometchat-onboarding/references/archetypes.md +51 -0
- package/skills/cometchat-onboarding/references/calling-first.md +77 -0
- package/skills/cometchat-onboarding/references/not-in-this-pack.md +8 -0
- package/skills/cometchat-onboarding/references/plan-artifact.md +133 -0
- package/skills/cometchat-onboarding/references/platforms.md +90 -0
- package/skills/cometchat-react-native-bare-patterns/SKILL.md +120 -0
- package/skills/cometchat-react-native-calls/SKILL.md +200 -0
- package/skills/cometchat-react-native-components/SKILL.md +142 -0
- package/skills/cometchat-react-native-core/SKILL.md +313 -0
- package/skills/cometchat-react-native-core/references/anti-patterns.md +79 -0
- package/skills/cometchat-react-native-core/references/component-props.md +105 -0
- package/skills/cometchat-react-native-core/references/dependencies.md +62 -0
- package/skills/cometchat-react-native-core/references/docs-map.md +126 -0
- package/skills/cometchat-react-native-core/references/layout.md +61 -0
- package/skills/cometchat-react-native-core/references/lifecycle.md +64 -0
- package/skills/cometchat-react-native-core/references/setup-credentials.md +39 -0
- package/skills/cometchat-react-native-customization/SKILL.md +198 -0
- package/skills/cometchat-react-native-expo-patterns/SKILL.md +116 -0
- package/skills/cometchat-react-native-features/SKILL.md +122 -0
- package/skills/cometchat-react-native-migration/SKILL.md +103 -0
- package/skills/cometchat-react-native-placement/SKILL.md +158 -0
- package/skills/cometchat-react-native-production/SKILL.md +78 -0
- package/skills/cometchat-react-native-push/SKILL.md +88 -0
- package/skills/cometchat-react-native-sdk/SKILL.md +126 -0
- package/skills/cometchat-react-native-sdk/references/method-map.md +239 -0
- package/skills/cometchat-react-native-sdk/references/page-map.md +96 -0
- package/skills/cometchat-react-native-testing/SKILL.md +94 -0
- package/skills/cometchat-react-native-troubleshooting/SKILL.md +116 -0
- package/skills/cometchat-react-native-v5-sdk/SKILL.md +150 -0
- package/skills/cometchat-react-native-v5-sdk/references/docs-map.md +127 -0
- package/skills/cometchat-react-native-v5-sdk/references/ringing-voip.md +369 -0
- package/skills/cometchat-react-v7-calls/SKILL.md +75 -0
- package/skills/cometchat-react-v7-components/SKILL.md +72 -0
- package/skills/cometchat-react-v7-core/SKILL.md +101 -0
- package/skills/cometchat-react-v7-core/references/anti-patterns.md +19 -0
- package/skills/cometchat-react-v7-core/references/component-props.md +82 -0
- package/skills/cometchat-react-v7-core/references/dependencies.md +23 -0
- package/skills/cometchat-react-v7-core/references/docs-map.md +117 -0
- package/skills/cometchat-react-v7-core/references/i18n-rtl-a11y.md +22 -0
- package/skills/cometchat-react-v7-core/references/layout.md +158 -0
- package/skills/cometchat-react-v7-core/references/lifecycle.md +74 -0
- package/skills/cometchat-react-v7-core/references/setup-credentials.md +58 -0
- package/skills/cometchat-react-v7-core/references/ssr.md +11 -0
- package/skills/cometchat-react-v7-core/references/theming.md +52 -0
- package/skills/cometchat-react-v7-core/references/troubleshooting.md +28 -0
- package/skills/cometchat-react-v7-customization/SKILL.md +84 -0
- package/skills/cometchat-react-v7-features/SKILL.md +92 -0
- package/skills/cometchat-react-v7-migration/SKILL.md +39 -0
- package/skills/cometchat-react-v7-migration/references/migration-guide.md +119 -0
- package/skills/cometchat-react-v7-patterns/SKILL.md +43 -0
- package/skills/cometchat-react-v7-placement/SKILL.md +106 -0
- package/skills/cometchat-react-v7-placement/references/recipes.md +354 -0
- package/skills/cometchat-react-v7-push/SKILL.md +53 -0
- package/test-suite/README.md +70 -0
- package/test-suite/catalogs/android-calls-v5.json +830 -0
- package/test-suite/catalogs/android-calls-v5.symbols.txt +499 -0
- package/test-suite/catalogs/android-v6.json +1646 -0
- package/test-suite/catalogs/android-v6.symbols.txt +298 -0
- package/test-suite/catalogs/angular-v5.json +7587 -0
- package/test-suite/catalogs/flutter-calls-v5.json +196 -0
- package/test-suite/catalogs/flutter-calls-v5.symbols.txt +59 -0
- package/test-suite/catalogs/flutter-v6.json +651 -0
- package/test-suite/catalogs/ios-calls-v5.json +158 -0
- package/test-suite/catalogs/ios-calls-v5.symbols.txt +70 -0
- package/test-suite/catalogs/ios-v5.json +1383 -0
- package/test-suite/catalogs/ios-v5.symbols.txt +699 -0
- package/test-suite/catalogs/rn-calls-v5.json +134 -0
- package/test-suite/catalogs/rn-sdk-v4.json +672 -0
- package/test-suite/catalogs/rn-v5.json +449 -0
- package/test-suite/catalogs/sdk-android-v5.json +514 -0
- package/test-suite/catalogs/sdk-android-v5.symbols.txt +254 -0
- package/test-suite/catalogs/sdk-jsv4.json +800 -0
- package/test-suite/catalogs/web-calls-v5.json +195 -0
- package/test-suite/catalogs/web-v7.json +1243 -0
- package/test-suite/scripts/build-registry.mjs +113 -0
- package/test-suite/scripts/catalog-refresh-dart.mjs +151 -0
- package/test-suite/scripts/catalog-refresh.mjs +100 -0
- package/test-suite/scripts/lint-skills.mjs +909 -0
- package/test-suite/scripts/measure-tokens.mjs +169 -0
- package/test-suite/scripts/typecheck-fences-dart.mjs +437 -0
- package/test-suite/scripts/typecheck-fences-kotlin.mjs +269 -0
- package/test-suite/scripts/typecheck-fences.mjs +206 -0
- package/test-suite/scripts/verify-catalog.mjs +242 -0
- package/test-suite/scripts/verify-contract-fences.mjs +94 -0
- package/test-suite/scripts/verify-detection.mjs +178 -0
- package/test-suite/scripts/verify-docs-base.mjs +91 -0
- package/test-suite/scripts/verify-family-agnostic.mjs +130 -0
- package/test-suite/scripts/verify-golden-path.mjs +138 -0
- package/test-suite/scripts/verify-oracle-components.mjs +133 -0
- package/test-suite/scripts/verify-template-bindings.mjs +148 -0
- package/test-suite/typecheck/angular-v5/package.json +21 -0
- package/test-suite/typecheck/angular-v5/tsconfig.json +21 -0
- package/test-suite/typecheck/flutter-v6/analysis_options.yaml +25 -0
- package/test-suite/typecheck/flutter-v6/pubspec.yaml +21 -0
- package/test-suite/typecheck/web-v7/package.json +18 -0
- package/test-suite/typecheck/web-v7/tsconfig.json +19 -0
- package/bin/install.js +0 -1003
- package/skills/.claude-plugin/marketplace.json +0 -86
- package/skills/cometchat/references/asking-questions.md +0 -48
- package/skills/cometchat-a11y/SKILL.md +0 -477
- package/skills/cometchat-android-v5/SKILL.md +0 -121
- package/skills/cometchat-android-v5-calls/SKILL.md +0 -429
- package/skills/cometchat-android-v5-calls/references/README.md +0 -53
- package/skills/cometchat-android-v5-calls/references/add-calls-to-existing-chat.md +0 -197
- package/skills/cometchat-android-v5-calls/references/audio-controls.md +0 -91
- package/skills/cometchat-android-v5-calls/references/background-handling.md +0 -168
- package/skills/cometchat-android-v5-calls/references/call-layouts.md +0 -131
- package/skills/cometchat-android-v5-calls/references/call-logs.md +0 -113
- package/skills/cometchat-android-v5-calls/references/call-session.md +0 -274
- package/skills/cometchat-android-v5-calls/references/custom-ui.md +0 -150
- package/skills/cometchat-android-v5-calls/references/event-listeners.md +0 -123
- package/skills/cometchat-android-v5-calls/references/group-calls.md +0 -205
- package/skills/cometchat-android-v5-calls/references/idle-timeout.md +0 -117
- package/skills/cometchat-android-v5-calls/references/in-call-chat.md +0 -134
- package/skills/cometchat-android-v5-calls/references/join-session.md +0 -120
- package/skills/cometchat-android-v5-calls/references/migration-v4-to-v5.md +0 -222
- package/skills/cometchat-android-v5-calls/references/participant-management.md +0 -110
- package/skills/cometchat-android-v5-calls/references/picture-in-picture.md +0 -106
- package/skills/cometchat-android-v5-calls/references/raise-hand.md +0 -248
- package/skills/cometchat-android-v5-calls/references/recording.md +0 -101
- package/skills/cometchat-android-v5-calls/references/ringing-integration.md +0 -152
- package/skills/cometchat-android-v5-calls/references/screen-sharing.md +0 -78
- package/skills/cometchat-android-v5-calls/references/server-fcm-voip.md +0 -215
- package/skills/cometchat-android-v5-calls/references/session-settings.md +0 -121
- package/skills/cometchat-android-v5-calls/references/setup.md +0 -137
- package/skills/cometchat-android-v5-calls/references/share-invite.md +0 -156
- package/skills/cometchat-android-v5-calls/references/video-controls.md +0 -87
- package/skills/cometchat-android-v5-calls/references/voip-calling.md +0 -526
- package/skills/cometchat-android-v5-components/SKILL.md +0 -431
- package/skills/cometchat-android-v5-core/SKILL.md +0 -486
- package/skills/cometchat-android-v5-customization/SKILL.md +0 -469
- package/skills/cometchat-android-v5-extensions/SKILL.md +0 -127
- package/skills/cometchat-android-v5-features/SKILL.md +0 -131
- package/skills/cometchat-android-v5-placement/SKILL.md +0 -277
- package/skills/cometchat-android-v5-production/SKILL.md +0 -95
- package/skills/cometchat-android-v5-push/SKILL.md +0 -619
- package/skills/cometchat-android-v5-testing/SKILL.md +0 -188
- package/skills/cometchat-android-v5-theming/SKILL.md +0 -167
- package/skills/cometchat-android-v5-troubleshooting/SKILL.md +0 -74
- package/skills/cometchat-android-v6/SKILL.md +0 -108
- package/skills/cometchat-android-v6-calls/references/add-calls-to-existing-chat.md +0 -97
- package/skills/cometchat-android-v6-calls/references/advanced-features.md +0 -87
- package/skills/cometchat-android-v6-calls/references/call-layouts.md +0 -100
- package/skills/cometchat-android-v6-calls/references/call-session.md +0 -163
- package/skills/cometchat-android-v6-calls/references/device-management.md +0 -172
- package/skills/cometchat-android-v6-calls/references/group-calls.md +0 -191
- package/skills/cometchat-android-v6-calls/references/idle-timeout.md +0 -110
- package/skills/cometchat-android-v6-calls/references/in-call-chat.md +0 -141
- package/skills/cometchat-android-v6-calls/references/raise-hand.md +0 -188
- package/skills/cometchat-android-v6-calls/references/ringing-integration.md +0 -176
- package/skills/cometchat-android-v6-calls/references/server-fcm-voip.md +0 -127
- package/skills/cometchat-android-v6-calls/references/share-invite.md +0 -105
- package/skills/cometchat-android-v6-compose-theming/SKILL.md +0 -250
- package/skills/cometchat-android-v6-kotlin-theming/SKILL.md +0 -233
- package/skills/cometchat-angular-calls/SKILL.md +0 -380
- package/skills/cometchat-angular-calls/references/add-calls-to-existing-chat.md +0 -96
- package/skills/cometchat-angular-calls/references/call-layouts.md +0 -123
- package/skills/cometchat-angular-calls/references/call-session.md +0 -167
- package/skills/cometchat-angular-calls/references/custom-ui.md +0 -239
- package/skills/cometchat-angular-calls/references/device-management.md +0 -154
- package/skills/cometchat-angular-calls/references/group-calls.md +0 -433
- package/skills/cometchat-angular-calls/references/idle-timeout.md +0 -161
- package/skills/cometchat-angular-calls/references/in-call-chat.md +0 -148
- package/skills/cometchat-angular-calls/references/lazy-loading-pitfalls.md +0 -205
- package/skills/cometchat-angular-calls/references/migration-v4-to-v5.md +0 -53
- package/skills/cometchat-angular-calls/references/ngzone-and-async-callbacks.md +0 -184
- package/skills/cometchat-angular-calls/references/picture-in-picture.md +0 -285
- package/skills/cometchat-angular-calls/references/raise-hand.md +0 -172
- package/skills/cometchat-angular-calls/references/recording-screen-share.md +0 -260
- package/skills/cometchat-angular-calls/references/ringing-integration.md +0 -133
- package/skills/cometchat-angular-calls/references/server-web-push-vapid.md +0 -100
- package/skills/cometchat-angular-calls/references/share-invite.md +0 -122
- package/skills/cometchat-angular-calls/references/virtual-background.md +0 -29
- package/skills/cometchat-angular-components/SKILL.md +0 -235
- package/skills/cometchat-angular-core/SKILL.md +0 -386
- package/skills/cometchat-angular-customization/SKILL.md +0 -616
- package/skills/cometchat-angular-features/SKILL.md +0 -329
- package/skills/cometchat-angular-patterns/SKILL.md +0 -256
- package/skills/cometchat-angular-placement/SKILL.md +0 -314
- package/skills/cometchat-angular-production/SKILL.md +0 -564
- package/skills/cometchat-angular-push/SKILL.md +0 -389
- package/skills/cometchat-angular-testing/SKILL.md +0 -605
- package/skills/cometchat-angular-theming/SKILL.md +0 -290
- package/skills/cometchat-angular-troubleshooting/SKILL.md +0 -364
- package/skills/cometchat-astro-patterns/SKILL.md +0 -708
- package/skills/cometchat-calls/SKILL.md +0 -364
- package/skills/cometchat-calls/references/use-case-broadcast.md +0 -199
- package/skills/cometchat-calls/references/use-case-marketplace.md +0 -157
- package/skills/cometchat-calls/references/use-case-support.md +0 -189
- package/skills/cometchat-calls/references/use-case-team.md +0 -180
- package/skills/cometchat-calls/references/use-case-telehealth.md +0 -153
- package/skills/cometchat-components/SKILL.md +0 -1142
- package/skills/cometchat-core/SKILL.md +0 -1048
- package/skills/cometchat-customization/SKILL.md +0 -580
- package/skills/cometchat-customization/references/component-catalog.md +0 -236
- package/skills/cometchat-features/SKILL.md +0 -861
- package/skills/cometchat-flutter-v5/SKILL.md +0 -173
- package/skills/cometchat-flutter-v5-calls/SKILL.md +0 -455
- package/skills/cometchat-flutter-v5-calls/references/add-calls-to-existing-chat.md +0 -193
- package/skills/cometchat-flutter-v5-calls/references/call-layouts.md +0 -103
- package/skills/cometchat-flutter-v5-calls/references/call-session.md +0 -285
- package/skills/cometchat-flutter-v5-calls/references/device-management.md +0 -124
- package/skills/cometchat-flutter-v5-calls/references/group-calls.md +0 -181
- package/skills/cometchat-flutter-v5-calls/references/idle-timeout.md +0 -99
- package/skills/cometchat-flutter-v5-calls/references/in-call-chat.md +0 -135
- package/skills/cometchat-flutter-v5-calls/references/migration-v4-to-v5.md +0 -210
- package/skills/cometchat-flutter-v5-calls/references/raise-hand.md +0 -255
- package/skills/cometchat-flutter-v5-calls/references/ringing-integration.md +0 -196
- package/skills/cometchat-flutter-v5-calls/references/server-push-bridge.md +0 -101
- package/skills/cometchat-flutter-v5-calls/references/share-invite.md +0 -148
- package/skills/cometchat-flutter-v5-conversations/SKILL.md +0 -118
- package/skills/cometchat-flutter-v5-core/SKILL.md +0 -239
- package/skills/cometchat-flutter-v5-customization/SKILL.md +0 -358
- package/skills/cometchat-flutter-v5-events/SKILL.md +0 -156
- package/skills/cometchat-flutter-v5-messages/SKILL.md +0 -259
- package/skills/cometchat-flutter-v5-production/SKILL.md +0 -216
- package/skills/cometchat-flutter-v5-push/SKILL.md +0 -285
- package/skills/cometchat-flutter-v5-testing/SKILL.md +0 -401
- package/skills/cometchat-flutter-v5-theming/SKILL.md +0 -161
- package/skills/cometchat-flutter-v5-troubleshooting/SKILL.md +0 -130
- package/skills/cometchat-flutter-v5-users-groups/SKILL.md +0 -152
- package/skills/cometchat-flutter-v6/SKILL.md +0 -446
- package/skills/cometchat-flutter-v6-calls/references/add-calls-to-existing-chat.md +0 -136
- package/skills/cometchat-flutter-v6-calls/references/advanced-features.md +0 -89
- package/skills/cometchat-flutter-v6-calls/references/call-layouts.md +0 -105
- package/skills/cometchat-flutter-v6-calls/references/call-session.md +0 -229
- package/skills/cometchat-flutter-v6-calls/references/device-management.md +0 -105
- package/skills/cometchat-flutter-v6-calls/references/group-calls.md +0 -190
- package/skills/cometchat-flutter-v6-calls/references/idle-timeout.md +0 -88
- package/skills/cometchat-flutter-v6-calls/references/in-call-chat.md +0 -125
- package/skills/cometchat-flutter-v6-calls/references/raise-hand.md +0 -275
- package/skills/cometchat-flutter-v6-calls/references/ringing-integration.md +0 -171
- package/skills/cometchat-flutter-v6-calls/references/server-push-bridge.md +0 -93
- package/skills/cometchat-flutter-v6-calls/references/share-invite.md +0 -102
- package/skills/cometchat-flutter-v6-conversations/SKILL.md +0 -238
- package/skills/cometchat-flutter-v6-messages/SKILL.md +0 -384
- package/skills/cometchat-flutter-v6-theming/SKILL.md +0 -293
- package/skills/cometchat-flutter-v6-users-groups/SKILL.md +0 -224
- package/skills/cometchat-i18n/SKILL.md +0 -419
- package/skills/cometchat-ios/SKILL.md +0 -325
- package/skills/cometchat-ios-calls/references/add-calls-to-existing-chat.md +0 -181
- package/skills/cometchat-ios-calls/references/audio-modes-and-controls.md +0 -45
- package/skills/cometchat-ios-calls/references/avaudiosession-routing.md +0 -188
- package/skills/cometchat-ios-calls/references/call-layouts.md +0 -76
- package/skills/cometchat-ios-calls/references/call-session.md +0 -218
- package/skills/cometchat-ios-calls/references/callkit-and-pushkit.md +0 -357
- package/skills/cometchat-ios-calls/references/custom-ui.md +0 -322
- package/skills/cometchat-ios-calls/references/device-management.md +0 -171
- package/skills/cometchat-ios-calls/references/group-calls.md +0 -359
- package/skills/cometchat-ios-calls/references/idle-timeout.md +0 -184
- package/skills/cometchat-ios-calls/references/in-call-chat.md +0 -179
- package/skills/cometchat-ios-calls/references/migration-v4-to-v5.md +0 -196
- package/skills/cometchat-ios-calls/references/picture-in-picture.md +0 -246
- package/skills/cometchat-ios-calls/references/raise-hand.md +0 -26
- package/skills/cometchat-ios-calls/references/recording-screen-share.md +0 -252
- package/skills/cometchat-ios-calls/references/ringing-integration.md +0 -143
- package/skills/cometchat-ios-calls/references/server-apns-pushkit.md +0 -267
- package/skills/cometchat-ios-calls/references/share-invite.md +0 -148
- package/skills/cometchat-ios-calls/references/swiftui-uikit-hosting.md +0 -246
- package/skills/cometchat-ios-calls/references/voip-cert-setup.md +0 -166
- package/skills/cometchat-ios-production/SKILL.md +0 -738
- package/skills/cometchat-ios-testing/SKILL.md +0 -376
- package/skills/cometchat-ios-theming/SKILL.md +0 -667
- package/skills/cometchat-ios-troubleshooting/SKILL.md +0 -1003
- package/skills/cometchat-native-bare-patterns/SKILL.md +0 -550
- package/skills/cometchat-native-calls/SKILL.md +0 -671
- package/skills/cometchat-native-calls/references/add-calls-to-existing-chat.md +0 -203
- package/skills/cometchat-native-calls/references/call-layouts.md +0 -125
- package/skills/cometchat-native-calls/references/call-session.md +0 -182
- package/skills/cometchat-native-calls/references/custom-ui.md +0 -254
- package/skills/cometchat-native-calls/references/device-management.md +0 -158
- package/skills/cometchat-native-calls/references/expo-vs-bare.md +0 -179
- package/skills/cometchat-native-calls/references/group-calls.md +0 -296
- package/skills/cometchat-native-calls/references/idle-timeout.md +0 -128
- package/skills/cometchat-native-calls/references/in-call-chat.md +0 -145
- package/skills/cometchat-native-calls/references/migration-v4-to-v5.md +0 -72
- package/skills/cometchat-native-calls/references/picture-in-picture.md +0 -276
- package/skills/cometchat-native-calls/references/raise-hand.md +0 -205
- package/skills/cometchat-native-calls/references/recording-screen-share.md +0 -169
- package/skills/cometchat-native-calls/references/ringing-integration.md +0 -118
- package/skills/cometchat-native-calls/references/server-push-bridge.md +0 -135
- package/skills/cometchat-native-calls/references/server-push-payloads.md +0 -193
- package/skills/cometchat-native-calls/references/share-invite.md +0 -146
- package/skills/cometchat-native-calls/references/voip-push-end-to-end.md +0 -319
- package/skills/cometchat-native-components/SKILL.md +0 -628
- package/skills/cometchat-native-core/SKILL.md +0 -679
- package/skills/cometchat-native-customization/SKILL.md +0 -716
- package/skills/cometchat-native-expo-patterns/SKILL.md +0 -552
- package/skills/cometchat-native-features/SKILL.md +0 -638
- package/skills/cometchat-native-placement/SKILL.md +0 -509
- package/skills/cometchat-native-production/SKILL.md +0 -700
- package/skills/cometchat-native-push/SKILL.md +0 -594
- package/skills/cometchat-native-testing/SKILL.md +0 -582
- package/skills/cometchat-native-theming/SKILL.md +0 -681
- package/skills/cometchat-native-troubleshooting/SKILL.md +0 -501
- package/skills/cometchat-nextjs-patterns/SKILL.md +0 -899
- package/skills/cometchat-placement/SKILL.md +0 -1360
- package/skills/cometchat-production/SKILL.md +0 -1027
- package/skills/cometchat-react-calls/SKILL.md +0 -752
- package/skills/cometchat-react-calls/references/add-calls-to-existing-chat.md +0 -146
- package/skills/cometchat-react-calls/references/call-layouts.md +0 -160
- package/skills/cometchat-react-calls/references/call-session.md +0 -235
- package/skills/cometchat-react-calls/references/custom-ui.md +0 -247
- package/skills/cometchat-react-calls/references/device-management.md +0 -210
- package/skills/cometchat-react-calls/references/group-calls.md +0 -257
- package/skills/cometchat-react-calls/references/idle-timeout.md +0 -188
- package/skills/cometchat-react-calls/references/in-call-chat.md +0 -212
- package/skills/cometchat-react-calls/references/migration-v4-to-v5.md +0 -170
- package/skills/cometchat-react-calls/references/picture-in-picture.md +0 -245
- package/skills/cometchat-react-calls/references/raise-hand.md +0 -238
- package/skills/cometchat-react-calls/references/recording-screen-share.md +0 -144
- package/skills/cometchat-react-calls/references/ringing-integration.md +0 -255
- package/skills/cometchat-react-calls/references/server-web-push-vapid.md +0 -241
- package/skills/cometchat-react-calls/references/share-invite.md +0 -175
- package/skills/cometchat-react-calls/references/testing-calls-on-web.md +0 -216
- package/skills/cometchat-react-calls/references/virtual-background.md +0 -32
- package/skills/cometchat-react-calls/references/voip-and-web-push.md +0 -165
- package/skills/cometchat-react-patterns/SKILL.md +0 -628
- package/skills/cometchat-react-push/SKILL.md +0 -634
- package/skills/cometchat-react-router-patterns/SKILL.md +0 -758
- package/skills/cometchat-react-testing/SKILL.md +0 -413
- package/skills/cometchat-theming/SKILL.md +0 -498
- package/skills/cometchat-troubleshooting/SKILL.md +0 -294
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cometchat-skills — the deterministic "hands" layer of the pack.
|
|
4
|
+
*
|
|
5
|
+
* The v5 pivot kept the brain (skills/) and the materials (catalog/features/
|
|
6
|
+
* contracts/peers) but dropped the CLI the legacy pack had. This restores a
|
|
7
|
+
* small, dependency-free, DETERMINISTIC CLI so the steps an LLM should NOT
|
|
8
|
+
* improvise — detect the project, install skills, run the verification gates,
|
|
9
|
+
* refresh the oracle — are real commands with stable JSON contracts.
|
|
10
|
+
*
|
|
11
|
+
* npx @cometchat/skills <command> [options]
|
|
12
|
+
*
|
|
13
|
+
* Commands:
|
|
14
|
+
* detect Probe the CWD → framework / installed UI Kit / version_conflict (tools/detect.md contract)
|
|
15
|
+
* add [--ide <a>] Install THIS project's family skills for an agent (claude default; cursor/codex/copilot/… or `all`)
|
|
16
|
+
* list List the skills this pack ships (from peers.yaml + skills/)
|
|
17
|
+
* info <slug> Show one skill's triggers + compatibility
|
|
18
|
+
* verify [tier] Run the safety-net gates: lint | catalog | fences | predicate | all (default all offline)
|
|
19
|
+
* catalog [--check] Refresh the Tier-1 symbol catalog from the installed kit (oracle refresh)
|
|
20
|
+
* doctor Environment health check (node, kit install, catalog freshness, review state)
|
|
21
|
+
* version Print the pack version
|
|
22
|
+
* help This message
|
|
23
|
+
*
|
|
24
|
+
* Scope: every family in peers.yaml. Family-scoped paths/labels resolve from
|
|
25
|
+
* peers.yaml + families.json (RULES.md §22) — never a hardcoded family.
|
|
26
|
+
* Dependency-free (Node >=18 built-ins only). Every command supports --json.
|
|
27
|
+
*/
|
|
28
|
+
import {
|
|
29
|
+
readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, cpSync, statSync, rmSync,
|
|
30
|
+
} from "node:fs";
|
|
31
|
+
import { join, dirname, resolve } from "node:path";
|
|
32
|
+
import { fileURLToPath } from "node:url";
|
|
33
|
+
import { spawnSync } from "node:child_process";
|
|
34
|
+
import { homedir } from "node:os";
|
|
35
|
+
|
|
36
|
+
const PACK_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
37
|
+
const CWD = process.cwd();
|
|
38
|
+
const argv = process.argv.slice(2);
|
|
39
|
+
const CMD = argv[0];
|
|
40
|
+
const JSON_OUT = argv.includes("--json");
|
|
41
|
+
const has = (f) => argv.includes(f);
|
|
42
|
+
const flagVal = (f, d) => {
|
|
43
|
+
const i = argv.indexOf(f);
|
|
44
|
+
if (i >= 0) return argv[i + 1];
|
|
45
|
+
const eq = argv.find((a) => a.startsWith(f + "=")); // also accept --flag=value (e.g. --ide=cursor)
|
|
46
|
+
return eq ? eq.slice(f.length + 1) : d;
|
|
47
|
+
};
|
|
48
|
+
// A value-taking flag given WITHOUT a value — a bare trailing `--ide`, or `--ide --global` —
|
|
49
|
+
// used to fall through to the default and silently install for Claude. Refuse it instead.
|
|
50
|
+
function requireFlagValue(f, supported) {
|
|
51
|
+
const i = argv.indexOf(f);
|
|
52
|
+
if (i < 0) return;
|
|
53
|
+
const v = argv[i + 1];
|
|
54
|
+
if (v === undefined || v.startsWith("-")) {
|
|
55
|
+
console.error(`${f} needs a value\nsupported: ${supported.join(", ")}`);
|
|
56
|
+
process.exit(2);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function out(obj, human) {
|
|
61
|
+
if (JSON_OUT) console.log(JSON.stringify(obj, null, 0));
|
|
62
|
+
else human();
|
|
63
|
+
}
|
|
64
|
+
function readJSON(p) { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; } }
|
|
65
|
+
// Resolve which family the CURRENT workspace belongs to, from families.json — the same single
|
|
66
|
+
// source the pack tools read (RULES.md §22). detect() already knows the framework; this maps it
|
|
67
|
+
// onto a family so no command has to name one.
|
|
68
|
+
// detect() -> the family THIS project actually uses. families.json is the dev
|
|
69
|
+
// oracle but is NOT packed (see package.json "files"), so an installed pack must
|
|
70
|
+
// resolve from peers.yaml, which ships. Hardcoding a family here is what put
|
|
71
|
+
// "React v7 UI Kit" atop every React Native AGENTS.md (AUDIT-101).
|
|
72
|
+
const FRAMEWORK_PLATFORM = {
|
|
73
|
+
"react-native": "react-native", expo: "react-native",
|
|
74
|
+
reactjs: "react", nextjs: "react", astro: "react", "react-router": "react",
|
|
75
|
+
angular: "angular",
|
|
76
|
+
// Native families detect() already recognises (pubspec / gradle / xcodeproj). Without
|
|
77
|
+
// these, `add` in a native iOS/Android/Flutter project resolved no family and exited 3
|
|
78
|
+
// with a false "no family" message even though detect() knew the framework.
|
|
79
|
+
flutter: "flutter", android: "android", ios: "ios",
|
|
80
|
+
};
|
|
81
|
+
// NO "nearest family" fallback. Defaulting to react is what titled an Angular
|
|
82
|
+
// project's router "React v7 UI Kit" and installed 9 React skills into it — the
|
|
83
|
+
// same silent-substitution defect as AUDIT-101, one framework over (AUDIT-105).
|
|
84
|
+
// An unresolved family is reported, never guessed. `--family <platform>` is the
|
|
85
|
+
// explicit escape hatch for a monorepo root, where detect() sees no framework.
|
|
86
|
+
function resolvePeer(fw) {
|
|
87
|
+
const live = (peers().peers || []).filter((p) => !p.parked && p.dirPrefix);
|
|
88
|
+
requireFlagValue("--family", live.map((p) => p.platform));
|
|
89
|
+
const forced = flagVal("--family");
|
|
90
|
+
if (forced) {
|
|
91
|
+
const hit = live.find((p) => p.platform === forced);
|
|
92
|
+
if (!hit) {
|
|
93
|
+
console.error(`unknown --family: ${forced}\nshippable: ${live.map((p) => p.platform).join(", ")}`);
|
|
94
|
+
process.exit(2);
|
|
95
|
+
}
|
|
96
|
+
return hit;
|
|
97
|
+
}
|
|
98
|
+
return live.find((p) => p.platform === FRAMEWORK_PLATFORM[fw])
|
|
99
|
+
|| (live.length === 1 ? live[0] : null)
|
|
100
|
+
|| null;
|
|
101
|
+
}
|
|
102
|
+
const FAMILIES_JSON = join(PACK_ROOT, ".claude/skills/cometchat-skill-reviewer/tools/families.json");
|
|
103
|
+
// The DEV-ONLY reviewer family for a peer (families.json is not packed → null on an npm install).
|
|
104
|
+
// Matched by CATALOG key — the one field both registries share. Matching by `platform` picked
|
|
105
|
+
// whichever family sorted first: an Angular project resolved to web-react-v7 (families.json's
|
|
106
|
+
// Angular platform is "web"), and React Native to rn-calls-v5 instead of rn-uikit-v5.
|
|
107
|
+
function reviewerFamily(peer) {
|
|
108
|
+
const F = readJSON(FAMILIES_JSON);
|
|
109
|
+
if (!peer || !F || !F.families) return null;
|
|
110
|
+
const key = (f) => (f.catalog || "").replace(/^catalog\//, "").replace(/\.json$/, "");
|
|
111
|
+
const hit = Object.entries(F.families)
|
|
112
|
+
.find(([, f]) => !/^template\b/i.test(f._status || "") && key(f) === peer.catalog);
|
|
113
|
+
if (!hit) return null;
|
|
114
|
+
const [name, f] = hit;
|
|
115
|
+
return { ...f, name, key: key(f), typecheckProject: f.typecheckProject || `test-suite/typecheck/${key(f)}` };
|
|
116
|
+
}
|
|
117
|
+
function resolveFamily() {
|
|
118
|
+
const det = detect() || {};
|
|
119
|
+
const fw = det.framework || "";
|
|
120
|
+
const peer = resolvePeer(fw);
|
|
121
|
+
if (!peer) failUnresolvedFamily(fw, det);
|
|
122
|
+
const base = { label: peer.label, dirPrefix: peer.dirPrefix, kit_package: peer.kit_package, major: peer.major };
|
|
123
|
+
const fam = reviewerFamily(peer);
|
|
124
|
+
if (fam) return { ...fam, ...base };
|
|
125
|
+
// ponytail: packed install — peers.yaml is the only shipped registry.
|
|
126
|
+
return { platform: peer.platform, key: null, typecheckProject: null, ...base };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function failUnresolvedFamily(fw, det = {}) {
|
|
130
|
+
{
|
|
131
|
+
const live = (peers().peers || []).filter((x) => !x.parked && x.dirPrefix);
|
|
132
|
+
console.error(
|
|
133
|
+
`cannot resolve a CometChat family for this project.\n` +
|
|
134
|
+
` detected framework: ${fw || "(none)"}\n` +
|
|
135
|
+
(det.hint ? ` hint: ${det.hint}\n` : "") +
|
|
136
|
+
` shippable families: ${live.map((x) => `${x.platform} (${x.label})`).join(", ") || "(none)"}\n\n` +
|
|
137
|
+
(fw
|
|
138
|
+
? `There is no CometChat skills family for "${fw}" in this pack. Installing the nearest one\n` +
|
|
139
|
+
`would hand you another framework's APIs — so nothing was written.\n`
|
|
140
|
+
: `No framework was detected (a monorepo root looks like this). Run this inside the app\n` +
|
|
141
|
+
`package, or name the family explicitly.\n`) +
|
|
142
|
+
`\n npx @cometchat/skills add --ide <agent> --family <${live.map((x) => x.platform).join("|")}>`
|
|
143
|
+
);
|
|
144
|
+
process.exit(3);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function packVersion() { return (readJSON(join(PACK_ROOT, "package.json")) || {}).version || "0.0.0"; }
|
|
149
|
+
|
|
150
|
+
// ── detect — tools/detect.md contract (deterministic, offline) ───────────────
|
|
151
|
+
const TARGET_MAJOR = 7; // react UI Kit major these skills target (fallback when peers.yaml is absent)
|
|
152
|
+
|
|
153
|
+
// The UI Kit major EACH family targets, from peers.yaml `kit_package: "<npm-name>@<major>"`
|
|
154
|
+
// (react → 7, angular → 5, react-native → 5). "Not v7" is only a conflict for React: Angular v5
|
|
155
|
+
// and RN v5 are the CORRECT majors for their own families.
|
|
156
|
+
function jsKitTargets() {
|
|
157
|
+
const t = {};
|
|
158
|
+
for (const p of peers().peers || []) {
|
|
159
|
+
const m = /^(@cometchat\/chat-uikit-[a-z-]+)@(\d+)$/.exec(p.kit_package || "");
|
|
160
|
+
if (m && !p.parked) t[m[1]] = parseInt(m[2], 10);
|
|
161
|
+
}
|
|
162
|
+
if (!Object.keys(t).length) t["@cometchat/chat-uikit-react"] = TARGET_MAJOR;
|
|
163
|
+
return t;
|
|
164
|
+
}
|
|
165
|
+
const FRAMEWORK_KIT = { angular: "@cometchat/chat-uikit-angular", "react-native": "@cometchat/chat-uikit-react-native", expo: "@cometchat/chat-uikit-react-native" };
|
|
166
|
+
|
|
167
|
+
function ccVersion(pkg, dep) {
|
|
168
|
+
const v = (pkg.dependencies && pkg.dependencies[dep]) || (pkg.devDependencies && pkg.devDependencies[dep]);
|
|
169
|
+
if (!v) return null;
|
|
170
|
+
const m = v.match(/(\d+)\.(\d+)\.(\d+)/) || v.match(/(\d+)/);
|
|
171
|
+
return { range: v, major: m ? parseInt(m[1], 10) : null };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// A REAL product package -- kit/sdk/calls, any peer, from peers.yaml -- never
|
|
175
|
+
// the AI-tooling package itself. The old check used "any @cometchat/* dependency
|
|
176
|
+
// or a .cometchat/ dir", and BOTH halves were tool-installation signals, not
|
|
177
|
+
// integration signals: @cometchat/skills always matches "@cometchat/*" (every
|
|
178
|
+
// project that can run this CLI has it installed), and .cometchat/skills is
|
|
179
|
+
// literally NEUTRAL_SKILLS_DIR -- the tool's OWN install target, created by any
|
|
180
|
+
// --ide antigravity/cursor/codex/... install regardless of whether the product
|
|
181
|
+
// was ever integrated. So existing_cometchat had read true from the moment the
|
|
182
|
+
// TOOL was installed, on every project, forever -- which matters because it is
|
|
183
|
+
// the load-bearing signal for the dispatcher's "already integrated, skip
|
|
184
|
+
// onboarding" branch (AUDIT-108).
|
|
185
|
+
function productPackageNames() {
|
|
186
|
+
const y = existsSync(join(PACK_ROOT, "peers.yaml")) ? readFileSync(join(PACK_ROOT, "peers.yaml"), "utf8") : "";
|
|
187
|
+
const raw = [...y.matchAll(/(?:kit|sdk|calls)_package:\s*"([^"]+)"/g)].map((m) => m[1]);
|
|
188
|
+
return new Set(raw.map((pkg) => pkg.replace(/@[^@/]+$/, ""))); // strip trailing @<major>, keep the leading @scope
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function detect() {
|
|
192
|
+
const pkgPath = join(CWD, "package.json");
|
|
193
|
+
const result = {
|
|
194
|
+
framework: null, flutter_version: null, android_variant: null,
|
|
195
|
+
installed_uikit: null, version_conflict: null, env_file: null, existing_cometchat: false,
|
|
196
|
+
// CometChat-relevant repo signals (for cometchat-onboarding). Additive: null when
|
|
197
|
+
// the evidence is absent (graceful degradation) — existing consumers ignore them.
|
|
198
|
+
auth: null, styling: null, state: null, routing: null, ssr: null,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// JS ecosystem
|
|
202
|
+
if (existsSync(pkgPath)) {
|
|
203
|
+
const pkg = readJSON(pkgPath) || {};
|
|
204
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
205
|
+
const hasDep = (d) => Object.prototype.hasOwnProperty.call(deps, d);
|
|
206
|
+
const hasAnyDep = (...ds) => ds.some(hasDep);
|
|
207
|
+
// some deps are namespaced (e.g. @clerk/*, @react-router/*) — match by prefix too.
|
|
208
|
+
const depKeys = Object.keys(deps);
|
|
209
|
+
const hasDepPrefix = (p) => depKeys.some((d) => d === p || d.startsWith(p));
|
|
210
|
+
const fileExists = (rel) => existsSync(join(CWD, rel));
|
|
211
|
+
const globish = (names) => names.some(fileExists);
|
|
212
|
+
|
|
213
|
+
let fwDep = null; // the dependency that decided the framework
|
|
214
|
+
if (hasDep("@angular/core")) { result.framework = "angular"; fwDep = "@angular/core"; }
|
|
215
|
+
else if (hasDep("expo")) { result.framework = "expo"; result.env_file = ".env"; fwDep = "expo"; }
|
|
216
|
+
else if (hasDep("react-native")) { result.framework = "react-native"; result.env_file = ".env"; fwDep = "react-native"; }
|
|
217
|
+
else if (hasDep("astro")) { result.framework = "astro"; result.env_file = ".env"; fwDep = "astro"; }
|
|
218
|
+
else if (hasDep("next")) { result.framework = "nextjs"; result.env_file = ".env.local"; fwDep = "next"; }
|
|
219
|
+
else if (hasDep("react-router-dom") || hasDep("@react-router/dev")) { result.framework = "react-router"; result.env_file = ".env"; fwDep = hasDep("react-router-dom") ? "react-router-dom" : "@react-router/dev"; }
|
|
220
|
+
else if (hasDep("react")) { result.framework = "reactjs"; result.env_file = ".env"; fwDep = "react"; }
|
|
221
|
+
|
|
222
|
+
if (result.framework === "angular") result.env_file = "src/environments/environment.ts";
|
|
223
|
+
|
|
224
|
+
// A WORKSPACE ROOT (package.json `workspaces`, or pnpm-workspace.yaml) whose framework dep is
|
|
225
|
+
// only a devDependency is tooling (storybook, lint, test config) — not an app. Auto-picking
|
|
226
|
+
// React there installed the React skills at a monorepo root. Report it; the user runs inside
|
|
227
|
+
// the app package or names the family. A root that lists the dep in `dependencies` IS an app.
|
|
228
|
+
const wsRoot = !!pkg.workspaces || fileExists("pnpm-workspace.yaml");
|
|
229
|
+
const inProdDeps = fwDep && pkg.dependencies && Object.prototype.hasOwnProperty.call(pkg.dependencies, fwDep);
|
|
230
|
+
if (result.framework && wsRoot && !inProdDeps) {
|
|
231
|
+
result.workspace_root = true;
|
|
232
|
+
result.hint = `workspace root (${pkg.workspaces ? 'package.json "workspaces"' : "pnpm-workspace.yaml"}) with ` +
|
|
233
|
+
`${fwDep} only in devDependencies — that is tooling, not an app. Run inside the app package ` +
|
|
234
|
+
`(e.g. apps/web), or pass --family <platform>.`;
|
|
235
|
+
result.framework = null; result.env_file = null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── CometChat-relevant repo signals (deterministic, offline, graceful) ─────
|
|
239
|
+
// auth
|
|
240
|
+
if (hasDepPrefix("@auth/") || hasDep("next-auth")) result.auth = "nextauth";
|
|
241
|
+
else if (hasDepPrefix("@clerk/")) result.auth = "clerk";
|
|
242
|
+
else if (hasDep("firebase") || hasDepPrefix("@firebase/")) result.auth = "firebase";
|
|
243
|
+
else if (hasDep("auth0") || hasDepPrefix("@auth0/")) result.auth = "auth0";
|
|
244
|
+
else if (hasDep("@supabase/supabase-js") || hasDepPrefix("@supabase/")) result.auth = "supabase";
|
|
245
|
+
// else null — custom auth isn't inferable from deps alone.
|
|
246
|
+
|
|
247
|
+
// styling / design system
|
|
248
|
+
if (hasDep("tailwindcss") || globish(["tailwind.config.js", "tailwind.config.ts", "tailwind.config.cjs", "tailwind.config.mjs"])) result.styling = "tailwind";
|
|
249
|
+
else if (hasDepPrefix("@mui/")) result.styling = "mui";
|
|
250
|
+
else if (hasDep("styled-components")) result.styling = "styled-components";
|
|
251
|
+
else if (hasDepPrefix("@chakra-ui/")) result.styling = "chakra";
|
|
252
|
+
// css-modules is a convention, not a dep — leave to onboarding's repo read; null here.
|
|
253
|
+
|
|
254
|
+
// state management
|
|
255
|
+
if (hasDep("@reduxjs/toolkit") || hasDep("react-redux") || hasDep("redux")) result.state = "redux";
|
|
256
|
+
else if (hasDep("zustand")) result.state = "zustand";
|
|
257
|
+
else if (hasDep("mobx") || hasDepPrefix("mobx-react")) result.state = "mobx";
|
|
258
|
+
else if (hasDep("recoil")) result.state = "recoil";
|
|
259
|
+
else if (result.framework && /^(reactjs|nextjs|react-router|expo|react-native)$/.test(result.framework)) result.state = "context"; // React's built-in default
|
|
260
|
+
// else null (e.g. angular has its own DI; leave null unless a lib is present)
|
|
261
|
+
|
|
262
|
+
// routing
|
|
263
|
+
if (result.framework === "nextjs") result.routing = fileExists("app") || fileExists("src/app") ? "next-app-router" : (fileExists("pages") || fileExists("src/pages") ? "next-pages-router" : "next-app-router");
|
|
264
|
+
else if (hasDep("react-router-dom") || hasDepPrefix("@react-router/")) result.routing = "react-router";
|
|
265
|
+
else if (hasDepPrefix("@tanstack/react-router")) result.routing = "tanstack-router";
|
|
266
|
+
else if (hasDep("vue-router")) result.routing = "vue-router";
|
|
267
|
+
else if (hasDep("expo-router")) result.routing = "expo-router";
|
|
268
|
+
else if (hasDep("@angular/router") || result.framework === "angular") result.routing = "angular-router";
|
|
269
|
+
// else null
|
|
270
|
+
|
|
271
|
+
// SSR/CSR boundary
|
|
272
|
+
if (hasDep("next") || hasDep("nuxt") || hasDep("@remix-run/react") || hasDepPrefix("@sveltejs/kit")) result.ssr = "ssr";
|
|
273
|
+
else if (hasDep("astro")) result.ssr = "islands";
|
|
274
|
+
else if (result.framework && /^(reactjs|react-router|expo|react-native)$/.test(result.framework)) result.ssr = "csr";
|
|
275
|
+
// else null
|
|
276
|
+
|
|
277
|
+
if (result.workspace_root) { result.auth = result.styling = result.state = result.routing = result.ssr = null; }
|
|
278
|
+
|
|
279
|
+
// installed UI Kit — each JS family's kit, checked against THAT family's target major.
|
|
280
|
+
const kits = [];
|
|
281
|
+
for (const [name, target] of Object.entries(jsKitTargets())) {
|
|
282
|
+
const v = ccVersion(pkg, name);
|
|
283
|
+
if (!v) continue;
|
|
284
|
+
// prefer the actually-installed version if node_modules is present
|
|
285
|
+
const inst = readJSON(join(CWD, "node_modules", name, "package.json"));
|
|
286
|
+
const version = inst && inst.version ? inst.version : v.range;
|
|
287
|
+
const major = inst && inst.version ? parseInt(inst.version.split(".")[0], 10) : v.major;
|
|
288
|
+
kits.push({ name, target, version, major });
|
|
289
|
+
}
|
|
290
|
+
if (kits.length) {
|
|
291
|
+
const k = kits.find((x) => x.name === (FRAMEWORK_KIT[result.framework] || "@cometchat/chat-uikit-react")) || kits[0];
|
|
292
|
+
result.installed_uikit = { package: k.name, version: k.version };
|
|
293
|
+
if (k.major && k.major !== k.target) {
|
|
294
|
+
result.version_conflict = { detail: `project has ${k.name}@${k.major} but these skills target v${k.target}` };
|
|
295
|
+
}
|
|
296
|
+
// two different UI Kits (e.g. react + angular) declared at different majors
|
|
297
|
+
if (kits.length > 1 && new Set(kits.map((x) => x.major)).size > 1) {
|
|
298
|
+
const short = (x) => x.name.replace("@cometchat/chat-uikit-", "");
|
|
299
|
+
result.version_conflict = { detail: `two UI Kit majors declared (${kits.map((x) => `${short(x)} v${x.major}`).join(" + ")}) — pick one cohort` };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// A REAL product package as a project dependency is the only reliable "already
|
|
303
|
+
// integrated" signal. Do NOT also treat a `.cometchat/` dir as proof: it is this
|
|
304
|
+
// TOOL's own install target (NEUTRAL_SKILLS_DIR), created on every `add`, so it made
|
|
305
|
+
// every project read as integrated the moment the tooling was installed — the other
|
|
306
|
+
// half of the AUDIT-108 false positive (the dependency half was already narrowed to
|
|
307
|
+
// productPackageNames; this removes the dir half).
|
|
308
|
+
result.existing_cometchat =
|
|
309
|
+
Object.keys(deps).some((d) => productPackageNames().has(d));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Non-JS scaffolds (light-touch — enough to fire version_conflict / routing)
|
|
313
|
+
if (!result.framework && existsSync(join(CWD, "pubspec.yaml"))) {
|
|
314
|
+
result.framework = "flutter";
|
|
315
|
+
const pub = readFileSync(join(CWD, "pubspec.yaml"), "utf8");
|
|
316
|
+
if (/cometchat_chat_uikit/.test(pub)) {
|
|
317
|
+
result.existing_cometchat = true;
|
|
318
|
+
const m = pub.match(/cometchat_chat_uikit:\s*["']?\^?(\d+)/);
|
|
319
|
+
if (m) result.flutter_version = parseInt(m[1], 10) >= 6 ? "v6" : "v5";
|
|
320
|
+
// installed_uikit was never populated on this branch, and version_conflict is
|
|
321
|
+
// derived from it — so a Flutter project sitting on the wrong major could never
|
|
322
|
+
// raise a conflict at all. Prefer the RESOLVED version in pubspec.lock over the
|
|
323
|
+
// declared range: "^6.1.0" says what is allowed, the lock says what is installed.
|
|
324
|
+
const declared = pub.match(/cometchat_chat_uikit:\s*["']?([^"'\s]+)/);
|
|
325
|
+
let version = declared ? declared[1] : null;
|
|
326
|
+
const lockPath = join(CWD, "pubspec.lock");
|
|
327
|
+
if (existsSync(lockPath)) {
|
|
328
|
+
const lock = readFileSync(lockPath, "utf8");
|
|
329
|
+
const lm = lock.match(/^\s{2}cometchat_chat_uikit:[\s\S]*?^\s{4}version:\s*["']?([\d.]+)/m);
|
|
330
|
+
if (lm) version = lm[1];
|
|
331
|
+
}
|
|
332
|
+
result.installed_uikit = { package: "cometchat_chat_uikit", version };
|
|
333
|
+
const major = version ? parseInt(String(version).replace(/^\D+/, ""), 10) : null;
|
|
334
|
+
if (major && major !== 6) {
|
|
335
|
+
result.version_conflict = {
|
|
336
|
+
detail: `project has cometchat_chat_uikit@${major} but these skills target v6`,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// The Flutter kit reads its credentials from a registered settings ASSET, not a dotenv
|
|
341
|
+
// file (commit a placeholder — a registered asset must exist for a fresh clone to build;
|
|
342
|
+
// cometchat-flutter-v6-core). Same reason the Android branch below names one. Leaving this
|
|
343
|
+
// null pointed the user at nothing on the one step that must precede any render.
|
|
344
|
+
result.env_file = "cometchat-settings.json";
|
|
345
|
+
}
|
|
346
|
+
// Android. BOTH DSLs — Groovy (.gradle) and Kotlin (.gradle.kts). Checking only the Groovy
|
|
347
|
+
// names missed every Kotlin-DSL project, which is the majority AND what this pack's own install
|
|
348
|
+
// block prescribes: `framework` came back null and the router could not route to Android at all.
|
|
349
|
+
const GRADLE_MARKERS = ["settings.gradle", "settings.gradle.kts", "app/build.gradle",
|
|
350
|
+
"app/build.gradle.kts", "build.gradle", "build.gradle.kts"];
|
|
351
|
+
if (!result.framework && GRADLE_MARKERS.some((f) => existsSync(join(CWD, f)))) {
|
|
352
|
+
result.framework = "android";
|
|
353
|
+
// Android credentials are NOT a dotenv file — they are the gitignored settings asset that
|
|
354
|
+
// `CometChatUIKit.initFromSettings` reads. Reporting "(n/a)" left the user with no pointer at
|
|
355
|
+
// all, on the one setup step that must happen before anything renders.
|
|
356
|
+
result.env_file = "app/src/main/assets/cometchat-settings.json";
|
|
357
|
+
const gradle = ["app/build.gradle", "app/build.gradle.kts", "build.gradle", "build.gradle.kts",
|
|
358
|
+
"gradle/libs.versions.toml"]
|
|
359
|
+
.map((f) => join(CWD, f)).filter(existsSync).map((f) => readFileSync(f, "utf8")).join("\n");
|
|
360
|
+
|
|
361
|
+
// Cohort. A bare /compose/ test matched "composer", the kit's own artifact name, and comments.
|
|
362
|
+
// Score STRUCTURAL signals instead, and report `mixed`/`unknown` honestly rather than guessing —
|
|
363
|
+
// emitting the wrong cohort is unrecoverable for the consumer.
|
|
364
|
+
const composeHits = [
|
|
365
|
+
/buildFeatures\s*\{[^}]*\bcompose\s*[= ]\s*true/s, // buildFeatures { compose = true }
|
|
366
|
+
/org\.jetbrains\.kotlin\.plugin\.compose/, // the compose compiler plugin
|
|
367
|
+
/androidx\.compose[.:]/, // androidx.compose.* dependency
|
|
368
|
+
/compose-bom/,
|
|
369
|
+
].filter((re) => re.test(gradle)).length;
|
|
370
|
+
const viewsHits = [
|
|
371
|
+
/buildFeatures\s*\{[^}]*viewBinding\s*[= ]\s*true/s,
|
|
372
|
+
/buildFeatures\s*\{[^}]*dataBinding\s*[= ]\s*true/s,
|
|
373
|
+
/com\.android\.databinding/,
|
|
374
|
+
].filter((re) => re.test(gradle)).length
|
|
375
|
+
+ (existsSync(join(CWD, "app/src/main/res/layout")) ? 1 : 0); // XML layouts on disk
|
|
376
|
+
|
|
377
|
+
result.android_variant =
|
|
378
|
+
composeHits && !viewsHits ? "compose" :
|
|
379
|
+
viewsHits && !composeHits ? "views" :
|
|
380
|
+
composeHits && viewsHits ? "mixed" : "unknown";
|
|
381
|
+
result.android_cohort_signals = { composeHits, viewsHits };
|
|
382
|
+
|
|
383
|
+
// Existing CometChat dependency + major — drives version_conflict and the upgrade offer.
|
|
384
|
+
const kit = gradle.match(/com\.cometchat:(chatuikit-(?:kotlin|compose|core)-android|chat-uikit-android)[:"'\s]*([0-9]+)?/);
|
|
385
|
+
if (kit) {
|
|
386
|
+
result.existing_cometchat = true;
|
|
387
|
+
result.installed_uikit = kit[1];
|
|
388
|
+
const major = kit[2] ? parseInt(kit[2], 10) : null;
|
|
389
|
+
if (major) result.android_uikit_major = `v${major}`;
|
|
390
|
+
if (major && major < 6) {
|
|
391
|
+
result.version_conflict = { detail: `android UI Kit v${major} installed; this pack targets v6` };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// iOS. peers.yaml lists Package.swift / *.xcodeproj / Podfile / *.xcworkspace; checking only the
|
|
396
|
+
// Podfile + .xcodeproj left an SPM-only app and an .xcworkspace-only checkout detecting as nothing.
|
|
397
|
+
// A root Package.swift is AMBIGUOUS on its own — a Swift server/CLI package has one too — so it
|
|
398
|
+
// counts only with an iOS signal: an `.iOS(` platform, or a CometChat iOS product dependency.
|
|
399
|
+
if (!result.framework) {
|
|
400
|
+
const entries = readdirSync(CWD);
|
|
401
|
+
const read = (rel) => { try { return readFileSync(join(CWD, rel), "utf8"); } catch { return ""; } };
|
|
402
|
+
const xcode = entries.filter((f) => f.endsWith(".xcodeproj") || f.endsWith(".xcworkspace"));
|
|
403
|
+
const spm = existsSync(join(CWD, "Package.swift")) ? read("Package.swift") : null;
|
|
404
|
+
const IOS_CC = /CometChatUIKitSwift|CometChatCallsSDK|CometChatSDK|cometchat-uikit-ios|chat-sdk-ios|calls-sdk-ios/;
|
|
405
|
+
const spmIsIos = spm !== null && (/\.iOS\s*\(/.test(spm) || IOS_CC.test(spm));
|
|
406
|
+
if (existsSync(join(CWD, "Podfile")) || xcode.length || spmIsIos) {
|
|
407
|
+
result.framework = "ios";
|
|
408
|
+
result.env_file = "Secrets.xcconfig";
|
|
409
|
+
// Existing integration: a CometChat iOS product in the Podfile, Package.swift, the Xcode
|
|
410
|
+
// project, or a resolved SPM pin.
|
|
411
|
+
const evidence = [read("Podfile"), spm || "", read("Package.resolved"),
|
|
412
|
+
...xcode.flatMap((x) => [read(`${x}/project.pbxproj`),
|
|
413
|
+
read(`${x}/xcshareddata/swiftpm/Package.resolved`),
|
|
414
|
+
read(`${x}/project.xcworkspace/xcshareddata/swiftpm/Package.resolved`)])].join("\n");
|
|
415
|
+
if (IOS_CC.test(evidence)) {
|
|
416
|
+
result.existing_cometchat = true;
|
|
417
|
+
// The kit id comes from peers.yaml (RULES §22 — no hardcoded family keys in tools).
|
|
418
|
+
const iosKit = ((peers().peers || []).find((p) => p.platform === "ios") || {}).kit_package;
|
|
419
|
+
if (iosKit && (evidence.includes(iosKit) || /cometchat-uikit-ios/.test(evidence))) result.installed_uikit = iosKit;
|
|
420
|
+
}
|
|
421
|
+
} else if (spm !== null) {
|
|
422
|
+
result.ambiguous = "ios";
|
|
423
|
+
result.hint = "Package.swift found, but no iOS signal (no .iOS(...) platform, no CometChat iOS dependency) — " +
|
|
424
|
+
"this looks like a Swift server/CLI package. If it IS an iOS app, pass --family ios.";
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return result;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ── registry (derived from peers.yaml) ───────────────────────────────────────
|
|
431
|
+
function peers() {
|
|
432
|
+
const p = join(PACK_ROOT, "peers.yaml");
|
|
433
|
+
if (!existsSync(p)) return { entry: "cometchat", peers: [] };
|
|
434
|
+
const y = readFileSync(p, "utf8");
|
|
435
|
+
const entry = (y.match(/^entry:\s*(\S+)/m) || [])[1] || "cometchat";
|
|
436
|
+
const list = [];
|
|
437
|
+
for (const block of y.split(/^\s*-\s*platform:/m).slice(1)) {
|
|
438
|
+
const platform = (block.match(/^\s*(\S+)/) || [])[1];
|
|
439
|
+
const major = (block.match(/major:\s*(\S+)/) || [])[1];
|
|
440
|
+
const kit = (block.match(/kit_package:\s*"([^"]+)"/) || [])[1];
|
|
441
|
+
const skills = (block.match(/skills:\s*\[([^\]]*)\]/) || [])[1];
|
|
442
|
+
const verified = /^\s*verified:\s*true\b/m.test(block); // line-anchored: don't match "verified:true" inside a trailing comment on a `verified: false` line
|
|
443
|
+
const label = (block.match(/label:\s*"([^"]+)"/) || [])[1];
|
|
444
|
+
const dirPrefix = (block.match(/dir_prefix:\s*"([^"]+)"/) || [])[1];
|
|
445
|
+
const parked = /^\s*parked:\s*true\b/m.test(block); // line-anchored, like `verified`
|
|
446
|
+
list.push({ platform, major, kit_package: kit, label, dirPrefix, parked, skills: skills ? skills.split(",").map((s) => s.trim()) : [], verified });
|
|
447
|
+
}
|
|
448
|
+
// `catalog` + `env_file` make doctor/detect family-aware instead of hardcoding one family.
|
|
449
|
+
// Enriched in a SECOND pass, deliberately: the loop body above is shared with the React Native
|
|
450
|
+
// branch (PR #170), and editing it there turns a clean 3-way merge into a hand-resolved conflict.
|
|
451
|
+
y.split(/^\s*-\s*platform:/m).slice(1).forEach((block, i) => {
|
|
452
|
+
if (!list[i]) return;
|
|
453
|
+
list[i].catalog = (block.match(/catalog:\s*(\S+)/) || [])[1];
|
|
454
|
+
list[i].env_file = (block.match(/env_file:\s*"([^"]+)"/) || [])[1];
|
|
455
|
+
list[i].features = (block.match(/features:\s*(\S+\.json)/) || [])[1];
|
|
456
|
+
list[i].contracts = (block.match(/contracts:\s*(\S+\.json)/) || [])[1];
|
|
457
|
+
});
|
|
458
|
+
return { entry, peers: list };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function shippedSkillDirs() {
|
|
462
|
+
const dir = join(PACK_ROOT, "skills");
|
|
463
|
+
if (!existsSync(dir)) return [];
|
|
464
|
+
return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ── commands ──────────────────────────────────────────────────────────────────
|
|
468
|
+
function cmdDetect() {
|
|
469
|
+
const r = detect();
|
|
470
|
+
out(r, () => {
|
|
471
|
+
console.log(`framework: ${r.framework || "(none detected)"}`);
|
|
472
|
+
// The react branch sets `{package, version}`; the android branch sets a bare artifact STRING
|
|
473
|
+
// (there is no lockfile to read a version from), so the object form printed
|
|
474
|
+
// "undefined@undefined" on every Android project that already had the kit.
|
|
475
|
+
if (r.installed_uikit) {
|
|
476
|
+
const u = r.installed_uikit;
|
|
477
|
+
console.log(`installed UI Kit: ${typeof u === "string" ? u : `${u.package}@${u.version}`}`);
|
|
478
|
+
}
|
|
479
|
+
console.log(`env file: ${r.env_file || "(n/a)"}`);
|
|
480
|
+
console.log(`existing CometChat: ${r.existing_cometchat}`);
|
|
481
|
+
// repo signals for onboarding (only print the ones we found)
|
|
482
|
+
const sig = [["auth", r.auth], ["styling", r.styling], ["state", r.state], ["routing", r.routing], ["ssr", r.ssr]].filter(([, v]) => v);
|
|
483
|
+
if (sig.length) console.log(`repo signals: ${sig.map(([k, v]) => `${k}=${v}`).join(" ")}`);
|
|
484
|
+
if (r.version_conflict) console.log(`⚠️ version_conflict: ${r.version_conflict.detail} → STOP & reconcile (RULES §3)`);
|
|
485
|
+
if (r.hint) console.log(`hint: ${r.hint}`);
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function cmdList() {
|
|
490
|
+
const reg = peers();
|
|
491
|
+
const dirs = shippedSkillDirs();
|
|
492
|
+
out({ entry: reg.entry, peers: reg.peers, skills: dirs }, () => {
|
|
493
|
+
console.log(`Entry: /${reg.entry}`);
|
|
494
|
+
// Parked peers are a routing SPEC only (no installable skill) — listing them with a
|
|
495
|
+
// "verified" badge and "declared but not shipped" rows read as a broken pack.
|
|
496
|
+
const parked = reg.peers.filter((p) => p.parked);
|
|
497
|
+
for (const p of reg.peers.filter((x) => !x.parked)) {
|
|
498
|
+
console.log(`\n${p.platform} ${p.major} (${p.kit_package}) ${p.verified ? "✓ verified" : "unverified"}`);
|
|
499
|
+
for (const s of p.skills) {
|
|
500
|
+
// `dir_prefix` is the peer's own answer and always wins. Without it, composing
|
|
501
|
+
// `cometchat-<platform>-<major>-<skill>` invents names: the SDK peer carries its
|
|
502
|
+
// consumption in `platform` (android-sdk) while the dir does not, and a platform with only
|
|
503
|
+
// one live major ships no <major> segment at all (NAMING.md) — e.g. cometchat-ios-core.
|
|
504
|
+
const base = p.platform.replace(/-(sdk|uikit)$/, "");
|
|
505
|
+
const direct = `cometchat-${p.platform}-${p.major}-${s}`;
|
|
506
|
+
const slug = [p.dirPrefix && `${p.dirPrefix}${s}`, direct,
|
|
507
|
+
`cometchat-${base}-${p.major}-${s}`,
|
|
508
|
+
`cometchat-${p.platform}-${s}`, `cometchat-${base}-${s}`]
|
|
509
|
+
.filter(Boolean).find((c) => dirs.includes(c)) || null;
|
|
510
|
+
console.log(slug ? ` - ${slug}` : ` - ${direct} ⚠️ declared in peers.yaml but not shipped`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (parked.length) console.log(`\nparked (routing spec only — no installable skills): ${parked.map((p) => `${p.platform} (${p.label || p.kit_package})`).join(", ")}`);
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function cmdInfo() {
|
|
518
|
+
const slug = argv[1];
|
|
519
|
+
if (!slug) { console.error("usage: cometchat-skills info <skill-dir>"); process.exit(2); }
|
|
520
|
+
const file = join(PACK_ROOT, "skills", slug, "SKILL.md");
|
|
521
|
+
if (!existsSync(file)) { console.error(`no such skill: ${slug}`); process.exit(2); }
|
|
522
|
+
const b = (readFileSync(file, "utf8").match(/^---\n([\s\S]*?)\n---/) || [])[1] || "";
|
|
523
|
+
const g = (k) => (b.match(new RegExp("^" + k + ':\\s*"?(.+?)"?$', "m")) || [])[1] || "";
|
|
524
|
+
out({ name: g("name"), description: g("description"), compatibility: g("compatibility") }, () => {
|
|
525
|
+
console.log(`${g("name")}\n\n${g("description")}\n\ncompatibility: ${g("compatibility")}`);
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Shared pack-root data files that skills reference by BARE name (RULES.md,
|
|
530
|
+
// features.json, …). The copy step used to ship only skill directories, so these
|
|
531
|
+
// references resolved to nothing post-install — the skills pointed at files that
|
|
532
|
+
// were never installed. Ship a copy alongside every skill so bare refs resolve.
|
|
533
|
+
// Discovered, not hard-coded. A per-family pack has one oracle pair PER FAMILY
|
|
534
|
+
// (features.json + contracts.json for web-react, features.rn-v5.json +
|
|
535
|
+
// contracts.rn-v5.json for react-native, …). A fixed list ships only the first family's,
|
|
536
|
+
// so every later family's skills point at oracles that were never installed — the exact
|
|
537
|
+
// bug this comment block was written about, recurring one family later.
|
|
538
|
+
const SHARED_ROOT_FILES = ["RULES.md", "peers.yaml"];
|
|
539
|
+
function sharedRootFiles() {
|
|
540
|
+
const dynamic = readdirSync(PACK_ROOT).filter(
|
|
541
|
+
(f) => /^(features|contracts)([.-][a-z0-9-]+)?\.json$/i.test(f)
|
|
542
|
+
);
|
|
543
|
+
return [...SHARED_ROOT_FILES, ...dynamic];
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Same problem as sharedRootFiles(), one level down: a skill's own prose can bake a
|
|
547
|
+
// literal runtime-fetchable path ("catalogs/rn-v5.json") as its anti-hallucination
|
|
548
|
+
// oracle -- "anything not in catalogs/rn-v5.json does not exist, do not emit it".
|
|
549
|
+
// sharedRootFiles()'s regex only ever matches top-level FILENAMES, so a directory
|
|
550
|
+
// referenced this way could never be discovered by it and never installed at all
|
|
551
|
+
// (AUDIT-107). Whole-directory shared items are rare and structural -- there is
|
|
552
|
+
// exactly one today -- so this is a short literal list, not a second regex scan.
|
|
553
|
+
const SHARED_ROOT_DIRS = ["catalogs"];
|
|
554
|
+
function sharedRootDirs() {
|
|
555
|
+
return SHARED_ROOT_DIRS.filter((d) => existsSync(join(PACK_ROOT, d)));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Only THIS project's family (+ the two platform-neutral skills) is installed.
|
|
559
|
+
// Installing every family let a React skill out-match its RN twin: React's
|
|
560
|
+
// features triggers are unqualified ('add polls'), RN's say "React Native", so an
|
|
561
|
+
// RN user asking "add polls" matched the WRONG framework's skill (AUDIT-101).
|
|
562
|
+
// The prefix match deliberately co-installs a family's headless-SDK sibling where the prefixes
|
|
563
|
+
// overlap: cometchat-ios-calls/-push route VoIP push to cometchat-ios-v5-sdk, and the Android
|
|
564
|
+
// calls-sdk skill's 1:1 ringing leans on the cometchat-android-v5-sdk recipe (and vice versa).
|
|
565
|
+
function familySkillDirs(fam) {
|
|
566
|
+
const all = shippedSkillDirs();
|
|
567
|
+
if (!fam || !fam.dirPrefix) return all;
|
|
568
|
+
const neutral = new Set([peers().entry || "cometchat", "cometchat-onboarding"]);
|
|
569
|
+
return all.filter((d) => neutral.has(d) || d.startsWith(fam.dirPrefix));
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Remove-on-add: drop the skill folders an EARLIER generation shipped, so a v4→v5 upgrade
|
|
573
|
+
// never leaves stale skills behind (v4's cometchat-core/features/theming lingering next to
|
|
574
|
+
// the v5 set, with cometchat-core still advertising "React UI Kit v6"). Scoped to the exact
|
|
575
|
+
// legacy names in bin/legacy-skills.json — NOT every `cometchat*` dir: a current-pack skill
|
|
576
|
+
// may belong to another family, a calls peer, or a --global install from another project
|
|
577
|
+
// (installing a second family must not wipe the first), and any other cometchat-* dir is the
|
|
578
|
+
// user's own. The authoring/meta skills are never pruned either (dev tooling, not installed by `add`).
|
|
579
|
+
const PROTECTED_SKILLS = new Set(["cometchat-skill-creator", "cometchat-skill-reviewer", "cometchat-skill-explorer"]);
|
|
580
|
+
function legacySkillNames() {
|
|
581
|
+
try { return new Set(JSON.parse(readFileSync(join(PACK_ROOT, "bin", "legacy-skills.json"), "utf8")).skills || []); }
|
|
582
|
+
catch { return new Set(); }
|
|
583
|
+
}
|
|
584
|
+
function pruneLegacySkills(target) {
|
|
585
|
+
if (!existsSync(target)) return [];
|
|
586
|
+
const legacy = legacySkillNames();
|
|
587
|
+
const current = new Set(shippedSkillDirs());
|
|
588
|
+
const removed = [];
|
|
589
|
+
for (const name of readdirSync(target)) {
|
|
590
|
+
if (!legacy.has(name) || current.has(name) || PROTECTED_SKILLS.has(name)) continue;
|
|
591
|
+
const p = join(target, name);
|
|
592
|
+
try { if (statSync(p).isDirectory()) { rmSync(p, { recursive: true, force: true }); removed.push(name); } } catch { /* ignore */ }
|
|
593
|
+
}
|
|
594
|
+
return removed;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function copySkillsTo(target, fam) {
|
|
598
|
+
const src = join(PACK_ROOT, "skills");
|
|
599
|
+
mkdirSync(target, { recursive: true });
|
|
600
|
+
const removed = pruneLegacySkills(target); // remove-on-add: drop stale earlier-generation skills
|
|
601
|
+
const shared = sharedRootFiles().filter((f) => existsSync(join(PACK_ROOT, f)));
|
|
602
|
+
const sharedDirs = sharedRootDirs();
|
|
603
|
+
for (const f of shared) cpSync(join(PACK_ROOT, f), join(target, f)); // tree-root copy so <target>/RULES.md (the router's ref) resolves
|
|
604
|
+
for (const d of sharedDirs) cpSync(join(PACK_ROOT, d), join(target, d), { recursive: true });
|
|
605
|
+
// Per-skill ORACLES. The root features.json/contracts.json are the WEB pack's, so copying them
|
|
606
|
+
// into every skill shipped `family: "web-react", major: "v7"` into all 19 Android skills — while
|
|
607
|
+
// the correct android files sat uninstalled. The skills cite these as ground truth, so an Android
|
|
608
|
+
// agent was reading React's feature list (down to CometChatErrorBoundary, a web-only component).
|
|
609
|
+
// Resolve each skill's family from its peer's dir_prefix and install THAT peer's oracles under
|
|
610
|
+
// the bare names the skills reference.
|
|
611
|
+
const peerList = (peers().peers || []).filter((p) => p.dirPrefix);
|
|
612
|
+
const oracleFor = (skillDir) => {
|
|
613
|
+
// Resolve to the peer that actually SHIPS this skill dir — dirPrefix + a slug in its
|
|
614
|
+
// `skills` list — so android-sdk vs android-calls (which share `cometchat-android-v5-`)
|
|
615
|
+
// and react-native vs react-native-calls each get their OWN oracle, not whichever peer
|
|
616
|
+
// sorts first. Fall back to the longest matching prefix for shared/neutral dirs.
|
|
617
|
+
const owns = (p) => skillDir.startsWith(p.dirPrefix)
|
|
618
|
+
&& (p.skills || []).includes(skillDir.slice(p.dirPrefix.length));
|
|
619
|
+
const peer = peerList.find(owns)
|
|
620
|
+
|| peerList.filter((p) => skillDir.startsWith(p.dirPrefix))
|
|
621
|
+
.sort((a, b) => b.dirPrefix.length - a.dirPrefix.length)[0];
|
|
622
|
+
return {
|
|
623
|
+
"features.json": peer && peer.features,
|
|
624
|
+
"contracts.json": peer && peer.contracts,
|
|
625
|
+
};
|
|
626
|
+
};
|
|
627
|
+
// Per-skill copies are what a skill's RELATIVE refs resolve against ("features.json",
|
|
628
|
+
// "catalogs/android-v6.json"). Copy only what this install's skills actually cite — plus the
|
|
629
|
+
// bare family-correct oracles and RULES/peers — not all 25 oracle files and every catalog into
|
|
630
|
+
// every folder (that made a React install 8.9 MB and an Android one 14 MB). The tree root above
|
|
631
|
+
// keeps the full set.
|
|
632
|
+
const names = familySkillDirs(fam);
|
|
633
|
+
const cited = citedSharedFiles(names.map((n) => join(src, n)));
|
|
634
|
+
const always = new Set([...SHARED_ROOT_FILES, "features.json", "contracts.json"]);
|
|
635
|
+
const perSkill = shared.filter((f) => always.has(f) || cited.files.has(f));
|
|
636
|
+
const installed = [];
|
|
637
|
+
for (const name of names) {
|
|
638
|
+
const dest = join(target, name);
|
|
639
|
+
cpSync(join(src, name), dest, { recursive: true }); // overlay, not rm+copy: files a user added inside survive a re-install
|
|
640
|
+
const oracles = oracleFor(name);
|
|
641
|
+
for (const f of perSkill) {
|
|
642
|
+
const override = oracles[f];
|
|
643
|
+
const from = override && existsSync(join(PACK_ROOT, override)) ? override : f;
|
|
644
|
+
cpSync(join(PACK_ROOT, from), join(dest, f)); // bare name, family-correct content
|
|
645
|
+
}
|
|
646
|
+
for (const d of sharedDirs) { // AUDIT-107: a skill's "catalogs/<file>" reference resolves same-folder
|
|
647
|
+
const files = [...(cited.dirs.get(d) || [])].filter((f) => existsSync(join(PACK_ROOT, d, f)));
|
|
648
|
+
if (!files.length) continue;
|
|
649
|
+
mkdirSync(join(dest, d), { recursive: true });
|
|
650
|
+
for (const f of files) cpSync(join(PACK_ROOT, d, f), join(dest, d, f));
|
|
651
|
+
}
|
|
652
|
+
installed.push(name);
|
|
653
|
+
}
|
|
654
|
+
return { installed, removed };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Every named oracle ("features.rn-v5.json") and shared-dir file ("catalogs/rn-v5.json") that the
|
|
658
|
+
// given skill dirs cite anywhere in their markdown.
|
|
659
|
+
function citedSharedFiles(skillDirs) {
|
|
660
|
+
const files = new Set();
|
|
661
|
+
const dirs = new Map(SHARED_ROOT_DIRS.map((d) => [d, new Set()]));
|
|
662
|
+
const dirRe = new RegExp(`\\b(${SHARED_ROOT_DIRS.join("|")})/([A-Za-z0-9._-]+\\.(?:json|txt))`, "g");
|
|
663
|
+
const walk = (p) => {
|
|
664
|
+
for (const e of readdirSync(p, { withFileTypes: true })) {
|
|
665
|
+
const q = join(p, e.name);
|
|
666
|
+
if (e.isDirectory()) { walk(q); continue; }
|
|
667
|
+
if (!e.name.endsWith(".md")) continue;
|
|
668
|
+
const text = readFileSync(q, "utf8");
|
|
669
|
+
for (const m of text.matchAll(/\b((?:features|contracts)\.[a-z0-9-]+\.json)\b/g)) files.add(m[1]);
|
|
670
|
+
for (const m of text.matchAll(dirRe)) dirs.get(m[1]).add(m[2]);
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
for (const d of skillDirs) if (existsSync(d)) walk(d);
|
|
674
|
+
return { files, dirs };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// ── multi-agent install targets ──────────────────────────────────────────────
|
|
678
|
+
// The v4 pack shipped a three-mode IDE_TARGETS map (multi / single / cli-only).
|
|
679
|
+
// v5 ports it forward, refreshed to 2026 conventions: AGENTS.md is now the
|
|
680
|
+
// near-universal single-file entrypoint, so Codex / Antigravity / Cursor /
|
|
681
|
+
// Continue read it and NO agent is left "CLI-only".
|
|
682
|
+
// • "multi" agents get the native SKILL.md folder tree (full progressive
|
|
683
|
+
// disclosure) at their own skills dir.
|
|
684
|
+
// • "single" agents get a short (~4KB) orienting ROUTER file at their rules
|
|
685
|
+
// location PLUS the full tree copied to a neutral ./.cometchat/skills so the
|
|
686
|
+
// router points at REAL files the agent reads ON DEMAND (keeps the token win;
|
|
687
|
+
// never a mega-concatenation — that burns tokens every turn).
|
|
688
|
+
// The skill CONTENT is identical across every target — only the delivery differs.
|
|
689
|
+
const NEUTRAL_SKILLS_DIR = ".cometchat/skills";
|
|
690
|
+
const IDE_TARGETS = {
|
|
691
|
+
claude: { mode: "multi", dir: ".claude/skills", globalDir: join(homedir(), ".claude", "skills") },
|
|
692
|
+
kiro: { mode: "multi", dir: ".kiro/skills" },
|
|
693
|
+
replit: { mode: "multi", dir: ".agents/skills" },
|
|
694
|
+
cursor: { mode: "single", base: ".", file: "AGENTS.md" },
|
|
695
|
+
codex: { mode: "single", base: ".", file: "AGENTS.md" },
|
|
696
|
+
antigravity: { mode: "single", base: ".", file: "AGENTS.md" },
|
|
697
|
+
continue: { mode: "single", base: ".", file: "AGENTS.md" },
|
|
698
|
+
windsurf: { mode: "single", base: ".", file: ".windsurfrules" },
|
|
699
|
+
copilot: { mode: "single", base: ".github", file: "copilot-instructions.md" },
|
|
700
|
+
cline: { mode: "single", base: ".clinerules", file: "cometchat.md" },
|
|
701
|
+
gemini: { mode: "single", base: ".", file: "GEMINI.md" },
|
|
702
|
+
aider: { mode: "single", base: ".", file: "CONVENTIONS.md" },
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
// ─── MCP auto-wiring (RULES.md "Canonical doc channel" tier 1) ──────────────
|
|
706
|
+
// Beyond placing skill files, wire the CometChat docs MCP into each agent's OWN
|
|
707
|
+
// config so live docs load automatically. Each agent's mechanism differs — JSON
|
|
708
|
+
// vs TOML, `url` vs `httpUrl` vs `url`+`type` — so there is NO single snippet.
|
|
709
|
+
// MERGE, never clobber: preserve the user's existing servers/settings, only add
|
|
710
|
+
// or refresh `cometchat-docs`. Skipped with `--no-mcp`. Claude also auto-connects
|
|
711
|
+
// via the plugin manifest (plugin-root .mcp.json); this covers the CLI path too.
|
|
712
|
+
const MCP_NAME = "cometchat-docs";
|
|
713
|
+
const MCP_URL = "https://mcp.cometchat.com/mcp";
|
|
714
|
+
// Claude's USER-level MCP lives in ~/.claude.json — a large file Claude Code itself rewrites
|
|
715
|
+
// while running, so the installer never edits it: `--global` prints the `claude mcp add
|
|
716
|
+
// --scope user` command instead of writing a project .mcp.json into whatever folder you ran from.
|
|
717
|
+
const CLAUDE_USER_MCP_CMD = `claude mcp add --scope user --transport http ${MCP_NAME} ${MCP_URL}`;
|
|
718
|
+
const MCP_TARGETS = {
|
|
719
|
+
claude: { kind: "json", proj: ".mcp.json", glob: null, shape: () => ({ type: "http", url: MCP_URL }) },
|
|
720
|
+
cursor: { kind: "json", proj: ".cursor/mcp.json", glob: join(homedir(), ".cursor", "mcp.json"), shape: () => ({ url: MCP_URL }) }, // Cursor: bare `url`, transport inferred
|
|
721
|
+
gemini: { kind: "json", proj: ".gemini/settings.json", glob: join(homedir(), ".gemini", "settings.json"), shape: () => ({ httpUrl: MCP_URL, timeout: 5000 }) }, // Gemini: `httpUrl` (its `url` = SSE!)
|
|
722
|
+
codex: { kind: "toml", proj: ".codex/config.toml", glob: join(homedir(), ".codex", "config.toml") }, // Codex: TOML + needs the RMCP flag
|
|
723
|
+
};
|
|
724
|
+
const isPlainObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
725
|
+
|
|
726
|
+
// How to add the server by hand when we could not (or must not) write the agent's config.
|
|
727
|
+
function manualMcpHint(ide, file) {
|
|
728
|
+
if (ide === "claude") return `claude mcp add --scope ${file ? "project" : "user"} --transport http ${MCP_NAME} ${MCP_URL}`;
|
|
729
|
+
if (ide === "codex") return `add to ${file}: experimental_use_rmcp_client = true (root) + [mcp_servers.${MCP_NAME}] url = "${MCP_URL}"`;
|
|
730
|
+
const m = MCP_TARGETS[ide];
|
|
731
|
+
return `add "${MCP_NAME}": ${JSON.stringify(m.shape())} under "mcpServers" in ${file}`;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Merge the server into a JSON `mcpServers` map without touching anything else. Anything that is
|
|
735
|
+
// not a JSON OBJECT at the top level (null, a string, a number, an array) — or whose `mcpServers`
|
|
736
|
+
// is not an object — is left untouched and reported, never "fixed": assigning a property onto
|
|
737
|
+
// `null` crashed the install AFTER the skills were copied, and onto a string/array it silently did
|
|
738
|
+
// nothing while reporting "added".
|
|
739
|
+
function mergeJsonMcp(file, serverShape) {
|
|
740
|
+
let cfg = {};
|
|
741
|
+
if (existsSync(file)) {
|
|
742
|
+
let parsed;
|
|
743
|
+
try { parsed = JSON.parse(readFileSync(file, "utf8")); }
|
|
744
|
+
catch (e) { return { file, status: "skipped-unparseable", reason: e && e.code === "EISDIR" ? "is a directory" : "not valid JSON" }; } // never corrupt a file we can't parse
|
|
745
|
+
if (!isPlainObject(parsed)) return { file, status: "skipped-unparseable", reason: `top-level JSON is ${Array.isArray(parsed) ? "an array" : parsed === null ? "null" : `a ${typeof parsed}`}, not an object` };
|
|
746
|
+
cfg = parsed;
|
|
747
|
+
}
|
|
748
|
+
if (cfg.mcpServers === undefined || cfg.mcpServers === null) cfg.mcpServers = {};
|
|
749
|
+
else if (!isPlainObject(cfg.mcpServers)) return { file, status: "skipped-unparseable", reason: `"mcpServers" is ${Array.isArray(cfg.mcpServers) ? "an array" : `a ${typeof cfg.mcpServers}`}, not an object` };
|
|
750
|
+
const existed = !!cfg.mcpServers[MCP_NAME];
|
|
751
|
+
cfg.mcpServers[MCP_NAME] = serverShape;
|
|
752
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
753
|
+
writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
754
|
+
return { file, status: existed ? "updated" : "added" };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Codex is TOML AND gates HTTP transport behind a ROOT-level flag that must
|
|
758
|
+
// precede the first table — so prepend the flag if missing, append the server
|
|
759
|
+
// table if missing. Idempotent; leaves an existing entry untouched.
|
|
760
|
+
function mergeCodexToml(file) {
|
|
761
|
+
const FLAG = "experimental_use_rmcp_client = true";
|
|
762
|
+
const TABLE = `[mcp_servers.${MCP_NAME}]`;
|
|
763
|
+
let txt = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
764
|
+
const hadTable = txt.includes(TABLE);
|
|
765
|
+
if (!/experimental_use_rmcp_client\s*=/.test(txt)) txt = `${FLAG}\n\n${txt}`; // root flag before any table
|
|
766
|
+
if (!hadTable) txt = `${txt.replace(/\s*$/, "")}\n\n${TABLE}\nurl = "${MCP_URL}"\n`;
|
|
767
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
768
|
+
writeFileSync(file, txt, "utf8");
|
|
769
|
+
return { file, status: hadTable ? "present" : "added" };
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function wireMcpForIde(ide, global) {
|
|
773
|
+
const m = MCP_TARGETS[ide];
|
|
774
|
+
if (!m) return null; // this agent has no supported HTTP-MCP wiring
|
|
775
|
+
if (global && !m.glob) return { file: null, status: "manual", command: CLAUDE_USER_MCP_CMD };
|
|
776
|
+
const file = global ? m.glob : join(CWD, m.proj);
|
|
777
|
+
// Never let a config-write failure (EACCES, a directory where the file should be, …) crash the
|
|
778
|
+
// run after the skills are already copied — report it with the by-hand fallback instead.
|
|
779
|
+
try { return m.kind === "toml" ? mergeCodexToml(file) : mergeJsonMcp(file, m.shape()); }
|
|
780
|
+
catch (e) { return { file, status: "skipped-error", reason: e.code || e.message }; }
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Read a shipped skill's frontmatter (name + description → summary + triggers).
|
|
784
|
+
// Drives the orienting router table from real data so it can't drift.
|
|
785
|
+
function skillMeta(name) {
|
|
786
|
+
const file = join(PACK_ROOT, "skills", name, "SKILL.md");
|
|
787
|
+
const b = existsSync(file) ? (readFileSync(file, "utf8").match(/^---\n([\s\S]*?)\n---/) || [])[1] || "" : "";
|
|
788
|
+
const g = (k) => (b.match(new RegExp("^" + k + ':\\s*"?(.+?)"?\\s*$', "m")) || [])[1] || "";
|
|
789
|
+
const desc = g("description");
|
|
790
|
+
const triggers = (desc.match(/Triggers?:\s*(.+)$/) || [])[1] || "";
|
|
791
|
+
const summary = desc.split(/Triggers?:/)[0].replace(/\s+/g, " ").trim();
|
|
792
|
+
return { name: g("name") || name, dir: name, summary, triggers };
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// The short (~4KB) orienting router written for "single" agents. refBase is the
|
|
796
|
+
// on-disk location of the full skill tree the agent reads on demand.
|
|
797
|
+
function generateOrientingMd(reg, dirs, refBase, fam) {
|
|
798
|
+
const entry = reg.entry || "cometchat";
|
|
799
|
+
const label = (fam && fam.label) || "UI Kit";
|
|
800
|
+
const kit = (fam && fam.kit_package) || "";
|
|
801
|
+
// Scope the table to THIS family + the platform-neutral router/gate. Listing
|
|
802
|
+
// every family's skills is what let an RN agent read React rows (AUDIT-101).
|
|
803
|
+
const shared = new Set([entry, "cometchat-onboarding"]);
|
|
804
|
+
const scoped = (fam && fam.dirPrefix)
|
|
805
|
+
? dirs.filter((d) => shared.has(d) || d.startsWith(fam.dirPrefix))
|
|
806
|
+
: dirs;
|
|
807
|
+
const metas = scoped.map(skillMeta);
|
|
808
|
+
let md = `# CometChat — AI Agent Instructions (${label})\n\n`;
|
|
809
|
+
md += `> Auto-generated by \`npx @cometchat/skills add --ide <agent>\` between the \`cometchat-skills\` markers — edits inside them are overwritten on re-run; anything outside them is yours and is kept.\n`;
|
|
810
|
+
md += `> ${metas.length} task-shaped skills ship as Markdown under \`${refBase}/\`. Read them ON DEMAND — do not inline them all (that burns tokens every turn).\n\n`;
|
|
811
|
+
|
|
812
|
+
md += `## For any CometChat chat / messaging / calls task\n\n`;
|
|
813
|
+
md += `This project integrates **CometChat** via the **${label}**${kit ? ` (\`${kit}\`)` : ""}. OPEN the matching skill file below and follow it — do **not** answer from training memory (the UI Kit API changes across majors; these skills are verified against the pinned kit).\n\n`;
|
|
814
|
+
md += `| When the user wants… | Read this skill |\n|---|---|\n`;
|
|
815
|
+
for (const m of metas) {
|
|
816
|
+
const purpose = (m.triggers || m.summary).replace(/\|/g, "\\|"); // ponytail: no slice — a cut trigger cannot match (AUDIT-102)
|
|
817
|
+
md += `| ${purpose} | \`${refBase}/${m.dir}/SKILL.md\` |\n`;
|
|
818
|
+
}
|
|
819
|
+
md += `\nEach skill lazily pulls deeper detail from its own \`references/*.md\` — open those only when the skill points you to them.\n\n`;
|
|
820
|
+
|
|
821
|
+
md += `## Start here\n\n`;
|
|
822
|
+
md += `1. **Detect** — \`npx @cometchat/skills detect --json\` (framework, installed UI Kit, version_conflict). If \`version_conflict\` is non-null, STOP and reconcile before touching code.\n`;
|
|
823
|
+
md += `2. **Already integrated?** If \`detect\` says \`existing_cometchat: true\`, this is a TASK, not an integration — go **straight** to the matching skill in the table above and skip onboarding. "Change my avatar", "add a logout button", "add polls", "restyle the bubble" are all tasks. Do NOT re-run the plan/approve journey, and do NOT fall back to reading \`node_modules\` when the phrasing is not an exact match — pick the closest row and open it.\n`;
|
|
824
|
+
md += `3. **Route** — open \`${refBase}/${entry}/SKILL.md\` (the dispatcher); it points to the right task skill for the request.\n`;
|
|
825
|
+
md += `4. **Follow the skill** — it carries the exact init→login→render, the component props, and the verify steps.\n\n`;
|
|
826
|
+
|
|
827
|
+
md += `## Hard rules (full text in \`${refBase}/RULES.md\`)\n\n`;
|
|
828
|
+
md += `- **APPEND, never replace** the user's code — additive diffs; confirm before bulk edits.\n`;
|
|
829
|
+
md += `- **One package per platform**; never mix UI Kit majors (v5/v6/v7).\n`;
|
|
830
|
+
md += `- **Never hardcode credentials** — the Auth Key is dev-only; use a server-minted auth token for production.\n`;
|
|
831
|
+
md += `- **Give the chat container real height + width** or it collapses; **never render a raw localization key** as a label.\n`;
|
|
832
|
+
md += `- **Render custom controls in the component's own view slot**, not as a sibling on top of it.\n`;
|
|
833
|
+
md += `- **Verify against the catalog + a real build** before "done"; fetch signatures from live docs, never from memory or \`node_modules\`.\n`;
|
|
834
|
+
md += `- **Need a CometChat API that is not in the table above? FETCH THE DOCS** — open the family \`core\` skill's \`references/docs-map.md\` and follow it. **Never** grep \`node_modules\` and **never** read \`*.d.ts\`: they carry internal, unsupported and version-drifting surface. If the docs do not have it, say so — do not infer it from types.\n\n`;
|
|
835
|
+
|
|
836
|
+
md += `## Want the native Claude Code install instead?\n\n`;
|
|
837
|
+
md += "```bash\n";
|
|
838
|
+
md += `npx @cometchat/skills add --ide claude # writes the .claude/skills tree (auto progressive disclosure)\n`;
|
|
839
|
+
md += "```\n";
|
|
840
|
+
return md;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// The single-file targets (AGENTS.md, GEMINI.md, CONVENTIONS.md, .windsurfrules,
|
|
844
|
+
// .github/copilot-instructions.md, .clinerules/cometchat.md) are often the USER's own file —
|
|
845
|
+
// AGENTS.md especially is shared by many tools. Overwriting it wholesale destroyed their content.
|
|
846
|
+
// So the router lives in a MANAGED BLOCK: re-runs replace only what is between the markers.
|
|
847
|
+
// Every one of these files is Markdown (or read raw as Markdown-ish rules text), where an HTML
|
|
848
|
+
// comment is invisible when rendered and inert when read raw — a safe fence for all of them.
|
|
849
|
+
const MARK_START = "<!-- cometchat-skills:start -->";
|
|
850
|
+
const MARK_END = "<!-- cometchat-skills:end -->";
|
|
851
|
+
// The unmarked router an earlier installer version wrote (whole-file, "Do not hand-edit"): replace
|
|
852
|
+
// exactly that span on upgrade instead of appending a second router next to it.
|
|
853
|
+
const LEGACY_ROUTER_RE = /^# CometChat — AI Agent Instructions[^\r\n]*\r?\n[\s\S]*?\r?\n## Want the native Claude Code install instead\?\r?\n(?:\r?\n)*```bash\r?\n[^\r\n]*\r?\n```[ \t]*(?:\r?\n)?/m;
|
|
854
|
+
// The router the PUBLISHED v4.2–4.4.x installer wrote (npm `latest` users have it on disk): from its
|
|
855
|
+
// "Auto-generated … Do not hand-edit" header to its closing "That writes N per-skill markdown files"
|
|
856
|
+
// line. Anything a user appended after that line is left alone. (4.0/4.1 wrote no router file.)
|
|
857
|
+
const LEGACY_V4_ROUTER_RE = /^# CometChat — AI Agent Instructions\r?\n\r?\n> Auto-generated by `npx @cometchat\/skills add`\. Do not hand-edit;[\s\S]*?\r?\nThat writes \d+ per-skill markdown files to `\.claude\/skills\/cometchat-\*\/SKILL\.md`\.[^\r\n]*(?:\r?\n)?/m;
|
|
858
|
+
function writeManagedBlock(dest, body) {
|
|
859
|
+
const block = (eol) => [MARK_START, body.replace(/\n+$/, ""), MARK_END].join("\n").replace(/\n/g, eol);
|
|
860
|
+
if (!existsSync(dest)) { writeFileSync(dest, block("\n") + "\n", "utf8"); return "created"; }
|
|
861
|
+
if (statSync(dest).isDirectory()) return "skipped-not-a-file"; // e.g. an AGENTS.md FOLDER — never write into or over it
|
|
862
|
+
const text = readFileSync(dest, "utf8");
|
|
863
|
+
const legacy = [LEGACY_ROUTER_RE, LEGACY_V4_ROUTER_RE].find((re) => re.test(text));
|
|
864
|
+
const eol = /\r\n/.test(text) ? "\r\n" : "\n"; // keep a CRLF file CRLF
|
|
865
|
+
const s = text.indexOf(MARK_START);
|
|
866
|
+
const e = text.indexOf(MARK_END);
|
|
867
|
+
let next, status;
|
|
868
|
+
if (s >= 0 && e > s) { next = text.slice(0, s) + block(eol) + text.slice(e + MARK_END.length); status = "updated"; }
|
|
869
|
+
else if (s >= 0 || e >= 0) return "skipped-malformed"; // one marker without its partner: don't guess where the block ends
|
|
870
|
+
else if (legacy) { next = text.replace(legacy, block(eol) + eol); status = "migrated"; }
|
|
871
|
+
else { next = text.replace(/(\r?\n)*$/, "") + (text.trim() ? eol + eol : "") + block(eol) + eol; status = "appended"; }
|
|
872
|
+
if (next === text) return "unchanged";
|
|
873
|
+
writeFileSync(dest, next, "utf8");
|
|
874
|
+
return status;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Install for one agent. "multi" → native SKILL.md tree; "single" → orienting
|
|
878
|
+
// router at the agent's rules file + the full tree at the neutral dir.
|
|
879
|
+
function installForIde(ide, global) {
|
|
880
|
+
const t = IDE_TARGETS[ide];
|
|
881
|
+
const fam = resolveFamily();
|
|
882
|
+
if (t.mode === "multi") {
|
|
883
|
+
const target = global && t.globalDir ? t.globalDir : join(CWD, t.dir);
|
|
884
|
+
const { installed, removed } = copySkillsTo(target, fam);
|
|
885
|
+
return { ide, mode: "multi", target, count: installed.length, removed };
|
|
886
|
+
}
|
|
887
|
+
const neutral = join(CWD, NEUTRAL_SKILLS_DIR);
|
|
888
|
+
const { installed, removed } = copySkillsTo(neutral, fam);
|
|
889
|
+
const base = join(CWD, t.base);
|
|
890
|
+
let dest = join(base, t.file), fileStatus;
|
|
891
|
+
try {
|
|
892
|
+
if (t.base !== "." && existsSync(base) && !statSync(base).isDirectory()) {
|
|
893
|
+
// The rules "folder" is a FILE. Cline's original single-file form is a `.clinerules` file, and
|
|
894
|
+
// Cline still reads it — so put the block there. Any other target: leave the user's file alone.
|
|
895
|
+
if (t.base === ".clinerules") dest = base;
|
|
896
|
+
else fileStatus = "skipped-not-a-directory";
|
|
897
|
+
} else mkdirSync(base, { recursive: true });
|
|
898
|
+
fileStatus ||= writeManagedBlock(dest, generateOrientingMd(peers(), shippedSkillDirs(), NEUTRAL_SKILLS_DIR, fam));
|
|
899
|
+
} catch (e) {
|
|
900
|
+
fileStatus = "skipped-error"; // never crash after the skills are already copied
|
|
901
|
+
return { ide, mode: "single", target: dest, neutral, count: installed.length, removed, file_status: fileStatus, reason: e.code || e.message };
|
|
902
|
+
}
|
|
903
|
+
return { ide, mode: "single", target: dest, neutral, count: installed.length, removed, file_status: fileStatus };
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function cmdAdd() {
|
|
907
|
+
const global = has("--global");
|
|
908
|
+
const supported = Object.keys(IDE_TARGETS);
|
|
909
|
+
requireFlagValue("--ide", [...supported, "all"]);
|
|
910
|
+
requireFlagValue("--target", [...supported, "all"]);
|
|
911
|
+
const ide = (flagVal("--ide") || flagVal("--target") || "claude").toLowerCase();
|
|
912
|
+
if (ide !== "all" && !IDE_TARGETS[ide]) {
|
|
913
|
+
console.error(`unknown --ide: ${ide}\nsupported: ${supported.join(", ")}, all`);
|
|
914
|
+
process.exit(2);
|
|
915
|
+
}
|
|
916
|
+
let ides = ide === "all" ? supported : [ide];
|
|
917
|
+
// --global needs a USER-level location. Only claude has one here (~/.claude/skills); kiro/replit
|
|
918
|
+
// and the single-file agents (AGENTS.md, GEMINI.md, …) are per-project by nature. They used to
|
|
919
|
+
// ignore --global and write into whatever folder you ran from — now they are skipped, loudly.
|
|
920
|
+
const noGlobal = global ? ides.filter((n) => !(IDE_TARGETS[n].mode === "multi" && IDE_TARGETS[n].globalDir)) : [];
|
|
921
|
+
if (noGlobal.length) {
|
|
922
|
+
ides = ides.filter((n) => !noGlobal.includes(n));
|
|
923
|
+
const msg = `--global: no user-level install location for ${noGlobal.join(", ")} — skipped (nothing written to ${CWD}). ` +
|
|
924
|
+
`Run \`npx @cometchat/skills add --ide <agent>\` inside the project for ${noGlobal.length > 1 ? "those" : "it"}.`;
|
|
925
|
+
if (!ides.length) {
|
|
926
|
+
if (JSON_OUT) console.log(JSON.stringify({ ok: false, error: msg, skipped: noGlobal }));
|
|
927
|
+
else console.error(msg);
|
|
928
|
+
process.exit(2);
|
|
929
|
+
}
|
|
930
|
+
if (!JSON_OUT) console.error(`↷ ${msg}`);
|
|
931
|
+
}
|
|
932
|
+
// Surface a version-conflict up front (non-fatal): a project already on a different UI Kit
|
|
933
|
+
// major still gets skills, but the agent must reconcile before emitting code (RULES §3).
|
|
934
|
+
const vc = (detect() || {}).version_conflict || null;
|
|
935
|
+
if (vc && !JSON_OUT) console.error(`⚠️ version_conflict: ${vc.detail} — reconcile before generating code (RULES §3).`);
|
|
936
|
+
const results = [];
|
|
937
|
+
const writtenSingles = new Set();
|
|
938
|
+
for (const name of ides) {
|
|
939
|
+
const t = IDE_TARGETS[name];
|
|
940
|
+
if (t.mode === "single") { // dedupe agents that share a file (cursor/codex/antigravity/continue → AGENTS.md)
|
|
941
|
+
const key = join(t.base, t.file);
|
|
942
|
+
if (writtenSingles.has(key)) { results.push({ ide: name, mode: "single", target: join(CWD, key), deduped: true }); continue; }
|
|
943
|
+
writtenSingles.add(key);
|
|
944
|
+
}
|
|
945
|
+
results.push(installForIde(name, global));
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Wire the docs MCP into each agent's own config (tier 1 auto-connect), unless opted out.
|
|
949
|
+
const mcpResults = [];
|
|
950
|
+
if (!has("--no-mcp")) {
|
|
951
|
+
const wroteMcp = new Set();
|
|
952
|
+
for (const name of ides) {
|
|
953
|
+
const m = MCP_TARGETS[name];
|
|
954
|
+
if (!m) continue; // agent has no supported HTTP-MCP wiring (e.g. copilot/windsurf/aider)
|
|
955
|
+
const file = global ? (m.glob || `user:${name}`) : join(CWD, m.proj);
|
|
956
|
+
if (wroteMcp.has(file)) continue;
|
|
957
|
+
wroteMcp.add(file);
|
|
958
|
+
const r = wireMcpForIde(name, global);
|
|
959
|
+
if (r) mcpResults.push({ ide: name, ...r });
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const FILE_VERB = { created: "created", updated: "updated (managed block)", migrated: "updated (replaced an earlier unmarked router)",
|
|
964
|
+
appended: "appended (your content kept)", unchanged: "unchanged" };
|
|
965
|
+
out({ ok: true, results, mcp: mcpResults, version_conflict: vc, ...(noGlobal.length ? { skipped: noGlobal } : {}) }, () => {
|
|
966
|
+
for (const r of results) {
|
|
967
|
+
if (r.deduped) console.log(`✅ ${r.ide.padEnd(11)} → shares ${r.target} (already written)`);
|
|
968
|
+
else if (r.mode === "multi") console.log(`✅ ${r.ide.padEnd(11)} → ${r.count} skill(s) in ${r.target}`);
|
|
969
|
+
else if (r.file_status === "skipped-malformed") console.log(`⚠️ ${r.ide.padEnd(11)} → ${r.target} has an unmatched cometchat-skills start/end marker — router NOT written; fix or remove the marker and re-run (+ ${r.count} skills in ${r.neutral})`);
|
|
970
|
+
else if (String(r.file_status).startsWith("skipped")) {
|
|
971
|
+
const why = { "skipped-not-a-file": "is a folder, not a file", "skipped-not-a-directory": "sits where a folder is expected" }[r.file_status] || `could not be written (${r.reason})`;
|
|
972
|
+
console.log(`⚠️ ${r.ide.padEnd(11)} → ${r.target.replace(CWD + "/", "")} ${why} — router NOT written; the skills are installed (+ ${r.count} skills in ${r.neutral})`);
|
|
973
|
+
}
|
|
974
|
+
else console.log(`✅ ${r.ide.padEnd(11)} → router ${r.target} ${FILE_VERB[r.file_status] || r.file_status} (+ ${r.count} skills in ${r.neutral})`);
|
|
975
|
+
if (r.removed && r.removed.length) console.log(` removed ${r.removed.length} stale skill(s) from an earlier version: ${r.removed.join(", ")}`);
|
|
976
|
+
}
|
|
977
|
+
for (const r of mcpResults) {
|
|
978
|
+
const rel = r.file ? r.file.replace(CWD + "/", "") : null;
|
|
979
|
+
if (r.status === "manual") console.log(`🔌 MCP ${r.ide.padEnd(8)} → --global: user-level MCP lives in ~/.claude.json (owned by Claude Code, not edited). Run:\n ${r.command}`);
|
|
980
|
+
else if (r.status.startsWith("skipped")) console.log(`⚠️ MCP ${r.ide.padEnd(8)} → ${rel} left untouched (${r.reason}) — add by hand: ${manualMcpHint(r.ide, rel)}`);
|
|
981
|
+
else console.log(`🔌 MCP ${r.ide.padEnd(8)} → ${MCP_NAME} ${r.status} in ${rel}`);
|
|
982
|
+
}
|
|
983
|
+
// Only point at the neutral tree when a router-file agent actually got one — a claude-only
|
|
984
|
+
// install has no .cometchat/skills, and naming it sent people to a path that doesn't exist.
|
|
985
|
+
const neutralDir = (results.find((r) => r.neutral) || {}).neutral;
|
|
986
|
+
console.log(`\nEntry: /cometchat add chat to my app` +
|
|
987
|
+
(neutralDir ? ` (agents without slash-skills: open ${neutralDir.replace(CWD + "/", "")}/cometchat/SKILL.md)` : ""));
|
|
988
|
+
if (mcpResults.length) console.log(`Docs MCP: ${MCP_URL} (auto-loads live docs; restart the agent if it doesn't connect. Skip with --no-mcp.)`);
|
|
989
|
+
// Claude Code gates a PROJECT .mcp.json server behind a one-time trust prompt (`claude mcp list`
|
|
990
|
+
// shows "Pending approval" until then) — say so, or "auto-loads" reads as broken on first run.
|
|
991
|
+
if (mcpResults.some((r) => r.ide === "claude" && (r.status === "added" || r.status === "updated")))
|
|
992
|
+
console.log(`Claude Code: approve "${MCP_NAME}" when \`claude\` asks on first start in this project — until then it shows "Pending approval" in \`claude mcp list\`. Skills fall back to the docs site if you decline.`);
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function runNode(scriptRel, extra = []) {
|
|
997
|
+
const r = spawnSync("node", [join(PACK_ROOT, scriptRel), ...extra], { stdio: "inherit" });
|
|
998
|
+
return r.status ?? 1;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// The Tier-2 fence for a family: which engine, against which pinned-kit project, and whether that
|
|
1002
|
+
// project is set up. A packed (npm) install has no families.json, so `key`/`typecheckProject` are
|
|
1003
|
+
// null — this returns { skip } instead of handing the engine `--project null` (which printed
|
|
1004
|
+
// "npm --prefix null ci" and exited 1 on every npm install).
|
|
1005
|
+
function fencePlan(f) {
|
|
1006
|
+
const proj = f && f.key && f.typecheckProject && existsSync(join(PACK_ROOT, f.typecheckProject)) ? f.typecheckProject : null;
|
|
1007
|
+
if (!proj) return { skip: `no pinned-kit typecheck project for ${(f && f.label) || "this family"} in this install` };
|
|
1008
|
+
const P = join(PACK_ROOT, proj);
|
|
1009
|
+
const has = (rel) => existsSync(join(P, rel));
|
|
1010
|
+
let plan;
|
|
1011
|
+
if (has("tsconfig.json")) {
|
|
1012
|
+
const fam = f.fenceNeedles && f.skillDirPattern;
|
|
1013
|
+
if (!fam) return { skip: `no TypeScript fence declared for ${f.key}` };
|
|
1014
|
+
plan = { engine: "typecheck-fences.mjs", args: ["--project", proj, "--tsconfig", "tsconfig.json", "--discover", f.key],
|
|
1015
|
+
ready: has("node_modules/@cometchat"), setup: `npm --prefix ${proj} ci` };
|
|
1016
|
+
} else if (has("pubspec.yaml")) {
|
|
1017
|
+
plan = { engine: "typecheck-fences-dart.mjs", args: ["--project", proj, "--discover", f.key],
|
|
1018
|
+
ready: has(".dart_tool/package_config.json"), setup: `(cd ${proj} && flutter pub get)` };
|
|
1019
|
+
} else if (has("settings.gradle.kts") || has("settings.gradle")) {
|
|
1020
|
+
plan = { engine: "typecheck-fences-kotlin.mjs", args: ["--project", proj, "--discover", f.key],
|
|
1021
|
+
ready: has("local.properties"), setup: `write sdk.dir=<Android SDK path> into ${proj}/local.properties` };
|
|
1022
|
+
} else return { skip: `no fence engine for ${proj}` };
|
|
1023
|
+
if (!existsSync(join(PACK_ROOT, "test-suite/scripts", plan.engine))) return { skip: `${plan.engine} not in this install` };
|
|
1024
|
+
return { ...plan, proj };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function cmdVerify() {
|
|
1028
|
+
const tier = argv[1] && !argv[1].startsWith("-") ? argv[1] : "all";
|
|
1029
|
+
// The predicate (review done-oracle) belongs to the reviewer, which is INTERNAL-DEV-ONLY
|
|
1030
|
+
// and NOT shipped in the published package (no e2e harness on npm). When it's absent —
|
|
1031
|
+
// i.e. running from an npm install rather than the dev repo — skip it cleanly instead of
|
|
1032
|
+
// erroring, so the end-user CLI never fails on the missing dev harness.
|
|
1033
|
+
const PREDICATE = ".claude/skills/cometchat-skill-reviewer/tools/predicate.mjs";
|
|
1034
|
+
const hasReviewer = existsSync(join(PACK_ROOT, PREDICATE));
|
|
1035
|
+
const gates = {
|
|
1036
|
+
lint: () => runNode("test-suite/scripts/lint-skills.mjs"),
|
|
1037
|
+
catalog: () => runNode("test-suite/scripts/verify-catalog.mjs"),
|
|
1038
|
+
// Family resolved from families.json (RULES.md §22). This hardcoded web-v7, so `fences`
|
|
1039
|
+
// silently checked only React regardless of which family the workspace actually used.
|
|
1040
|
+
fences: () => {
|
|
1041
|
+
if (!resolvePeer((detect() || {}).framework || "")) {
|
|
1042
|
+
console.log("↷ fences — DEV-ONLY Tier-2 gate: no family resolved for this directory (pass --family <platform>); skipped.");
|
|
1043
|
+
return 0;
|
|
1044
|
+
}
|
|
1045
|
+
const p = fencePlan(resolveFamily());
|
|
1046
|
+
if (p.skip) { console.log(`↷ fences — DEV-ONLY Tier-2 gate (${p.skip}); skipped.`); return 0; }
|
|
1047
|
+
if (!p.ready) { console.log(`↷ fences — DEV-ONLY Tier-2 gate: pinned kit not set up in ${p.proj}; skipped. Set up: ${p.setup}`); return 0; }
|
|
1048
|
+
return runNode(`test-suite/scripts/${p.engine}`, p.args);
|
|
1049
|
+
},
|
|
1050
|
+
predicate: () => {
|
|
1051
|
+
if (!hasReviewer) { console.log("↷ predicate — DEV-ONLY review gate (e2e harness not present in this install); skipped."); return 0; }
|
|
1052
|
+
return runNode(PREDICATE);
|
|
1053
|
+
},
|
|
1054
|
+
};
|
|
1055
|
+
let seq;
|
|
1056
|
+
if (tier === "all") seq = ["lint", "catalog", "predicate"]; // offline gates (fences needs the kit)
|
|
1057
|
+
else if (tier === "ci") seq = ["lint", "catalog", "fences", "predicate"];
|
|
1058
|
+
else if (gates[tier]) seq = [tier];
|
|
1059
|
+
else { console.error(`unknown tier: ${tier} (lint|catalog|fences|predicate|all|ci)`); process.exit(2); }
|
|
1060
|
+
let bad = 0;
|
|
1061
|
+
for (const g of seq) { const code = gates[g](); if (code !== 0) bad++; }
|
|
1062
|
+
process.exit(bad ? 1 : 0);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function cmdCatalog() {
|
|
1066
|
+
process.exit(runNode("test-suite/scripts/catalog-refresh.mjs", has("--check") ? ["--check"] : []));
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function cmdDoctor() {
|
|
1070
|
+
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
1071
|
+
// Family-aware. Reporting the WEB catalog (405 symbols) and "npm --prefix cometchat-react-app ci"
|
|
1072
|
+
// inside an Android or iOS project was worse than noise: it told the user their oracle was fine
|
|
1073
|
+
// while reading React's. Resolve the peer the project actually is.
|
|
1074
|
+
// Resolved exactly like `add` (FRAMEWORK_PLATFORM + --family): Expo → react-native, Next/Astro →
|
|
1075
|
+
// react. The old platform-string match fell through to React for Expo, and with NO family it
|
|
1076
|
+
// printed "harness/null". No family ⇒ say so; never substitute React's catalog.
|
|
1077
|
+
const det = detect();
|
|
1078
|
+
const peer = resolvePeer(det.framework || "");
|
|
1079
|
+
const fam = reviewerFamily(peer); // dev-only (families.json); null on an npm install
|
|
1080
|
+
const noFamily = "n/a — no family resolved (run inside the app package, or pass --family <platform>)";
|
|
1081
|
+
const catalog = peer && peer.catalog ? readJSON(join(PACK_ROOT, `test-suite/catalogs/${peer.catalog}.json`)) : null;
|
|
1082
|
+
// The pinned kit (Tier-2 typecheck project) and the review app/harness (Tier-3) for THIS family.
|
|
1083
|
+
let kit;
|
|
1084
|
+
if (!peer) kit = { ok: false, detail: noFamily };
|
|
1085
|
+
else if (!fam) kit = { ok: false, detail: "n/a — dev-only (pinned kit + review harness are not in this install)" };
|
|
1086
|
+
else {
|
|
1087
|
+
const plan = fencePlan({ ...fam, label: peer.label });
|
|
1088
|
+
const dirs = [plan.proj, fam.appDir].filter((d, i, a) => d && a.indexOf(d) === i);
|
|
1089
|
+
const installed = (d) => {
|
|
1090
|
+
const P = join(PACK_ROOT, d);
|
|
1091
|
+
if (!existsSync(P)) return false;
|
|
1092
|
+
if (existsSync(join(P, "package.json"))) return existsSync(join(P, "node_modules", "@cometchat"));
|
|
1093
|
+
if (existsSync(join(P, "pubspec.yaml"))) return existsSync(join(P, ".dart_tool"));
|
|
1094
|
+
return true; // native harness: present = usable (its own gate reports toolchain gaps)
|
|
1095
|
+
};
|
|
1096
|
+
const hit = dirs.find(installed);
|
|
1097
|
+
kit = hit ? { ok: true, detail: hit }
|
|
1098
|
+
: { ok: false, detail: plan.setup ? `not set up — ${plan.setup}` : dirs.length ? `${dirs.join(" / ")} not set up` : "no pinned kit or harness declared for this family" };
|
|
1099
|
+
}
|
|
1100
|
+
// Review state for THIS family only (reviews/<family>/review-state.*.json) — never React's as a fallback.
|
|
1101
|
+
let state = null;
|
|
1102
|
+
const reviewDir = fam ? join(PACK_ROOT, "reviews", fam.name) : null;
|
|
1103
|
+
if (reviewDir && existsSync(reviewDir)) {
|
|
1104
|
+
for (const f of readdirSync(reviewDir).filter((n) => n.startsWith("review-state."))) {
|
|
1105
|
+
const j = readJSON(join(reviewDir, f));
|
|
1106
|
+
if (j && (!state || ((j.totals || {}).passed || 0) > ((state.totals || {}).passed || 0))) state = j;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
// The pinned kit and the review harness/state are INTERNAL-DEV concerns — neither ships
|
|
1110
|
+
// in the published package. Mark those checks devOnly so `doctor` stays green on an npm
|
|
1111
|
+
// install (they surface as advisory ⚠️, not failures).
|
|
1112
|
+
const reviewerPresent = existsSync(join(PACK_ROOT, ".claude/skills/cometchat-skill-reviewer"));
|
|
1113
|
+
const checks = [
|
|
1114
|
+
{ name: "node >= 18", ok: nodeMajor >= 18, detail: `v${process.versions.node}` },
|
|
1115
|
+
{ name: `family`, ok: !!peer, detail: peer ? `${peer.platform} (${peer.label})` : noFamily, advisory: true },
|
|
1116
|
+
{ name: "Tier-1 catalog present", ok: !!catalog,
|
|
1117
|
+
detail: !peer ? noFamily : catalog ? `${peer.catalog}: ${catalog.symbolCount} symbols` : `test-suite/catalogs/${peer.catalog}.json missing (npm run catalog:refresh)`,
|
|
1118
|
+
advisory: !peer },
|
|
1119
|
+
{ name: "pinned kit / harness (dev-only Tier-2/3)", ok: kit.ok, detail: kit.detail, devOnly: true },
|
|
1120
|
+
{ name: "review state (dev-only)", ok: !!state,
|
|
1121
|
+
detail: state ? `${(state.totals || {}).passed || 0}/${(state.totals || {}).count || 0} passed`
|
|
1122
|
+
: !peer ? noFamily : !reviewerPresent ? "n/a — e2e review harness is dev-only, not in this install" : "no review yet", devOnly: true },
|
|
1123
|
+
];
|
|
1124
|
+
const ok = checks.every((c) => c.ok || c.devOnly || c.advisory); // kit-install + review state are dev-only; no-family is advisory
|
|
1125
|
+
out({ ok, checks }, () => {
|
|
1126
|
+
console.log(`cometchat-skills doctor (v${packVersion()})`);
|
|
1127
|
+
for (const c of checks) console.log(` ${c.ok ? "✅" : "⚠️ "} ${c.name} — ${c.detail}`);
|
|
1128
|
+
});
|
|
1129
|
+
process.exit(ok ? 0 : 1);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function help() {
|
|
1133
|
+
console.log(`cometchat-skills v${packVersion()} — deterministic CLI for the CometChat skills pack
|
|
1134
|
+
|
|
1135
|
+
Usage: npx @cometchat/skills <command> [--json]
|
|
1136
|
+
|
|
1137
|
+
detect Probe the current project (framework, UI Kit, version_conflict)
|
|
1138
|
+
add [--ide <a>] Install the skills for an AI agent (default: claude)
|
|
1139
|
+
--ide claude|kiro|replit → native SKILL.md tree
|
|
1140
|
+
--ide cursor|codex|antigravity|continue|windsurf|copilot|cline|gemini|aider
|
|
1141
|
+
→ orienting router + ./.cometchat/skills tree
|
|
1142
|
+
(router goes in a <!-- cometchat-skills:start/end -->
|
|
1143
|
+
block; your own content in that file is kept)
|
|
1144
|
+
--ide all → every supported agent
|
|
1145
|
+
--global → user-level dir: claude only (~/.claude/skills; prints
|
|
1146
|
+
the \`claude mcp add --scope user\` command instead of
|
|
1147
|
+
writing .mcp.json). Agents with no user-level location
|
|
1148
|
+
(kiro, replit, the router-file agents) are skipped.
|
|
1149
|
+
(also wires the docs MCP into the agent's config so live
|
|
1150
|
+
docs auto-load: claude .mcp.json · cursor .cursor/mcp.json ·
|
|
1151
|
+
codex .codex/config.toml · gemini .gemini/settings.json)
|
|
1152
|
+
--no-mcp → skip the MCP wiring
|
|
1153
|
+
--family <platform> → force the family (monorepo root / ambiguous detect;
|
|
1154
|
+
e.g. js-calls, ios-calls to add a headless calls skill
|
|
1155
|
+
alongside the UI Kit skills)
|
|
1156
|
+
list List the skills this pack ships
|
|
1157
|
+
info <slug> Show a skill's triggers + compatibility
|
|
1158
|
+
verify [tier] Run gates: lint | catalog | fences | predicate | all | ci
|
|
1159
|
+
catalog [--check] Refresh the Tier-1 catalog from the installed kit (oracle refresh)
|
|
1160
|
+
doctor Environment health check
|
|
1161
|
+
version | help
|
|
1162
|
+
|
|
1163
|
+
Notes:
|
|
1164
|
+
• Credentials/provisioning (auth, provision, config, features) are a
|
|
1165
|
+
SEPARATE published tool, @cometchat/skills-cli — see the core skill's
|
|
1166
|
+
references/setup-credentials.md. This CLI is the skill-management + verify layer.`);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
switch (CMD) {
|
|
1170
|
+
case "detect": cmdDetect(); break;
|
|
1171
|
+
case "add": case "install": cmdAdd(); break;
|
|
1172
|
+
case "list": cmdList(); break;
|
|
1173
|
+
case "info": cmdInfo(); break;
|
|
1174
|
+
case "verify": cmdVerify(); break;
|
|
1175
|
+
case "catalog": cmdCatalog(); break;
|
|
1176
|
+
case "doctor": cmdDoctor(); break;
|
|
1177
|
+
case "version": case "--version": case "-v": console.log(packVersion()); break;
|
|
1178
|
+
case "help": case "--help": case "-h": case undefined: help(); break;
|
|
1179
|
+
default: console.error(`unknown command: ${CMD}\n`); help(); process.exit(2);
|
|
1180
|
+
}
|