@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.
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/assets/rootly-logo-glyph-purple.png +0 -0
- package/assets/rootly-logo-glyph.png +0 -0
- package/assets/welcome-screen.png +0 -0
- package/package.json +47 -0
- package/src/actions/guided.js +87 -0
- package/src/actions/inspect.js +341 -0
- package/src/actions/integrations.js +75 -0
- package/src/actions/mcp.js +52 -0
- package/src/actions/oneshot.js +243 -0
- package/src/actions/phone.js +122 -0
- package/src/actions/registry.js +373 -0
- package/src/actions/setup.js +574 -0
- package/src/actions/testing.js +141 -0
- package/src/actions/workflow.js +47 -0
- package/src/auth.js +553 -0
- package/src/cli.js +199 -0
- package/src/detect-state.js +232 -0
- package/src/format.js +43 -0
- package/src/mcp.js +254 -0
- package/src/rootly-api.js +325 -0
- package/src/runtime.js +55 -0
- package/src/tui/components/AppShell.js +68 -0
- package/src/tui/components/Banner.js +145 -0
- package/src/tui/components/BigText.js +46 -0
- package/src/tui/components/Celebration.js +47 -0
- package/src/tui/components/KeyValueList.js +22 -0
- package/src/tui/components/MenuList.js +170 -0
- package/src/tui/components/MultiSelectList.js +181 -0
- package/src/tui/components/NoticeBox.js +56 -0
- package/src/tui/components/SlideReveal.js +52 -0
- package/src/tui/index.js +2078 -0
- package/src/tui/screens/ListScreen.js +29 -0
- package/src/tui/screens/LoadFailedScreen.js +30 -0
- package/src/tui/screens/LoadingScreen.js +32 -0
- package/src/tui/screens/MainMenuScreen.js +81 -0
- package/src/tui/screens/MultiSelectScreen.js +17 -0
- package/src/tui/screens/OneShotRunnerScreen.js +186 -0
- package/src/tui/screens/OptionScreen.js +24 -0
- package/src/tui/screens/ResultScreen.js +35 -0
- package/src/tui/screens/StatusScreen.js +70 -0
- package/src/tui/screens/TextEntryScreen.js +115 -0
- package/src/tui/screens/WelcomeScreen.js +66 -0
- package/src/tui/theme.js +69 -0
- 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
|
+
}
|