@amplitude/wizard 1.0.0-beta.1 → 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 CHANGED
@@ -201,12 +201,13 @@ void (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
201
201
  // returning users. This skips RegionSelect and Auth without requiring
202
202
  // a persisted OAuth token.
203
203
  const installDir = session.installDir;
204
- const [{ getStoredUser, getStoredToken }, { readAmpliConfig }, { getAPIKey }, { getHostFromRegion }, { logToFile },] = await Promise.all([
204
+ const [{ getStoredUser, getStoredToken }, { readAmpliConfig }, { getAPIKey }, { getHostFromRegion }, { logToFile }, { fetchAmplitudeUser },] = await Promise.all([
205
205
  import('./src/utils/ampli-settings.js'),
206
206
  import('./src/lib/ampli-config.js'),
207
207
  import('./src/utils/get-api-key.js'),
208
208
  import('./src/utils/urls.js'),
209
209
  import('./src/utils/debug.js'),
210
+ import('./src/lib/api.js'),
210
211
  ]);
211
212
  // Zone: prefer a real (non-pending) stored user, fall back to
212
213
  // the Zone field in the project-level ampli.json.
@@ -222,43 +223,108 @@ void (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
222
223
  }
223
224
  // Skip Auth when we have a stored OAuth token — use it to fetch
224
225
  // (or look up) the project API key, then pre-populate credentials.
226
+ // When the workspace has multiple environments (projects), defer to
227
+ // AuthScreen so the user can pick which project to instrument.
225
228
  if (zone) {
226
229
  const storedToken = realUser
227
230
  ? getStoredToken(realUser.id, realUser.zone)
228
231
  : getStoredToken(undefined, zone);
229
232
  if (storedToken) {
230
- logToFile(`[bin] getAPIKey: zone=${zone} hasWorkspaceId=${!!session.selectedWorkspaceId}`);
231
- const projectApiKey = await getAPIKey({
232
- installDir,
233
- idToken: storedToken.idToken,
234
- zone,
235
- workspaceId: session.selectedWorkspaceId ?? undefined,
236
- });
237
- if (projectApiKey) {
238
- logToFile('[bin] getAPIKey: resolved project API key');
239
- (0, api_key_store_1.persistApiKey)(projectApiKey, installDir);
233
+ // Check local storage first — if a key is already persisted
234
+ // for this install dir, use it without fetching user data.
235
+ const { readApiKeyWithSource } = await import('./src/utils/api-key-store.js');
236
+ const localKey = readApiKeyWithSource(installDir);
237
+ if (localKey) {
238
+ logToFile('[bin] using locally stored API key');
240
239
  session.credentials = {
241
240
  accessToken: storedToken.idToken,
242
241
  idToken: storedToken.idToken,
243
- projectApiKey,
242
+ projectApiKey: localKey.key,
244
243
  host: getHostFromRegion(zone),
245
244
  projectId: 0,
246
245
  };
247
- // Pre-populate activationLevel so DataSetup is also skipped,
248
- // giving a single wipe from Intro → Run/Setup.
249
- // DataSetup would set 'none' anyway (projectId=0 prevents the
250
- // real check), so this is equivalent — just earlier.
251
246
  session.activationLevel = 'none';
252
247
  session.projectHasData = false;
253
248
  }
254
249
  else {
255
- logToFile('[bin] getAPIKey: returned null showing apiKeyNotice');
256
- // Region is already pre-populated above; prompt for the
257
- // key manually with a hint about org-admin permissions.
258
- session.apiKeyNotice =
259
- "Your API key couldn't be fetched automatically. " +
260
- 'Only organization admins can access project API keys — ' +
261
- 'if you need one, ask an admin to share it with you.';
250
+ // Fetch user data to check how many environments are available.
251
+ const { fetchAmplitudeUser } = await import('./src/lib/api.js');
252
+ try {
253
+ const userInfo = await fetchAmplitudeUser(storedToken.idToken, zone);
254
+ const workspaceId = session.selectedWorkspaceId ?? undefined;
255
+ // Find the relevant workspace and its environments
256
+ let envsWithKey = [];
257
+ for (const org of userInfo.orgs) {
258
+ const ws = workspaceId
259
+ ? org.workspaces.find((w) => w.id === workspaceId)
260
+ : org.workspaces[0];
261
+ if (ws?.environments) {
262
+ envsWithKey = ws.environments
263
+ .filter((env) => env.app?.apiKey)
264
+ .sort((a, b) => a.rank - b.rank);
265
+ break;
266
+ }
267
+ }
268
+ if (envsWithKey.length === 1) {
269
+ // Single environment — auto-select as before
270
+ const apiKey = envsWithKey[0].app.apiKey;
271
+ session.selectedProjectName = envsWithKey[0].name;
272
+ logToFile('[bin] single environment — auto-selecting API key');
273
+ (0, api_key_store_1.persistApiKey)(apiKey, installDir);
274
+ session.credentials = {
275
+ accessToken: storedToken.idToken,
276
+ idToken: storedToken.idToken,
277
+ projectApiKey: apiKey,
278
+ host: getHostFromRegion(zone),
279
+ projectId: 0,
280
+ };
281
+ session.activationLevel = 'none';
282
+ session.projectHasData = false;
283
+ }
284
+ else if (envsWithKey.length > 1) {
285
+ // Multiple environments — show the project picker via
286
+ // AuthScreen instead of auto-selecting.
287
+ logToFile(`[bin] ${envsWithKey.length} environments found — deferring to project picker`);
288
+ session.pendingOrgs = userInfo.orgs;
289
+ session.pendingAuthIdToken = storedToken.idToken;
290
+ session.pendingAuthAccessToken = storedToken.idToken;
291
+ }
292
+ else {
293
+ logToFile('[bin] no environments with API keys — showing apiKeyNotice');
294
+ session.apiKeyNotice =
295
+ "Your API key couldn't be fetched automatically. " +
296
+ 'Only organization admins can access project API keys — ' +
297
+ 'if you need one, ask an admin to share it with you.';
298
+ }
299
+ }
300
+ catch (err) {
301
+ logToFile(`[bin] fetchAmplitudeUser failed: ${err instanceof Error ? err.message : 'unknown'}`);
302
+ // Fall back to getAPIKey for backward compatibility
303
+ const projectApiKey = await getAPIKey({
304
+ installDir,
305
+ idToken: storedToken.idToken,
306
+ zone,
307
+ workspaceId: session.selectedWorkspaceId ?? undefined,
308
+ });
309
+ if (projectApiKey) {
310
+ (0, api_key_store_1.persistApiKey)(projectApiKey, installDir);
311
+ session.credentials = {
312
+ accessToken: storedToken.idToken,
313
+ idToken: storedToken.idToken,
314
+ projectApiKey,
315
+ host: getHostFromRegion(zone),
316
+ projectId: 0,
317
+ };
318
+ session.activationLevel = 'none';
319
+ session.projectHasData = false;
320
+ }
321
+ else {
322
+ session.apiKeyNotice =
323
+ "Your API key couldn't be fetched automatically. " +
324
+ 'Only organization admins can access project API keys — ' +
325
+ 'if you need one, ask an admin to share it with you.';
326
+ }
327
+ }
262
328
  }
263
329
  }
264
330
  }
@@ -271,8 +337,46 @@ void (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
271
337
  if (projectConfig.ok && projectConfig.config.WorkspaceId) {
272
338
  session.selectedWorkspaceId = projectConfig.config.WorkspaceId;
273
339
  }
340
+ // Resolve org/workspace display names so /whoami shows them.
341
+ // Uses the stored token to fetch user info — fire-and-forget so it
342
+ // doesn't block startup.
343
+ if (zone && session.selectedOrgId) {
344
+ const storedToken = realUser
345
+ ? getStoredToken(realUser.id, realUser.zone)
346
+ : getStoredToken(undefined, zone);
347
+ if (storedToken) {
348
+ fetchAmplitudeUser(storedToken.idToken, zone)
349
+ .then((userInfo) => {
350
+ const org = userInfo.orgs.find((o) => o.id === session.selectedOrgId);
351
+ if (org) {
352
+ session.selectedOrgName = org.name;
353
+ const ws = session.selectedWorkspaceId
354
+ ? org.workspaces.find((w) => w.id === session.selectedWorkspaceId)
355
+ : undefined;
356
+ if (ws) {
357
+ session.selectedWorkspaceName = ws.name;
358
+ }
359
+ // Update the store if it's already been assigned
360
+ if (tui.store.session === session) {
361
+ tui.store.emitChange();
362
+ }
363
+ }
364
+ })
365
+ .catch(() => {
366
+ // Non-fatal — /whoami will just show (none)
367
+ });
368
+ }
369
+ }
274
370
  }
275
371
  tui.store.session = session;
372
+ // Initialize Amplitude Experiment feature flags (non-blocking).
373
+ const { initFeatureFlags } = await import('./src/lib/feature-flags.js');
374
+ await initFeatureFlags().catch(() => {
375
+ // Flag init failure is non-fatal — all flags default to off
376
+ });
377
+ // Apply SDK-level opt-out based on feature flags
378
+ const { analytics } = await import('./src/utils/analytics.js');
379
+ analytics.applyOptOut();
276
380
  const { FRAMEWORK_REGISTRY } = await import('./src/lib/registry.js');
277
381
  const { detectIntegration } = await import('./src/run.js');
278
382
  const installDir = session.installDir ?? process.cwd();
@@ -427,22 +531,27 @@ void (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
427
531
  tui.store.addDiscoveredFeature(DiscoveredFeature.Stripe);
428
532
  }
429
533
  // LLM SDK detection — sourced from Amplitude LLM analytics skill
430
- const LLM_PACKAGES = [
431
- 'openai',
432
- '@anthropic-ai/sdk',
433
- 'ai',
434
- '@ai-sdk/openai',
435
- 'langchain',
436
- '@langchain/openai',
437
- '@langchain/langgraph',
438
- '@google/generative-ai',
439
- '@google/genai',
440
- '@instructor-ai/instructor',
441
- '@mastra/core',
442
- 'portkey-ai',
443
- ];
444
- if (depNames.some((d) => LLM_PACKAGES.includes(d))) {
445
- tui.store.addDiscoveredFeature(DiscoveredFeature.LLM);
534
+ // Gated by the wizard-llm-analytics feature flag.
535
+ const { isFlagEnabled } = await import('./src/lib/feature-flags.js');
536
+ const { FLAG_LLM_ANALYTICS } = await import('./src/lib/feature-flags.js');
537
+ if (isFlagEnabled(FLAG_LLM_ANALYTICS)) {
538
+ const LLM_PACKAGES = [
539
+ 'openai',
540
+ '@anthropic-ai/sdk',
541
+ 'ai',
542
+ '@ai-sdk/openai',
543
+ 'langchain',
544
+ '@langchain/openai',
545
+ '@langchain/langgraph',
546
+ '@google/generative-ai',
547
+ '@google/genai',
548
+ '@instructor-ai/instructor',
549
+ '@mastra/core',
550
+ 'portkey-ai',
551
+ ];
552
+ if (depNames.some((d) => LLM_PACKAGES.includes(d))) {
553
+ tui.store.addDiscoveredFeature(DiscoveredFeature.LLM);
554
+ }
446
555
  }
447
556
  }
448
557
  catch {
@@ -611,6 +720,7 @@ void (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
611
720
  if (!message) {
612
721
  (0, ui_1.getUI)().log.error('Usage: amplitude-wizard feedback <message> or feedback --message <message>');
613
722
  process.exit(1);
723
+ return;
614
724
  }
615
725
  try {
616
726
  const { trackWizardFeedback } = await import('./src/utils/track-wizard-feedback.js');
@@ -77,7 +77,7 @@ export declare const OUTBOUND_URLS: {
77
77
  /** New dashboard — opened from the Checklist screen. */
78
78
  newDashboard: (zone: AmplitudeZone, orgId?: string | null) => string;
79
79
  /** Slack integration settings — opened from the Slack screen. */
80
- slackSettings: (zone: AmplitudeZone, orgName?: string | null) => string;
80
+ slackSettings: (zone: AmplitudeZone, orgId?: string | null, orgName?: string | null) => string;
81
81
  /** Products page — shown in the Outro for sign-up users. */
82
82
  products: (zone: AmplitudeZone) => string;
83
83
  /** SDK overview — opened from the Activation Options screen. */
@@ -108,11 +108,15 @@ exports.OUTBOUND_URLS = {
108
108
  },
109
109
  // ── Post-setup ────────────────────────────────────────────────────────────
110
110
  /** Slack integration settings — opened from the Slack screen. */
111
- slackSettings: (zone, orgName) => {
111
+ slackSettings: (zone, orgId, orgName) => {
112
112
  const base = exports.OUTBOUND_URLS.overview[zone];
113
- return orgName
114
- ? `${base}/analytics/${encodeURIComponent(orgName)}/settings/profile`
115
- : `${base}/settings/profile`;
113
+ if (orgName) {
114
+ return `${base}/analytics/${encodeURIComponent(orgName)}/settings/profile`;
115
+ }
116
+ if (orgId) {
117
+ return `${base}/${orgId}/settings/profile`;
118
+ }
119
+ return `${base}/settings/profile`;
116
120
  },
117
121
  /** Products page — shown in the Outro for sign-up users. */
118
122
  products: (zone) => `${exports.OUTBOUND_URLS.overview[zone]}/products?source=wizard`,
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Feature flags powered by Amplitude Experiment (server-side local evaluation).
3
+ *
4
+ * Flags are fetched once at startup via `initFeatureFlags()` and evaluated
5
+ * synchronously thereafter — no per-check network calls.
6
+ */
7
+ /** Gate for the LLM analytics additional-feature flow. */
8
+ export declare const FLAG_LLM_ANALYTICS = "wizard-llm-analytics";
9
+ /** Gate for agent-level analytics / telemetry instrumented by the wizard. */
10
+ export declare const FLAG_AGENT_ANALYTICS = "wizard-agent-analytics";
11
+ /**
12
+ * Initialize the Experiment local-evaluation client and pre-fetch flag configs.
13
+ * Safe to call multiple times — subsequent calls are no-ops.
14
+ *
15
+ * @param userId Optional user ID for targeted flag evaluation.
16
+ * @param deviceId Optional device ID for targeted flag evaluation.
17
+ */
18
+ export declare function initFeatureFlags(userId?: string, deviceId?: string): Promise<void>;
19
+ /**
20
+ * Evaluate a single feature flag. Returns the string variant value,
21
+ * or `undefined` if the flag is not set / client not initialized.
22
+ */
23
+ export declare function getFlag(flagKey: string): string | undefined;
24
+ /**
25
+ * Check whether a flag is enabled (variant is `'on'` or `'true'`).
26
+ * Returns `false` when the flag is absent or the client is not initialized.
27
+ */
28
+ export declare function isFlagEnabled(flagKey: string): boolean;
29
+ /**
30
+ * Return a snapshot of all evaluated flags (key -> string value).
31
+ */
32
+ export declare function getAllFlags(): Record<string, string>;
33
+ /**
34
+ * Re-evaluate flags for a specific user (e.g. after login).
35
+ * Updates the cached flags in place.
36
+ */
37
+ export declare function refreshFlags(userId?: string, deviceId?: string): Promise<void>;
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ /**
3
+ * Feature flags powered by Amplitude Experiment (server-side local evaluation).
4
+ *
5
+ * Flags are fetched once at startup via `initFeatureFlags()` and evaluated
6
+ * synchronously thereafter — no per-check network calls.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.FLAG_AGENT_ANALYTICS = exports.FLAG_LLM_ANALYTICS = void 0;
10
+ exports.initFeatureFlags = initFeatureFlags;
11
+ exports.getFlag = getFlag;
12
+ exports.isFlagEnabled = isFlagEnabled;
13
+ exports.getAllFlags = getAllFlags;
14
+ exports.refreshFlags = refreshFlags;
15
+ const experiment_node_server_1 = require("@amplitude/experiment-node-server");
16
+ const debug_1 = require("../utils/debug");
17
+ // ── Flag keys ────────────────────────────────────────────────────────
18
+ /** Gate for the LLM analytics additional-feature flow. */
19
+ exports.FLAG_LLM_ANALYTICS = 'wizard-llm-analytics';
20
+ /** Gate for agent-level analytics / telemetry instrumented by the wizard. */
21
+ exports.FLAG_AGENT_ANALYTICS = 'wizard-agent-analytics';
22
+ // ── Deployment key ───────────────────────────────────────────────────
23
+ /**
24
+ * Server deployment key for local evaluation.
25
+ * Override with `AMPLITUDE_EXPERIMENT_DEPLOYMENT_KEY` env var.
26
+ */
27
+ const DEFAULT_DEPLOYMENT_KEY = 'server-YOTsk4MS1RWzLyIf1DmvNDUsdOGqkRdM';
28
+ function resolveDeploymentKey() {
29
+ const fromEnv = process.env.AMPLITUDE_EXPERIMENT_DEPLOYMENT_KEY;
30
+ return (fromEnv ?? DEFAULT_DEPLOYMENT_KEY).trim();
31
+ }
32
+ // ── Singleton client ─────────────────────────────────────────────────
33
+ let client = null;
34
+ let cachedFlags = {};
35
+ /**
36
+ * Initialize the Experiment local-evaluation client and pre-fetch flag configs.
37
+ * Safe to call multiple times — subsequent calls are no-ops.
38
+ *
39
+ * @param userId Optional user ID for targeted flag evaluation.
40
+ * @param deviceId Optional device ID for targeted flag evaluation.
41
+ */
42
+ async function initFeatureFlags(userId, deviceId) {
43
+ if (client)
44
+ return; // already initialized
45
+ const deploymentKey = resolveDeploymentKey();
46
+ if (!deploymentKey) {
47
+ (0, debug_1.debug)('feature-flags: no deployment key — all flags default to off');
48
+ return;
49
+ }
50
+ try {
51
+ client = experiment_node_server_1.Experiment.initializeLocal(deploymentKey);
52
+ await client.start();
53
+ const user = {};
54
+ if (userId)
55
+ user.user_id = userId;
56
+ if (deviceId)
57
+ user.device_id = deviceId;
58
+ const variants = client.evaluateV2(user);
59
+ for (const [key, variant] of Object.entries(variants)) {
60
+ if (variant.value !== undefined && variant.value !== null) {
61
+ cachedFlags[key] = String(variant.value);
62
+ }
63
+ }
64
+ (0, debug_1.debug)('feature-flags: initialized with flags', cachedFlags);
65
+ }
66
+ catch (err) {
67
+ (0, debug_1.debug)('feature-flags: initialization failed, defaulting all flags off', err);
68
+ client = null;
69
+ }
70
+ }
71
+ /**
72
+ * Evaluate a single feature flag. Returns the string variant value,
73
+ * or `undefined` if the flag is not set / client not initialized.
74
+ */
75
+ function getFlag(flagKey) {
76
+ return cachedFlags[flagKey];
77
+ }
78
+ /**
79
+ * Check whether a flag is enabled (variant is `'on'` or `'true'`).
80
+ * Returns `false` when the flag is absent or the client is not initialized.
81
+ */
82
+ function isFlagEnabled(flagKey) {
83
+ const value = cachedFlags[flagKey];
84
+ return value === 'on' || value === 'true';
85
+ }
86
+ /**
87
+ * Return a snapshot of all evaluated flags (key -> string value).
88
+ */
89
+ function getAllFlags() {
90
+ return { ...cachedFlags };
91
+ }
92
+ /**
93
+ * Re-evaluate flags for a specific user (e.g. after login).
94
+ * Updates the cached flags in place.
95
+ */
96
+ async function refreshFlags(userId, deviceId) {
97
+ if (!client)
98
+ return;
99
+ try {
100
+ // Re-fetch flag configs in case they changed
101
+ await client.start();
102
+ const user = {};
103
+ if (userId)
104
+ user.user_id = userId;
105
+ if (deviceId)
106
+ user.device_id = deviceId;
107
+ const variants = client.evaluateV2(user);
108
+ cachedFlags = {};
109
+ for (const [key, variant] of Object.entries(variants)) {
110
+ if (variant.value !== undefined && variant.value !== null) {
111
+ cachedFlags[key] = String(variant.value);
112
+ }
113
+ }
114
+ (0, debug_1.debug)('feature-flags: refreshed flags', cachedFlags);
115
+ }
116
+ catch (err) {
117
+ (0, debug_1.debug)('feature-flags: refresh failed', err);
118
+ }
119
+ }
@@ -132,6 +132,14 @@ export interface WizardSession {
132
132
  workspaces: Array<{
133
133
  id: string;
134
134
  name: string;
135
+ environments?: Array<{
136
+ name: string;
137
+ rank: number;
138
+ app: {
139
+ id: string;
140
+ apiKey?: string | null;
141
+ } | null;
142
+ }> | null;
135
143
  }>;
136
144
  }> | null;
137
145
  /** OAuth id_token held during SUSI account-setup steps. */
@@ -146,6 +154,8 @@ export interface WizardSession {
146
154
  /** Workspace selected during SUSI (written to ampli.json). */
147
155
  selectedWorkspaceId: string | null;
148
156
  selectedWorkspaceName: string | null;
157
+ /** Project/environment selected during SUSI (displayed in TitleBar). */
158
+ selectedProjectName: string | null;
149
159
  /**
150
160
  * Notice shown on the API key entry step of AuthScreen.
151
161
  * Set when auto-fetch fails (e.g. user is not an org admin) so the user
@@ -109,6 +109,7 @@ function buildSession(args) {
109
109
  selectedOrgName: null,
110
110
  selectedWorkspaceId: null,
111
111
  selectedWorkspaceName: null,
112
+ selectedProjectName: null,
112
113
  loginUrl: null,
113
114
  credentials: null,
114
115
  apiKeyNotice: null,
@@ -30,5 +30,5 @@ export const App = ({ store }) => {
30
30
  const contentAreaWidth = Math.max(10, innerWidth - 2);
31
31
  const direction = store.lastNavDirection === 'pop' ? 'right' : 'left';
32
32
  const activeScreen = screens[store.currentScreen] ?? null;
33
- return (_jsx(CommandModeContext.Provider, { value: store.commandMode, children: _jsx(Box, { flexDirection: "column", height: rows, width: columns, alignItems: "center", justifyContent: "flex-start", children: _jsxs(ConsoleView, { store: store, width: width, height: rows, children: [_jsx(TitleBar, { version: store.version, width: innerWidth }), _jsx(Box, { height: 1 }), _jsx(Box, { flexDirection: "column", flexGrow: 1, paddingX: 1, overflow: "hidden", children: _jsx(DissolveTransition, { transitionKey: store.currentScreen, width: contentAreaWidth, height: contentHeight, direction: direction, children: _jsx(ScreenErrorBoundary, { store: store, retryToken: store.screenErrorRetry, children: activeScreen }) }) })] }) }) }));
33
+ return (_jsx(CommandModeContext.Provider, { value: store.commandMode, children: _jsx(Box, { flexDirection: "column", height: rows, width: columns, alignItems: "center", justifyContent: "flex-start", children: _jsxs(ConsoleView, { store: store, width: width, height: rows, children: [_jsx(TitleBar, { version: store.version, width: innerWidth, orgName: store.session.selectedOrgName, projectName: store.session.selectedProjectName }), _jsx(Box, { height: 1 }), _jsx(Box, { flexDirection: "column", flexGrow: 1, paddingX: 1, overflow: "hidden", children: _jsx(DissolveTransition, { transitionKey: store.currentScreen, width: contentAreaWidth, height: contentHeight, direction: direction, children: _jsx(ScreenErrorBoundary, { store: store, retryToken: store.screenErrorRetry, children: activeScreen }) }) })] }) }) }));
34
34
  };
@@ -21,7 +21,7 @@ import { PickerMenu } from '../primitives/PickerMenu.js';
21
21
  import { Colors, Icons } from '../styles.js';
22
22
  import { Overlay } from '../router.js';
23
23
  import { queryConsole, resolveConsoleCredentials, buildSessionContext, } from '../../../lib/console-query.js';
24
- import { COMMANDS, getWhoamiText, getHelpText, parseFeedbackSlashInput, TEST_PROMPT, } from '../console-commands.js';
24
+ import { COMMANDS, getWhoamiText, parseFeedbackSlashInput, TEST_PROMPT, } from '../console-commands.js';
25
25
  import { analytics } from '../../../utils/analytics.js';
26
26
  import { trackWizardFeedback } from '../../../utils/track-wizard-feedback.js';
27
27
  function executeCommand(raw, store) {
@@ -66,12 +66,9 @@ function executeCommand(raw, store) {
66
66
  case '/exit':
67
67
  store.setOutroData({ kind: OutroKind.Cancel, message: 'Exited.' });
68
68
  break;
69
- case '/help':
70
- store.setCommandFeedback(getHelpText());
71
- break;
72
69
  default:
73
70
  if (cmd)
74
- store.setCommandFeedback(`Unknown command: ${cmd}. Type /help.`);
71
+ store.setCommandFeedback(`Unknown command: ${cmd}. Type / to see available commands.`);
75
72
  }
76
73
  }
77
74
  export const ConsoleView = ({ store, width, height, children, }) => {
@@ -1,6 +1,8 @@
1
1
  interface TitleBarProps {
2
2
  version: string;
3
3
  width: number;
4
+ orgName?: string | null;
5
+ projectName?: string | null;
4
6
  }
5
- export declare const TitleBar: ({ version, width }: TitleBarProps) => import("react/jsx-runtime").JSX.Element;
7
+ export declare const TitleBar: ({ version, width, orgName, projectName, }: TitleBarProps) => import("react/jsx-runtime").JSX.Element;
6
8
  export {};
@@ -1,16 +1,27 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { Colors } from '../styles.js';
4
4
  const FEEDBACK = 'Feedback: wizard@amplitude.com ';
5
5
  const FEEDBACK_SHORT = ' wizard@amplitude.com ';
6
- export const TitleBar = ({ version, width }) => {
6
+ export const TitleBar = ({ version, width, orgName, projectName, }) => {
7
7
  const fullTitle = ` Amplitude Wizard v${version}`;
8
- const needShort = width < fullTitle.length + FEEDBACK.length;
8
+ // Build context string from org + project names
9
+ const contextParts = [];
10
+ if (orgName)
11
+ contextParts.push(orgName);
12
+ if (projectName)
13
+ contextParts.push(projectName);
14
+ const contextStr = contextParts.length > 0 ? ` ${contextParts.join(' / ')} ` : '';
15
+ const needShort = width < fullTitle.length + contextStr.length + FEEDBACK.length;
9
16
  const feedback = needShort ? FEEDBACK_SHORT : FEEDBACK;
10
- const title = needShort && fullTitle.length + feedback.length > width
17
+ const title = needShort && fullTitle.length + contextStr.length + feedback.length > width
11
18
  ? ` Wizard v${version}`
12
19
  : fullTitle;
13
- const gap = Math.max(0, width - title.length - feedback.length);
20
+ // If context doesn't fit even with the short title, drop it
21
+ const showContext = contextStr.length > 0 &&
22
+ title.length + contextStr.length + feedback.length <= width;
23
+ const middleText = showContext ? contextStr : '';
24
+ const gap = Math.max(0, width - title.length - middleText.length - feedback.length);
14
25
  const padding = ' '.repeat(gap);
15
- return (_jsx(Box, { width: width, overflow: "hidden", children: _jsxs(Text, { backgroundColor: Colors.accent, color: "white", bold: true, children: [title, padding, feedback] }) }));
26
+ return (_jsxs(Box, { width: width, overflow: "hidden", children: [_jsx(Text, { backgroundColor: Colors.accent, color: "white", bold: true, children: title }), middleText ? (_jsxs(Text, { backgroundColor: Colors.accent, color: "white", dimColor: true, children: [padding, middleText] })) : (_jsx(Text, { backgroundColor: Colors.accent, color: "white", children: padding })), _jsx(Text, { backgroundColor: Colors.accent, color: "white", bold: true, children: feedback })] }));
16
27
  };
@@ -12,8 +12,6 @@ export declare const COMMANDS: {
12
12
  export declare const TEST_PROMPT: string;
13
13
  /** Returns the feedback text for the /whoami command. */
14
14
  export declare function getWhoamiText(session: Pick<WizardSession, 'selectedOrgName' | 'selectedWorkspaceName' | 'region'>): string;
15
- /** Returns the feedback text for the /help command. */
16
- export declare function getHelpText(): string;
17
15
  /**
18
16
  * Parses `/feedback <message>` from a slash command line.
19
17
  * Returns `undefined` if the line is not a feedback command or the message is empty.
@@ -18,7 +18,6 @@ export const COMMANDS = [
18
18
  { cmd: '/test', desc: 'Run a prompt-skill demo (confirm + choose)' },
19
19
  { cmd: '/snake', desc: 'Play Snake' },
20
20
  { cmd: '/exit', desc: 'Exit the wizard' },
21
- { cmd: '/help', desc: 'List available slash commands' },
22
21
  ];
23
22
  export const TEST_PROMPT = 'Demo the wizard prompt tools. ' +
24
23
  'First, use the wizard-tools:confirm tool to ask if I want to continue. ' +
@@ -28,11 +27,6 @@ export const TEST_PROMPT = 'Demo the wizard prompt tools. ' +
28
27
  export function getWhoamiText(session) {
29
28
  return `org: ${session.selectedOrgName ?? '(none)'} workspace: ${session.selectedWorkspaceName ?? '(none)'} region: ${session.region ?? '(none)'}`;
30
29
  }
31
- /** Returns the feedback text for the /help command. */
32
- export function getHelpText() {
33
- const maxCmd = Math.max(...COMMANDS.map((c) => c.cmd.length));
34
- return COMMANDS.map((c) => `${c.cmd.padEnd(maxCmd)} ${c.desc}`).join('\n');
35
- }
36
30
  /**
37
31
  * Parses `/feedback <message>` from a slash command line.
38
32
  * Returns `undefined` if the line is not a feedback command or the message is empty.
@@ -5,7 +5,8 @@
5
5
  * 1. OAuth waiting — spinner + login URL while browser auth happens
6
6
  * 2. Org selection — picker if the user belongs to multiple orgs
7
7
  * 3. Workspace selection — picker if the org has multiple workspaces
8
- * 4. API key entry text input for the Amplitude analytics write key
8
+ * 4. Project selectionpicker if the workspace has multiple environments
9
+ * 5. API key entry — text input (only if no project key could be resolved)
9
10
  *
10
11
  * The screen drives itself from session.pendingOrgs + session.credentials.
11
12
  * When credentials are set the router resolves past this screen.
@@ -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.
@@ -18,29 +19,67 @@ import { LoadingBox, PickerMenu } from '../primitives/index.js';
18
19
  import { Colors } from '../styles.js';
19
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
- // Resolve API key: local storage first, then Amplitude backend (same as bin.ts
41
- // for returning users) once org/workspace are on the session after OAuth.
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
42
71
  useEffect(() => {
43
- 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)
44
83
  return;
45
84
  let cancelled = false;
46
85
  void (async () => {
@@ -50,6 +89,7 @@ export const AuthScreen = ({ store }) => {
50
89
  const { readApiKeyWithSource, persistApiKey } = await import('../../../utils/api-key-store.js');
51
90
  if (cancelled)
52
91
  return;
92
+ // 1. Check local storage first
53
93
  const local = readApiKeyWithSource(s.installDir);
54
94
  if (local) {
55
95
  setSavedKeySource(local.source);
@@ -67,6 +107,31 @@ export const AuthScreen = ({ store }) => {
67
107
  store.setApiKeyNotice(null);
68
108
  return;
69
109
  }
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)
70
135
  const idToken = s.pendingAuthIdToken;
71
136
  if (!idToken)
72
137
  return;
@@ -109,6 +174,8 @@ export const AuthScreen = ({ store }) => {
109
174
  };
110
175
  }, [
111
176
  workspaceChosen,
177
+ envResolved,
178
+ selectedEnv,
112
179
  session.credentials,
113
180
  session.selectedWorkspaceId,
114
181
  session.pendingAuthIdToken,
@@ -119,8 +186,15 @@ export const AuthScreen = ({ store }) => {
119
186
  const needsOrgPick = pendingOrgs !== null && pendingOrgs.length > 1 && effectiveOrg === null;
120
187
  const needsWorkspacePick = effectiveOrg !== null &&
121
188
  effectiveOrg.workspaces.length > 1 &&
122
- !session.selectedWorkspaceId;
123
- 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;
124
198
  const handleApiKeySubmit = (value) => {
125
199
  const trimmed = value.trim();
126
200
  if (!trimmed) {
@@ -158,7 +232,15 @@ export const AuthScreen = ({ store }) => {
158
232
  value: ws,
159
233
  })), onSelect: (value) => {
160
234
  const ws = Array.isArray(value) ? value[0] : value;
235
+ setSelectedWorkspace(ws);
161
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);
162
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'
163
245
  ? 'API key saved to system keychain'
164
246
  : 'API key saved to .env.local'] }))] }))] }));
@@ -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);
@@ -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(() => {
@@ -97,6 +97,7 @@ export declare class WizardStore {
97
97
  setRunPhase(phase: RunPhase): void;
98
98
  setCredentials(credentials: WizardSession['credentials']): void;
99
99
  setApiKeyNotice(notice: string | null): void;
100
+ setSelectedProjectName(name: string | null): void;
100
101
  setFrameworkConfig(integration: WizardSession['integration'], config: WizardSession['frameworkConfig']): void;
101
102
  setDetectionComplete(): void;
102
103
  setDetectedFramework(label: string): void;
@@ -188,6 +189,8 @@ export declare class WizardStore {
188
189
  /**
189
190
  * Enable an additional feature: enqueue it for the stop hook
190
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.
191
194
  */
192
195
  enableFeature(feature: AdditionalFeature): void;
193
196
  setAmplitudePreDetected(): void;
@@ -14,6 +14,7 @@ import { TaskStatus } from '../wizard-ui.js';
14
14
  import { AdditionalFeature, McpOutcome, SlackOutcome, RunPhase, buildSession, } from '../../lib/wizard-session.js';
15
15
  import { WizardRouter, Screen, Overlay, Flow, } from './router.js';
16
16
  import { analytics, sessionPropertiesCompact } from '../../utils/analytics.js';
17
+ import { FLAG_LLM_ANALYTICS } from '../../lib/feature-flags.js';
17
18
  export { TaskStatus, Screen, Overlay, Flow, RunPhase, McpOutcome, SlackOutcome, };
18
19
  export class WizardStore {
19
20
  // ── Internal nanostore atoms ─────────────────────────────────────
@@ -128,6 +129,10 @@ export class WizardStore {
128
129
  this.$session.setKey('apiKeyNotice', notice);
129
130
  this.emitChange();
130
131
  }
132
+ setSelectedProjectName(name) {
133
+ this.$session.setKey('selectedProjectName', name);
134
+ this.emitChange();
135
+ }
131
136
  setFrameworkConfig(integration, config) {
132
137
  this.$session.setKey('integration', integration);
133
138
  this.$session.setKey('frameworkConfig', config);
@@ -405,8 +410,16 @@ export class WizardStore {
405
410
  /**
406
411
  * Enable an additional feature: enqueue it for the stop hook
407
412
  * and set any feature-specific session flags.
413
+ * Respects Amplitude Experiment feature flags — if the corresponding
414
+ * flag is off the feature is silently skipped.
408
415
  */
409
416
  enableFeature(feature) {
417
+ // Gate LLM analytics behind the wizard-llm-analytics feature flag
418
+ if (feature === AdditionalFeature.LLM) {
419
+ if (!analytics.isFeatureFlagEnabled(FLAG_LLM_ANALYTICS)) {
420
+ return;
421
+ }
422
+ }
410
423
  if (!this.session.additionalFeatureQueue.includes(feature)) {
411
424
  this.session.additionalFeatureQueue.push(feature);
412
425
  }
@@ -36,8 +36,26 @@ export declare class Analytics {
36
36
  * per Amplitude quickstart taxonomy guidelines.
37
37
  */
38
38
  wizardCapture(eventName: string, properties?: Record<string, unknown>): void;
39
+ /**
40
+ * Apply feature-flag–based opt-out to the Amplitude SDK.
41
+ * Defaults to ON — only opts out when `wizard-agent-analytics` is explicitly 'off'/'false'.
42
+ */
43
+ applyOptOut(): void;
39
44
  private ensureInitStarted;
45
+ /**
46
+ * Initialize the Amplitude Experiment feature-flag client.
47
+ * Call once early in startup (e.g. after obtaining a user/device ID).
48
+ */
49
+ initFlags(): Promise<void>;
50
+ /**
51
+ * Re-evaluate flags after the user identity changes (e.g. post-login).
52
+ */
53
+ refreshFlags(): Promise<void>;
40
54
  getFeatureFlag(flagKey: string): Promise<string | boolean | undefined>;
55
+ /**
56
+ * Check if a flag is enabled (variant is 'on' or 'true').
57
+ */
58
+ isFeatureFlagEnabled(flagKey: string): boolean;
41
59
  /**
42
60
  * Evaluate all feature flags for the current user at the start of a run.
43
61
  * Result is cached; subsequent calls in the same run return the same map.
@@ -9,6 +9,7 @@ exports.captureWizardError = captureWizardError;
9
9
  const analytics_node_1 = require("@amplitude/analytics-node");
10
10
  const uuid_1 = require("uuid");
11
11
  const debug_1 = require("./debug");
12
+ const feature_flags_1 = require("../lib/feature-flags");
12
13
  const DEFAULT_TELEMETRY_API_KEY = 'e5a2c9bdffe949f7da77e6b481e118fa';
13
14
  /**
14
15
  * Telemetry project API key. Empty or whitespace-only env value means “no key”
@@ -107,6 +108,18 @@ class Analytics {
107
108
  wizardCapture(eventName, properties) {
108
109
  this.capture(`wizard: ${eventName}`, properties);
109
110
  }
111
+ /**
112
+ * Apply feature-flag–based opt-out to the Amplitude SDK.
113
+ * Defaults to ON — only opts out when `wizard-agent-analytics` is explicitly 'off'/'false'.
114
+ */
115
+ applyOptOut() {
116
+ const flagValue = (0, feature_flags_1.getFlag)(feature_flags_1.FLAG_AGENT_ANALYTICS);
117
+ const optOut = flagValue === 'off' || flagValue === 'false';
118
+ this.client.setOptOut(optOut);
119
+ if (optOut) {
120
+ (0, debug_1.debug)('analytics: opted out via wizard-agent-analytics flag');
121
+ }
122
+ }
110
123
  ensureInitStarted() {
111
124
  if (this.initPromise !== null) {
112
125
  return;
@@ -119,10 +132,29 @@ class Analytics {
119
132
  serverUrl: getAmplitudeNodeServerUrl(),
120
133
  }).promise;
121
134
  }
135
+ /**
136
+ * Initialize the Amplitude Experiment feature-flag client.
137
+ * Call once early in startup (e.g. after obtaining a user/device ID).
138
+ */
139
+ async initFlags() {
140
+ await (0, feature_flags_1.initFeatureFlags)(this.distinctId, this.anonymousId);
141
+ }
142
+ /**
143
+ * Re-evaluate flags after the user identity changes (e.g. post-login).
144
+ */
145
+ async refreshFlags() {
146
+ await (0, feature_flags_1.refreshFlags)(this.distinctId, this.anonymousId);
147
+ this.activeFlags = (0, feature_flags_1.getAllFlags)();
148
+ }
122
149
  // eslint-disable-next-line @typescript-eslint/require-await
123
150
  async getFeatureFlag(flagKey) {
124
- (0, debug_1.debug)('getFeatureFlag (noop):', flagKey);
125
- return undefined;
151
+ return (0, feature_flags_1.getFlag)(flagKey);
152
+ }
153
+ /**
154
+ * Check if a flag is enabled (variant is 'on' or 'true').
155
+ */
156
+ isFeatureFlagEnabled(flagKey) {
157
+ return (0, feature_flags_1.isFlagEnabled)(flagKey);
126
158
  }
127
159
  /**
128
160
  * Evaluate all feature flags for the current user at the start of a run.
@@ -134,7 +166,7 @@ class Analytics {
134
166
  if (this.activeFlags !== null) {
135
167
  return this.activeFlags;
136
168
  }
137
- this.activeFlags = {};
169
+ this.activeFlags = (0, feature_flags_1.getAllFlags)();
138
170
  return this.activeFlags;
139
171
  }
140
172
  async shutdown(status) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amplitude/wizard",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.2",
4
4
  "homepage": "https://github.com/amplitude/wizard",
5
5
  "repository": "https://github.com/amplitude/wizard",
6
6
  "description": "The Amplitude wizard helps you to configure your project",
@@ -38,6 +38,7 @@
38
38
  "dependencies": {
39
39
  "@amplitude/analytics-browser": "^2.0.0",
40
40
  "@amplitude/analytics-node": "^1.5.51",
41
+ "@amplitude/experiment-node-server": "^1.13.4",
41
42
  "@anthropic-ai/claude-agent-sdk": "0.2.7",
42
43
  "@inkjs/ui": "^2.0.0",
43
44
  "@inquirer/prompts": "^8.3.0",
@@ -103,7 +104,15 @@
103
104
  "node": ">=20",
104
105
  "npm": ">=3.10.7"
105
106
  },
106
- "packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b",
107
+ "lint-staged": {
108
+ "*.{js,ts,tsx,jsx}": "pnpm fix"
109
+ },
110
+ "author": "Amplitude",
111
+ "license": "MIT",
112
+ "volta": {
113
+ "node": "20.18.2",
114
+ "pnpm": "10.23.0"
115
+ },
107
116
  "scripts": {
108
117
  "clean": "rm -rf ./dist",
109
118
  "prebuild": "pnpm clean",
@@ -125,21 +134,11 @@
125
134
  "proxy:bypass": "cd \"${JS_REPO:-../javascript}\"/server/packages/thunder && WIZARD_PROXY_DEV_BYPASS=1 aws-sso exec --profile us-prod-dev -- pnpm debug",
126
135
  "dev": "pnpm build && pnpm link --global && (trap 'kill 0' EXIT; pnpm proxy & pnpm build:watch)",
127
136
  "test:watch": "vitest",
128
- "prepare": "husky",
129
137
  "flows": "node scripts/render-flows.mjs",
130
138
  "test:bdd": "cucumber-js",
131
139
  "test:bdd:watch": "cucumber-js --retry 0 2>&1 | npx nodemon --watch features --exec cucumber-js",
132
140
  "test:proxy": "vitest run --config vitest.config.proxy.ts",
133
141
  "test:proxy:smoke": "vitest run --config vitest.config.proxy.ts -t 'proxy:smoke'",
134
142
  "skills:refresh": "bash scripts/refresh-instrumentation-skills.sh"
135
- },
136
- "lint-staged": {
137
- "*.{js,ts,tsx,jsx}": "pnpm fix"
138
- },
139
- "author": "Amplitude",
140
- "license": "MIT",
141
- "volta": {
142
- "node": "20.18.2",
143
- "pnpm": "10.23.0"
144
143
  }
145
- }
144
+ }
package/dist/package.json DELETED
@@ -1,145 +0,0 @@
1
- {
2
- "name": "@amplitude/wizard",
3
- "version": "1.0.0-beta.1",
4
- "homepage": "https://github.com/amplitude/wizard",
5
- "repository": "https://github.com/amplitude/wizard",
6
- "description": "The Amplitude wizard helps you to configure your project",
7
- "keywords": [
8
- "amplitude",
9
- "wizard",
10
- "sdk",
11
- "cli",
12
- "project",
13
- "setup",
14
- "install",
15
- "configure"
16
- ],
17
- "bin": {
18
- "amplitude-wizard": "dist/bin.js"
19
- },
20
- "publishConfig": {
21
- "access": "public"
22
- },
23
- "man": [
24
- "man/amplitude-wizard.1"
25
- ],
26
- "files": [
27
- "dist/bin.*",
28
- "dist/src",
29
- "man/",
30
- "package.json",
31
- "README.md"
32
- ],
33
- "main": "dist/index.js",
34
- "typings": "dist/index.d.ts",
35
- "typescript": {
36
- "definition": "dist/index.d.ts"
37
- },
38
- "dependencies": {
39
- "@amplitude/analytics-browser": "^2.0.0",
40
- "@amplitude/analytics-node": "^1.5.51",
41
- "@anthropic-ai/claude-agent-sdk": "0.2.7",
42
- "@inkjs/ui": "^2.0.0",
43
- "@inquirer/prompts": "^8.3.0",
44
- "@langchain/core": "^1.1.33",
45
- "@pavus/snake-game": "^1.1.1",
46
- "axios": "1.13.5",
47
- "chalk": "^2.4.1",
48
- "client-oauth2": "^4.3.3",
49
- "conf": "^15.1.0",
50
- "dotenv": "^16.4.7",
51
- "fast-glob": "^3.3.3",
52
- "glob": "9.3.5",
53
- "ink": "^6.8.0",
54
- "inquirer": "^6.2.0",
55
- "jsonc-parser": "^3.3.1",
56
- "lodash": "^4.17.21",
57
- "magicast": "^0.2.10",
58
- "nanostores": "^1.1.1",
59
- "opn": "^5.4.0",
60
- "pkce-challenge": "^6.0.0",
61
- "randomstring": "^1.3.1",
62
- "react": "^19.2.4",
63
- "read-env": "^1.3.0",
64
- "recast": "^0.23.3",
65
- "semver": "^7.5.3",
66
- "uuid": "^11.1.0",
67
- "xcode": "3.0.1",
68
- "xml-js": "^1.6.11",
69
- "yargs": "^16.2.0",
70
- "zod": "^4.3.6",
71
- "zod-to-json-schema": "^3.25.1"
72
- },
73
- "devDependencies": {
74
- "@anthropic-ai/sdk": "^0.79.0",
75
- "@cucumber/cucumber": "^12.7.0",
76
- "@eslint/js": "^10.0.1",
77
- "@types/chai": "^4.3.17",
78
- "@types/glob": "^7.2.0",
79
- "@types/inquirer": "^0.0.43",
80
- "@types/lodash": "^4.17.15",
81
- "@types/node": "^20.19.37",
82
- "@types/opn": "5.1.0",
83
- "@types/react": "^19.2.14",
84
- "@types/rimraf": "^3.0.2",
85
- "@types/semver": "^7.3.7",
86
- "@types/yargs": "^16.0.9",
87
- "@vitest/coverage-v8": "^4.0.18",
88
- "eslint": "^9.39.4",
89
- "eslint-config-prettier": "^9.1.2",
90
- "globals": "^17.4.0",
91
- "husky": "^9.1.7",
92
- "lint-staged": "^15.5.1",
93
- "msw": "^2.10.4",
94
- "prettier": "^2.8.7",
95
- "rimraf": "^6.1.3",
96
- "ts-node": "^10.9.1",
97
- "tsx": "^4.20.3",
98
- "typescript": "^5.0.4",
99
- "typescript-eslint": "^8.57.1",
100
- "vitest": "^4.0.18"
101
- },
102
- "engines": {
103
- "node": ">=20",
104
- "npm": ">=3.10.7"
105
- },
106
- "packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b",
107
- "scripts": {
108
- "clean": "rm -rf ./dist",
109
- "prebuild": "pnpm clean",
110
- "build:watch": "pnpm tsc -w",
111
- "build": "pnpm tsc",
112
- "postbuild": "chmod +x ./dist/bin.js && pnpm test:smoke",
113
- "test:smoke": "node -e \"require('./dist/bin.js')\" 2>&1 | head -5 | grep -q 'Amplitude Wizard' || (echo 'Smoke test failed: compiled binary crashed on load' && exit 1)",
114
- "lint": "pnpm lint:prettier && pnpm lint:eslint",
115
- "lint:prettier": "prettier --check \"{lib,src,test}/**/*.ts\"",
116
- "lint:eslint": "eslint . --cache --format stylish",
117
- "fix": "pnpm fix:eslint && pnpm fix:prettier",
118
- "fix:prettier": "prettier --write \"{lib,src,test}/**/*.ts\"",
119
- "fix:eslint": "eslint . --format stylish --fix",
120
- "test": "vitest run",
121
- "test:e2e": "pnpm build && ./e2e-tests/run.sh",
122
- "test:e2e-record": "export RECORD_FIXTURES=true && pnpm build && ./e2e-tests/run.sh",
123
- "try": "tsx bin.ts",
124
- "proxy": "cd \"${JS_REPO:-../javascript}\"/server/packages/thunder && aws-sso exec --profile us-prod-dev -- pnpm debug",
125
- "proxy:bypass": "cd \"${JS_REPO:-../javascript}\"/server/packages/thunder && WIZARD_PROXY_DEV_BYPASS=1 aws-sso exec --profile us-prod-dev -- pnpm debug",
126
- "dev": "pnpm build && pnpm link --global && (trap 'kill 0' EXIT; pnpm proxy & pnpm build:watch)",
127
- "test:watch": "vitest",
128
- "prepare": "husky",
129
- "flows": "node scripts/render-flows.mjs",
130
- "test:bdd": "cucumber-js",
131
- "test:bdd:watch": "cucumber-js --retry 0 2>&1 | npx nodemon --watch features --exec cucumber-js",
132
- "test:proxy": "vitest run --config vitest.config.proxy.ts",
133
- "test:proxy:smoke": "vitest run --config vitest.config.proxy.ts -t 'proxy:smoke'",
134
- "skills:refresh": "bash scripts/refresh-instrumentation-skills.sh"
135
- },
136
- "lint-staged": {
137
- "*.{js,ts,tsx,jsx}": "pnpm fix"
138
- },
139
- "author": "Amplitude",
140
- "license": "MIT",
141
- "volta": {
142
- "node": "20.18.2",
143
- "pnpm": "10.23.0"
144
- }
145
- }