@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,373 @@
1
+ import {
2
+ getEscalationPoliciesAction,
3
+ getEnvironmentsAction,
4
+ getIncidentTypesAction,
5
+ getReadinessAction,
6
+ getSchedulesAction,
7
+ getServicesAction,
8
+ getSeveritiesAction,
9
+ getStatusAction,
10
+ getTeamMembersAction,
11
+ getTeamsAction,
12
+ getUsersAction
13
+ } from './inspect.js';
14
+ import { getRecommendedNextStepAction } from './workflow.js';
15
+ import {
16
+ addTeamMembersAction,
17
+ createAlertSourceAction,
18
+ createEscalationPolicyAction,
19
+ createScheduleAction,
20
+ createStatusPageAction,
21
+ createTeamAction
22
+ } from './setup.js';
23
+ import { createTestAlertAction, createTestIncidentAction } from './testing.js';
24
+ import { startWebHandoffAction } from './integrations.js';
25
+ import { applyMcpSetupAction, previewMcpSetupAction } from './mcp.js';
26
+ import { runGuidedSetupAction } from './guided.js';
27
+ import { runOneShotSetupAction } from './oneshot.js';
28
+
29
+ // Each action declares whether it mutates Rootly and a lightweight input schema
30
+ // (field -> { type, required?, default?, description }). The schema powers
31
+ // `action describe`, boundary input validation, and dry-run previews.
32
+ export const ACTIONS = {
33
+ 'get-status': {
34
+ mutates: false,
35
+ description: 'Workspace status summary with the recommended next step.',
36
+ input: {},
37
+ handler: getStatusAction
38
+ },
39
+ 'get-readiness': {
40
+ mutates: false,
41
+ description: 'Full onboarding readiness report (steps, readiness, teams).',
42
+ input: {},
43
+ handler: getReadinessAction
44
+ },
45
+ 'get-recommended-next-step': {
46
+ mutates: false,
47
+ description: 'The single next best setup action and a human label.',
48
+ input: {},
49
+ handler: getRecommendedNextStepAction
50
+ },
51
+ 'list-teams': {
52
+ mutates: false,
53
+ description: 'Teams with member/schedule/escalation coverage counts.',
54
+ input: {},
55
+ handler: getTeamsAction
56
+ },
57
+ 'list-schedules': {
58
+ mutates: false,
59
+ description: 'On-call schedules with coverage summary.',
60
+ input: {},
61
+ handler: getSchedulesAction
62
+ },
63
+ 'list-escalation-policies': {
64
+ mutates: false,
65
+ description: 'Escalation policies with team/repeat summary.',
66
+ input: {},
67
+ handler: getEscalationPoliciesAction
68
+ },
69
+ 'list-services': {
70
+ mutates: false,
71
+ description: 'Services as {id, name} for resolving service IDs.',
72
+ input: {},
73
+ handler: getServicesAction
74
+ },
75
+ 'list-severities': {
76
+ mutates: false,
77
+ description: 'Severities as {id, name} for resolving severity IDs.',
78
+ input: {},
79
+ handler: getSeveritiesAction
80
+ },
81
+ 'list-environments': {
82
+ mutates: false,
83
+ description: 'Environments as {id, name} for resolving environment IDs.',
84
+ input: {},
85
+ handler: getEnvironmentsAction
86
+ },
87
+ 'list-incident-types': {
88
+ mutates: false,
89
+ description: 'Incident types as {id, name} for resolving incident type IDs.',
90
+ input: {},
91
+ handler: getIncidentTypesAction
92
+ },
93
+ 'list-users': {
94
+ mutates: false,
95
+ description: 'Users as {id, email, name} (first page) for resolving members.',
96
+ input: {},
97
+ handler: getUsersAction
98
+ },
99
+ 'list-team-members': {
100
+ mutates: false,
101
+ description: 'Members ({id, email, name}) of a specific team.',
102
+ input: {
103
+ teamId: { type: 'string', required: true, description: 'Team ID.' }
104
+ },
105
+ handler: getTeamMembersAction
106
+ },
107
+ 'create-team': {
108
+ mutates: true,
109
+ description: 'Create a team, optionally inviting members by email.',
110
+ input: {
111
+ name: { type: 'string', required: true, description: 'Team name.' },
112
+ description: { type: 'string', description: 'Team description.' },
113
+ memberEmails: { type: 'array', items: 'string', description: 'Emails to resolve and add as members.' },
114
+ enableAlertsAndBroadcast: { type: 'boolean', description: 'Enable alert emails and incident broadcast.' }
115
+ },
116
+ handler: createTeamAction
117
+ },
118
+ 'add-team-members': {
119
+ mutates: true,
120
+ description: 'Add members to an existing team by email.',
121
+ input: {
122
+ teamId: { type: 'string', required: true, description: 'Team ID.' },
123
+ emails: { type: 'array', items: 'string', required: true, description: 'Emails to add.' }
124
+ },
125
+ handler: addTeamMembersAction
126
+ },
127
+ 'create-schedule': {
128
+ mutates: true,
129
+ description: 'Create an on-call schedule with a daily rotation.',
130
+ input: {
131
+ teamId: { type: 'string', required: true, description: 'Owning team ID.' },
132
+ name: { type: 'string', required: true, description: 'Schedule name.' },
133
+ handoffTime: { type: 'string', default: '09:00', description: 'Daily handoff time (HH:MM).' },
134
+ memberIds: { type: 'array', items: 'number', description: 'User IDs for the rotation.' }
135
+ },
136
+ handler: createScheduleAction
137
+ },
138
+ 'create-escalation-policy': {
139
+ mutates: true,
140
+ description: 'Create an escalation policy, optionally with a default path.',
141
+ input: {
142
+ teamId: { type: 'string', required: true, description: 'Owning team ID.' },
143
+ name: { type: 'string', required: true, description: 'Policy name.' },
144
+ repeatCount: { type: 'integer', default: 1, description: 'Times the policy repeats.' },
145
+ createDefaultPath: { type: 'boolean', description: 'Also create a default escalation path.' }
146
+ },
147
+ handler: createEscalationPolicyAction
148
+ },
149
+ 'create-status-page': {
150
+ mutates: true,
151
+ description: 'Create a status page (internal by default).',
152
+ input: {
153
+ title: { type: 'string', required: true, description: 'Status page title.' },
154
+ description: { type: 'string', description: 'Internal description shown to Rootly admins. Defaults to a wizard note.' },
155
+ isPublic: { type: 'boolean', description: 'Make it public (default false = internal).' }
156
+ },
157
+ handler: createStatusPageAction
158
+ },
159
+ 'create-alert-source': {
160
+ mutates: true,
161
+ description: 'Create a generic webhook alert source.',
162
+ input: {
163
+ teamId: { type: 'string', description: 'Owning team ID.' },
164
+ name: { type: 'string', default: 'Generic webhook', description: 'Alert source name.' },
165
+ sourceType: { type: 'string', default: 'generic_webhook', description: 'Rootly alert source type.' }
166
+ },
167
+ handler: createAlertSourceAction
168
+ },
169
+ 'create-test-alert': {
170
+ mutates: true,
171
+ description: 'Create a test alert to verify paging.',
172
+ input: {
173
+ summary: { type: 'string', required: true, description: 'Alert summary.' },
174
+ description: { type: 'string', description: 'Alert description.' },
175
+ groupIds: { type: 'array', items: 'string', description: 'Team/group IDs.' },
176
+ serviceIds: { type: 'array', items: 'string', description: 'Service IDs.' },
177
+ environmentIds: { type: 'array', items: 'string', description: 'Environment IDs.' }
178
+ },
179
+ handler: createTestAlertAction
180
+ },
181
+ 'create-test-incident': {
182
+ mutates: true,
183
+ description: 'Create a test incident.',
184
+ input: {
185
+ title: { type: 'string', required: true, description: 'Incident title.' },
186
+ summary: { type: 'string', description: 'Incident summary.' },
187
+ groupIds: { type: 'array', items: 'string', description: 'Team/group IDs.' },
188
+ serviceIds: { type: 'array', items: 'string', description: 'Service IDs.' },
189
+ environmentIds: { type: 'array', items: 'string', description: 'Environment IDs.' },
190
+ incidentTypeIds: { type: 'array', items: 'string', description: 'Incident type IDs.' },
191
+ severityId: { type: 'string', description: 'Severity ID.' },
192
+ isPrivate: { type: 'boolean', description: 'Create as a private incident.' }
193
+ },
194
+ handler: createTestIncidentAction
195
+ },
196
+ 'run-guided-setup': {
197
+ mutates: true,
198
+ description: 'Create team + schedule + escalation policy + webhook in one call.',
199
+ input: {
200
+ teamName: { type: 'string', default: 'Rootly team', description: 'Team name.' },
201
+ memberEmails: { type: 'array', items: 'string', description: 'Emails to add as members.' },
202
+ handoffTime: { type: 'string', default: '09:00', description: 'Daily handoff time (HH:MM).' },
203
+ repeatCount: { type: 'integer', default: 1, description: 'Escalation policy repeat count.' },
204
+ includeAlertSource: { type: 'boolean', default: true, description: 'Also create a generic webhook source.' }
205
+ },
206
+ handler: runGuidedSetupAction
207
+ },
208
+ 'one-shot-setup': {
209
+ mutates: true,
210
+ description: 'End-to-end setup: team + schedule + escalation + alert source, then fire a test alert and incident. Auto-detects sign-in capability.',
211
+ input: {
212
+ teamName: { type: 'string', default: 'Incident Response', description: 'Team name to create or reuse.' },
213
+ handoffTime: { type: 'string', default: '09:00', description: 'Daily on-call handoff time (HH:MM).' },
214
+ memberIds: { type: 'array', items: 'string', description: 'User IDs to add as team members and put on the rotation. Defaults to the current identity.' }
215
+ },
216
+ handler: runOneShotSetupAction
217
+ },
218
+ 'start-web-handoff': {
219
+ mutates: false,
220
+ description: 'Return the Rootly web URL for an integration handoff (e.g. Slack).',
221
+ input: {
222
+ kind: { type: 'string', required: true, description: 'Integration name (Slack, Datadog, ...).' },
223
+ open: { type: 'boolean', description: 'Attempt to open the URL in a browser.' }
224
+ },
225
+ handler: startWebHandoffAction
226
+ },
227
+ 'preview-mcp-setup': {
228
+ mutates: false,
229
+ description: 'Preview MCP client config without writing files.',
230
+ input: {
231
+ clients: { type: 'array', items: 'string', description: 'Clients (Cursor, Claude Code, ...).' },
232
+ auth: { type: 'string', description: 'Auth mode label.' }
233
+ },
234
+ handler: previewMcpSetupAction
235
+ },
236
+ 'apply-mcp-setup': {
237
+ mutates: true,
238
+ description: 'Write MCP client config files for the selected clients.',
239
+ input: {
240
+ clients: { type: 'array', items: 'string', required: true, description: 'Clients to configure.' },
241
+ auth: { type: 'string', description: 'Auth mode label.' }
242
+ },
243
+ handler: applyMcpSetupAction
244
+ }
245
+ };
246
+
247
+ export function buildActionCatalog() {
248
+ return Object.entries(ACTIONS).map(([name, entry]) => ({
249
+ name,
250
+ mutates: entry.mutates,
251
+ description: entry.description
252
+ }));
253
+ }
254
+
255
+ export function describeAction(name) {
256
+ const entry = ACTIONS[name];
257
+ if (!entry) {
258
+ return null;
259
+ }
260
+
261
+ return {
262
+ name,
263
+ mutates: entry.mutates,
264
+ supportsDryRun: entry.mutates,
265
+ description: entry.description,
266
+ input: entry.input
267
+ };
268
+ }
269
+
270
+ // Convert the registry into function-calling tool definitions (JSON Schema
271
+ // input_schema), compatible with Anthropic/OpenAI tool formats. Tool name ==
272
+ // action name, so a tool call maps directly to `action <name> <args>`.
273
+ export function buildToolSpecs() {
274
+ return Object.entries(ACTIONS).map(([name, entry]) => {
275
+ const properties = {};
276
+ const required = [];
277
+
278
+ for (const [field, spec] of Object.entries(entry.input)) {
279
+ const property = { type: spec.type === 'integer' ? 'integer' : spec.type, description: spec.description };
280
+ if (spec.type === 'array') {
281
+ property.items = { type: spec.items || 'string' };
282
+ }
283
+ if (spec.default !== undefined) {
284
+ property.default = spec.default;
285
+ }
286
+ properties[field] = property;
287
+ if (spec.required) {
288
+ required.push(field);
289
+ }
290
+ }
291
+
292
+ if (entry.mutates) {
293
+ properties.dryRun = { type: 'boolean', description: 'Preview the action without executing it.' };
294
+ }
295
+
296
+ return {
297
+ name,
298
+ description: entry.description,
299
+ input_schema: { type: 'object', properties, required }
300
+ };
301
+ });
302
+ }
303
+
304
+ export function validateInput(schema, payload) {
305
+ for (const [field, spec] of Object.entries(schema || {})) {
306
+ const value = payload?.[field];
307
+ const present = value !== undefined && value !== null && value !== '';
308
+
309
+ if (spec.required && !present) {
310
+ return { field, message: `${field} is required` };
311
+ }
312
+
313
+ if (present && spec.type) {
314
+ const matches = spec.type === 'array'
315
+ ? Array.isArray(value)
316
+ : spec.type === 'integer'
317
+ ? Number.isInteger(value)
318
+ : typeof value === spec.type;
319
+
320
+ if (!matches) {
321
+ return { field, message: `${field} must be of type ${spec.type}` };
322
+ }
323
+ }
324
+ }
325
+
326
+ return null;
327
+ }
328
+
329
+ export function toStructuredError(actionName, error) {
330
+ const raw = (error?.message || 'Action failed.').replace(/^Rootly API request failed for [^:]+:\s*/, '');
331
+
332
+ let status = null;
333
+ let detail = raw;
334
+ let field = null;
335
+
336
+ const withBody = raw.match(/^(\d{3})\s*-\s*(\{[\s\S]*\})$/);
337
+ if (withBody) {
338
+ status = Number(withBody[1]);
339
+ try {
340
+ const body = JSON.parse(withBody[2]);
341
+ const first = Array.isArray(body?.errors) ? body.errors[0] : null;
342
+ detail = first?.detail || first?.title || raw;
343
+ const pointer = first?.source?.pointer || '';
344
+ const pointerMatch = pointer.match(/\/([^/]+)$/);
345
+ field = pointerMatch ? pointerMatch[1] : null;
346
+ } catch {
347
+ // keep raw detail
348
+ }
349
+ } else {
350
+ const statusOnly = raw.match(/^(\d{3})\b/);
351
+ if (statusOnly) {
352
+ status = Number(statusOnly[1]);
353
+ }
354
+ }
355
+
356
+ const code = status === 401 ? 'UNAUTHORIZED'
357
+ : status === 403 ? 'FORBIDDEN'
358
+ : status === 404 ? 'NOT_FOUND'
359
+ : status === 422 ? 'VALIDATION'
360
+ : (error?.name === 'TimeoutError' || /timed out|aborted/i.test(raw)) ? 'TIMEOUT'
361
+ : 'ACTION_FAILED';
362
+
363
+ return {
364
+ ok: false,
365
+ code,
366
+ summary: `Action ${actionName} failed.`,
367
+ error: detail,
368
+ field,
369
+ status,
370
+ retryable: code === 'TIMEOUT' || (typeof status === 'number' && status >= 500),
371
+ data: null
372
+ };
373
+ }