@amplitude/wizard 1.0.0-beta.0 → 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/bin.js +191 -47
  2. package/dist/src/lib/agent-interface.js +10 -22
  3. package/dist/src/lib/agent-runner.js +4 -6
  4. package/dist/src/lib/commandments.js +1 -1
  5. package/dist/src/lib/constants.d.ts +1 -4
  6. package/dist/src/lib/constants.js +9 -8
  7. package/dist/src/lib/feature-flags.d.ts +37 -0
  8. package/dist/src/lib/feature-flags.js +119 -0
  9. package/dist/src/lib/wizard-session.d.ts +16 -0
  10. package/dist/src/lib/wizard-session.js +2 -0
  11. package/dist/src/run.js +1 -1
  12. package/dist/src/steps/add-mcp-server-to-clients/index.js +3 -3
  13. package/dist/src/steps/add-or-update-environment-variables.js +5 -17
  14. package/dist/src/steps/run-prettier.js +1 -1
  15. package/dist/src/steps/upload-environment-variables/index.js +2 -2
  16. package/dist/src/ui/tui/App.js +1 -1
  17. package/dist/src/ui/tui/components/ConsoleView.js +17 -6
  18. package/dist/src/ui/tui/components/TitleBar.d.ts +3 -1
  19. package/dist/src/ui/tui/components/TitleBar.js +17 -6
  20. package/dist/src/ui/tui/console-commands.d.ts +5 -2
  21. package/dist/src/ui/tui/console-commands.js +14 -5
  22. package/dist/src/ui/tui/screens/AuthScreen.d.ts +2 -1
  23. package/dist/src/ui/tui/screens/AuthScreen.js +166 -26
  24. package/dist/src/ui/tui/screens/ChecklistScreen.js +1 -1
  25. package/dist/src/ui/tui/screens/DataIngestionCheckScreen.js +13 -2
  26. package/dist/src/ui/tui/screens/IntroScreen.js +2 -2
  27. package/dist/src/ui/tui/screens/McpScreen.js +42 -27
  28. package/dist/src/ui/tui/screens/OutroScreen.js +1 -2
  29. package/dist/src/ui/tui/screens/SlackScreen.d.ts +0 -5
  30. package/dist/src/ui/tui/screens/SlackScreen.js +1 -11
  31. package/dist/src/ui/tui/store.d.ts +20 -0
  32. package/dist/src/ui/tui/store.js +68 -19
  33. package/dist/src/utils/analytics.d.ts +45 -3
  34. package/dist/src/utils/analytics.js +118 -47
  35. package/dist/src/utils/oauth.js +1 -1
  36. package/dist/src/utils/setup-utils.d.ts +11 -0
  37. package/dist/src/utils/setup-utils.js +81 -4
  38. package/dist/src/utils/shell-completions.d.ts +2 -2
  39. package/dist/src/utils/shell-completions.js +8 -1
  40. package/dist/src/utils/track-wizard-feedback.d.ts +5 -0
  41. package/dist/src/utils/track-wizard-feedback.js +25 -0
  42. package/package.json +13 -13
  43. package/dist/package.json +0 -144
@@ -6,7 +6,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
6
6
  * 1. OAuth waiting — spinner + login URL while browser auth happens
7
7
  * 2. Org selection — picker if the user belongs to multiple orgs
8
8
  * 3. Workspace selection — picker if the org has multiple workspaces
9
- * 4. API key entry text input for the Amplitude analytics write key
9
+ * 4. Project selectionpicker if the workspace has multiple environments
10
+ * 5. API key entry — text input (only if no project key could be resolved)
10
11
  *
11
12
  * The screen drives itself from session.pendingOrgs + session.credentials.
12
13
  * When credentials are set the router resolves past this screen.
@@ -16,54 +17,184 @@ import { useState, useEffect, useSyncExternalStore } from 'react';
16
17
  import { TextInput } from '@inkjs/ui';
17
18
  import { LoadingBox, PickerMenu } from '../primitives/index.js';
18
19
  import { Colors } from '../styles.js';
19
- import { DEFAULT_HOST_URL } from '../../../lib/constants.js';
20
+ import { DEFAULT_HOST_URL, } from '../../../lib/constants.js';
20
21
  import { analytics } from '../../../utils/analytics.js';
