@get-dx/cli 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.
@@ -0,0 +1,286 @@
1
+ import dayjs from "dayjs";
2
+ import { renderRichText } from "../../renderers.js";
3
+ import * as ui from "../../ui.js";
4
+ export function renderEntityList(entities, nextCursor) {
5
+ const blocks = [ui.h1("Entities")];
6
+ blocks.push(ui.p(`Displaying ${ui.bold(entities.length.toString())} entities.`));
7
+ if (nextCursor) {
8
+ blocks.push(ui.p(`Next cursor: ${ui.code(nextCursor)}`));
9
+ }
10
+ for (const entity of entities) {
11
+ if (entity.name && entity.identifier) {
12
+ blocks.push(ui.h2(`${entity.name} (${ui.code(entity.identifier)})`));
13
+ }
14
+ else if (entity.identifier) {
15
+ blocks.push(ui.h2(ui.code(entity.identifier)));
16
+ }
17
+ else {
18
+ blocks.push(ui.h2("Entity"));
19
+ }
20
+ if (entity.identifier) {
21
+ blocks.push(ui.h3("Core attributes"));
22
+ blocks.push(...coreContent(entity));
23
+ }
24
+ if (entity.owner_teams || entity.owner_users) {
25
+ blocks.push(ui.h3("Owners"));
26
+ blocks.push(...ownersContent(entity));
27
+ }
28
+ if (entity.properties) {
29
+ blocks.push(ui.h3("Properties"));
30
+ blocks.push(...propertiesContent(entity));
31
+ }
32
+ if (entity.aliases) {
33
+ blocks.push(ui.h3("Aliases"));
34
+ blocks.push(...aliasesContent(entity));
35
+ }
36
+ }
37
+ renderRichText(blocks);
38
+ }
39
+ export function renderEntity(entity, title = "Entity Information") {
40
+ renderRichText([
41
+ ui.h1(title),
42
+ entity.identifier
43
+ ? [ui.h2("Core attributes"), ...coreContent(entity)]
44
+ : null,
45
+ entity.owner_teams ? [ui.h2("Owners"), ...ownersContent(entity)] : null,
46
+ entity.properties
47
+ ? [ui.h2("Properties"), ...propertiesContent(entity)]
48
+ : null,
49
+ entity.aliases ? [ui.h2("Aliases"), ...aliasesContent(entity)] : null,
50
+ ]);
51
+ }
52
+ export function renderEntityDeleted(entity) {
53
+ renderRichText([
54
+ ui.h1(`${ui.success(ui.GLYPHS.CHECK)} Entity deleted`),
55
+ ui.p(`The entity ${ui.code(entity.identifier)} has been deleted.`),
56
+ ]);
57
+ }
58
+ export function renderEntityScorecardList(scorecards, nextCursor) {
59
+ const blocks = [ui.h1("Scorecards")];
60
+ blocks.push(ui.p(`Displaying ${ui.bold(scorecards.length.toString())} scorecards.`));
61
+ if (nextCursor) {
62
+ blocks.push(ui.p(`Next cursor: ${ui.code(nextCursor)}`));
63
+ }
64
+ for (const scorecard of scorecards) {
65
+ blocks.push(ui.h2(scorecard.name));
66
+ blocks.push(...scorecardReportContent(scorecard));
67
+ }
68
+ renderRichText(blocks);
69
+ }
70
+ export function renderEntityTaskList(tasks, nextCursor) {
71
+ const blocks = [ui.h1("Tasks")];
72
+ blocks.push(ui.p(`Displaying ${ui.bold(tasks.length.toString())} tasks.`));
73
+ if (nextCursor) {
74
+ blocks.push(ui.p(`Next cursor: ${ui.code(nextCursor)}`));
75
+ }
76
+ for (const task of tasks) {
77
+ blocks.push(ui.h2(task.check.name));
78
+ blocks.push(...taskContent(task));
79
+ }
80
+ renderRichText(blocks);
81
+ }
82
+ function scorecardReportContent(scorecard) {
83
+ const blocks = [];
84
+ if (scorecard.type === "POINTS") {
85
+ const total = scorecard.points_meta?.points_total ?? 0;
86
+ const achieved = scorecard.points_meta?.points_achieved ?? 0;
87
+ const pct = total === 0 ? 0 : Math.round((achieved / total) * 100);
88
+ blocks.push(ui.p(`Points: ${ui.bold(`${achieved}/${total}`)} ${ui.dim(`(${pct}%)`)}`));
89
+ }
90
+ else {
91
+ const levelName = scorecard.current_level?.name ??
92
+ scorecard.empty_level?.label ??
93
+ "No level";
94
+ blocks.push(ui.p(`Current level: ${ui.bold(levelName)}`));
95
+ }
96
+ if (scorecard.type === "LEVEL" && scorecard.levels) {
97
+ for (const level of scorecard.levels) {
98
+ const checksInLevel = scorecard.checks.filter((check) => check.level?.id === level.id);
99
+ if (checksInLevel.length === 0)
100
+ continue;
101
+ blocks.push(ui.h3(level.name));
102
+ blocks.push(ui.ul(checksInLevel.map(scorecardCheckListItem)));
103
+ }
104
+ }
105
+ else if (scorecard.type === "POINTS" && scorecard.check_groups) {
106
+ const sortedGroups = [...scorecard.check_groups].sort((a, b) => a.ordering - b.ordering);
107
+ for (const group of sortedGroups) {
108
+ const checksInGroup = scorecard.checks.filter((check) => check.check_group?.id === group.id);
109
+ if (checksInGroup.length === 0)
110
+ continue;
111
+ blocks.push(ui.h3(group.name));
112
+ blocks.push(ui.ul(checksInGroup.map(scorecardCheckListItem)));
113
+ }
114
+ }
115
+ else if (scorecard.checks.length > 0) {
116
+ blocks.push(ui.ul(scorecard.checks.map(scorecardCheckListItem)));
117
+ }
118
+ return blocks;
119
+ }
120
+ function scorecardCheckListItem(check) {
121
+ let statusBadge;
122
+ if (check.status === "PASS") {
123
+ statusBadge = ui.success(`${ui.GLYPHS.CHECK} Passed`);
124
+ }
125
+ else if (check.status === "WARN") {
126
+ statusBadge = ui.warning(`${ui.GLYPHS.WARNING} Warning`);
127
+ }
128
+ else {
129
+ statusBadge = ui.error(`${ui.GLYPHS.ERROR} Not passed`);
130
+ }
131
+ const evalTime = check.executed_at
132
+ ? ui.dim(`(${dayjs(check.executed_at).fromNow()})`)
133
+ : "";
134
+ const parts = [check.name, evalTime, statusBadge].filter(Boolean);
135
+ return ui.li([ui.p(parts.join(" "), false)]);
136
+ }
137
+ function taskContent(task) {
138
+ const { check, initiative, owner } = task;
139
+ return [
140
+ ui.h3("Check:"),
141
+ ui.dl([
142
+ ui.dli("Name", [ui.p(check.name, false)]),
143
+ ui.dli("ID", [ui.p(ui.code(check.id), false)]),
144
+ ui.dli("External URL", [
145
+ ui.p(check.external_url ? ui.link(check.external_url) : ui.dim("(none)"), false),
146
+ ]),
147
+ ui.dli("Description", [
148
+ ui.p(check.description || ui.dim("(none)"), false),
149
+ ]),
150
+ ], { termWidth: 14 }),
151
+ ui.h3("Initiative:"),
152
+ ui.dl([
153
+ ui.dli("Name", [ui.p(initiative.name, false)]),
154
+ ui.dli("Priority", [ui.p(`P${initiative.priority}`, false)]),
155
+ ui.dli("Owner", [
156
+ ui.p(`${owner.name} (${ui.code(owner.email)})`, false),
157
+ ]),
158
+ ui.dli("Due", [ui.p(dueDateText(initiative.complete_by), false)]),
159
+ ], { termWidth: 10 }),
160
+ ui.p(ui.bold("Description:")),
161
+ ui.p(initiative.description || ui.dim("(none)")),
162
+ ];
163
+ }
164
+ function dueDateText(completeBy) {
165
+ const now = dayjs();
166
+ const dueDate = dayjs(completeBy);
167
+ const formattedDate = new Date(completeBy).toLocaleDateString(undefined, {
168
+ month: "short",
169
+ day: "numeric",
170
+ timeZone: "UTC",
171
+ });
172
+ if (dueDate.isBefore(now)) {
173
+ return ui.error(formattedDate);
174
+ }
175
+ else if (dueDate.isBefore(now.add(2, "week"))) {
176
+ return ui.warning(formattedDate);
177
+ }
178
+ else {
179
+ return formattedDate;
180
+ }
181
+ }
182
+ function coreContent(entity) {
183
+ return [
184
+ ui.dl([
185
+ ui.dli("Name", [ui.p(entity.name ?? ui.dim("(None)"), false)]),
186
+ ui.dli("Identifier", [ui.p(ui.code(entity.identifier), false)]),
187
+ ui.dli("Type", [ui.p(ui.code(entity.type), false)]),
188
+ ui.dli("Created", [
189
+ ui.p(ui.timestampSummary(entity.created_at), false),
190
+ ]),
191
+ ui.dli("Last updated", [ui.p(ui.timestampSummary(entity.updated_at))]),
192
+ ui.dli("Description", [
193
+ ui.p(entity.description ?? ui.dim("(None)"), false),
194
+ ]),
195
+ ], { termWidth: 14 }),
196
+ ];
197
+ }
198
+ function ownersContent(entity) {
199
+ const results = [];
200
+ const teamCount = entity.owner_teams?.length ?? 0;
201
+ const userCount = entity.owner_users?.length ?? 0;
202
+ if (teamCount === 0 && userCount === 0) {
203
+ results.push(ui.p(ui.dim("(No owners assigned)")));
204
+ }
205
+ if (entity.owner_teams && teamCount > 0) {
206
+ results.push(ui.p(ui.bold("Teams:")));
207
+ results.push(ui.ul([
208
+ ...entity.owner_teams.map((team) => ui.li([ui.p(`${team.name} (${team.id})`, false)])),
209
+ ]));
210
+ }
211
+ if (entity.owner_users && userCount > 0) {
212
+ results.push(ui.p(ui.bold("Users:")));
213
+ results.push(ui.ul([
214
+ ...entity.owner_users.map((user) => ui.li([ui.p(`${user.email} (${user.id})`, false)])),
215
+ ]));
216
+ }
217
+ return results;
218
+ }
219
+ function propertiesContent(entity) {
220
+ const propertiesListItems = [];
221
+ for (const [identifier, value] of Object.entries(entity.properties)) {
222
+ const serialized = propertyValueContent(identifier, value);
223
+ if (typeof serialized === "string") {
224
+ propertiesListItems.push(ui.li([ui.p(`${ui.bold(identifier)}: ${serialized}`, false)]));
225
+ }
226
+ else {
227
+ propertiesListItems.push(ui.li([ui.p(`${ui.bold(identifier)}:`, false), serialized]));
228
+ }
229
+ }
230
+ return [ui.ul(propertiesListItems)];
231
+ }
232
+ function aliasesContent(entity) {
233
+ return [ui.json({ ...entity.aliases })];
234
+ }
235
+ function propertyValueContent(identifier, value) {
236
+ // Property-specific
237
+ if (isFileMatchingRule(value)) {
238
+ const matchText = value.match_count === 1 ? "match" : "matches";
239
+ return `File matching rule: ${ui.code(value.status)} (${value.match_count} ${matchText})`;
240
+ }
241
+ else if (isUrl(value)) {
242
+ return ui.link(value);
243
+ }
244
+ // Primitives
245
+ if (value === null) {
246
+ return ui.code("null");
247
+ }
248
+ else if (typeof value === "string") {
249
+ return value;
250
+ }
251
+ else if (typeof value === "number") {
252
+ return value.toString();
253
+ }
254
+ else if (typeof value === "boolean") {
255
+ return value.toString();
256
+ }
257
+ // General arrays
258
+ if (isStringArray(value)) {
259
+ return value.join(", ");
260
+ }
261
+ // Fallback to JSON
262
+ return ui.json(value);
263
+ }
264
+ function isStringArray(value) {
265
+ return (Array.isArray(value) && value.every((item) => typeof item === "string"));
266
+ }
267
+ function isUrl(value) {
268
+ if (typeof value !== "string" || !value.startsWith("http")) {
269
+ return false;
270
+ }
271
+ try {
272
+ new URL(value);
273
+ return true;
274
+ }
275
+ catch {
276
+ return false;
277
+ }
278
+ }
279
+ function isFileMatchingRule(value) {
280
+ return (typeof value === "object" &&
281
+ value !== null &&
282
+ typeof value === "object" &&
283
+ "status" in value &&
284
+ typeof value.status === "string" &&
285
+ ["FOUND_MATCHES", "NO_MATCHES", "FILE_NOT_FOUND"].includes(value.status));
286
+ }
@@ -0,0 +1,51 @@
1
+ # Entity type configuration
2
+ #
3
+ # Edit this file, then run:
4
+ #
5
+ # ```
6
+ # dx catalog entityTypes create --from-file <path>
7
+ # ```
8
+ #
9
+ # After the entity type is created, make updates by running:
10
+ #
11
+ # ```
12
+ # dx catalog entityTypes update <identifier> --from-file <path>
13
+ # ```
14
+
15
+ identifier: ""
16
+ name: ""
17
+ description: ""
18
+ icon: server
19
+
20
+ # Properties for this entity type (optional)
21
+ properties:
22
+ - identifier: tier
23
+ name: Tier
24
+ type: select
25
+ description: null
26
+ definition:
27
+ options:
28
+ - value: Tier-1
29
+ color: "#ef4444"
30
+ - value: Tier-2
31
+ color: "#fbbf24"
32
+ - value: Tier-3
33
+ color: "#38bdf8"
34
+ is_required: false
35
+ visibility: visible
36
+ ordering: 0
37
+ - identifier: incident-runbook
38
+ name: Incident runbook
39
+ type: url
40
+ description: null
41
+ definition:
42
+ call_to_action: globe
43
+ call_to_action_type: icon
44
+ is_required: false
45
+ visibility: visible
46
+ ordering: 1
47
+
48
+ # Aliases available for this entity type (optional)
49
+ # aliases:
50
+ # github_repo: true
51
+ aliases: {}