@rootly/wizard 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/assets/rootly-logo-glyph-purple.png +0 -0
  4. package/assets/rootly-logo-glyph.png +0 -0
  5. package/assets/welcome-screen.png +0 -0
  6. package/package.json +47 -0
  7. package/src/actions/guided.js +87 -0
  8. package/src/actions/inspect.js +341 -0
  9. package/src/actions/integrations.js +75 -0
  10. package/src/actions/mcp.js +52 -0
  11. package/src/actions/oneshot.js +243 -0
  12. package/src/actions/phone.js +122 -0
  13. package/src/actions/registry.js +373 -0
  14. package/src/actions/setup.js +574 -0
  15. package/src/actions/testing.js +141 -0
  16. package/src/actions/workflow.js +47 -0
  17. package/src/auth.js +553 -0
  18. package/src/cli.js +199 -0
  19. package/src/detect-state.js +232 -0
  20. package/src/format.js +43 -0
  21. package/src/mcp.js +254 -0
  22. package/src/rootly-api.js +325 -0
  23. package/src/runtime.js +55 -0
  24. package/src/tui/components/AppShell.js +68 -0
  25. package/src/tui/components/Banner.js +145 -0
  26. package/src/tui/components/BigText.js +46 -0
  27. package/src/tui/components/Celebration.js +47 -0
  28. package/src/tui/components/KeyValueList.js +22 -0
  29. package/src/tui/components/MenuList.js +170 -0
  30. package/src/tui/components/MultiSelectList.js +181 -0
  31. package/src/tui/components/NoticeBox.js +56 -0
  32. package/src/tui/components/SlideReveal.js +52 -0
  33. package/src/tui/index.js +2078 -0
  34. package/src/tui/screens/ListScreen.js +29 -0
  35. package/src/tui/screens/LoadFailedScreen.js +30 -0
  36. package/src/tui/screens/LoadingScreen.js +32 -0
  37. package/src/tui/screens/MainMenuScreen.js +81 -0
  38. package/src/tui/screens/MultiSelectScreen.js +17 -0
  39. package/src/tui/screens/OneShotRunnerScreen.js +186 -0
  40. package/src/tui/screens/OptionScreen.js +24 -0
  41. package/src/tui/screens/ResultScreen.js +35 -0
  42. package/src/tui/screens/StatusScreen.js +70 -0
  43. package/src/tui/screens/TextEntryScreen.js +115 -0
  44. package/src/tui/screens/WelcomeScreen.js +66 -0
  45. package/src/tui/theme.js +69 -0
  46. package/src/tui-legacy-bridge.js +371 -0