22
+ /**
23
+ * Returns the environments with usable API keys from a workspace, sorted by rank.
24
+ */
25
+ function getSelectableEnvironments(workspace) {
26
+ if (!workspace?.environments)
27
+ return [];
28
+ return workspace.environments
29
+ .filter((env) => env.app?.apiKey)
30
+ .sort((a, b) => a.rank - b.rank);
31
+ }
21
32
  export const AuthScreen = ({ store }) => {
22
33
  useSyncExternalStore((cb) => store.subscribe(cb), () => store.getSnapshot());
23
34
  const { session } = store;
24
35
  // Local step state — which org the user has selected in this render session
25
36
  const [selectedOrg, setSelectedOrg] = useState(null);
37
+ // Track the selected workspace locally so we can access its environments
38
+ const [selectedWorkspace, setSelectedWorkspace] = useState(null);
39
+ const [selectedEnv, setSelectedEnv] = useState(null);
26
40
  const [apiKeyError, setApiKeyError] = useState('');
27
41
  const [savedKeySource, setSavedKeySource] = useState(null);
28
42
  const pendingOrgs = session.pendingOrgs;
29
- // Auto-select the org when there's only one
30
- const effectiveOrg = selectedOrg ?? (pendingOrgs?.length === 1 ? pendingOrgs[0] : null);
31
- // Auto-select workspace when org has only one
43
+ // Resolve org: user-picked > single-org auto-select > pre-populated from session
44
+ const prePopulatedOrg = session.selectedOrgId && pendingOrgs
45
+ ? pendingOrgs.find((o) => o.id === session.selectedOrgId) ?? null
46
+ : null;
47
+ const effectiveOrg = selectedOrg ??
48
+ (pendingOrgs?.length === 1 ? pendingOrgs[0] : null) ??
49
+ prePopulatedOrg;
50
+ // Resolve workspace: user-picked > single-workspace auto-select > pre-populated from session
32
51
  const singleWorkspace = effectiveOrg?.workspaces.length === 1 ? effectiveOrg.workspaces[0] : null;
52
+ const prePopulatedWorkspace = session.selectedWorkspaceId && effectiveOrg
53
+ ? effectiveOrg.workspaces.find((ws) => ws.id === session.selectedWorkspaceId) ?? null
54
+ : null;
55
+ const effectiveWorkspace = selectedWorkspace ?? singleWorkspace ?? prePopulatedWorkspace ?? null;
33
56
  useEffect(() => {
34
- if (effectiveOrg && singleWorkspace && !session.selectedWorkspaceId) {
35
- store.setOrgAndWorkspace(effectiveOrg, singleWorkspace, session.installDir);
57
+ if (effectiveOrg && effectiveWorkspace && !session.selectedWorkspaceId) {
58
+ store.setOrgAndWorkspace(effectiveOrg, effectiveWorkspace, session.installDir);
36
59
  }
37
- }, [effectiveOrg?.id, singleWorkspace?.id, session.selectedWorkspaceId]);
38
- const workspaceChosen = session.selectedWorkspaceId !== null ||
39
- (effectiveOrg !== null && effectiveOrg.workspaces.length === 1);
40
- // Auto-advance past API key step if a saved key exists for this project
60
+ }, [effectiveOrg?.id, effectiveWorkspace?.id, session.selectedWorkspaceId]);
61
+ // workspaceChosen requires the local workspace object (effectiveWorkspace)
62
+ // rather than just session.selectedWorkspaceId, because we need the
63
+ // environments list to drive the project picker. When selectedWorkspaceId is
64
+ // pre-populated from ampli.json but no workspace object exists yet,
65
+ // selectableEnvs would be empty and the picker would be bypassed.
66
+ const workspaceChosen = effectiveWorkspace !== null;
67
+ // Environments available in the selected workspace
68
+ const selectableEnvs = getSelectableEnvironments(effectiveWorkspace);
69
+ const hasMultipleEnvs = selectableEnvs.length > 1;
70
+ // Auto-select the environment when there's only one with an API key
41
71
  useEffect(() => {
42
- if (!workspaceChosen || session.credentials !== null)
72
+ if (workspaceChosen && !selectedEnv && selectableEnvs.length === 1) {
73
+ setSelectedEnv(selectableEnvs[0]);
74
+ store.setSelectedProjectName(selectableEnvs[0].name);
75
+ }
76
+ }, [workspaceChosen, selectedEnv, selectableEnvs.length]);
77
+ // True once the user has picked an environment (or it was auto-selected),
78
+ // or there are no environments to pick from (falls through to manual key entry).
79
+ const envResolved = selectedEnv !== null || selectableEnvs.length === 0;
80
+ // Resolve API key from local storage, selected environment, or backend fetch.
81
+ useEffect(() => {
82
+ if (!workspaceChosen || !envResolved || session.credentials !== null)
43
83
  return;
44
- void import('../../../utils/api-key-store.js').then(({ readApiKeyWithSource }) => {
45
- const result = readApiKeyWithSource(session.installDir);
46
- if (result) {
47
- setSavedKeySource(result.source);
48
- analytics.wizardCapture('api key submitted', {
49
- key_source: result.source,
84
+ let cancelled = false;
85
+ void (async () => {
86
+ const s = store.session;
87
+ if (s.credentials !== null)
88
+ return;
89
+ const { readApiKeyWithSource, persistApiKey } = await import('../../../utils/api-key-store.js');
90
+ if (cancelled)
91
+ return;
92
+ // 1. Check local storage first
93
+ const local = readApiKeyWithSource(s.installDir);
94
+ if (local) {
95
+ setSavedKeySource(local.source);
96
+ analytics.wizardCapture('API Key Submitted', {
97
+ key_source: local.source,
50
98
  });
51
99
  store.setCredentials({
52
- accessToken: session.pendingAuthAccessToken ?? '',
53
- idToken: session.pendingAuthIdToken ?? undefined,
54
- projectApiKey: result.key,
100
+ accessToken: s.pendingAuthAccessToken ?? '',
101
+ idToken: s.pendingAuthIdToken ?? undefined,
102
+ projectApiKey: local.key,
55
103
  host: DEFAULT_HOST_URL,
56
104
  projectId: 0,
57
105
  });
58
106
  store.setProjectHasData(false);
107
+ store.setApiKeyNotice(null);
108
+ return;
59
109
  }
60
- });
61
- }, [workspaceChosen, session.credentials]);
110
+ // 2. Use the API key from the selected environment
111
+ if (selectedEnv?.app?.apiKey) {
112
+ const apiKey = selectedEnv.app.apiKey;
113
+ const zone = (s.region ??
114
+ s.pendingAuthCloudRegion ??
115
+ 'us');
116
+ const { getHostFromRegion } = await import('../../../utils/urls.js');
117
+ if (cancelled || store.session.credentials !== null)
118
+ return;
119
+ persistApiKey(apiKey, s.installDir);
120
+ analytics.wizardCapture('API Key Submitted', {
121
+ key_source: 'environment_picker',
122
+ });
123
+ store.setCredentials({
124
+ accessToken: s.pendingAuthAccessToken ?? '',
125
+ idToken: s.pendingAuthIdToken ?? undefined,
126
+ projectApiKey: apiKey,
127
+ host: getHostFromRegion(zone),
128
+ projectId: 0,
129
+ });
130
+ store.setProjectHasData(false);
131
+ store.setApiKeyNotice(null);
132
+ return;
133
+ }
134
+ // 3. Fall back to backend fetch (no environments with keys available)
135
+ const idToken = s.pendingAuthIdToken;
136
+ if (!idToken)
137
+ return;
138
+ const zone = (s.region ??
139
+ s.pendingAuthCloudRegion ??
140
+ 'us');
141
+ const { getAPIKey } = await import('../../../utils/get-api-key.js');
142
+ const { getHostFromRegion } = await import('../../../utils/urls.js');
143
+ const projectApiKey = await getAPIKey({
144
+ installDir: s.installDir,
145
+ idToken,
146
+ zone,
147
+ workspaceId: s.selectedWorkspaceId ?? undefined,
148
+ });
149
+ if (cancelled || store.session.credentials !== null)
150
+ return;
151
+ if (projectApiKey) {
152
+ persistApiKey(projectApiKey, s.installDir);
153
+ analytics.wizardCapture('API Key Submitted', {
154
+ key_source: 'backend_fetch',
155
+ });
156
+ store.setCredentials({
157
+ accessToken: s.pendingAuthAccessToken ?? '',
158
+ idToken: s.pendingAuthIdToken ?? undefined,
159
+ projectApiKey,
160
+ host: getHostFromRegion(zone),
161
+ projectId: 0,
162
+ });
163
+ store.setProjectHasData(false);
164
+ store.setApiKeyNotice(null);
165
+ }
166
+ else {
167
+ store.setApiKeyNotice("Your API key couldn't be fetched automatically. " +
168
+ 'Only organization admins can access project API keys — ' +
169
+ 'if you need one, ask an admin to share it with you.');
170
+ }
171
+ })();
172
+ return () => {
173
+ cancelled = true;
174
+ };
175
+ }, [
176
+ workspaceChosen,
177
+ envResolved,
178
+ selectedEnv,
179
+ session.credentials,
180
+ session.selectedWorkspaceId,
181
+ session.pendingAuthIdToken,
182
+ session.region,
183
+ session.pendingAuthCloudRegion,
184
+ session.installDir,
185
+ ]);
62
186
  const needsOrgPick = pendingOrgs !== null && pendingOrgs.length > 1 && effectiveOrg === null;
63
187
  const needsWorkspacePick = effectiveOrg !== null &&
64
188
  effectiveOrg.workspaces.length > 1 &&
65
- !session.selectedWorkspaceId;
66
- const needsApiKey = effectiveOrg !== null && workspaceChosen && session.credentials === null;
189
+ !selectedWorkspace;
190
+ const needsProjectPick = workspaceChosen && hasMultipleEnvs && !selectedEnv;
191
+ const needsApiKey = effectiveOrg !== null &&
192
+ workspaceChosen &&
193
+ envResolved &&
194
+ session.credentials === null &&
195
+ // Only show manual input if there's no selected env with a key
196
+ // (either no envs available, or the env had no key)
197
+ !selectedEnv?.app?.apiKey;
67
198
  const handleApiKeySubmit = (value) => {
68
199
  const trimmed = value.trim();
69
200
  if (!trimmed) {
@@ -71,9 +202,10 @@ export const AuthScreen = ({ store }) => {
71
202
  return;
72
203
  }
73
204
  setApiKeyError('');
74
- analytics.wizardCapture('api key submitted', {
205
+ analytics.wizardCapture('API Key Submitted', {
75
206
  key_source: 'manual_entry',
76
207
  });
208
+ store.setApiKeyNotice(null);
77
209
  store.setCredentials({
78
210
  accessToken: session.pendingAuthAccessToken ?? '',
79
211
  idToken: session.pendingAuthIdToken ?? undefined,
@@ -100,7 +232,15 @@ export const AuthScreen = ({ store }) => {
100
232
  value: ws,
101
233
  })), onSelect: (value) => {
102
234
  const ws = Array.isArray(value) ? value[0] : value;
235
+ setSelectedWorkspace(ws);
103
236
  store.setOrgAndWorkspace(effectiveOrg, ws, session.installDir);
237
+ } })] })), needsProjectPick && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: Colors.muted, children: "Select a project:" }), _jsx(PickerMenu, { options: selectableEnvs.map((env) => ({
238
+ label: env.name,
239
+ value: env,
240
+ })), onSelect: (value) => {
241
+ const env = Array.isArray(value) ? value[0] : value;
242
+ setSelectedEnv(env);
243
+ store.setSelectedProjectName(env.name);
104
244
  } })] })), needsApiKey && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Enter your Amplitude project ", _jsx(Text, { bold: true, children: "API Key" })] }), _jsx(Text, { color: Colors.muted, children: "Amplitude \u2192 Settings \u2192 Projects \u2192 [your project] \u2192 API Keys" }), session.apiKeyNotice && (_jsx(Text, { color: "yellow", children: session.apiKeyNotice }))] }), _jsx(TextInput, { placeholder: "Paste API key here\u2026", onSubmit: handleApiKeySubmit }), apiKeyError && _jsx(Text, { color: "red", children: apiKeyError }), savedKeySource && (_jsxs(Text, { color: "green", children: ['✔ ', savedKeySource === 'keychain'
