workos 0.17.1 → 0.19.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/README.md +11 -149
- package/dist/bin.js +155 -23
- package/dist/bin.js.map +1 -1
- package/dist/cli.config.d.ts +0 -2
- package/dist/cli.config.js +3 -3
- package/dist/cli.config.js.map +1 -1
- package/dist/commands/api/index.js +18 -10
- package/dist/commands/api/index.js.map +1 -1
- package/dist/commands/api/interactive.js +11 -11
- package/dist/commands/api/interactive.js.map +1 -1
- package/dist/commands/claim.js +28 -16
- package/dist/commands/claim.js.map +1 -1
- package/dist/commands/connection.js +3 -3
- package/dist/commands/connection.js.map +1 -1
- package/dist/commands/debug.d.ts +11 -0
- package/dist/commands/debug.js +30 -14
- package/dist/commands/debug.js.map +1 -1
- package/dist/commands/dev.d.ts +7 -8
- package/dist/commands/dev.js +14 -14
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/directory.js +3 -3
- package/dist/commands/directory.js.map +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/emulate.d.ts +1 -0
- package/dist/commands/emulate.js +3 -2
- package/dist/commands/emulate.js.map +1 -1
- package/dist/commands/env.d.ts +10 -0
- package/dist/commands/env.js +94 -18
- package/dist/commands/env.js.map +1 -1
- package/dist/commands/install.js +9 -10
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/login.d.ts +25 -15
- package/dist/commands/login.js +126 -71
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.js +4 -4
- package/dist/commands/logout.js.map +1 -1
- package/dist/commands/mcp.d.ts +14 -0
- package/dist/commands/mcp.js +99 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/migrations.js +2 -1
- package/dist/commands/migrations.js.map +1 -1
- package/dist/commands/seed.js +5 -4
- package/dist/commands/seed.js.map +1 -1
- package/dist/commands/setup.d.ts +50 -0
- package/dist/commands/setup.js +237 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/vault-run.js +3 -2
- package/dist/commands/vault-run.js.map +1 -1
- package/dist/commands/verify-login.d.ts +25 -0
- package/dist/commands/verify-login.js +158 -0
- package/dist/commands/verify-login.js.map +1 -0
- package/dist/doctor/checks/ai-analysis.js +2 -1
- package/dist/doctor/checks/ai-analysis.js.map +1 -1
- package/dist/doctor/checks/mcp.d.ts +15 -0
- package/dist/doctor/checks/mcp.js +34 -0
- package/dist/doctor/checks/mcp.js.map +1 -0
- package/dist/doctor/index.js +7 -1
- package/dist/doctor/index.js.map +1 -1
- package/dist/doctor/issues.js +18 -1
- package/dist/doctor/issues.js.map +1 -1
- package/dist/doctor/output.js +18 -3
- package/dist/doctor/output.js.map +1 -1
- package/dist/doctor/types.d.ts +17 -0
- package/dist/doctor/types.js.map +1 -1
- package/dist/integrations/dotnet/index.js +1 -1
- package/dist/integrations/dotnet/index.js.map +1 -1
- package/dist/integrations/elixir/index.js +1 -1
- package/dist/integrations/elixir/index.js.map +1 -1
- package/dist/integrations/go/index.js +1 -1
- package/dist/integrations/go/index.js.map +1 -1
- package/dist/integrations/nextjs/index.js +8 -5
- package/dist/integrations/nextjs/index.js.map +1 -1
- package/dist/integrations/nextjs/utils.d.ts +1 -1
- package/dist/integrations/nextjs/utils.js +21 -6
- package/dist/integrations/nextjs/utils.js.map +1 -1
- package/dist/integrations/react-router/index.js +5 -5
- package/dist/integrations/react-router/index.js.map +1 -1
- package/dist/integrations/react-router/utils.js +28 -12
- package/dist/integrations/react-router/utils.js.map +1 -1
- package/dist/integrations/ruby/index.js +1 -1
- package/dist/integrations/ruby/index.js.map +1 -1
- package/dist/lib/adapters/cli-adapter.d.ts +33 -4
- package/dist/lib/adapters/cli-adapter.js +251 -135
- package/dist/lib/adapters/cli-adapter.js.map +1 -1
- package/dist/lib/adapters/dashboard-adapter.js +3 -3
- package/dist/lib/adapters/dashboard-adapter.js.map +1 -1
- package/dist/lib/adapters/headless-adapter.d.ts +7 -0
- package/dist/lib/adapters/headless-adapter.js +42 -5
- package/dist/lib/adapters/headless-adapter.js.map +1 -1
- package/dist/lib/agent-interface.js +19 -5
- package/dist/lib/agent-interface.js.map +1 -1
- package/dist/lib/agent-runner.js +1 -1
- package/dist/lib/agent-runner.js.map +1 -1
- package/dist/lib/ai-content.js +2 -1
- package/dist/lib/ai-content.js.map +1 -1
- package/dist/lib/api-key.js +2 -1
- package/dist/lib/api-key.js.map +1 -1
- package/dist/lib/completion-data.d.ts +33 -0
- package/dist/lib/completion-data.js +33 -0
- package/dist/lib/completion-data.js.map +1 -0
- package/dist/lib/config-store.d.ts +13 -0
- package/dist/lib/config-store.js +24 -0
- package/dist/lib/config-store.js.map +1 -1
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/config.js +1 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/constants.d.ts +17 -0
- package/dist/lib/constants.js +17 -0
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/events.d.ts +34 -1
- package/dist/lib/events.js.map +1 -1
- package/dist/lib/framework-config.d.ts +6 -0
- package/dist/lib/framework-config.js.map +1 -1
- package/dist/lib/installer-core.d.ts +34 -21
- package/dist/lib/installer-core.js +24 -5
- package/dist/lib/installer-core.js.map +1 -1
- package/dist/lib/installer-core.types.d.ts +3 -1
- package/dist/lib/installer-core.types.js.map +1 -1
- package/dist/lib/mcp-clients.d.ts +45 -0
- package/dist/lib/mcp-clients.js +297 -0
- package/dist/lib/mcp-clients.js.map +1 -0
- package/dist/lib/port-detection.js +19 -13
- package/dist/lib/port-detection.js.map +1 -1
- package/dist/lib/preferences.d.ts +42 -0
- package/dist/lib/preferences.js +34 -5
- package/dist/lib/preferences.js.map +1 -1
- package/dist/lib/run-with-core.js +42 -4
- package/dist/lib/run-with-core.js.map +1 -1
- package/dist/lib/settings.d.ts +5 -14
- package/dist/lib/settings.js +5 -16
- package/dist/lib/settings.js.map +1 -1
- package/dist/lib/startup-notice-gate.d.ts +21 -0
- package/dist/lib/startup-notice-gate.js +29 -0
- package/dist/lib/startup-notice-gate.js.map +1 -0
- package/dist/lib/telemetry-notice.d.ts +5 -5
- package/dist/lib/telemetry-notice.js +10 -8
- package/dist/lib/telemetry-notice.js.map +1 -1
- package/dist/lib/unclaimed-env-provision.js +15 -12
- package/dist/lib/unclaimed-env-provision.js.map +1 -1
- package/dist/lib/unclaimed-warning.js +7 -3
- package/dist/lib/unclaimed-warning.js.map +1 -1
- package/dist/lib/validation/security-checks.js +2 -1
- package/dist/lib/validation/security-checks.js.map +1 -1
- package/dist/lib/validation/validator.js +30 -3
- package/dist/lib/validation/validator.js.map +1 -1
- package/dist/lib/workos-management.js +26 -19
- package/dist/lib/workos-management.js.map +1 -1
- package/dist/run.d.ts +1 -0
- package/dist/run.js +5 -1
- package/dist/run.js.map +1 -1
- package/dist/steps/add-or-update-environment-variables.js +10 -10
- package/dist/steps/add-or-update-environment-variables.js.map +1 -1
- package/dist/steps/run-prettier.js +3 -3
- package/dist/steps/run-prettier.js.map +1 -1
- package/dist/steps/upload-environment-variables/index.js +15 -3
- package/dist/steps/upload-environment-variables/index.js.map +1 -1
- package/dist/steps/upload-environment-variables/providers/vercel.js +2 -2
- package/dist/steps/upload-environment-variables/providers/vercel.js.map +1 -1
- package/dist/utils/analytics.d.ts +1 -0
- package/dist/utils/analytics.js +5 -1
- package/dist/utils/analytics.js.map +1 -1
- package/dist/utils/box.d.ts +6 -30
- package/dist/utils/box.js +8 -95
- package/dist/utils/box.js.map +1 -1
- package/dist/utils/cli-symbols.d.ts +14 -2
- package/dist/utils/cli-symbols.js +12 -0
- package/dist/utils/cli-symbols.js.map +1 -1
- package/dist/utils/debug.js +3 -3
- package/dist/utils/debug.js.map +1 -1
- package/dist/utils/environment.js +1 -1
- package/dist/utils/environment.js.map +1 -1
- package/dist/utils/help-json.js +153 -4
- package/dist/utils/help-json.js.map +1 -1
- package/dist/utils/interaction-mode.d.ts +1 -1
- package/dist/utils/interaction-mode.js +0 -3
- package/dist/utils/interaction-mode.js.map +1 -1
- package/dist/utils/output.d.ts +5 -3
- package/dist/utils/output.js +5 -5
- package/dist/utils/output.js.map +1 -1
- package/dist/utils/package-manager.js +1 -1
- package/dist/utils/package-manager.js.map +1 -1
- package/dist/utils/recovery-hints.d.ts +5 -0
- package/dist/utils/recovery-hints.js +12 -0
- package/dist/utils/recovery-hints.js.map +1 -1
- package/dist/utils/summary-box.d.ts +8 -1
- package/dist/utils/summary-box.js +67 -4
- package/dist/utils/summary-box.js.map +1 -1
- package/dist/utils/telemetry-types.d.ts +1 -1
- package/dist/utils/telemetry-types.js.map +1 -1
- package/dist/utils/types.d.ts +10 -0
- package/dist/utils/types.js.map +1 -1
- package/dist/utils/{clack-utils.d.ts → ui-utils.d.ts} +1 -1
- package/dist/utils/{clack-utils.js → ui-utils.js} +64 -37
- package/dist/utils/ui-utils.js.map +1 -0
- package/dist/utils/ui.d.ts +148 -0
- package/dist/utils/ui.js +368 -0
- package/dist/utils/ui.js.map +1 -0
- package/dist/utils/urls.d.ts +9 -3
- package/dist/utils/urls.js +10 -4
- package/dist/utils/urls.js.map +1 -1
- package/package.json +13 -20
- package/dist/check-coverage.ts +0 -237
- package/dist/emulate/core/id.d.ts +0 -48
- package/dist/emulate/core/id.js +0 -73
- package/dist/emulate/core/id.js.map +0 -1
- package/dist/emulate/core/index.d.ts +0 -8
- package/dist/emulate/core/index.js +0 -8
- package/dist/emulate/core/index.js.map +0 -1
- package/dist/emulate/core/jwt.d.ts +0 -28
- package/dist/emulate/core/jwt.js +0 -78
- package/dist/emulate/core/jwt.js.map +0 -1
- package/dist/emulate/core/middleware/auth.d.ts +0 -15
- package/dist/emulate/core/middleware/auth.js +0 -17
- package/dist/emulate/core/middleware/auth.js.map +0 -1
- package/dist/emulate/core/middleware/error-handler.d.ts +0 -22
- package/dist/emulate/core/middleware/error-handler.js +0 -72
- package/dist/emulate/core/middleware/error-handler.js.map +0 -1
- package/dist/emulate/core/pagination.d.ts +0 -27
- package/dist/emulate/core/pagination.js +0 -43
- package/dist/emulate/core/pagination.js.map +0 -1
- package/dist/emulate/core/plugin.d.ts +0 -15
- package/dist/emulate/core/plugin.js +0 -2
- package/dist/emulate/core/plugin.js.map +0 -1
- package/dist/emulate/core/server.d.ts +0 -17
- package/dist/emulate/core/server.js +0 -90
- package/dist/emulate/core/server.js.map +0 -1
- package/dist/emulate/core/store.d.ts +0 -44
- package/dist/emulate/core/store.js +0 -169
- package/dist/emulate/core/store.js.map +0 -1
- package/dist/emulate/index.d.ts +0 -25
- package/dist/emulate/index.js +0 -47
- package/dist/emulate/index.js.map +0 -1
- package/dist/emulate/workos/constants.d.ts +0 -56
- package/dist/emulate/workos/constants.js +0 -56
- package/dist/emulate/workos/constants.js.map +0 -1
- package/dist/emulate/workos/entities.d.ts +0 -360
- package/dist/emulate/workos/entities.js +0 -2
- package/dist/emulate/workos/entities.js.map +0 -1
- package/dist/emulate/workos/event-bus.d.ts +0 -17
- package/dist/emulate/workos/event-bus.js +0 -70
- package/dist/emulate/workos/event-bus.js.map +0 -1
- package/dist/emulate/workos/helpers.d.ts +0 -72
- package/dist/emulate/workos/helpers.js +0 -211
- package/dist/emulate/workos/helpers.js.map +0 -1
- package/dist/emulate/workos/index.d.ts +0 -93
- package/dist/emulate/workos/index.js +0 -326
- package/dist/emulate/workos/index.js.map +0 -1
- package/dist/emulate/workos/role-helpers.d.ts +0 -21
- package/dist/emulate/workos/role-helpers.js +0 -130
- package/dist/emulate/workos/role-helpers.js.map +0 -1
- package/dist/emulate/workos/routes/api-keys.d.ts +0 -2
- package/dist/emulate/workos/routes/api-keys.js +0 -32
- package/dist/emulate/workos/routes/api-keys.js.map +0 -1
- package/dist/emulate/workos/routes/audit-logs.d.ts +0 -2
- package/dist/emulate/workos/routes/audit-logs.js +0 -104
- package/dist/emulate/workos/routes/audit-logs.js.map +0 -1
- package/dist/emulate/workos/routes/auth-challenges.d.ts +0 -2
- package/dist/emulate/workos/routes/auth-challenges.js +0 -51
- package/dist/emulate/workos/routes/auth-challenges.js.map +0 -1
- package/dist/emulate/workos/routes/auth-factors.d.ts +0 -2
- package/dist/emulate/workos/routes/auth-factors.js +0 -51
- package/dist/emulate/workos/routes/auth-factors.js.map +0 -1
- package/dist/emulate/workos/routes/auth.d.ts +0 -2
- package/dist/emulate/workos/routes/auth.js +0 -350
- package/dist/emulate/workos/routes/auth.js.map +0 -1
- package/dist/emulate/workos/routes/authorization-checks.d.ts +0 -10
- package/dist/emulate/workos/routes/authorization-checks.js +0 -123
- package/dist/emulate/workos/routes/authorization-checks.js.map +0 -1
- package/dist/emulate/workos/routes/authorization-org-roles.d.ts +0 -2
- package/dist/emulate/workos/routes/authorization-org-roles.js +0 -64
- package/dist/emulate/workos/routes/authorization-org-roles.js.map +0 -1
- package/dist/emulate/workos/routes/authorization-permissions.d.ts +0 -2
- package/dist/emulate/workos/routes/authorization-permissions.js +0 -67
- package/dist/emulate/workos/routes/authorization-permissions.js.map +0 -1
- package/dist/emulate/workos/routes/authorization-resources.d.ts +0 -2
- package/dist/emulate/workos/routes/authorization-resources.js +0 -117
- package/dist/emulate/workos/routes/authorization-resources.js.map +0 -1
- package/dist/emulate/workos/routes/authorization-roles.d.ts +0 -2
- package/dist/emulate/workos/routes/authorization-roles.js +0 -13
- package/dist/emulate/workos/routes/authorization-roles.js.map +0 -1
- package/dist/emulate/workos/routes/config.d.ts +0 -2
- package/dist/emulate/workos/routes/config.js +0 -57
- package/dist/emulate/workos/routes/config.js.map +0 -1
- package/dist/emulate/workos/routes/connect.d.ts +0 -2
- package/dist/emulate/workos/routes/connect.js +0 -65
- package/dist/emulate/workos/routes/connect.js.map +0 -1
- package/dist/emulate/workos/routes/connections.d.ts +0 -2
- package/dist/emulate/workos/routes/connections.js +0 -73
- package/dist/emulate/workos/routes/connections.js.map +0 -1
- package/dist/emulate/workos/routes/data-integrations.d.ts +0 -2
- package/dist/emulate/workos/routes/data-integrations.js +0 -55
- package/dist/emulate/workos/routes/data-integrations.js.map +0 -1
- package/dist/emulate/workos/routes/directories.d.ts +0 -2
- package/dist/emulate/workos/routes/directories.js +0 -90
- package/dist/emulate/workos/routes/directories.js.map +0 -1
- package/dist/emulate/workos/routes/email-verification.d.ts +0 -2
- package/dist/emulate/workos/routes/email-verification.js +0 -49
- package/dist/emulate/workos/routes/email-verification.js.map +0 -1
- package/dist/emulate/workos/routes/events.d.ts +0 -2
- package/dist/emulate/workos/routes/events.js +0 -18
- package/dist/emulate/workos/routes/events.js.map +0 -1
- package/dist/emulate/workos/routes/feature-flags.d.ts +0 -2
- package/dist/emulate/workos/routes/feature-flags.js +0 -103
- package/dist/emulate/workos/routes/feature-flags.js.map +0 -1
- package/dist/emulate/workos/routes/invitations.d.ts +0 -2
- package/dist/emulate/workos/routes/invitations.js +0 -122
- package/dist/emulate/workos/routes/invitations.js.map +0 -1
- package/dist/emulate/workos/routes/legacy-mfa.d.ts +0 -2
- package/dist/emulate/workos/routes/legacy-mfa.js +0 -75
- package/dist/emulate/workos/routes/legacy-mfa.js.map +0 -1
- package/dist/emulate/workos/routes/magic-auth.d.ts +0 -2
- package/dist/emulate/workos/routes/magic-auth.js +0 -32
- package/dist/emulate/workos/routes/magic-auth.js.map +0 -1
- package/dist/emulate/workos/routes/memberships.d.ts +0 -2
- package/dist/emulate/workos/routes/memberships.js +0 -114
- package/dist/emulate/workos/routes/memberships.js.map +0 -1
- package/dist/emulate/workos/routes/organization-domains.d.ts +0 -2
- package/dist/emulate/workos/routes/organization-domains.js +0 -58
- package/dist/emulate/workos/routes/organization-domains.js.map +0 -1
- package/dist/emulate/workos/routes/organizations.d.ts +0 -2
- package/dist/emulate/workos/routes/organizations.js +0 -131
- package/dist/emulate/workos/routes/organizations.js.map +0 -1
- package/dist/emulate/workos/routes/password-reset.d.ts +0 -2
- package/dist/emulate/workos/routes/password-reset.js +0 -61
- package/dist/emulate/workos/routes/password-reset.js.map +0 -1
- package/dist/emulate/workos/routes/pipes.d.ts +0 -2
- package/dist/emulate/workos/routes/pipes.js +0 -82
- package/dist/emulate/workos/routes/pipes.js.map +0 -1
- package/dist/emulate/workos/routes/portal.d.ts +0 -2
- package/dist/emulate/workos/routes/portal.js +0 -18
- package/dist/emulate/workos/routes/portal.js.map +0 -1
- package/dist/emulate/workos/routes/radar.d.ts +0 -2
- package/dist/emulate/workos/routes/radar.js +0 -41
- package/dist/emulate/workos/routes/radar.js.map +0 -1
- package/dist/emulate/workos/routes/sessions.d.ts +0 -2
- package/dist/emulate/workos/routes/sessions.js +0 -51
- package/dist/emulate/workos/routes/sessions.js.map +0 -1
- package/dist/emulate/workos/routes/sso.d.ts +0 -2
- package/dist/emulate/workos/routes/sso.js +0 -161
- package/dist/emulate/workos/routes/sso.js.map +0 -1
- package/dist/emulate/workos/routes/user-features.d.ts +0 -2
- package/dist/emulate/workos/routes/user-features.js +0 -50
- package/dist/emulate/workos/routes/user-features.js.map +0 -1
- package/dist/emulate/workos/routes/users.d.ts +0 -2
- package/dist/emulate/workos/routes/users.js +0 -129
- package/dist/emulate/workos/routes/users.js.map +0 -1
- package/dist/emulate/workos/routes/webhook-endpoints.d.ts +0 -2
- package/dist/emulate/workos/routes/webhook-endpoints.js +0 -67
- package/dist/emulate/workos/routes/webhook-endpoints.js.map +0 -1
- package/dist/emulate/workos/routes/widgets.d.ts +0 -2
- package/dist/emulate/workos/routes/widgets.js +0 -27
- package/dist/emulate/workos/routes/widgets.js.map +0 -1
- package/dist/emulate/workos/store.d.ts +0 -48
- package/dist/emulate/workos/store.js +0 -102
- package/dist/emulate/workos/store.js.map +0 -1
- package/dist/emulate/workos/webhook-signer.d.ts +0 -1
- package/dist/emulate/workos/webhook-signer.js +0 -8
- package/dist/emulate/workos/webhook-signer.js.map +0 -1
- package/dist/gen-routes-lib.spec.ts +0 -659
- package/dist/gen-routes-lib.ts +0 -647
- package/dist/gen-routes.ts +0 -96
- package/dist/utils/clack-utils.js.map +0 -1
- package/dist/utils/clack.d.ts +0 -5
- package/dist/utils/clack.js +0 -34
- package/dist/utils/clack.js.map +0 -1
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { access, mkdir, readFile, writeFile } from 'fs/promises';
|
|
4
|
+
import * as jsonc from 'jsonc-parser';
|
|
5
|
+
import { execFileNoThrow } from '../utils/exec-file.js';
|
|
6
|
+
import { MCP_SERVER_NAME, MCP_SERVER_URL } from './constants.js';
|
|
7
|
+
/**
|
|
8
|
+
* Client-writer library for the WorkOS MCP server.
|
|
9
|
+
*
|
|
10
|
+
* One `McpClientTarget` interface, three implementations. Claude Code and Codex
|
|
11
|
+
* are configured by shelling out to their own CLIs (robust to their config
|
|
12
|
+
* format changes); Cursor has no CLI, so we merge `~/.cursor/mcp.json` directly
|
|
13
|
+
* with `jsonc-parser` (hand-edited files may contain comments / trailing
|
|
14
|
+
* commas). Every operation degrades to a reported outcome — a missing binary,
|
|
15
|
+
* an old client without HTTP-transport support, or an unparseable config is a
|
|
16
|
+
* `failed`/`skipped`/`not-installed` result, never a thrown error.
|
|
17
|
+
*
|
|
18
|
+
* The library owns detection and per-client config writing; callers (the `mcp`
|
|
19
|
+
* command today; install-flow / doctor in Phase 2) stay thin. Nothing here may
|
|
20
|
+
* assume it runs inside the `mcp` command.
|
|
21
|
+
*/
|
|
22
|
+
/** 10s ceiling on every CLI shell-out so a hung client can't wedge the caller. */
|
|
23
|
+
const EXEC_TIMEOUT_MS = 10_000;
|
|
24
|
+
/** jsonc parse options: tolerate the comments / trailing commas real users
|
|
25
|
+
* hand-edit into `~/.cursor/mcp.json`, and an empty file, so only genuinely
|
|
26
|
+
* malformed JSON is flagged. */
|
|
27
|
+
const JSONC_PARSE_OPTIONS = {
|
|
28
|
+
allowTrailingComma: true,
|
|
29
|
+
allowEmptyContent: true,
|
|
30
|
+
};
|
|
31
|
+
const JSONC_MODIFY_OPTIONS = {
|
|
32
|
+
formattingOptions: { tabSize: 2, insertSpaces: true },
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Human phrasing for each outcome (JSON mode emits the raw `outcome` value).
|
|
36
|
+
* Lives with the `McpOutcome` type so every consumer (`mcp` command, `setup`)
|
|
37
|
+
* stays in lockstep when an outcome is added.
|
|
38
|
+
*/
|
|
39
|
+
export const MCP_OUTCOME_LABELS = {
|
|
40
|
+
installed: 'installed',
|
|
41
|
+
'already-installed': 'already installed',
|
|
42
|
+
removed: 'removed',
|
|
43
|
+
'not-installed': 'not installed',
|
|
44
|
+
skipped: 'skipped',
|
|
45
|
+
failed: 'failed',
|
|
46
|
+
};
|
|
47
|
+
/** Stable list of known agent keys, for `--agent` validation by callers. */
|
|
48
|
+
export const MCP_AGENT_KEYS = ['claude-code', 'codex', 'cursor'];
|
|
49
|
+
/** Async existence check — `access` rejects with ENOENT when the path is missing. */
|
|
50
|
+
async function pathExists(p) {
|
|
51
|
+
try {
|
|
52
|
+
await access(p);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Trim a shell-out message to a single-line excerpt for the `error` field. */
|
|
60
|
+
function excerpt(raw) {
|
|
61
|
+
const text = raw.trim();
|
|
62
|
+
if (!text)
|
|
63
|
+
return 'Unknown error';
|
|
64
|
+
const firstLine = text.split('\n').find((l) => l.trim().length > 0) ?? text;
|
|
65
|
+
return firstLine.trim().slice(0, 300);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Does a CLI `mcp list` output contain our server?
|
|
69
|
+
*
|
|
70
|
+
* Matches the first whitespace-delimited token of any line (with an optional
|
|
71
|
+
* trailing colon stripped), which covers both list shapes we support:
|
|
72
|
+
* - Claude Code: `workos: https://mcp.workos.com/mcp (HTTP) - ✔ Connected`
|
|
73
|
+
* - Codex table: `workos https://mcp.workos.com/mcp ...`
|
|
74
|
+
*
|
|
75
|
+
* First-token matching avoids the substring collision a naive
|
|
76
|
+
* `output.includes('workos')` would hit on an unrelated `workos-docs` entry.
|
|
77
|
+
*/
|
|
78
|
+
function listHasServer(stdout, name) {
|
|
79
|
+
return stdout.split('\n').some((line) => {
|
|
80
|
+
const firstToken = line.trim().split(/\s+/)[0] ?? '';
|
|
81
|
+
return firstToken.replace(/:$/, '') === name;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Shared shell-out client for Claude Code and Codex. The two differ only in
|
|
86
|
+
* binary name, config dir, and the `add` arg shape; every outcome-mapping rule
|
|
87
|
+
* is identical, so they share one implementation.
|
|
88
|
+
*/
|
|
89
|
+
function createCliClient(config) {
|
|
90
|
+
const { key, displayName, binary, configDir, addArgs, removeArgs } = config;
|
|
91
|
+
const result = (outcome, error) => ({
|
|
92
|
+
agent: key,
|
|
93
|
+
displayName,
|
|
94
|
+
outcome,
|
|
95
|
+
...(error ? { error } : {}),
|
|
96
|
+
});
|
|
97
|
+
async function checkInstalled() {
|
|
98
|
+
const res = await execFileNoThrow(binary, ['mcp', 'list'], { timeout: EXEC_TIMEOUT_MS });
|
|
99
|
+
if (res.status !== 0)
|
|
100
|
+
return false;
|
|
101
|
+
return listHasServer(res.stdout, MCP_SERVER_NAME);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
key,
|
|
105
|
+
displayName,
|
|
106
|
+
async isAvailable() {
|
|
107
|
+
// Two layers: the config dir can exist while the binary isn't on PATH,
|
|
108
|
+
// and vice versa — require both before we try to shell out.
|
|
109
|
+
if (!(await pathExists(join(homedir(), configDir))))
|
|
110
|
+
return false;
|
|
111
|
+
const res = await execFileNoThrow(binary, ['--version'], { timeout: EXEC_TIMEOUT_MS });
|
|
112
|
+
return res.status === 0;
|
|
113
|
+
},
|
|
114
|
+
isInstalled: checkInstalled,
|
|
115
|
+
async add() {
|
|
116
|
+
const res = await execFileNoThrow(binary, addArgs, { timeout: EXEC_TIMEOUT_MS });
|
|
117
|
+
if (res.status === 0)
|
|
118
|
+
return result('installed');
|
|
119
|
+
// Idempotent: an "already exists" collision is a success, not a failure.
|
|
120
|
+
const combined = `${res.stdout}\n${res.stderr}`.toLowerCase();
|
|
121
|
+
if (combined.includes('already exists') || combined.includes('already configured')) {
|
|
122
|
+
return result('already-installed');
|
|
123
|
+
}
|
|
124
|
+
// A non-zero exit doesn't always mean the write failed: Codex persists the
|
|
125
|
+
// server config and THEN starts an OAuth flow that blocks (no browser /
|
|
126
|
+
// callback available here) until our timeout kills it with a non-zero
|
|
127
|
+
// status. Confirm against the actual config before declaring failure —
|
|
128
|
+
// more robust than matching each client's success wording.
|
|
129
|
+
if (await checkInstalled())
|
|
130
|
+
return result('installed');
|
|
131
|
+
// Otherwise a real failure — an old client lacking `--transport http` /
|
|
132
|
+
// `--url`, a bad invocation, etc. Reported, never thrown.
|
|
133
|
+
return result('failed', excerpt(res.stderr || res.stdout));
|
|
134
|
+
},
|
|
135
|
+
async remove() {
|
|
136
|
+
const res = await execFileNoThrow(binary, removeArgs, { timeout: EXEC_TIMEOUT_MS });
|
|
137
|
+
const combined = `${res.stdout}\n${res.stderr}`.toLowerCase();
|
|
138
|
+
// Claude exits 1 and Codex exits 0 when the server is absent; both print
|
|
139
|
+
// "No MCP server named ...". Treat either as an idempotent no-op.
|
|
140
|
+
if (combined.includes('no mcp server'))
|
|
141
|
+
return result('not-installed');
|
|
142
|
+
if (res.status === 0)
|
|
143
|
+
return result('removed');
|
|
144
|
+
return result('failed', excerpt(res.stderr || res.stdout));
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function createClaudeCodeClient() {
|
|
149
|
+
return createCliClient({
|
|
150
|
+
key: 'claude-code',
|
|
151
|
+
displayName: 'Claude Code',
|
|
152
|
+
binary: 'claude',
|
|
153
|
+
configDir: '.claude',
|
|
154
|
+
// User scope: the management MCP is account-level, not per-repo — project
|
|
155
|
+
// scope would nag teammates via checked-in config.
|
|
156
|
+
addArgs: ['mcp', 'add', '--transport', 'http', '--scope', 'user', MCP_SERVER_NAME, MCP_SERVER_URL],
|
|
157
|
+
removeArgs: ['mcp', 'remove', '--scope', 'user', MCP_SERVER_NAME],
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function createCodexClient() {
|
|
161
|
+
return createCliClient({
|
|
162
|
+
key: 'codex',
|
|
163
|
+
displayName: 'Codex',
|
|
164
|
+
binary: 'codex',
|
|
165
|
+
configDir: '.codex',
|
|
166
|
+
addArgs: ['mcp', 'add', MCP_SERVER_NAME, '--url', MCP_SERVER_URL],
|
|
167
|
+
removeArgs: ['mcp', 'remove', MCP_SERVER_NAME],
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Cursor has no CLI, so we read-modify-write `~/.cursor/mcp.json` directly with
|
|
172
|
+
* `jsonc-parser`, preserving existing servers, comments, and formatting.
|
|
173
|
+
*/
|
|
174
|
+
function createCursorClient() {
|
|
175
|
+
const key = 'cursor';
|
|
176
|
+
const displayName = 'Cursor';
|
|
177
|
+
const cursorDir = () => join(homedir(), '.cursor');
|
|
178
|
+
const configPath = () => join(cursorDir(), 'mcp.json');
|
|
179
|
+
const result = (outcome, error) => ({
|
|
180
|
+
agent: key,
|
|
181
|
+
displayName,
|
|
182
|
+
outcome,
|
|
183
|
+
...(error ? { error } : {}),
|
|
184
|
+
});
|
|
185
|
+
/** Read + parse the config. `parseError` is true only for genuinely malformed
|
|
186
|
+
* JSON (not comments / trailing commas / empty). */
|
|
187
|
+
async function readConfig() {
|
|
188
|
+
const path = configPath();
|
|
189
|
+
if (!(await pathExists(path))) {
|
|
190
|
+
return { content: '', parsed: undefined, parseError: false, exists: false };
|
|
191
|
+
}
|
|
192
|
+
const content = await readFile(path, 'utf8');
|
|
193
|
+
const errors = [];
|
|
194
|
+
const parsed = jsonc.parse(content, errors, JSONC_PARSE_OPTIONS);
|
|
195
|
+
return { content, parsed, parseError: errors.length > 0, exists: true };
|
|
196
|
+
}
|
|
197
|
+
function hasWorkosServer(parsed) {
|
|
198
|
+
const servers = parsed?.mcpServers;
|
|
199
|
+
return Boolean(servers && typeof servers === 'object' && MCP_SERVER_NAME in servers);
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
key,
|
|
203
|
+
displayName,
|
|
204
|
+
async isAvailable() {
|
|
205
|
+
return pathExists(cursorDir());
|
|
206
|
+
},
|
|
207
|
+
async isInstalled() {
|
|
208
|
+
try {
|
|
209
|
+
const { parsed, parseError } = await readConfig();
|
|
210
|
+
if (parseError)
|
|
211
|
+
return false;
|
|
212
|
+
return hasWorkosServer(parsed);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
async add() {
|
|
219
|
+
const path = configPath();
|
|
220
|
+
try {
|
|
221
|
+
const { content, parseError, exists } = await readConfig();
|
|
222
|
+
// Never overwrite a file we couldn't parse — the user must fix it first.
|
|
223
|
+
if (exists && parseError) {
|
|
224
|
+
return result('failed', `Could not parse ${path}; fix it manually and retry.`);
|
|
225
|
+
}
|
|
226
|
+
await mkdir(cursorDir(), { recursive: true });
|
|
227
|
+
// Converge to our URL — an existing `workos` key with a different URL is
|
|
228
|
+
// overwritten (reported as `installed`).
|
|
229
|
+
const edits = jsonc.modify(content, ['mcpServers', MCP_SERVER_NAME], { url: MCP_SERVER_URL }, JSONC_MODIFY_OPTIONS);
|
|
230
|
+
await writeFile(path, jsonc.applyEdits(content, edits), 'utf8');
|
|
231
|
+
return result('installed');
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
return result('failed', error instanceof Error ? error.message : 'Unknown error');
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
async remove() {
|
|
238
|
+
const path = configPath();
|
|
239
|
+
try {
|
|
240
|
+
const { content, parsed, parseError, exists } = await readConfig();
|
|
241
|
+
if (!exists)
|
|
242
|
+
return result('not-installed');
|
|
243
|
+
if (parseError) {
|
|
244
|
+
return result('failed', `Could not parse ${path}; fix it manually and retry.`);
|
|
245
|
+
}
|
|
246
|
+
if (!hasWorkosServer(parsed))
|
|
247
|
+
return result('not-installed');
|
|
248
|
+
const edits = jsonc.modify(content, ['mcpServers', MCP_SERVER_NAME], undefined, JSONC_MODIFY_OPTIONS);
|
|
249
|
+
await writeFile(path, jsonc.applyEdits(content, edits), 'utf8');
|
|
250
|
+
return result('removed');
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
return result('failed', error instanceof Error ? error.message : 'Unknown error');
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/** The three client targets, in a stable order. */
|
|
259
|
+
export function createMcpClients() {
|
|
260
|
+
return [createClaudeCodeClient(), createCodexClient(), createCursorClient()];
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The URL the WorkOS server is configured with in Cursor's `~/.cursor/mcp.json`,
|
|
264
|
+
* or null when the file/entry is absent, unreadable, or unparseable.
|
|
265
|
+
*
|
|
266
|
+
* Cursor is the only client whose config we read directly (the CLI clients don't
|
|
267
|
+
* expose per-entry URLs), so this powers doctor's URL-drift ("misconfigured")
|
|
268
|
+
* check without a second jsonc reader duplicating the config schema. Read-only
|
|
269
|
+
* and never throws — a problem reading just yields null.
|
|
270
|
+
*/
|
|
271
|
+
export async function getCursorConfiguredUrl() {
|
|
272
|
+
try {
|
|
273
|
+
const path = join(homedir(), '.cursor', 'mcp.json');
|
|
274
|
+
if (!(await pathExists(path)))
|
|
275
|
+
return null;
|
|
276
|
+
const content = await readFile(path, 'utf8');
|
|
277
|
+
const parsed = jsonc.parse(content, [], JSONC_PARSE_OPTIONS);
|
|
278
|
+
const servers = parsed?.mcpServers;
|
|
279
|
+
const entry = servers?.[MCP_SERVER_NAME];
|
|
280
|
+
return typeof entry?.url === 'string' ? entry.url : null;
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Return only the clients available on this machine, optionally narrowed to
|
|
288
|
+
* `agentFilter` keys. Availability is probed in parallel. An unknown filter key
|
|
289
|
+
* simply matches nothing here — callers validate keys and surface the error.
|
|
290
|
+
*/
|
|
291
|
+
export async function detectMcpClients(agentFilter) {
|
|
292
|
+
const clients = createMcpClients();
|
|
293
|
+
const filtered = agentFilter ? clients.filter((c) => agentFilter.includes(c.key)) : clients;
|
|
294
|
+
const availability = await Promise.all(filtered.map((c) => c.isAvailable()));
|
|
295
|
+
return filtered.filter((_, i) => availability[i]);
|
|
296
|
+
}
|
|
297
|
+
//# sourceMappingURL=mcp-clients.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-clients.js","sourceRoot":"","sources":["../../src/lib/mcp-clients.ts"],"names":[],"mappings":"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","sourcesContent":["import { homedir } from 'os';\nimport { join } from 'path';\nimport { access, mkdir, readFile, writeFile } from 'fs/promises';\nimport * as jsonc from 'jsonc-parser';\nimport { execFileNoThrow } from '../utils/exec-file.js';\nimport { MCP_SERVER_NAME, MCP_SERVER_URL } from './constants.js';\n\n/**\n * Client-writer library for the WorkOS MCP server.\n *\n * One `McpClientTarget` interface, three implementations. Claude Code and Codex\n * are configured by shelling out to their own CLIs (robust to their config\n * format changes); Cursor has no CLI, so we merge `~/.cursor/mcp.json` directly\n * with `jsonc-parser` (hand-edited files may contain comments / trailing\n * commas). Every operation degrades to a reported outcome — a missing binary,\n * an old client without HTTP-transport support, or an unparseable config is a\n * `failed`/`skipped`/`not-installed` result, never a thrown error.\n *\n * The library owns detection and per-client config writing; callers (the `mcp`\n * command today; install-flow / doctor in Phase 2) stay thin. Nothing here may\n * assume it runs inside the `mcp` command.\n */\n\n/** 10s ceiling on every CLI shell-out so a hung client can't wedge the caller. */\nconst EXEC_TIMEOUT_MS = 10_000;\n\n/** jsonc parse options: tolerate the comments / trailing commas real users\n * hand-edit into `~/.cursor/mcp.json`, and an empty file, so only genuinely\n * malformed JSON is flagged. */\nconst JSONC_PARSE_OPTIONS: jsonc.ParseOptions = {\n allowTrailingComma: true,\n allowEmptyContent: true,\n};\n\nconst JSONC_MODIFY_OPTIONS: jsonc.ModificationOptions = {\n formattingOptions: { tabSize: 2, insertSpaces: true },\n};\n\nexport type McpAgentKey = 'claude-code' | 'codex' | 'cursor';\n\nexport type McpOutcome = 'installed' | 'already-installed' | 'removed' | 'not-installed' | 'skipped' | 'failed';\n\n/**\n * Human phrasing for each outcome (JSON mode emits the raw `outcome` value).\n * Lives with the `McpOutcome` type so every consumer (`mcp` command, `setup`)\n * stays in lockstep when an outcome is added.\n */\nexport const MCP_OUTCOME_LABELS: Record<McpOutcome, string> = {\n installed: 'installed',\n 'already-installed': 'already installed',\n removed: 'removed',\n 'not-installed': 'not installed',\n skipped: 'skipped',\n failed: 'failed',\n};\n\nexport interface McpClientResult {\n agent: McpAgentKey;\n displayName: string;\n outcome: McpOutcome;\n /** stderr/message excerpt when `outcome === 'failed'`. */\n error?: string;\n}\n\nexport interface McpClientTarget {\n key: McpAgentKey;\n displayName: string;\n /** Agent is usable on this machine (config dir present AND, for CLI clients, binary runnable). */\n isAvailable(): Promise<boolean>;\n /** The WorkOS server is present in this client's config. */\n isInstalled(): Promise<boolean>;\n add(): Promise<McpClientResult>;\n remove(): Promise<McpClientResult>;\n}\n\n/** Stable list of known agent keys, for `--agent` validation by callers. */\nexport const MCP_AGENT_KEYS: McpAgentKey[] = ['claude-code', 'codex', 'cursor'];\n\n/** Async existence check — `access` rejects with ENOENT when the path is missing. */\nasync function pathExists(p: string): Promise<boolean> {\n try {\n await access(p);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Trim a shell-out message to a single-line excerpt for the `error` field. */\nfunction excerpt(raw: string): string {\n const text = raw.trim();\n if (!text) return 'Unknown error';\n const firstLine = text.split('\\n').find((l) => l.trim().length > 0) ?? text;\n return firstLine.trim().slice(0, 300);\n}\n\n/**\n * Does a CLI `mcp list` output contain our server?\n *\n * Matches the first whitespace-delimited token of any line (with an optional\n * trailing colon stripped), which covers both list shapes we support:\n * - Claude Code: `workos: https://mcp.workos.com/mcp (HTTP) - ✔ Connected`\n * - Codex table: `workos https://mcp.workos.com/mcp ...`\n *\n * First-token matching avoids the substring collision a naive\n * `output.includes('workos')` would hit on an unrelated `workos-docs` entry.\n */\nfunction listHasServer(stdout: string, name: string): boolean {\n return stdout.split('\\n').some((line) => {\n const firstToken = line.trim().split(/\\s+/)[0] ?? '';\n return firstToken.replace(/:$/, '') === name;\n });\n}\n\n/**\n * Shared shell-out client for Claude Code and Codex. The two differ only in\n * binary name, config dir, and the `add` arg shape; every outcome-mapping rule\n * is identical, so they share one implementation.\n */\nfunction createCliClient(config: {\n key: McpAgentKey;\n displayName: string;\n binary: string;\n configDir: string;\n addArgs: string[];\n removeArgs: string[];\n}): McpClientTarget {\n const { key, displayName, binary, configDir, addArgs, removeArgs } = config;\n const result = (outcome: McpOutcome, error?: string): McpClientResult => ({\n agent: key,\n displayName,\n outcome,\n ...(error ? { error } : {}),\n });\n\n async function checkInstalled(): Promise<boolean> {\n const res = await execFileNoThrow(binary, ['mcp', 'list'], { timeout: EXEC_TIMEOUT_MS });\n if (res.status !== 0) return false;\n return listHasServer(res.stdout, MCP_SERVER_NAME);\n }\n\n return {\n key,\n displayName,\n async isAvailable() {\n // Two layers: the config dir can exist while the binary isn't on PATH,\n // and vice versa — require both before we try to shell out.\n if (!(await pathExists(join(homedir(), configDir)))) return false;\n const res = await execFileNoThrow(binary, ['--version'], { timeout: EXEC_TIMEOUT_MS });\n return res.status === 0;\n },\n isInstalled: checkInstalled,\n async add() {\n const res = await execFileNoThrow(binary, addArgs, { timeout: EXEC_TIMEOUT_MS });\n if (res.status === 0) return result('installed');\n // Idempotent: an \"already exists\" collision is a success, not a failure.\n const combined = `${res.stdout}\\n${res.stderr}`.toLowerCase();\n if (combined.includes('already exists') || combined.includes('already configured')) {\n return result('already-installed');\n }\n // A non-zero exit doesn't always mean the write failed: Codex persists the\n // server config and THEN starts an OAuth flow that blocks (no browser /\n // callback available here) until our timeout kills it with a non-zero\n // status. Confirm against the actual config before declaring failure —\n // more robust than matching each client's success wording.\n if (await checkInstalled()) return result('installed');\n // Otherwise a real failure — an old client lacking `--transport http` /\n // `--url`, a bad invocation, etc. Reported, never thrown.\n return result('failed', excerpt(res.stderr || res.stdout));\n },\n async remove() {\n const res = await execFileNoThrow(binary, removeArgs, { timeout: EXEC_TIMEOUT_MS });\n const combined = `${res.stdout}\\n${res.stderr}`.toLowerCase();\n // Claude exits 1 and Codex exits 0 when the server is absent; both print\n // \"No MCP server named ...\". Treat either as an idempotent no-op.\n if (combined.includes('no mcp server')) return result('not-installed');\n if (res.status === 0) return result('removed');\n return result('failed', excerpt(res.stderr || res.stdout));\n },\n };\n}\n\nfunction createClaudeCodeClient(): McpClientTarget {\n return createCliClient({\n key: 'claude-code',\n displayName: 'Claude Code',\n binary: 'claude',\n configDir: '.claude',\n // User scope: the management MCP is account-level, not per-repo — project\n // scope would nag teammates via checked-in config.\n addArgs: ['mcp', 'add', '--transport', 'http', '--scope', 'user', MCP_SERVER_NAME, MCP_SERVER_URL],\n removeArgs: ['mcp', 'remove', '--scope', 'user', MCP_SERVER_NAME],\n });\n}\n\nfunction createCodexClient(): McpClientTarget {\n return createCliClient({\n key: 'codex',\n displayName: 'Codex',\n binary: 'codex',\n configDir: '.codex',\n addArgs: ['mcp', 'add', MCP_SERVER_NAME, '--url', MCP_SERVER_URL],\n removeArgs: ['mcp', 'remove', MCP_SERVER_NAME],\n });\n}\n\n/**\n * Cursor has no CLI, so we read-modify-write `~/.cursor/mcp.json` directly with\n * `jsonc-parser`, preserving existing servers, comments, and formatting.\n */\nfunction createCursorClient(): McpClientTarget {\n const key: McpAgentKey = 'cursor';\n const displayName = 'Cursor';\n const cursorDir = () => join(homedir(), '.cursor');\n const configPath = () => join(cursorDir(), 'mcp.json');\n const result = (outcome: McpOutcome, error?: string): McpClientResult => ({\n agent: key,\n displayName,\n outcome,\n ...(error ? { error } : {}),\n });\n\n /** Read + parse the config. `parseError` is true only for genuinely malformed\n * JSON (not comments / trailing commas / empty). */\n async function readConfig(): Promise<{\n content: string;\n parsed: Record<string, unknown> | undefined;\n parseError: boolean;\n exists: boolean;\n }> {\n const path = configPath();\n if (!(await pathExists(path))) {\n return { content: '', parsed: undefined, parseError: false, exists: false };\n }\n const content = await readFile(path, 'utf8');\n const errors: jsonc.ParseError[] = [];\n const parsed = jsonc.parse(content, errors, JSONC_PARSE_OPTIONS) as Record<string, unknown> | undefined;\n return { content, parsed, parseError: errors.length > 0, exists: true };\n }\n\n function hasWorkosServer(parsed: Record<string, unknown> | undefined): boolean {\n const servers = parsed?.mcpServers;\n return Boolean(servers && typeof servers === 'object' && MCP_SERVER_NAME in (servers as object));\n }\n\n return {\n key,\n displayName,\n async isAvailable() {\n return pathExists(cursorDir());\n },\n async isInstalled() {\n try {\n const { parsed, parseError } = await readConfig();\n if (parseError) return false;\n return hasWorkosServer(parsed);\n } catch {\n return false;\n }\n },\n async add() {\n const path = configPath();\n try {\n const { content, parseError, exists } = await readConfig();\n // Never overwrite a file we couldn't parse — the user must fix it first.\n if (exists && parseError) {\n return result('failed', `Could not parse ${path}; fix it manually and retry.`);\n }\n await mkdir(cursorDir(), { recursive: true });\n // Converge to our URL — an existing `workos` key with a different URL is\n // overwritten (reported as `installed`).\n const edits = jsonc.modify(\n content,\n ['mcpServers', MCP_SERVER_NAME],\n { url: MCP_SERVER_URL },\n JSONC_MODIFY_OPTIONS,\n );\n await writeFile(path, jsonc.applyEdits(content, edits), 'utf8');\n return result('installed');\n } catch (error) {\n return result('failed', error instanceof Error ? error.message : 'Unknown error');\n }\n },\n async remove() {\n const path = configPath();\n try {\n const { content, parsed, parseError, exists } = await readConfig();\n if (!exists) return result('not-installed');\n if (parseError) {\n return result('failed', `Could not parse ${path}; fix it manually and retry.`);\n }\n if (!hasWorkosServer(parsed)) return result('not-installed');\n const edits = jsonc.modify(content, ['mcpServers', MCP_SERVER_NAME], undefined, JSONC_MODIFY_OPTIONS);\n await writeFile(path, jsonc.applyEdits(content, edits), 'utf8');\n return result('removed');\n } catch (error) {\n return result('failed', error instanceof Error ? error.message : 'Unknown error');\n }\n },\n };\n}\n\n/** The three client targets, in a stable order. */\nexport function createMcpClients(): McpClientTarget[] {\n return [createClaudeCodeClient(), createCodexClient(), createCursorClient()];\n}\n\n/**\n * The URL the WorkOS server is configured with in Cursor's `~/.cursor/mcp.json`,\n * or null when the file/entry is absent, unreadable, or unparseable.\n *\n * Cursor is the only client whose config we read directly (the CLI clients don't\n * expose per-entry URLs), so this powers doctor's URL-drift (\"misconfigured\")\n * check without a second jsonc reader duplicating the config schema. Read-only\n * and never throws — a problem reading just yields null.\n */\nexport async function getCursorConfiguredUrl(): Promise<string | null> {\n try {\n const path = join(homedir(), '.cursor', 'mcp.json');\n if (!(await pathExists(path))) return null;\n const content = await readFile(path, 'utf8');\n const parsed = jsonc.parse(content, [], JSONC_PARSE_OPTIONS) as Record<string, unknown> | undefined;\n const servers = parsed?.mcpServers as Record<string, unknown> | undefined;\n const entry = servers?.[MCP_SERVER_NAME] as { url?: unknown } | undefined;\n return typeof entry?.url === 'string' ? entry.url : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Return only the clients available on this machine, optionally narrowed to\n * `agentFilter` keys. Availability is probed in parallel. An unknown filter key\n * simply matches nothing here — callers validate keys and surface the error.\n */\nexport async function detectMcpClients(agentFilter?: string[]): Promise<McpClientTarget[]> {\n const clients = createMcpClients();\n const filtered = agentFilter ? clients.filter((c) => agentFilter.includes(c.key)) : clients;\n const availability = await Promise.all(filtered.map((c) => c.isAvailable()));\n return filtered.filter((_, i) => availability[i]);\n}\n"]}
|
|
@@ -45,6 +45,20 @@ function parseViteConfigPort(configPath) {
|
|
|
45
45
|
}
|
|
46
46
|
return null;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Try vite.config.{ts,js,mjs} in installDir for a server.port value.
|
|
50
|
+
* NOTE: parseViteConfigPort uses a greedy first-match /port\s*:\s*.../ regex,
|
|
51
|
+
* so an unrelated `port:` (e.g. hmr.port) appearing before server.port wins.
|
|
52
|
+
* Pre-existing behavior for all Vite frameworks — not a regression here.
|
|
53
|
+
*/
|
|
54
|
+
function parseViteConfigPortFromDir(installDir) {
|
|
55
|
+
for (const p of ['vite.config.ts', 'vite.config.js', 'vite.config.mjs']) {
|
|
56
|
+
const port = parseViteConfigPort(join(installDir, p));
|
|
57
|
+
if (port)
|
|
58
|
+
return port;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
48
62
|
/**
|
|
49
63
|
* Parse port from Next.js package.json scripts.
|
|
50
64
|
* Next.js uses: "dev": "next dev -p 4000" or --port 4000
|
|
@@ -202,24 +216,16 @@ export function detectPort(integration, installDir) {
|
|
|
202
216
|
detectedPort = parseNextConfigPort(installDir);
|
|
203
217
|
break;
|
|
204
218
|
case 'tanstack-start':
|
|
205
|
-
|
|
219
|
+
// Modern @tanstack/react-start is Vite-based (port in vite.config.ts);
|
|
220
|
+
// legacy Vinxi projects use app.config.ts. Try Vite first.
|
|
221
|
+
detectedPort = parseViteConfigPortFromDir(installDir) ?? parseTanStackPort(installDir);
|
|
206
222
|
break;
|
|
207
223
|
case 'react':
|
|
208
224
|
case 'react-router':
|
|
209
|
-
case 'vanilla-js':
|
|
225
|
+
case 'vanilla-js':
|
|
210
226
|
// Vite-based frameworks
|
|
211
|
-
|
|
212
|
-
join(installDir, 'vite.config.ts'),
|
|
213
|
-
join(installDir, 'vite.config.js'),
|
|
214
|
-
join(installDir, 'vite.config.mjs'),
|
|
215
|
-
];
|
|
216
|
-
for (const configPath of viteConfigs) {
|
|
217
|
-
detectedPort = parseViteConfigPort(configPath);
|
|
218
|
-
if (detectedPort)
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
227
|
+
detectedPort = parseViteConfigPortFromDir(installDir);
|
|
221
228
|
break;
|
|
222
|
-
}
|
|
223
229
|
case 'dotnet':
|
|
224
230
|
detectedPort = parseDotnetPort(installDir);
|
|
225
231
|
break;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"port-detection.js","sourceRoot":"","sources":["../../src/lib/port-detection.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport { join } from 'node:path';\nimport type { Integration } from './constants.js';\nimport { getConfig } from './settings.js';\n\nconst settings = getConfig();\n\nconst INTEGRATION_TO_SETTINGS_KEY: Record<string, string> = {\n nextjs: 'nextjs',\n react: 'react',\n 'tanstack-start': 'tanstackStart',\n 'react-router': 'reactRouter',\n 'vanilla-js': 'vanillaJs',\n python: 'python',\n ruby: 'ruby',\n php: 'php',\n 'php-laravel': 'phpLaravel',\n go: 'go',\n dotnet: 'dotnet',\n elixir: 'elixir',\n kotlin: 'kotlin',\n};\n\nconst DEFAULT_PORT = 3000;\nconst DEFAULT_CALLBACK_PATH = '/auth/callback';\n\nfunction getDefaultPort(integration: Integration): number {\n const settingsKey = INTEGRATION_TO_SETTINGS_KEY[integration];\n return settings.frameworks[settingsKey]?.port ?? DEFAULT_PORT;\n}\n\nexport function getCallbackPath(integration: Integration): string {\n const settingsKey = INTEGRATION_TO_SETTINGS_KEY[integration];\n return settings.frameworks[settingsKey]?.callbackPath ?? DEFAULT_CALLBACK_PATH;\n}\n\n/**\n * Parse port from Vite config file.\n * Looks for server.port in vite.config.{ts,js,mjs}\n */\nfunction parseViteConfigPort(configPath: string): number | null {\n try {\n const content = fs.readFileSync(configPath, 'utf-8');\n // Match: port: 3000 or port: \"3000\" or port: '3000'\n const portMatch = content.match(/port\\s*:\\s*['\"]?(\\d+)['\"]?/);\n if (portMatch) {\n return parseInt(portMatch[1], 10);\n }\n } catch {\n // File doesn't exist or can't be read\n }\n return null;\n}\n\n/**\n * Parse port from Next.js package.json scripts.\n * Next.js uses: \"dev\": \"next dev -p 4000\" or --port 4000\n */\nfunction parseNextConfigPort(installDir: string): number | null {\n try {\n const packageJsonPath = join(installDir, 'package.json');\n const content = fs.readFileSync(packageJsonPath, 'utf-8');\n const packageJson = JSON.parse(content);\n\n const devScript = packageJson.scripts?.dev || '';\n // Match: -p 4000, --port 4000, --port=4000\n const portMatch = devScript.match(/-p\\s+(\\d+)|--port[=\\s]+(\\d+)/);\n if (portMatch) {\n return parseInt(portMatch[1] || portMatch[2], 10);\n }\n } catch {\n // Can't read package.json\n }\n return null;\n}\n\n/**\n * Parse port from TanStack Start app.config.ts.\n * Uses Vinxi: server: { port: N }\n */\nfunction parseTanStackPort(installDir: string): number | null {\n const configPaths = [join(installDir, 'app.config.ts'), join(installDir, 'app.config.js')];\n\n for (const configPath of configPaths) {\n try {\n const content = fs.readFileSync(configPath, 'utf-8');\n // Match server config with port\n const portMatch = content.match(/server\\s*:\\s*\\{[^}]*port\\s*:\\s*(\\d+)/);\n if (portMatch) {\n return parseInt(portMatch[1], 10);\n }\n } catch {\n // Config file doesn't exist\n }\n }\n return null;\n}\n\n/**\n * Parse port from .NET Properties/launchSettings.json.\n * VS/Rider scaffold: profiles[*].applicationUrl = \"http://localhost:5000;https://localhost:5001\"\n */\nfunction parseDotnetPort(installDir: string): number | null {\n try {\n const configPath = join(installDir, 'Properties', 'launchSettings.json');\n const content = fs.readFileSync(configPath, 'utf-8');\n const parsed = JSON.parse(content) as { profiles?: Record<string, { applicationUrl?: string }> };\n for (const profile of Object.values(parsed.profiles ?? {})) {\n const match = profile.applicationUrl?.match(/http:\\/\\/[^:/]+:(\\d+)/);\n if (match) return parseInt(match[1], 10);\n }\n } catch {\n // File doesn't exist or can't parse\n }\n return null;\n}\n\n/**\n * Parse port from Phoenix config/dev.exs or config/runtime.exs.\n * Looks for `port: NNNN` — typically inside `http: [...]` but a bare regex is fine.\n */\nfunction parseElixirPort(installDir: string): number | null {\n for (const relPath of ['config/dev.exs', 'config/runtime.exs']) {\n try {\n const content = fs.readFileSync(join(installDir, relPath), 'utf-8');\n const match = content.match(/port:\\s*(\\d+)/);\n if (match) return parseInt(match[1], 10);\n } catch {\n // skip\n }\n }\n return null;\n}\n\n/**\n * Parse port from Spring Boot application.properties or application.yml.\n * Both the default `src/main/resources/` location and a top-level file are checked.\n */\nfunction parseKotlinPort(installDir: string): number | null {\n const propsPaths = [\n join(installDir, 'src', 'main', 'resources', 'application.properties'),\n join(installDir, 'application.properties'),\n ];\n for (const propsPath of propsPaths) {\n try {\n const content = fs.readFileSync(propsPath, 'utf-8');\n const match = content.match(/^server\\.port\\s*=\\s*(\\d+)/m);\n if (match) return parseInt(match[1], 10);\n } catch {\n // skip\n }\n }\n\n const ymlPaths = [\n join(installDir, 'src', 'main', 'resources', 'application.yml'),\n join(installDir, 'src', 'main', 'resources', 'application.yaml'),\n join(installDir, 'application.yml'),\n join(installDir, 'application.yaml'),\n ];\n for (const ymlPath of ymlPaths) {\n try {\n const content = fs.readFileSync(ymlPath, 'utf-8');\n // `server:\\n port: 8080` — shallow YAML parse via regex\n const match = content.match(/server\\s*:\\s*\\n[^\\S\\n]+port\\s*:\\s*(\\d+)/);\n if (match) return parseInt(match[1], 10);\n } catch {\n // skip\n }\n }\n return null;\n}\n\n/**\n * Parse port from Rails config/puma.rb.\n * Common forms:\n * port ENV.fetch(\"PORT\") { 3000 }\n * port ENV.fetch(\"PORT\", 3000)\n * port 3000\n */\nfunction parseRubyPort(installDir: string): number | null {\n try {\n const configPath = join(installDir, 'config', 'puma.rb');\n const content = fs.readFileSync(configPath, 'utf-8');\n const blockFetch = content.match(/port\\s+ENV\\.fetch\\([^)]*\\)\\s*\\{\\s*(\\d+)\\s*\\}/);\n if (blockFetch) return parseInt(blockFetch[1], 10);\n const argFetch = content.match(/port\\s+ENV\\.fetch\\([^,]+,\\s*(\\d+)\\)/);\n if (argFetch) return parseInt(argFetch[1], 10);\n const literal = content.match(/^\\s*port\\s+(\\d+)/m);\n if (literal) return parseInt(literal[1], 10);\n } catch {\n // skip\n }\n return null;\n}\n\n/**\n * Detect the dev server port for a framework.\n * Checks config files first, falls back to framework default.\n */\nexport function detectPort(integration: Integration, installDir: string): number {\n let detectedPort: number | null = null;\n\n switch (integration) {\n case 'nextjs':\n detectedPort = parseNextConfigPort(installDir);\n break;\n\n case 'tanstack-start':\n detectedPort = parseTanStackPort(installDir);\n break;\n\n case 'react':\n case 'react-router':\n case 'vanilla-js': {\n // Vite-based frameworks\n const viteConfigs = [\n join(installDir, 'vite.config.ts'),\n join(installDir, 'vite.config.js'),\n join(installDir, 'vite.config.mjs'),\n ];\n for (const configPath of viteConfigs) {\n detectedPort = parseViteConfigPort(configPath);\n if (detectedPort) break;\n }\n break;\n }\n\n case 'dotnet':\n detectedPort = parseDotnetPort(installDir);\n break;\n\n case 'elixir':\n detectedPort = parseElixirPort(installDir);\n break;\n\n case 'kotlin':\n detectedPort = parseKotlinPort(installDir);\n break;\n\n case 'ruby':\n detectedPort = parseRubyPort(installDir);\n break;\n }\n\n return detectedPort ?? getDefaultPort(integration);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"port-detection.js","sourceRoot":"","sources":["../../src/lib/port-detection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE1C,MAAM,QAAQ,GAAG,SAAS,EAAE,CAAC;AAE7B,MAAM,2BAA2B,GAA2B;IAC1D,MAAM,EAAE,QAAQ;IAChB,KAAK,EAAE,OAAO;IACd,gBAAgB,EAAE,eAAe;IACjC,cAAc,EAAE,aAAa;IAC7B,YAAY,EAAE,WAAW;IACzB,MAAM,EAAE,QAAQ;IAChB,IAAI,EAAE,MAAM;IACZ,GAAG,EAAE,KAAK;IACV,aAAa,EAAE,YAAY;IAC3B,EAAE,EAAE,IAAI;IACR,MAAM,EAAE,QAAQ;IAChB,MAAM,EAAE,QAAQ;IAChB,MAAM,EAAE,QAAQ;CACjB,CAAC;AAEF,MAAM,YAAY,GAAG,IAAI,CAAC;AAC1B,MAAM,qBAAqB,GAAG,gBAAgB,CAAC;AAE/C,SAAS,cAAc,CAAC,WAAwB;IAC9C,MAAM,WAAW,GAAG,2BAA2B,CAAC,WAAW,CAAC,CAAC;IAC7D,OAAO,QAAQ,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,IAAI,IAAI,YAAY,CAAC;AAChE,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,WAAwB;IACtD,MAAM,WAAW,GAAG,2BAA2B,CAAC,WAAW,CAAC,CAAC;IAC7D,OAAO,QAAQ,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,YAAY,IAAI,qBAAqB,CAAC;AACjF,CAAC;AAED;;;GAGG;AACH,SAAS,mBAAmB,CAAC,UAAkB;IAC7C,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QACrD,oDAAoD;QACpD,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC;QAC9D,IAAI,SAAS,EAAE,CAAC;YACd,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,sCAAsC;IACxC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,SAAS,0BAA0B,CAAC,UAAkB;IACpD,KAAK,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,gBAAgB,EAAE,iBAAiB,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,GAAG,mBAAmB,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC;QACtD,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC;IACxB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,SAAS,mBAAmB,CAAC,UAAkB;IAC7C,IAAI,CAAC;QACH,MAAM,eAAe,GAAG,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;QACzD,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;QAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAExC,MAAM,SAAS,GAAG,WAAW,CAAC,OAAO,EAAE,GAAG,IAAI,EAAE,CAAC;QACjD,2CAA2C;QAC3C,MAAM,SAAS,GAAG,SAAS,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAClE,IAAI,SAAS,EAAE,CAAC;YACd,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,0BAA0B;IAC5B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,SAAS,iBAAiB,CAAC,UAAkB;IAC3C,MAAM,WAAW,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,eAAe,CAAC,EAAE,IAAI,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC,CAAC;IAE3F,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACrC,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YACrD,gCAAgC;YAChC,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC;YACxE,IAAI,SAAS,EAAE,CAAC;gBACd,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACpC,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,4BAA4B;QAC9B,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,SAAS,eAAe,CAAC,UAAkB;IACzC,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,EAAE,YAAY,EAAE,qBAAqB,CAAC,CAAC;QACzE,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QACrD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAA+D,CAAC;QACjG,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC,EAAE,CAAC;YAC3D,MAAM,KAAK,GAAG,OAAO,CAAC,cAAc,EAAE,KAAK,CAAC,uBAAuB,CAAC,CAAC;YACrE,IAAI,KAAK;gBAAE,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,oCAAoC;IACtC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,SAAS,eAAe,CAAC,UAAkB;IACzC,KAAK,MAAM,OAAO,IAAI,CAAC,gBAAgB,EAAE,oBAAoB,CAAC,EAAE,CAAC;QAC/D,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC;YACpE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;YAC7C,IAAI,KAAK;gBAAE,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO;QACT,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,SAAS,eAAe,CAAC,UAAkB;IACzC,MAAM,UAAU,GAAG;QACjB,IAAI,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,wBAAwB,CAAC;QACtE,IAAI,CAAC,UAAU,EAAE,wBAAwB,CAAC;KAC3C,CAAC;IACF,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YACpD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC;YAC1D,IAAI,KAAK;gBAAE,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO;QACT,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG;QACf,IAAI,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,iBAAiB,CAAC;QAC/D,IAAI,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,kBAAkB,CAAC;QAChE,IAAI,CAAC,UAAU,EAAE,iBAAiB,CAAC;QACnC,IAAI,CAAC,UAAU,EAAE,kBAAkB,CAAC;KACrC,CAAC;IACF,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAClD,yDAAyD;YACzD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,yCAAyC,CAAC,CAAC;YACvE,IAAI,KAAK;gBAAE,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO;QACT,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;GAMG;AACH,SAAS,aAAa,CAAC,UAAkB;IACvC,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;QACzD,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QACrD,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;QACjF,IAAI,UAAU;YAAE,OAAO,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACnD,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,qCAAqC,CAAC,CAAC;QACtE,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC/C,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;QACnD,IAAI,OAAO;YAAE,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;IACT,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,WAAwB,EAAE,UAAkB;IACrE,IAAI,YAAY,GAAkB,IAAI,CAAC;IAEvC,QAAQ,WAAW,EAAE,CAAC;QACpB,KAAK,QAAQ;YACX,YAAY,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAC;YAC/C,MAAM;QAER,KAAK,gBAAgB;YACnB,uEAAuE;YACvE,2DAA2D;YAC3D,YAAY,GAAG,0BAA0B,CAAC,UAAU,CAAC,IAAI,iBAAiB,CAAC,UAAU,CAAC,CAAC;YACvF,MAAM;QAER,KAAK,OAAO,CAAC;QACb,KAAK,cAAc,CAAC;QACpB,KAAK,YAAY;YACf,wBAAwB;YACxB,YAAY,GAAG,0BAA0B,CAAC,UAAU,CAAC,CAAC;YACtD,MAAM;QAER,KAAK,QAAQ;YACX,YAAY,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;YAC3C,MAAM;QAER,KAAK,QAAQ;YACX,YAAY,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;YAC3C,MAAM;QAER,KAAK,QAAQ;YACX,YAAY,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;YAC3C,MAAM;QAER,KAAK,MAAM;YACT,YAAY,GAAG,aAAa,CAAC,UAAU,CAAC,CAAC;YACzC,MAAM;IACV,CAAC;IAED,OAAO,YAAY,IAAI,cAAc,CAAC,WAAW,CAAC,CAAC;AACrD,CAAC","sourcesContent":["import * as fs from 'node:fs';\nimport { join } from 'node:path';\nimport type { Integration } from './constants.js';\nimport { getConfig } from './settings.js';\n\nconst settings = getConfig();\n\nconst INTEGRATION_TO_SETTINGS_KEY: Record<string, string> = {\n nextjs: 'nextjs',\n react: 'react',\n 'tanstack-start': 'tanstackStart',\n 'react-router': 'reactRouter',\n 'vanilla-js': 'vanillaJs',\n python: 'python',\n ruby: 'ruby',\n php: 'php',\n 'php-laravel': 'phpLaravel',\n go: 'go',\n dotnet: 'dotnet',\n elixir: 'elixir',\n kotlin: 'kotlin',\n};\n\nconst DEFAULT_PORT = 3000;\nconst DEFAULT_CALLBACK_PATH = '/auth/callback';\n\nfunction getDefaultPort(integration: Integration): number {\n const settingsKey = INTEGRATION_TO_SETTINGS_KEY[integration];\n return settings.frameworks[settingsKey]?.port ?? DEFAULT_PORT;\n}\n\nexport function getCallbackPath(integration: Integration): string {\n const settingsKey = INTEGRATION_TO_SETTINGS_KEY[integration];\n return settings.frameworks[settingsKey]?.callbackPath ?? DEFAULT_CALLBACK_PATH;\n}\n\n/**\n * Parse port from Vite config file.\n * Looks for server.port in vite.config.{ts,js,mjs}\n */\nfunction parseViteConfigPort(configPath: string): number | null {\n try {\n const content = fs.readFileSync(configPath, 'utf-8');\n // Match: port: 3000 or port: \"3000\" or port: '3000'\n const portMatch = content.match(/port\\s*:\\s*['\"]?(\\d+)['\"]?/);\n if (portMatch) {\n return parseInt(portMatch[1], 10);\n }\n } catch {\n // File doesn't exist or can't be read\n }\n return null;\n}\n\n/**\n * Try vite.config.{ts,js,mjs} in installDir for a server.port value.\n * NOTE: parseViteConfigPort uses a greedy first-match /port\\s*:\\s*.../ regex,\n * so an unrelated `port:` (e.g. hmr.port) appearing before server.port wins.\n * Pre-existing behavior for all Vite frameworks — not a regression here.\n */\nfunction parseViteConfigPortFromDir(installDir: string): number | null {\n for (const p of ['vite.config.ts', 'vite.config.js', 'vite.config.mjs']) {\n const port = parseViteConfigPort(join(installDir, p));\n if (port) return port;\n }\n return null;\n}\n\n/**\n * Parse port from Next.js package.json scripts.\n * Next.js uses: \"dev\": \"next dev -p 4000\" or --port 4000\n */\nfunction parseNextConfigPort(installDir: string): number | null {\n try {\n const packageJsonPath = join(installDir, 'package.json');\n const content = fs.readFileSync(packageJsonPath, 'utf-8');\n const packageJson = JSON.parse(content);\n\n const devScript = packageJson.scripts?.dev || '';\n // Match: -p 4000, --port 4000, --port=4000\n const portMatch = devScript.match(/-p\\s+(\\d+)|--port[=\\s]+(\\d+)/);\n if (portMatch) {\n return parseInt(portMatch[1] || portMatch[2], 10);\n }\n } catch {\n // Can't read package.json\n }\n return null;\n}\n\n/**\n * Parse port from TanStack Start app.config.ts.\n * Uses Vinxi: server: { port: N }\n */\nfunction parseTanStackPort(installDir: string): number | null {\n const configPaths = [join(installDir, 'app.config.ts'), join(installDir, 'app.config.js')];\n\n for (const configPath of configPaths) {\n try {\n const content = fs.readFileSync(configPath, 'utf-8');\n // Match server config with port\n const portMatch = content.match(/server\\s*:\\s*\\{[^}]*port\\s*:\\s*(\\d+)/);\n if (portMatch) {\n return parseInt(portMatch[1], 10);\n }\n } catch {\n // Config file doesn't exist\n }\n }\n return null;\n}\n\n/**\n * Parse port from .NET Properties/launchSettings.json.\n * VS/Rider scaffold: profiles[*].applicationUrl = \"http://localhost:5000;https://localhost:5001\"\n */\nfunction parseDotnetPort(installDir: string): number | null {\n try {\n const configPath = join(installDir, 'Properties', 'launchSettings.json');\n const content = fs.readFileSync(configPath, 'utf-8');\n const parsed = JSON.parse(content) as { profiles?: Record<string, { applicationUrl?: string }> };\n for (const profile of Object.values(parsed.profiles ?? {})) {\n const match = profile.applicationUrl?.match(/http:\\/\\/[^:/]+:(\\d+)/);\n if (match) return parseInt(match[1], 10);\n }\n } catch {\n // File doesn't exist or can't parse\n }\n return null;\n}\n\n/**\n * Parse port from Phoenix config/dev.exs or config/runtime.exs.\n * Looks for `port: NNNN` — typically inside `http: [...]` but a bare regex is fine.\n */\nfunction parseElixirPort(installDir: string): number | null {\n for (const relPath of ['config/dev.exs', 'config/runtime.exs']) {\n try {\n const content = fs.readFileSync(join(installDir, relPath), 'utf-8');\n const match = content.match(/port:\\s*(\\d+)/);\n if (match) return parseInt(match[1], 10);\n } catch {\n // skip\n }\n }\n return null;\n}\n\n/**\n * Parse port from Spring Boot application.properties or application.yml.\n * Both the default `src/main/resources/` location and a top-level file are checked.\n */\nfunction parseKotlinPort(installDir: string): number | null {\n const propsPaths = [\n join(installDir, 'src', 'main', 'resources', 'application.properties'),\n join(installDir, 'application.properties'),\n ];\n for (const propsPath of propsPaths) {\n try {\n const content = fs.readFileSync(propsPath, 'utf-8');\n const match = content.match(/^server\\.port\\s*=\\s*(\\d+)/m);\n if (match) return parseInt(match[1], 10);\n } catch {\n // skip\n }\n }\n\n const ymlPaths = [\n join(installDir, 'src', 'main', 'resources', 'application.yml'),\n join(installDir, 'src', 'main', 'resources', 'application.yaml'),\n join(installDir, 'application.yml'),\n join(installDir, 'application.yaml'),\n ];\n for (const ymlPath of ymlPaths) {\n try {\n const content = fs.readFileSync(ymlPath, 'utf-8');\n // `server:\\n port: 8080` — shallow YAML parse via regex\n const match = content.match(/server\\s*:\\s*\\n[^\\S\\n]+port\\s*:\\s*(\\d+)/);\n if (match) return parseInt(match[1], 10);\n } catch {\n // skip\n }\n }\n return null;\n}\n\n/**\n * Parse port from Rails config/puma.rb.\n * Common forms:\n * port ENV.fetch(\"PORT\") { 3000 }\n * port ENV.fetch(\"PORT\", 3000)\n * port 3000\n */\nfunction parseRubyPort(installDir: string): number | null {\n try {\n const configPath = join(installDir, 'config', 'puma.rb');\n const content = fs.readFileSync(configPath, 'utf-8');\n const blockFetch = content.match(/port\\s+ENV\\.fetch\\([^)]*\\)\\s*\\{\\s*(\\d+)\\s*\\}/);\n if (blockFetch) return parseInt(blockFetch[1], 10);\n const argFetch = content.match(/port\\s+ENV\\.fetch\\([^,]+,\\s*(\\d+)\\)/);\n if (argFetch) return parseInt(argFetch[1], 10);\n const literal = content.match(/^\\s*port\\s+(\\d+)/m);\n if (literal) return parseInt(literal[1], 10);\n } catch {\n // skip\n }\n return null;\n}\n\n/**\n * Detect the dev server port for a framework.\n * Checks config files first, falls back to framework default.\n */\nexport function detectPort(integration: Integration, installDir: string): number {\n let detectedPort: number | null = null;\n\n switch (integration) {\n case 'nextjs':\n detectedPort = parseNextConfigPort(installDir);\n break;\n\n case 'tanstack-start':\n // Modern @tanstack/react-start is Vite-based (port in vite.config.ts);\n // legacy Vinxi projects use app.config.ts. Try Vite first.\n detectedPort = parseViteConfigPortFromDir(installDir) ?? parseTanStackPort(installDir);\n break;\n\n case 'react':\n case 'react-router':\n case 'vanilla-js':\n // Vite-based frameworks\n detectedPort = parseViteConfigPortFromDir(installDir);\n break;\n\n case 'dotnet':\n detectedPort = parseDotnetPort(installDir);\n break;\n\n case 'elixir':\n detectedPort = parseElixirPort(installDir);\n break;\n\n case 'kotlin':\n detectedPort = parseKotlinPort(installDir);\n break;\n\n case 'ruby':\n detectedPort = parseRubyPort(installDir);\n break;\n }\n\n return detectedPort ?? getDefaultPort(integration);\n}\n"]}
|
|
@@ -18,6 +18,34 @@ export interface CliPreferences {
|
|
|
18
18
|
/** ISO timestamp the first-run notice was shown — written in Phase 2 only. */
|
|
19
19
|
noticeShownAt?: string;
|
|
20
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Legacy MCP-install onboarding state. The MCP offer folded into `setup` (see
|
|
23
|
+
* commands/setup.ts); these fields are retained for back-compat so a user who
|
|
24
|
+
* declined the old standalone MCP offer is still treated as setup-declined
|
|
25
|
+
* (see isSetupDeclined). Kept in this plain-JSON store alongside telemetry.
|
|
26
|
+
*/
|
|
27
|
+
mcp?: {
|
|
28
|
+
/**
|
|
29
|
+
* true => the user explicitly declined an automatic MCP install offer. The
|
|
30
|
+
* decline is absolute: no automatic surface (prompt or banner) asks again;
|
|
31
|
+
* `workos mcp install` stays available manually.
|
|
32
|
+
*/
|
|
33
|
+
promptDeclined?: boolean;
|
|
34
|
+
/** ISO timestamp the one-time MCP banner was shown. */
|
|
35
|
+
bannerShownAt?: string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Consolidated agent-setup onboarding state (skills + MCP). Owned by
|
|
39
|
+
* commands/setup.ts. `declined` is absolute for the automatic surfaces
|
|
40
|
+
* (post-login / post-install); `workos setup` stays available manually.
|
|
41
|
+
* Legacy `mcp.promptDeclined` is treated as an implicit setup-decline on read
|
|
42
|
+
* (see isSetupDeclined) so users who already declined MCP are never re-asked.
|
|
43
|
+
*/
|
|
44
|
+
setup?: {
|
|
45
|
+
declined?: boolean;
|
|
46
|
+
/** ISO timestamp the user completed a setup run. */
|
|
47
|
+
completedAt?: string;
|
|
48
|
+
};
|
|
21
49
|
}
|
|
22
50
|
/** Effective source of the resolved telemetry-enabled decision. */
|
|
23
51
|
export type TelemetrySource = 'env' | 'preference' | 'default';
|
|
@@ -56,6 +84,20 @@ export declare function isNoticeShown(): boolean;
|
|
|
56
84
|
* telemetry-notice.ts swallows it so a read-only FS never blocks a command.
|
|
57
85
|
*/
|
|
58
86
|
export declare function markNoticeShown(): void;
|
|
87
|
+
/**
|
|
88
|
+
* Whether an automatic setup offer should be suppressed. Absolute once the user
|
|
89
|
+
* declines. Treats the legacy `mcp.promptDeclined` flag as an implicit decline
|
|
90
|
+
* so anyone who declined the old MCP offer is never re-prompted by the new flow.
|
|
91
|
+
*/
|
|
92
|
+
export declare function isSetupDeclined(): boolean;
|
|
93
|
+
/** Whether the user has completed a setup run (ever). */
|
|
94
|
+
export declare function isSetupCompleted(): boolean;
|
|
95
|
+
/** Persist an explicit setup decline. Throws on write failure (see savePreferences). */
|
|
96
|
+
export declare function recordSetupDeclined(): void;
|
|
97
|
+
/** Persist a completed setup run, stamping the current time. */
|
|
98
|
+
export declare function recordSetupCompleted(): void;
|
|
99
|
+
/** Clear the setup decline (new + legacy) so automatic offers resume. For `workos setup --reset`. */
|
|
100
|
+
export declare function clearSetupDecline(): void;
|
|
59
101
|
/**
|
|
60
102
|
* Tri-state env override for telemetry.
|
|
61
103
|
*
|
package/dist/lib/preferences.js
CHANGED
|
@@ -97,11 +97,15 @@ export function savePreferences(next) {
|
|
|
97
97
|
catch {
|
|
98
98
|
// No existing file (or unreadable) — start from empty.
|
|
99
99
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
// Shallow-merge each known nested group (one level) so writing one field
|
|
101
|
+
// (e.g. setup.completedAt) never clobbers a sibling written at a different
|
|
102
|
+
// time (e.g. setup.declined). A new nested group is one entry in this list.
|
|
103
|
+
const merged = { ...current, ...next };
|
|
104
|
+
for (const key of ['telemetry', 'mcp', 'setup']) {
|
|
105
|
+
if (current[key] || next[key]) {
|
|
106
|
+
Object.assign(merged, { [key]: { ...current[key], ...next[key] } });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
105
109
|
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
106
110
|
fs.writeFileSync(filePath, JSON.stringify(merged), { encoding: 'utf8', mode: 0o600 });
|
|
107
111
|
cached = merged;
|
|
@@ -127,6 +131,31 @@ export function isNoticeShown() {
|
|
|
127
131
|
export function markNoticeShown() {
|
|
128
132
|
savePreferences({ telemetry: { noticeShownAt: new Date().toISOString() } });
|
|
129
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Whether an automatic setup offer should be suppressed. Absolute once the user
|
|
136
|
+
* declines. Treats the legacy `mcp.promptDeclined` flag as an implicit decline
|
|
137
|
+
* so anyone who declined the old MCP offer is never re-prompted by the new flow.
|
|
138
|
+
*/
|
|
139
|
+
export function isSetupDeclined() {
|
|
140
|
+
const prefs = getPreferences();
|
|
141
|
+
return prefs.setup?.declined === true || prefs.mcp?.promptDeclined === true;
|
|
142
|
+
}
|
|
143
|
+
/** Whether the user has completed a setup run (ever). */
|
|
144
|
+
export function isSetupCompleted() {
|
|
145
|
+
return !!getPreferences().setup?.completedAt;
|
|
146
|
+
}
|
|
147
|
+
/** Persist an explicit setup decline. Throws on write failure (see savePreferences). */
|
|
148
|
+
export function recordSetupDeclined() {
|
|
149
|
+
savePreferences({ setup: { declined: true } });
|
|
150
|
+
}
|
|
151
|
+
/** Persist a completed setup run, stamping the current time. */
|
|
152
|
+
export function recordSetupCompleted() {
|
|
153
|
+
savePreferences({ setup: { completedAt: new Date().toISOString() } });
|
|
154
|
+
}
|
|
155
|
+
/** Clear the setup decline (new + legacy) so automatic offers resume. For `workos setup --reset`. */
|
|
156
|
+
export function clearSetupDecline() {
|
|
157
|
+
savePreferences({ setup: { declined: false }, mcp: { promptDeclined: false } });
|
|
158
|
+
}
|
|
130
159
|
/**
|
|
131
160
|
* Tri-state env override for telemetry.
|
|
132
161
|
*
|