@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.
- package/dist/bin.js +191 -47
- package/dist/src/lib/agent-interface.js +10 -22
- package/dist/src/lib/agent-runner.js +4 -6
- package/dist/src/lib/commandments.js +1 -1
- package/dist/src/lib/constants.d.ts +1 -4
- package/dist/src/lib/constants.js +9 -8
- package/dist/src/lib/feature-flags.d.ts +37 -0
- package/dist/src/lib/feature-flags.js +119 -0
- package/dist/src/lib/wizard-session.d.ts +16 -0
- package/dist/src/lib/wizard-session.js +2 -0
- package/dist/src/run.js +1 -1
- package/dist/src/steps/add-mcp-server-to-clients/index.js +3 -3
- package/dist/src/steps/add-or-update-environment-variables.js +5 -17
- package/dist/src/steps/run-prettier.js +1 -1
- package/dist/src/steps/upload-environment-variables/index.js +2 -2
- package/dist/src/ui/tui/App.js +1 -1
- package/dist/src/ui/tui/components/ConsoleView.js +17 -6
- package/dist/src/ui/tui/components/TitleBar.d.ts +3 -1
- package/dist/src/ui/tui/components/TitleBar.js +17 -6
- package/dist/src/ui/tui/console-commands.d.ts +5 -2
- package/dist/src/ui/tui/console-commands.js +14 -5
- package/dist/src/ui/tui/screens/AuthScreen.d.ts +2 -1
- package/dist/src/ui/tui/screens/AuthScreen.js +166 -26
- package/dist/src/ui/tui/screens/ChecklistScreen.js +1 -1
- package/dist/src/ui/tui/screens/DataIngestionCheckScreen.js +13 -2
- package/dist/src/ui/tui/screens/IntroScreen.js +2 -2
- package/dist/src/ui/tui/screens/McpScreen.js +42 -27
- package/dist/src/ui/tui/screens/OutroScreen.js +1 -2
- package/dist/src/ui/tui/screens/SlackScreen.d.ts +0 -5
- package/dist/src/ui/tui/screens/SlackScreen.js +1 -11
- package/dist/src/ui/tui/store.d.ts +20 -0
- package/dist/src/ui/tui/store.js +68 -19
- package/dist/src/utils/analytics.d.ts +45 -3
- package/dist/src/utils/analytics.js +118 -47
- package/dist/src/utils/oauth.js +1 -1
- package/dist/src/utils/setup-utils.d.ts +11 -0
- package/dist/src/utils/setup-utils.js +81 -4
- package/dist/src/utils/shell-completions.d.ts +2 -2
- package/dist/src/utils/shell-completions.js +8 -1
- package/dist/src/utils/track-wizard-feedback.d.ts +5 -0
- package/dist/src/utils/track-wizard-feedback.js +25 -0
- package/package.json +13 -13
- 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.
|
|
9
|
+
* 4. Project selection — picker 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
|
-
//
|
|
30
|
-
const
|
|
31
|
-
|
|
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 &&
|
|
35
|
-
store.setOrgAndWorkspace(effectiveOrg,
|
|
57
|
+
if (effectiveOrg && effectiveWorkspace && !session.selectedWorkspaceId) {
|
|
58
|
+
store.setOrgAndWorkspace(effectiveOrg, effectiveWorkspace, session.installDir);
|
|
36
59
|
}
|
|
37
|
-
}, [effectiveOrg?.id,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
//
|
|
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 (
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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:
|
|
53
|
-
idToken:
|
|
54
|
-
projectApiKey:
|
|
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
|
-
|
|
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
|
-
!
|
|
66
|
-
const
|
|
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('
|
|
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('
|
|
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(
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
-
|
|
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('
|
|
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('
|
|
85
|
+
analytics.wizardCapture('MCP Install Confirmed', { clients: names });
|
|
83
86
|
void doInstall(names);
|
|
84
87
|
}
|
|
85
88
|
else {
|
|
86
|
-
analytics.wizardCapture('
|
|
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('
|
|
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('
|
|
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('
|
|
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!' }) })),
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
|
|
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('
|
|
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 =
|
|
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;
|