105
245
  ? 'API key saved to system keychain'
106
246
  : 'API key saved to .env.local'] }))] }))] }));
@@ -56,7 +56,7 @@ export const ChecklistScreen = ({ store }) => {
56
56
  const dashboardUrl = OUTBOUND_URLS.newDashboard(zone, selectedOrgId);
57
57
  function openInBrowser(url, item) {
58
58
  setOpening(item);
59
- analytics.wizardCapture('checklist item opened', { item });
59
+ analytics.wizardCapture('Checklist Step Opened', { item });
60
60
  opn(url, { wait: false })
61
61
  .catch(() => {
62
62
  /* fire-and-forget */
@@ -45,11 +45,23 @@ export const DataIngestionCheckScreen = ({ store, }) => {
45
45
  return;
46
46
  }
47
47
  const zone = (region ?? 'us');
48
+ const idToken = credentials.idToken ?? credentials.accessToken;
48
49
  try {
49
- const status = await fetchProjectActivationStatus(credentials.idToken ?? credentials.accessToken, zone, appId, session.selectedOrgId);
50
+ const status = await fetchProjectActivationStatus(idToken, zone, appId, session.selectedOrgId);
50
51
  logToFile(`[DataIngestionCheck] poll result: hasAnyEvents=${status.hasAnyEvents} hasDetSource=${status.hasDetSource}`);
51
52
  if (status.hasAnyEvents) {
52
53
  store.setDataIngestionConfirmed();
54
+ return;
55
+ }
56
+ // The activation API only checks autocapture events (page_viewed,
57
+ // session_start, session_end). Custom track() calls won't appear there.
58
+ // Fall back to the event catalog which includes all event types.
59
+ if (session.selectedOrgId && session.selectedWorkspaceId) {
60
+ const catalogEvents = await fetchWorkspaceEventTypes(idToken, zone, session.selectedOrgId, session.selectedWorkspaceId);
61
+ logToFile(`[DataIngestionCheck] catalog fallback: ${catalogEvents.length} event types found`);
62
+ if (catalogEvents.length > 0) {
63
+ store.setDataIngestionConfirmed();
64
+ }
53
65
  }
54
66
  }
55
67
  catch (err) {
@@ -57,7 +69,6 @@ export const DataIngestionCheckScreen = ({ store, }) => {
57
69
  setApiUnavailable(true);
58
70
  // Fetch cataloged event types from the data API as a proxy for "events arrived"
59
71
  if (session.selectedOrgId && session.selectedWorkspaceId) {
60
- const idToken = credentials.idToken ?? credentials.accessToken;
61
72
  fetchWorkspaceEventTypes(idToken, zone, session.selectedOrgId, session.selectedWorkspaceId)
62
73
  .then((names) => {
63
74
  setEventTypes(names);
@@ -46,7 +46,7 @@ export const IntroScreen = ({ store }) => {
46
46
  { label: 'Cancel', value: 'cancel' },
47
47
  ], onSelect: (value) => {
48
48
  const choice = Array.isArray(value) ? value[0] : value;
49
- analytics.wizardCapture('intro action', {
49
+ analytics.wizardCapture('Intro Action', {
50
50
  action: choice,
51
51
  integration: session.integration,
52
52
  detected_framework: session.detectedFrameworkLabel,
@@ -75,7 +75,7 @@ const FrameworkPicker = ({ store, onComplete, }) => {
75
75
  }));
76
76
  return (_jsx(PickerMenu, { centered: true, columns: 2, message: "Select your framework", options: options, onSelect: (value) => {
77
77
  const integration = Array.isArray(value) ? value[0] : value;
78
- analytics.wizardCapture('framework manually selected', { integration });
78
+ analytics.wizardCapture('Framework Manually Selected', { integration });
79
79
  void import('../../../lib/registry.js').then(({ FRAMEWORK_REGISTRY }) => {
80
80
  const config = FRAMEWORK_REGISTRY[integration];
81
81
  store.setFrameworkConfig(integration, config);
@@ -17,7 +17,7 @@ import { useSyncExternalStore } from 'react';
17
17
  import { McpOutcome, RunPhase } from '../store.js';
18
18
  import { ConfirmationInput, PickerMenu } from '../primitives/index.js';
19
19
  import { Colors } from '../styles.js';
20
- import { analytics } from '../../../utils/analytics.js';
20
+ import { analytics, captureWizardError } from '../../../utils/analytics.js';
21
21
  var Phase;
22
22
  (function (Phase) {
23
23
  Phase["Detecting"] = "detecting";
@@ -41,22 +41,25 @@ const markDone = (store, outcome, clients = [], standalone = false, onComplete)
41
41
  export const McpScreen = ({ store, installer, mode = 'install', standalone = false, onComplete, }) => {
42
42
  useSyncExternalStore((cb) => store.subscribe(cb), () => store.getSnapshot());
43
43
  const isRemove = mode === 'remove';
44
- const { runPhase, amplitudePreDetected } = store.session;
44
+ const { runPhase, amplitudePreDetected, amplitudePreDetectedChoicePending } = store.session;
45
45
  const dataSetupComplete = runPhase === RunPhase.Completed;
46
46
  const [phase, setPhase] = useState(Phase.Detecting);
47
47
  const [clients, setClients] = useState([]);
48
48
  const [resultClients, setResultClients] = useState([]);
49
49
  useEffect(() => {
50
+ if (amplitudePreDetectedChoicePending) {
51
+ return;
52
+ }
50
53
  void (async () => {
51
54
  try {
52
55
  const detected = await installer.detectClients();
53
56
  if (detected.length === 0) {
54
- analytics.wizardCapture('mcp no clients detected', { mode });
57
+ analytics.wizardCapture('MCP No Clients Detected', { mode });
55
58
  setPhase(Phase.None);
56
59
  setTimeout(() => markDone(store, McpOutcome.NoClients, [], standalone, onComplete), 1500);
57
60
  }
58
61
  else {
59
- analytics.wizardCapture('mcp clients detected', {
62
+ analytics.wizardCapture('MCP Clients Detected', {
60
63
  mode,
61
64
  clients: detected.map((c) => c.name),
62
65
  count: detected.length,
@@ -66,31 +69,31 @@ export const McpScreen = ({ store, installer, mode = 'install', standalone = fal
66
69
  }
67
70
  }
68
71
  catch {
69
- analytics.wizardCapture('mcp detection failed', { mode });
72
+ captureWizardError('MCP Client Detection', 'Editor client detection failed', 'McpScreen', { mode });
70
73
  setPhase(Phase.None);
71
74
  setTimeout(() => markDone(store, McpOutcome.Failed, [], standalone, onComplete), 1500);
72
75
  }
73
76
  })();
74
- }, [installer]);
77
+ }, [installer, amplitudePreDetectedChoicePending]);
75
78
  const handleConfirm = () => {
76
79
  if (isRemove) {
77
- analytics.wizardCapture('mcp remove confirmed');
80
+ analytics.wizardCapture('MCP Remove Confirmed');
78
81
  void doRemove();
79
82
  }
80
83
  else if (clients.length === 1) {
81
84
  const names = clients.map((c) => c.name);
82
- analytics.wizardCapture('mcp install confirmed', { clients: names });
85
+ analytics.wizardCapture('MCP Install Confirmed', { clients: names });
83
86
  void doInstall(names);
84
87
  }
85
88
  else {
86
- analytics.wizardCapture('mcp client picker shown', {
89
+ analytics.wizardCapture('MCP Client Picker Shown', {
87
90
  available_clients: clients.map((c) => c.name),
88
91
  });
89
92
  setPhase(Phase.Pick);
90
93
  }
91
94
  };
92
95
  const handleSkip = () => {
93
- analytics.wizardCapture('mcp skipped', { mode });
96
+ analytics.wizardCapture('MCP Skipped', { mode });
94
97
  markDone(store, McpOutcome.Skipped, [], standalone, onComplete);
95
98
  };
96
99
  const doInstall = async (names) => {
@@ -104,7 +107,7 @@ export const McpScreen = ({ store, installer, mode = 'install', standalone = fal
104
107
  setResultClients([]);
105
108
  }
106
109
  const failed = names.filter((n) => !result.includes(n));
107
- analytics.wizardCapture('mcp install complete', {
110
+ analytics.wizardCapture('MCP Install Complete', {
108
111
  installed: result,
109
112
  failed,
110
113
  attempted: names,
@@ -123,26 +126,38 @@ export const McpScreen = ({ store, installer, mode = 'install', standalone = fal
123
126
  catch {
124
127
  setResultClients([]);
125
128
  }
126
- analytics.wizardCapture('mcp remove complete', { removed: result });
129
+ analytics.wizardCapture('MCP Remove Complete', { removed: result });
127
130
  setPhase(Phase.Done);
128
131
  const outcome = result.length > 0 ? McpOutcome.Installed : McpOutcome.Failed;
129
132
  setTimeout(() => markDone(store, outcome, result, standalone, onComplete), 2000);
130
133
  };
131
134
  return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [dataSetupComplete && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "green", bold: true, children: amplitudePreDetected
132
135
  ? '\u2714 Amplitude is already configured in this project!'
133
- : '\u2714 Data setup complete!' }) })), _jsxs(Text, { bold: true, color: Colors.accent, children: ["MCP Server ", isRemove ? 'Removal' : 'Setup'] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [phase === Phase.Detecting && (_jsx(Text, { color: Colors.muted, children: "Detecting supported editors..." })), phase === Phase.None && (_jsxs(Text, { color: Colors.muted, children: ["No ", isRemove ? 'installed' : 'supported', " MCP clients detected. Skipping..."] })), phase === Phase.Ask && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: Colors.muted, children: ["Detected: ", clients.map((c) => c.name).join(', ')] }), _jsx(Box, { marginTop: 1, children: _jsx(ConfirmationInput, { message: isRemove
134
- ? 'Remove the Amplitude MCP server from your editor?'
135
- : 'Install the Amplitude MCP server to your editor?', confirmLabel: isRemove ? 'Remove MCP' : 'Install MCP', cancelLabel: "No thanks", onConfirm: handleConfirm, onCancel: handleSkip }) })] })), phase === Phase.Pick && (_jsx(PickerMenu, { message: "Select editor to install MCP server", options: clients.map((c) => ({
136
- label: c.name,
137
- value: c.name,
138
- })), mode: "multi", onSelect: (selected) => {
139
- const names = Array.isArray(selected) ? selected : [selected];
140
- if (names.length === 0)
141
- return;
142
- analytics.wizardCapture('mcp clients selected', {
143
- selected_clients: names,
144
- available_clients: clients.map((c) => c.name),
145
- });
146
- void doInstall(names);
147
- } })), phase === Phase.Working && (_jsxs(Text, { color: Colors.muted, children: [isRemove ? 'Removing' : 'Installing', " MCP server..."] })), phase === Phase.Done && (_jsx(Box, { flexDirection: "column", children: resultClients.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "green", bold: true, children: ['\u2714', " MCP server", ' ', isRemove ? 'removed from' : 'installed for', ":"] }), resultClients.map((name, i) => (_jsxs(Text, { children: [' ', '\u2022', " ", name] }, i)))] })) : (_jsxs(Text, { color: Colors.muted, children: [isRemove ? 'Removal' : 'Installation', " skipped."] })) }))] })] }));
136
+ : '\u2714 Data setup complete!' }) })), amplitudePreDetectedChoicePending && !isRemove && (_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsx(Text, { color: Colors.muted, children: "The installer skipped the automated setup step because Amplitude is already present. You can continue to editor MCP setup, or run the full setup wizard if you want to review or change the integration." }), _jsx(Box, { marginTop: 1, children: _jsx(PickerMenu, { message: "How would you like to proceed?", options: [
137
+ { label: 'Continue to MCP setup', value: 'continue' },
138
+ {
139
+ label: 'Run setup wizard anyway',
140
+ value: 'wizard',
141
+ },
142
+ ], onSelect: (value) => {
143
+ const runWizard = value === 'wizard';
144
+ analytics.wizardCapture('Amplitude Pre-Detected Choice', {
145
+ run_wizard_anyway: runWizard,
146
+ });
147
+ store.resolvePreDetectedChoice(runWizard);
148
+ } }) })] })), !amplitudePreDetectedChoicePending && (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, color: Colors.accent, children: ["MCP Server ", isRemove ? 'Removal' : 'Setup'] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [phase === Phase.Detecting && (_jsx(Text, { color: Colors.muted, children: "Detecting supported editors..." })), phase === Phase.None && (_jsxs(Text, { color: Colors.muted, children: ["No ", isRemove ? 'installed' : 'supported', " MCP clients detected. Skipping..."] })), phase === Phase.Ask && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: Colors.muted, children: ["Detected: ", clients.map((c) => c.name).join(', ')] }), _jsx(Box, { marginTop: 1, children: _jsx(ConfirmationInput, { message: isRemove
149
+ ? 'Remove the Amplitude MCP server from your editor?'
150
+ : 'Install the Amplitude MCP server to your editor?', confirmLabel: isRemove ? 'Remove MCP' : 'Install MCP', cancelLabel: "No thanks", onConfirm: handleConfirm, onCancel: handleSkip }) })] })), phase === Phase.Pick && (_jsx(PickerMenu, { message: "Select editor to install MCP server", options: clients.map((c) => ({
151
+ label: c.name,
152
+ value: c.name,
153
+ })), mode: "multi", onSelect: (selected) => {
154
+ const names = Array.isArray(selected) ? selected : [selected];
155
+ if (names.length === 0)
156
+ return;
157
+ analytics.wizardCapture('MCP Clients Selected', {
158
+ selected_clients: names,
159
+ available_clients: clients.map((c) => c.name),
160
+ });
161
+ void doInstall(names);
162
+ } })), phase === Phase.Working && (_jsxs(Text, { color: Colors.muted, children: [isRemove ? 'Removing' : 'Installing', " MCP server..."] })), phase === Phase.Done && (_jsx(Box, { flexDirection: "column", children: resultClients.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "green", bold: true, children: ['\u2714', " MCP server", ' ', isRemove ? 'removed from' : 'installed for', ":"] }), resultClients.map((name, i) => (_jsxs(Text, { children: [' ', '\u2022', " ", name] }, i)))] })) : (_jsxs(Text, { color: Colors.muted, children: [isRemove ? 'Removal' : 'Installation', " skipped."] })) }))] })] }))] }));
148
163
  };