@@ -0,0 +1,66 @@
1
+ import { createElement as h, useEffect, useState } from 'react';
2
+ import { Box } from 'ink';
3
+ import { AppShell } from '../components/AppShell.js';
4
+ import { Banner } from '../components/Banner.js';
5
+ import { SlideReveal } from '../components/SlideReveal.js';
6
+ import { MenuList } from '../components/MenuList.js';
7
+ import { palette } from '../theme.js';
8
+
9
+ // Ramp tops out at the muted resting color so inactive rows (e.g. Exit) don't
10
+ // overshoot to near-white and flash; only the selected row brightens on arrival.
11
+ const MENU_RAMP = ['#3C3C46', '#56565F', '#71717C', '#8B8B97'];
12
+
13
+ export function WelcomeScreen({ lines, onContinue, onExit }) {
14
+ // Stage the entrance: banner flashes in, the copy slides in, then the menu
15
+ // fades in once the copy has landed.
16
+ const [phase, setPhase] = useState(0);
17
+ const [menuStep, setMenuStep] = useState(0);
18
+
19
+ useEffect(() => {
20
+ const startReveal = setTimeout(() => setPhase(1), 650);
21
+ return () => clearTimeout(startReveal);
22
+ }, []);
23
+
24
+ useEffect(() => {
25
+ if (phase < 2 || menuStep >= MENU_RAMP.length) return undefined;
26
+ const tick = setTimeout(() => setMenuStep((current) => current + 1), 70);
27
+ return () => clearTimeout(tick);
28
+ }, [phase, menuStep]);
29
+
30
+ const menuTint = phase >= 2 && menuStep < MENU_RAMP.length ? MENU_RAMP[menuStep] : undefined;
31
+
32
+ const menuOptions = [
33
+ { label: 'Continue', value: 'continue' },
34
+ { label: 'Exit', value: 'exit' }
35
+ ];
36
+
37
+ // Reserve the copy and menu rows up front so the bordered card opens at its
38
+ // final size — sections fade in within this fixed footprint instead of
39
+ // stretching the box as each stage arrives.
40
+ return h(
41
+ AppShell,
42
+ { context: 'welcome', keyColor: palette.accent },
43
+ h(Banner, null),
44
+ h(
45
+ Box,
46
+ // Gap between the hero and the intro copy so the splash doesn't feel stacked.
47
+ { flexDirection: 'column', marginTop: 1, minHeight: lines.length },
48
+ phase >= 1 ? h(SlideReveal, { lines, onDone: () => setPhase(2) }) : null
49
+ ),
50
+ h(
51
+ Box,
52
+ { marginTop: 2, minHeight: menuOptions.length },
53
+ phase >= 2
54
+ ? h(MenuList, {
55
+ tint: menuTint,
56
+ options: menuOptions,
57
+ onSelect: (option) => {
58
+ if (option.value === 'continue') onContinue?.();
59
+ else onExit?.();
60
+ },
61
+ onCancel: onExit
62
+ })
63
+ : null
64
+ )
65
+ );
66
+ }
@@ -0,0 +1,69 @@
1
+ // Shared design tokens for the Ink wizard UI.
2
+ // Aligned to the Rootly brand guidelines: purple is primary (P700 is the
3
+ // official *brand color), orange is the secondary accent, restrained neutrals,
4
+ // and a small glyph set used consistently across screens.
5
+ //
6
+ // Brand ramp for reference — P900 #3B2D7A · P800 #5942B5 · P700 #7C5CE6 (*brand)
7
+ // · P500 #9D86F0 · P300 #C9BEF7 · P200 #E0DAFB · P100 #F2EFFC.
8
+ // Secondary orange — S900 #E8822E · S700 #F5A24F · S500 #F9C58A.
9
+
10
+ export const palette = {
11
+ brand: '#7C5CE6', // Rootly purple (P700, *brand color) — primary accent / selection
12
+ accent: '#F5A24F', // brand orange (S700) — numbers, bullets, secondary accent
13
+ text: '#ECECEE', // primary text (N100)
14
+ muted: '#8B8B97', // secondary / hint text (N500)
15
+ success: '#5BD18B',
16
+ warning: '#E8822E', // brand orange (S900)
17
+ danger: '#F4787B',
18
+ border: '#3C3C46', // panel borders, inactive glyphs (N900→N700)
19
+ };
20
+
21
+ // Subtle resting shimmer — a faint highlight drifting over the purple brand
22
+ // base (no white, no dark dips), so the settled wordmark only gently glints.
23
+ export const shimmerRamp = [
24
+ '#7C5CE6',
25
+ '#7C5CE6',
26
+ '#7C5CE6',
27
+ '#8D70EB',
28
+ '#A289F1',
29
+ '#8D70EB',
30
+ '#7C5CE6',
31
+ '#7C5CE6',
32
+ '#7C5CE6',
33
+ '#7C5CE6',
34
+ ];
35
+
36
+ export const glyphs = {
37
+ logo: '✦',
38
+ cursor: '❯',
39
+ dot: '•',
40
+ star: '★',
41
+ check: '◉',
42
+ uncheck: '◯',
43
+ barOn: '▰',
44
+ barOff: '▱',
45
+ more: '·',
46
+ };
47
+
48
+ // Footer key-hint presets. Each item renders as a brand-colored key + muted label.
49
+ export const HINTS = {
50
+ nav: [
51
+ { key: '↑↓', label: 'navigate' },
52
+ { key: '1–9', label: 'jump' },
53
+ { key: 'esc', label: 'back' },
54
+ ],
55
+ multi: [
56
+ { key: '↵/space', label: 'check' },
57
+ { key: 'a', label: 'all' },
58
+ { key: 'esc', label: 'back' },
59
+ ],
60
+ entry: [
61
+ { key: 'type', label: 'to edit' },
62
+ { key: '↵', label: 'continue' },
63
+ { key: 'esc', label: 'back' },
64
+ ],
65
+ back: [
66
+ { key: 'esc', label: 'back' },
67
+ ],
68
+ none: [],
69
+ };
@@ -0,0 +1,371 @@
1
+ import { loadOnboardingState } from './runtime.js';
2
+ import { getTeamsAction, getSchedulesAction, getEscalationPoliciesAction } from './actions/inspect.js';
3
+ import { getTeamMembersAction, getAddableTeamMembersAction, getDirectoryUsersAction, getStatusPageComponentsAction, createCustomComponentAction } from './actions/inspect.js';
4
+ import { getAuthSummary, getStoredToken, startOAuthLogin, storeToken, validateToken } from './auth.js';
5
+ import {
6
+ createTeamAction,
7
+ addTeamMembersAction,
8
+ addTeamMembersByIdsAction,
9
+ createScheduleAction,
10
+ createEscalationPolicyAction,
11
+ createAlertSourceAction,
12
+ createStatusPageAction,
13
+ getStatusPagesAction,
14
+ publishStatusPageAction,
15
+ updateStatusPageAction
16
+ } from './actions/setup.js';
17
+ import { createTestAlertAction, createTestIncidentAction } from './actions/testing.js';
18
+ import { runOneShotSetupAction } from './actions/oneshot.js';
19
+ import {
20
+ startPhoneVerificationAction,
21
+ confirmPhoneVerificationAction,
22
+ resendPhoneVerificationAction,
23
+ getCurrentUserPhoneAction
24
+ } from './actions/phone.js';
25
+ import { deleteToken } from './auth.js';
26
+ import { startWebHandoffAction, openUrl } from './actions/integrations.js';
27
+ import { previewMcpSetupAction, applyMcpSetupAction } from './actions/mcp.js';
28
+
29
+ function isWorkspaceAccessFailure(error) {
30
+ const message = error?.message || String(error);
31
+ return (
32
+ (
33
+ message.includes('/v1/teams') ||
34
+ message.includes('/v1/schedules') ||
35
+ message.includes('/v1/escalation_policies')
36
+ ) &&
37
+ (/\b401\b/.test(message) || /\b403\b/.test(message) || /Not found or unauthorized/i.test(message))
38
+ );
39
+ }
40
+
41
+ export async function loadOnboardingStateInteractive() {
42
+ try {
43
+ return { ok: true, state: await loadOnboardingState() };
44
+ } catch (error) {
45
+ if (isWorkspaceAccessFailure(error)) {
46
+ const authSummary = await getAuthSummary();
47
+ return {
48
+ ok: false,
49
+ reason: 'auth-capability',
50
+ isBrowserSession: authSummary?.mode === 'oauth',
51
+ label: authSummary?.label || 'A stored Rootly sign-in was found.'
52
+ };
53
+ }
54
+ return { ok: false, reason: 'error', message: error?.message || String(error) };
55
+ }
56
+ }
57
+
58
+ export async function loadAuthContextForTui() {
59
+ const envToken = process.env.ROOTLY_TOKEN?.trim() || null;
60
+ const storedToken = await getStoredToken();
61
+ const authSummary = await getAuthSummary();
62
+
63
+ return {
64
+ hasAuth: Boolean(envToken || storedToken),
65
+ source: envToken ? 'env-token' : storedToken ? 'stored-token' : 'none',
66
+ label: authSummary?.label || (envToken ? 'Using ROOTLY_TOKEN from environment' : storedToken ? 'Stored Rootly sign-in found' : 'No Rootly sign-in found')
67
+ };
68
+ }
69
+
70
+ export async function authenticateWithApiTokenForTui(token) {
71
+ const clean = String(token || '').trim();
72
+ if (!clean) {
73
+ return { ok: false, summary: 'API token is required.' };
74
+ }
75
+
76
+ const userPayload = await validateToken(clean);
77
+ if (!userPayload) {
78
+ return { ok: false, summary: 'Token did not validate against Rootly.' };
79
+ }
80
+
81
+ const stored = await storeToken(clean);
82
+ return {
83
+ ok: true,
84
+ summary: 'API token validated.',
85
+ data: {
86
+ stored,
87
+ userPayload
88
+ }
89
+ };
90
+ }
91
+
92
+ export async function authenticateWithBrowserForTui() {
93
+ try {
94
+ const result = await startOAuthLogin();
95
+ return {
96
+ ok: true,
97
+ summary: 'Browser sign-in completed successfully.',
98
+ data: result
99
+ };
100
+ } catch (error) {
101
+ return {
102
+ ok: false,
103
+ summary: error?.message || 'Browser sign-in failed.'
104
+ };
105
+ }
106
+ }
107
+
108
+ export async function createTeamForTui(input) {
109
+ try {
110
+ return await createTeamAction(input);
111
+ } catch (error) {
112
+ return {
113
+ ok: false,
114
+ summary: error?.message || 'The wizard could not create the team.'
115
+ };
116
+ }
117
+ }
118
+
119
+ export async function addTeamMembersForTui(input) {
120
+ try {
121
+ return await addTeamMembersAction(input);
122
+ } catch (error) {
123
+ return {
124
+ ok: false,
125
+ summary: error?.message || 'The wizard could not update team membership.'
126
+ };
127
+ }
128
+ }
129
+
130
+ export async function addTeamMembersByIdsForTui(input) {
131
+ try {
132
+ return await addTeamMembersByIdsAction(input);
133
+ } catch (error) {
134
+ return {
135
+ ok: false,
136
+ summary: error?.message || 'The wizard could not add team members.'
137
+ };
138
+ }
139
+ }
140
+
141
+ export async function loadAddableUsersForTui(teamId) {
142
+ const result = await getAddableTeamMembersAction({ teamId });
143
+ return result.ok ? result.data : null;
144
+ }
145
+
146
+ export async function loadDirectoryUsersForTui() {
147
+ const result = await getDirectoryUsersAction();
148
+ return result.ok ? result.data : null;
149
+ }
150
+
151
+ export async function createScheduleForTui(input) {
152
+ try {
153
+ return await createScheduleAction(input);
154
+ } catch (error) {
155
+ return {
156
+ ok: false,
157
+ summary: error?.message || 'The wizard could not create the schedule.'
158
+ };
159
+ }
160
+ }
161
+
162
+ export async function createEscalationPolicyForTui(input) {
163
+ try {
164
+ // Standalone flow: reuse an existing policy of the same name instead of
165
+ // creating a duplicate on re-run. (The one-shot calls the action directly.)
166
+ return await createEscalationPolicyAction({ reuseByName: true, ...input });
167
+ } catch (error) {
168
+ return {
169
+ ok: false,
170
+ summary: error?.message || 'The wizard could not create the escalation policy.'
171
+ };
172
+ }
173
+ }
174
+
175
+ export async function createAlertSourceForTui(input) {
176
+ try {
177
+ // Standalone flow: reuse an existing source of the same name instead of
178
+ // creating a duplicate on re-run. (The one-shot calls the action directly.)
179
+ return await createAlertSourceAction({ reuseByName: true, ...input });
180
+ } catch (error) {
181
+ return {
182
+ ok: false,
183
+ summary: error?.message || 'The wizard could not create the alert source.'
184
+ };
185
+ }
186
+ }
187
+
188
+ export async function loadStatusPageComponentsForTui() {
189
+ try {
190
+ const result = await getStatusPageComponentsAction();
191
+ return result.ok ? result.data : { components: [] };
192
+ } catch {
193
+ return { components: [] };
194
+ }
195
+ }
196
+
197
+ export async function createCustomComponentForTui(input) {
198
+ try {
199
+ return await createCustomComponentAction(input);
200
+ } catch (error) {
201
+ return { ok: false, summary: error?.message || 'The wizard could not create the component.' };
202
+ }
203
+ }
204
+
205
+ export async function loadStatusPagesForTui() {
206
+ try {
207
+ const result = await getStatusPagesAction();
208
+ return result.ok ? result.data : { pages: [] };
209
+ } catch {
210
+ return { pages: [] };
211
+ }
212
+ }
213
+
214
+ export async function publishStatusPageForTui(input) {
215
+ try {
216
+ return await publishStatusPageAction(input);
217
+ } catch (error) {
218
+ return { ok: false, summary: error?.message || 'The wizard could not publish the status page.' };
219
+ }
220
+ }
221
+
222
+ export async function updateStatusPageForTui(input) {
223
+ try {
224
+ return await updateStatusPageAction(input);
225
+ } catch (error) {
226
+ return { ok: false, summary: error?.message || 'The wizard could not update the status page.' };
227
+ }
228
+ }
229
+
230
+ export async function createStatusPageForTui(input) {
231
+ try {
232
+ return await createStatusPageAction(input);
233
+ } catch (error) {
234
+ return {
235
+ ok: false,
236
+ summary: error?.message || 'The wizard could not create the status page.'
237
+ };
238
+ }
239
+ }
240
+
241
+ export async function createTestAlertForTui(input) {
242
+ try {
243
+ return await createTestAlertAction(input);
244
+ } catch (error) {
245
+ return {
246
+ ok: false,
247
+ summary: error?.message || 'The wizard could not create the test alert.'
248
+ };
249
+ }
250
+ }
251
+
252
+ export async function createTestIncidentForTui(input) {
253
+ try {
254
+ return await createTestIncidentAction(input);
255
+ } catch (error) {
256
+ return {
257
+ ok: false,
258
+ summary: error?.message || 'The wizard could not create the test incident.'
259
+ };
260
+ }
261
+ }
262
+
263
+ export async function deleteTokenForTui() {
264
+ return await deleteToken();
265
+ }
266
+
267
+ export async function runOneShotSetupForTui(input, onStep) {
268
+ try {
269
+ return await runOneShotSetupAction(input, onStep);
270
+ } catch (error) {
271
+ return {
272
+ ok: false,
273
+ summary: error?.message || 'The wizard could not complete one-shot setup.'
274
+ };
275
+ }
276
+ }
277
+
278
+ export async function openExternalUrlForTui(url) {
279
+ try {
280
+ const opened = await openUrl(url);
281
+ return { ok: true, opened: Boolean(opened) };
282
+ } catch {
283
+ return { ok: false, opened: false };
284
+ }
285
+ }
286
+
287
+ export async function loadCurrentUserPhoneForTui() {
288
+ try {
289
+ const result = await getCurrentUserPhoneAction();
290
+ return result.ok ? result.data : null;
291
+ } catch {
292
+ return null;
293
+ }
294
+ }
295
+
296
+ export async function startPhoneVerificationForTui(input) {
297
+ try {
298
+ return await startPhoneVerificationAction(input);
299
+ } catch (error) {
300
+ return { ok: false, summary: error?.message || 'Could not start phone verification.' };
301
+ }
302
+ }
303
+
304
+ export async function confirmPhoneVerificationForTui(input) {
305
+ try {
306
+ return await confirmPhoneVerificationAction(input);
307
+ } catch (error) {
308
+ return { ok: false, summary: error?.message || 'Could not verify the phone number.' };
309
+ }
310
+ }
311
+
312
+ export async function resendPhoneVerificationForTui(input) {
313
+ try {
314
+ return await resendPhoneVerificationAction(input);
315
+ } catch (error) {
316
+ return { ok: false, summary: error?.message || 'Could not resend the verification code.' };
317
+ }
318
+ }
319
+
320
+ export async function startWebHandoffForTui(input) {
321
+ try {
322
+ return await startWebHandoffAction(input);
323
+ } catch (error) {
324
+ return {
325
+ ok: false,
326
+ summary: error?.message || 'Could not prepare the Rootly web handoff.'
327
+ };
328
+ }
329
+ }
330
+
331
+ export async function previewMcpForTui(input) {
332
+ try {
333
+ return await previewMcpSetupAction(input);
334
+ } catch (error) {
335
+ return {
336
+ ok: false,
337
+ summary: error?.message || 'Could not preview MCP setup.'
338
+ };
339
+ }
340
+ }
341
+
342
+ export async function applyMcpForTui(input) {
343
+ try {
344
+ return await applyMcpSetupAction(input);
345
+ } catch (error) {
346
+ return {
347
+ ok: false,
348
+ summary: error?.message || 'Could not apply MCP setup.'
349
+ };
350
+ }
351
+ }
352
+
353
+ export async function loadTeamsForTui() {
354
+ const result = await getTeamsAction();
355
+ return result.ok ? result.data : null;
356
+ }
357
+
358
+ export async function loadSchedulesForTui() {
359
+ const result = await getSchedulesAction();
360
+ return result.ok ? result.data : null;
361
+ }
362
+
363
+ export async function loadEscalationPoliciesForTui() {
364
+ const result = await getEscalationPoliciesAction();
365
+ return result.ok ? result.data : null;
366
+ }
367
+
368
+ export async function loadTeamMembersForTui(teamId) {
369
+ const result = await getTeamMembersAction({ teamId });
370
+ return result.ok ? result.data : null;
371
+ }