@@ -47,7 +47,7 @@ export const OutroScreen = ({ store }) => {
47
47
  { label: 'Exit', value: 'exit' },
48
48
  ], onSelect: (value) => {
49
49
  const choice = Array.isArray(value) ? value[0] : value;
50
- analytics.wizardCapture('outro action', {
50
+ analytics.wizardCapture('Outro Action', {
51
51
  action: choice,
52
52
  outro_kind: outroData.kind,
53
53
  });
@@ -60,7 +60,6 @@ export const OutroScreen = ({ store }) => {
60
60
  opn(url, { wait: false }).catch(() => {
61
61
  /* fire-and-forget */
62
62
  });
63
- process.exit(0);
64
63
  }
65
64
  else {
66
65
  process.exit(0);
@@ -16,10 +16,5 @@ interface SlackScreenProps {
16
16
  /** When provided, called on completion instead of setSlackComplete (overlay mode). */
17
17
  onComplete?: () => void;
18
18
  }
19
- /**
20
- * Build the Amplitude settings URL for Slack connection.
21
- * Uses the org name from the API; falls back to base URL.
22
- */
23
- export declare function slackSettingsUrl(baseUrl: string, orgName: string | null): string;
24
19
  export declare const SlackScreen: ({ store, standalone, onComplete, }: SlackScreenProps) => import("react/jsx-runtime").JSX.Element;
25
20
  export {};
@@ -26,16 +26,6 @@ var Phase;
26
26
  Phase["Waiting"] = "waiting";
27
27
  Phase["Done"] = "done";
28
28
  })(Phase || (Phase = {}));
29
- /**
30
- * Build the Amplitude settings URL for Slack connection.
31
- * Uses the org name from the API; falls back to base URL.
32
- */
33
- export function slackSettingsUrl(baseUrl, orgName) {
34
- if (orgName) {
35
- return `${baseUrl}/analytics/${encodeURIComponent(orgName)}/settings/profile`;
36
- }
37
- return `${baseUrl}/settings/profile`;
38
- }
39
29
  const markDone = (store, outcome, standalone, onComplete) => {
40
30
  if (onComplete) {
41
31
  onComplete();
@@ -78,7 +68,7 @@ export const SlackScreen = ({ store, standalone = false, onComplete, }) => {
78
68
  logToFile(`[SlackScreen] API fetch failed: ${err instanceof Error ? err.message : String(err)}`);
79
69
  });
80
70
  }, []);
81
- const settingsUrl = slackSettingsUrl(OUTBOUND_URLS.overview[(region ?? 'us')], resolvedOrgName);
71
+ const settingsUrl = OUTBOUND_URLS.slackSettings((region ?? 'us'), store.session.selectedOrgId, resolvedOrgName);
82
72
  const handleConnect = () => {
83
73
  setPhase(Phase.Opening);
84
74
  opn(settingsUrl, { wait: false }).catch(() => {
@@ -72,6 +72,8 @@ export declare class WizardStore {
72
72
  private _backupAndFixSettings;
73
73
  /** Pending confirmation or choice prompt from the agent. */
74
74
  private $pendingPrompt;
75
+ /** Resolves when the user picks continue vs run wizard after Amplitude pre-detection. */
76
+ private _preDetectedChoiceResolver;
75
77
  constructor(flow?: Flow);
76
78
  get session(): WizardSession;
77
79
  set session(value: WizardSession);
@@ -94,6 +96,8 @@ export declare class WizardStore {
94
96
  completeSetup(): void;
95
97
  setRunPhase(phase: RunPhase): void;
96
98
  setCredentials(credentials: WizardSession['credentials']): void;
99
+ setApiKeyNotice(notice: string | null): void;
100
+ setSelectedProjectName(name: string | null): void;
97
101
  setFrameworkConfig(integration: WizardSession['integration'], config: WizardSession['frameworkConfig']): void;
98
102
  setDetectionComplete(): void;
99
103
  setDetectedFramework(label: string): void;
@@ -185,9 +189,25 @@ export declare class WizardStore {
185
189
  /**
186
190
  * Enable an additional feature: enqueue it for the stop hook
187
191
  * and set any feature-specific session flags.
192
+ * Respects Amplitude Experiment feature flags — if the corresponding
193
+ * flag is off the feature is silently skipped.
188
194
  */
189
195
  enableFeature(feature: AdditionalFeature): void;
190
196
  setAmplitudePreDetected(): void;
197
+ /**
198
+ * Blocks bin.ts until McpScreen resolves via resolvePreDetectedChoice().
199
+ */
200
+ waitForPreDetectedChoice(): Promise<boolean>;
201
+ /**
202
+ * Called from McpScreen when the user chooses to skip the agent (continue to
203
+ * MCP) or run the full setup wizard anyway.
204
+ */
205
+ resolvePreDetectedChoice(runWizardAnyway: boolean): void;
206
+ /**
207
+ * Undo the pre-detection fast-path so runWizard can run; used when the user
208
+ * opts into the setup agent after Amplitude was already found in the project.
209
+ */
210
+ resetForAgentAfterPreDetected(): void;
191
211
  setMcpComplete(outcome?: McpOutcome, installedClients?: string[]): void;
192
212
  setSlackComplete(outcome?: SlackOutcome): void;
193
213
  setOutroData(data: OutroData): void;