@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.
- package/README.md +44 -0
- package/dist/cli.js +50 -0
- package/dist/commandHelpers.js +110 -0
- package/dist/commands/auth.js +70 -0
- package/dist/commands/authRendering.js +40 -0
- package/dist/commands/catalog/entities.js +596 -0
- package/dist/commands/catalog/entitiesRendering.js +286 -0
- package/dist/commands/catalog/entity-type-blank-template.yaml +51 -0
- package/dist/commands/catalog/entityTypes.js +402 -0
- package/dist/commands/catalog/entityTypesRendering.js +159 -0
- package/dist/commands/catalog.js +11 -0
- package/dist/commands/scorecard-blank-template.yaml +82 -0
- package/dist/commands/scorecards.js +480 -0
- package/dist/commands/scorecardsRendering.js +219 -0
- package/dist/commands/studio/query.js +270 -0
- package/dist/commands/studio/queryRendering.js +99 -0
- package/dist/commands/studio.js +9 -0
- package/dist/config.js +38 -0
- package/dist/errors.js +24 -0
- package/dist/http.js +128 -0
- package/dist/index.js +3 -0
- package/dist/logger.js +70 -0
- package/dist/renderers.js +29 -0
- package/dist/runtime.js +22 -0
- package/dist/secrets.js +80 -0
- package/dist/types.js +1 -0
- package/dist/ui/blocks.js +94 -0
- package/dist/ui/glyphs.js +5 -0
- package/dist/ui.js +109 -0
- package/package.json +56 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
4
|
+
import { createExampleText, getContext, parsePositiveIntOption, wrapAction, } from "../../commandHelpers.js";
|
|
5
|
+
import { CliError, EXIT_CODES, HttpError } from "../../errors.js";
|
|
6
|
+
import { request } from "../../http.js";
|
|
7
|
+
import { renderJson, renderRichText } from "../../renderers.js";
|
|
8
|
+
import { buildRuntime } from "../../runtime.js";
|
|
9
|
+
import * as ui from "../../ui.js";
|
|
10
|
+
import { renderEntityType, renderEntityTypeDeleted, renderEntityTypeList, } from "./entityTypesRendering.js";
|
|
11
|
+
export function entityTypesCommand() {
|
|
12
|
+
const entityTypes = new Command()
|
|
13
|
+
.name("entityTypes")
|
|
14
|
+
.description("Manage catalog entity types");
|
|
15
|
+
entityTypes
|
|
16
|
+
.command("create")
|
|
17
|
+
.description("Create a new entity type from a YAML file or stdin. The `init` command can be used to generate a starting template.")
|
|
18
|
+
.option("--from-file <path>", "Read a YAML file and create an entity type from its contents")
|
|
19
|
+
.option("--from-stdin", "Read YAML from stdin and create an entity type from its contents")
|
|
20
|
+
.addHelpText("afterAll", createExampleText([
|
|
21
|
+
{
|
|
22
|
+
label: "Generate a blank template first",
|
|
23
|
+
command: "dx catalog entityTypes init ./my-entity-type.yaml",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
label: "Create an entity type from a YAML file",
|
|
27
|
+
command: "dx catalog entityTypes create --from-file ./my-entity-type.yaml",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
label: "Create an entity type from stdin",
|
|
31
|
+
command: "cat ./my-entity-type.yaml | dx catalog entityTypes create --from-stdin",
|
|
32
|
+
},
|
|
33
|
+
]))
|
|
34
|
+
.action(wrapAction(async (options, command) => {
|
|
35
|
+
const modeCount = [options.fromFile, options.fromStdin].filter(Boolean).length;
|
|
36
|
+
if (modeCount === 0) {
|
|
37
|
+
throw new CliError("One of --from-file or --from-stdin is required", EXIT_CODES.ARGUMENT_ERROR);
|
|
38
|
+
}
|
|
39
|
+
if (modeCount > 1) {
|
|
40
|
+
throw new CliError("--from-file and --from-stdin are mutually exclusive", EXIT_CODES.ARGUMENT_ERROR);
|
|
41
|
+
}
|
|
42
|
+
const runtime = buildRuntime(getContext(command));
|
|
43
|
+
let raw;
|
|
44
|
+
if (options.fromFile) {
|
|
45
|
+
raw = readYamlFile(options.fromFile);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
raw = await readYamlStdin();
|
|
49
|
+
}
|
|
50
|
+
const payload = buildCreatePayload(raw);
|
|
51
|
+
const response = await createEntityType(runtime, payload);
|
|
52
|
+
if (runtime.context.json) {
|
|
53
|
+
renderJson({ ok: true, entity_type: response.entity_type });
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
renderEntityType(response.entity_type, `${ui.success(ui.GLYPHS.CHECK)} Entity type created`);
|
|
57
|
+
}
|
|
58
|
+
}));
|
|
59
|
+
entityTypes
|
|
60
|
+
.command("delete")
|
|
61
|
+
.description("Delete an entity type from your software catalog")
|
|
62
|
+
.argument("<identifier>", "Entity type identifier")
|
|
63
|
+
.addHelpText("afterAll", createExampleText([
|
|
64
|
+
{
|
|
65
|
+
label: "Delete the `service` entity type",
|
|
66
|
+
command: "dx catalog entityTypes delete service",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
label: "Delete an entity type and return JSON",
|
|
70
|
+
command: "dx catalog entityTypes delete service --json",
|
|
71
|
+
},
|
|
72
|
+
]))
|
|
73
|
+
.action(wrapAction(async (identifier, _options, command) => {
|
|
74
|
+
const runtime = buildRuntime(getContext(command));
|
|
75
|
+
const response = await deleteEntityType(runtime, identifier);
|
|
76
|
+
if (runtime.context.json) {
|
|
77
|
+
renderJson(response);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
renderEntityTypeDeleted(response.entity_type);
|
|
81
|
+
}
|
|
82
|
+
}));
|
|
83
|
+
entityTypes
|
|
84
|
+
.command("info")
|
|
85
|
+
.argument("<identifier>", "Entity type identifier")
|
|
86
|
+
.option("--include <include>", "Show only these comma-separated sections: core, properties, aliases")
|
|
87
|
+
.addHelpText("afterAll", createExampleText([
|
|
88
|
+
{
|
|
89
|
+
label: "Fetch info for the `service` entity type",
|
|
90
|
+
command: "dx catalog entityTypes info service",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
label: "Fetch info and return as JSON",
|
|
94
|
+
command: "dx catalog entityTypes info service --json",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
label: "Fetch info but only include the `core` and `properties` sections",
|
|
98
|
+
command: "dx catalog entityTypes info service --include core,properties",
|
|
99
|
+
},
|
|
100
|
+
]))
|
|
101
|
+
.action(wrapAction(async (identifier, options, command) => {
|
|
102
|
+
const runtime = buildRuntime(getContext(command));
|
|
103
|
+
const response = await getEntityType(runtime, identifier);
|
|
104
|
+
const processedEntityType = processEntityTypeIncludes(response.entity_type, options);
|
|
105
|
+
if (runtime.context.json) {
|
|
106
|
+
renderJson(processedEntityType);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
renderEntityType(processedEntityType);
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
entityTypes
|
|
113
|
+
.command("init")
|
|
114
|
+
.description("Write an entity type YAML file, either from an existing entity type or as a blank template")
|
|
115
|
+
.argument("<path>", "File path to write the YAML to")
|
|
116
|
+
.option("--identifier <identifier>", "Fetch an existing entity type and use it as the template")
|
|
117
|
+
.addHelpText("afterAll", createExampleText([
|
|
118
|
+
{
|
|
119
|
+
label: "Write a blank entity type template",
|
|
120
|
+
command: "dx catalog entityTypes init ./my-entity-type.yaml",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
label: "Initialize from an existing entity type",
|
|
124
|
+
command: "dx catalog entityTypes init ./my-entity-type.yaml --identifier service",
|
|
125
|
+
},
|
|
126
|
+
]))
|
|
127
|
+
.action(wrapAction(async (path, options, command) => {
|
|
128
|
+
const runtime = buildRuntime(getContext(command));
|
|
129
|
+
if (options.identifier) {
|
|
130
|
+
const identifier = options.identifier;
|
|
131
|
+
let entityTypeResponse;
|
|
132
|
+
try {
|
|
133
|
+
entityTypeResponse = await getEntityType(runtime, identifier);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
const exitCode = err instanceof HttpError &&
|
|
137
|
+
err.status !== undefined &&
|
|
138
|
+
err.status < 500
|
|
139
|
+
? EXIT_CODES.ARGUMENT_ERROR
|
|
140
|
+
: EXIT_CODES.RETRY_RECOMMENDED;
|
|
141
|
+
throw new CliError(`Failed to fetch entity type "${identifier}": ${err instanceof Error ? err.message : String(err)}`, exitCode);
|
|
142
|
+
}
|
|
143
|
+
const yaml = entityTypeToYaml(entityTypeResponse.entity_type);
|
|
144
|
+
fs.writeFileSync(path, yaml, "utf8");
|
|
145
|
+
if (runtime.context.json) {
|
|
146
|
+
renderJson({ ok: true, identifier, path });
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
renderRichText([
|
|
150
|
+
ui.p(`${ui.success(ui.GLYPHS.CHECK)} Entity type written to ${ui.code(path)}.`),
|
|
151
|
+
ui.p(`Edit the file, then run: ${ui.code(`dx catalog entityTypes update ${identifier} --from-file ${path}`)}`),
|
|
152
|
+
]);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
fs.writeFileSync(path, ENTITY_TYPE_BLANK_TEMPLATE_YAML, "utf8");
|
|
157
|
+
if (runtime.context.json) {
|
|
158
|
+
renderJson({ ok: true, path });
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
renderRichText([
|
|
162
|
+
ui.p(`${ui.success(ui.GLYPHS.CHECK)} Blank template written to ${ui.code(path)}.`),
|
|
163
|
+
ui.p(`Edit the file, then run: ${ui.code(`dx catalog entityTypes create --from-file ${path}`)}`),
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}));
|
|
168
|
+
entityTypes
|
|
169
|
+
.command("list")
|
|
170
|
+
.description("List all entity types in your software catalog")
|
|
171
|
+
.option("--cursor <cursor>", "Cursor for the next page of results")
|
|
172
|
+
.option("--limit <n>", "Max entity types per page (default is 50)", (value) => parsePositiveIntOption(value, "--limit"))
|
|
173
|
+
.option("--include <include>", "Show only these comma-separated sections: core, properties, aliases")
|
|
174
|
+
.addHelpText("afterAll", createExampleText([
|
|
175
|
+
{
|
|
176
|
+
label: "List the first page of entity types",
|
|
177
|
+
command: "dx catalog entityTypes list",
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
label: "List with a limit and return JSON",
|
|
181
|
+
command: "dx catalog entityTypes list --limit 10 --json",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
label: "Fetch the next page using a cursor from the prior response",
|
|
185
|
+
command: "dx catalog entityTypes list --cursor avsgf30ccan3",
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
label: "List and only include the core section",
|
|
189
|
+
command: "dx catalog entityTypes list --include core",
|
|
190
|
+
},
|
|
191
|
+
]))
|
|
192
|
+
.action(wrapAction(async (options, command) => {
|
|
193
|
+
const runtime = buildRuntime(getContext(command));
|
|
194
|
+
const response = await listEntityTypes(runtime, {
|
|
195
|
+
cursor: options.cursor,
|
|
196
|
+
limit: options.limit,
|
|
197
|
+
});
|
|
198
|
+
const processedEntityTypes = response.entity_types.map((entityType) => processEntityTypeIncludes(entityType, options));
|
|
199
|
+
if (runtime.context.json) {
|
|
200
|
+
renderJson({ ...response, entity_types: processedEntityTypes });
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
renderEntityTypeList(processedEntityTypes, response.response_metadata?.next_cursor ?? null);
|
|
204
|
+
}
|
|
205
|
+
}));
|
|
206
|
+
entityTypes
|
|
207
|
+
.command("update")
|
|
208
|
+
.description("Update an existing entity type from a YAML file or stdin. The `init` command can be used to initialize the entity type file.")
|
|
209
|
+
.argument("<identifier>", "The unique identifier of the entity type")
|
|
210
|
+
.option("--from-file <path>", "Read a YAML file and update the entity type with its contents")
|
|
211
|
+
.option("--from-stdin", "Read YAML from stdin and update the entity type with its contents")
|
|
212
|
+
.addHelpText("afterAll", createExampleText([
|
|
213
|
+
{
|
|
214
|
+
label: "Initialize a file first",
|
|
215
|
+
command: "dx catalog entityTypes init --identifier service ./my-entity-type.yaml",
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
label: "Update an entity type from a YAML file",
|
|
219
|
+
command: "dx catalog entityTypes update service --from-file ./my-entity-type.yaml",
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
label: "Update an entity type from stdin",
|
|
223
|
+
command: "cat ./my-entity-type.yaml | dx catalog entityTypes update service --from-stdin",
|
|
224
|
+
},
|
|
225
|
+
]))
|
|
226
|
+
.action(wrapAction(async (identifier, options, command) => {
|
|
227
|
+
const modeCount = [options.fromFile, options.fromStdin].filter(Boolean).length;
|
|
228
|
+
if (modeCount === 0) {
|
|
229
|
+
throw new CliError("One of --from-file or --from-stdin is required", EXIT_CODES.ARGUMENT_ERROR);
|
|
230
|
+
}
|
|
231
|
+
if (modeCount > 1) {
|
|
232
|
+
throw new CliError("--from-file and --from-stdin are mutually exclusive", EXIT_CODES.ARGUMENT_ERROR);
|
|
233
|
+
}
|
|
234
|
+
const runtime = buildRuntime(getContext(command));
|
|
235
|
+
let raw;
|
|
236
|
+
if (options.fromFile) {
|
|
237
|
+
raw = readYamlFile(options.fromFile);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
raw = await readYamlStdin();
|
|
241
|
+
}
|
|
242
|
+
const payload = buildUpdatePayload(identifier, raw);
|
|
243
|
+
const response = await updateEntityType(runtime, payload);
|
|
244
|
+
if (runtime.context.json) {
|
|
245
|
+
renderJson({ ok: true, entity_type: response.entity_type });
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
renderEntityType(response.entity_type, `${ui.success(ui.GLYPHS.CHECK)} Entity type updated`);
|
|
249
|
+
}
|
|
250
|
+
}));
|
|
251
|
+
return entityTypes;
|
|
252
|
+
}
|
|
253
|
+
export async function getEntityType(runtime, identifier) {
|
|
254
|
+
const response = await request(runtime, "/catalog.entityTypes.info", {
|
|
255
|
+
method: "GET",
|
|
256
|
+
query: { identifier },
|
|
257
|
+
});
|
|
258
|
+
return response.body;
|
|
259
|
+
}
|
|
260
|
+
async function deleteEntityType(runtime, identifier) {
|
|
261
|
+
const response = await request(runtime, "/catalog.entityTypes.delete", {
|
|
262
|
+
method: "POST",
|
|
263
|
+
query: { identifier },
|
|
264
|
+
});
|
|
265
|
+
return response.body;
|
|
266
|
+
}
|
|
267
|
+
async function listEntityTypes(runtime, params) {
|
|
268
|
+
const query = {};
|
|
269
|
+
if (params.cursor !== undefined)
|
|
270
|
+
query.cursor = params.cursor;
|
|
271
|
+
if (params.limit !== undefined)
|
|
272
|
+
query.limit = params.limit;
|
|
273
|
+
const response = await request(runtime, "/catalog.entityTypes.list", {
|
|
274
|
+
method: "GET",
|
|
275
|
+
query,
|
|
276
|
+
});
|
|
277
|
+
return response.body;
|
|
278
|
+
}
|
|
279
|
+
async function createEntityType(runtime, payload) {
|
|
280
|
+
const response = await request(runtime, "/catalog.entityTypes.create", {
|
|
281
|
+
method: "POST",
|
|
282
|
+
body: payload,
|
|
283
|
+
});
|
|
284
|
+
return response.body;
|
|
285
|
+
}
|
|
286
|
+
async function updateEntityType(runtime, payload) {
|
|
287
|
+
const response = await request(runtime, "/catalog.entityTypes.update", {
|
|
288
|
+
method: "POST",
|
|
289
|
+
body: payload,
|
|
290
|
+
});
|
|
291
|
+
return response.body;
|
|
292
|
+
}
|
|
293
|
+
// --- Include helpers ---
|
|
294
|
+
const ENTITY_TYPE_INCLUDE_SECTIONS = ["core", "properties", "aliases"];
|
|
295
|
+
const ENTITY_TYPE_SECTION_KEYS = {
|
|
296
|
+
core: [
|
|
297
|
+
"identifier",
|
|
298
|
+
"name",
|
|
299
|
+
"description",
|
|
300
|
+
"icon",
|
|
301
|
+
"ordering",
|
|
302
|
+
"created_at",
|
|
303
|
+
"updated_at",
|
|
304
|
+
],
|
|
305
|
+
properties: ["properties"],
|
|
306
|
+
aliases: ["aliases"],
|
|
307
|
+
};
|
|
308
|
+
function processEntityTypeIncludes(entityType, options) {
|
|
309
|
+
const includeSections = parseEntityTypeIncludeSections(options.include);
|
|
310
|
+
if (includeSections.length === 0) {
|
|
311
|
+
return entityType;
|
|
312
|
+
}
|
|
313
|
+
const out = {};
|
|
314
|
+
for (const section of includeSections) {
|
|
315
|
+
for (const key of ENTITY_TYPE_SECTION_KEYS[section]) {
|
|
316
|
+
if (key in entityType) {
|
|
317
|
+
out[key] = entityType[key];
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
function parseEntityTypeIncludeSections(include) {
|
|
324
|
+
if (!include) {
|
|
325
|
+
return [];
|
|
326
|
+
}
|
|
327
|
+
const results = include.split(",");
|
|
328
|
+
for (const result of results) {
|
|
329
|
+
if (!ENTITY_TYPE_INCLUDE_SECTIONS.includes(result)) {
|
|
330
|
+
throw new CliError(`Invalid --include "${result}". Expected one of: ${ENTITY_TYPE_INCLUDE_SECTIONS.join(", ")}`, EXIT_CODES.ARGUMENT_ERROR);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return results;
|
|
334
|
+
}
|
|
335
|
+
// --- YAML helpers ---
|
|
336
|
+
const ENTITY_TYPE_BLANK_TEMPLATE_YAML = fs.readFileSync(new URL("./entity-type-blank-template.yaml", import.meta.url), "utf8");
|
|
337
|
+
const ENTITY_TYPE_IGNORED_KEYS = [
|
|
338
|
+
// Reporting on state
|
|
339
|
+
"created_at",
|
|
340
|
+
"updated_at",
|
|
341
|
+
];
|
|
342
|
+
const PROPERTY_IGNORED_KEYS = [
|
|
343
|
+
// Reporting on state
|
|
344
|
+
"created_at",
|
|
345
|
+
"updated_at",
|
|
346
|
+
];
|
|
347
|
+
function entityTypeToYaml(entityType) {
|
|
348
|
+
const obj = { ...entityType };
|
|
349
|
+
for (const key of ENTITY_TYPE_IGNORED_KEYS) {
|
|
350
|
+
delete obj[key];
|
|
351
|
+
}
|
|
352
|
+
if (obj.properties) {
|
|
353
|
+
obj.properties = obj.properties.map((prop) => {
|
|
354
|
+
const p = { ...prop };
|
|
355
|
+
for (const key of PROPERTY_IGNORED_KEYS) {
|
|
356
|
+
delete p[key];
|
|
357
|
+
}
|
|
358
|
+
return p;
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return stringifyYaml(obj, { blockQuote: "literal" });
|
|
362
|
+
}
|
|
363
|
+
function readYamlFile(filePath) {
|
|
364
|
+
let content;
|
|
365
|
+
try {
|
|
366
|
+
content = fs.readFileSync(filePath, "utf8");
|
|
367
|
+
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
throw new CliError(`Could not read file "${filePath}": ${err.message}`, EXIT_CODES.ARGUMENT_ERROR);
|
|
370
|
+
}
|
|
371
|
+
return parseYaml(content);
|
|
372
|
+
}
|
|
373
|
+
async function readYamlStdin() {
|
|
374
|
+
return new Promise((resolve, reject) => {
|
|
375
|
+
const chunks = [];
|
|
376
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
377
|
+
process.stdin.on("end", () => {
|
|
378
|
+
const content = Buffer.concat(chunks).toString("utf8");
|
|
379
|
+
try {
|
|
380
|
+
resolve(parseYaml(content));
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
reject(new CliError(`Failed to parse YAML from stdin: ${err.message}`, EXIT_CODES.ARGUMENT_ERROR));
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
process.stdin.on("error", (err) => {
|
|
387
|
+
reject(new CliError(`Failed to read from stdin: ${err.message}`, EXIT_CODES.ARGUMENT_ERROR));
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
function buildCreatePayload(raw) {
|
|
392
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
393
|
+
throw new CliError("YAML content must be an object", EXIT_CODES.ARGUMENT_ERROR);
|
|
394
|
+
}
|
|
395
|
+
return raw;
|
|
396
|
+
}
|
|
397
|
+
function buildUpdatePayload(identifier, raw) {
|
|
398
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
399
|
+
throw new CliError("YAML content must be an object", EXIT_CODES.ARGUMENT_ERROR);
|
|
400
|
+
}
|
|
401
|
+
return { ...raw, identifier };
|
|
402
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { renderRichText } from "../../renderers.js";
|
|
2
|
+
import * as ui from "../../ui.js";
|
|
3
|
+
export function renderEntityTypeList(entityTypes, nextCursor) {
|
|
4
|
+
const blocks = [ui.h1("Entity Types")];
|
|
5
|
+
blocks.push(ui.p(`Displaying ${ui.bold(entityTypes.length.toString())} entity types.`));
|
|
6
|
+
if (nextCursor) {
|
|
7
|
+
blocks.push(ui.p(`Next cursor: ${ui.code(nextCursor)}`));
|
|
8
|
+
}
|
|
9
|
+
for (const entityType of entityTypes) {
|
|
10
|
+
if (entityType.name && entityType.identifier) {
|
|
11
|
+
blocks.push(ui.h2(`${entityType.name} (${ui.code(entityType.identifier)})`));
|
|
12
|
+
}
|
|
13
|
+
else if (entityType.identifier) {
|
|
14
|
+
blocks.push(ui.h2(ui.code(entityType.identifier)));
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
blocks.push(ui.h2("Entity Type"));
|
|
18
|
+
}
|
|
19
|
+
if (entityType.identifier) {
|
|
20
|
+
blocks.push(ui.h3("Core attributes"));
|
|
21
|
+
blocks.push(...coreContent(entityType));
|
|
22
|
+
}
|
|
23
|
+
if (entityType.properties) {
|
|
24
|
+
blocks.push(ui.h3("Properties"));
|
|
25
|
+
blocks.push(...propertiesContent(entityType.properties));
|
|
26
|
+
}
|
|
27
|
+
if (entityType.aliases) {
|
|
28
|
+
blocks.push(ui.h3("Aliases"));
|
|
29
|
+
blocks.push(...aliasesContent(entityType.aliases));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
renderRichText(blocks);
|
|
33
|
+
}
|
|
34
|
+
export function renderEntityType(entityType, title = "Entity Type Information") {
|
|
35
|
+
renderRichText([
|
|
36
|
+
ui.h1(title),
|
|
37
|
+
entityType.identifier
|
|
38
|
+
? [ui.h2("Core attributes"), ...coreContent(entityType)]
|
|
39
|
+
: null,
|
|
40
|
+
entityType.properties
|
|
41
|
+
? [ui.h2("Properties"), ...propertiesContent(entityType.properties)]
|
|
42
|
+
: null,
|
|
43
|
+
entityType.aliases
|
|
44
|
+
? [ui.h2("Aliases"), ...aliasesContent(entityType.aliases)]
|
|
45
|
+
: null,
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
export function renderEntityTypeDeleted(entityType) {
|
|
49
|
+
renderRichText([
|
|
50
|
+
ui.h1(`${ui.success("✓")} Entity type deleted`),
|
|
51
|
+
ui.p(`The entity type ${ui.code(entityType.identifier)} has been deleted.`),
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
function coreContent(entityType) {
|
|
55
|
+
return [
|
|
56
|
+
ui.dl([
|
|
57
|
+
ui.dli("Name", [ui.p(entityType.name ?? ui.dim("(None)"), false)]),
|
|
58
|
+
ui.dli("Identifier", [ui.p(ui.code(entityType.identifier), false)]),
|
|
59
|
+
ui.dli("Description", [
|
|
60
|
+
ui.p(entityType.description || ui.dim("(None)"), false),
|
|
61
|
+
]),
|
|
62
|
+
ui.dli("Icon", [ui.p(entityType.icon ?? ui.dim("(None)"), false)]),
|
|
63
|
+
ui.dli("Ordering", [
|
|
64
|
+
ui.p(entityType.ordering?.toString() ?? ui.dim("(None)"), false),
|
|
65
|
+
]),
|
|
66
|
+
ui.dli("Created", [
|
|
67
|
+
ui.p(ui.timestampSummary(entityType.created_at), false),
|
|
68
|
+
]),
|
|
69
|
+
ui.dli("Last updated", [
|
|
70
|
+
ui.p(ui.timestampSummary(entityType.updated_at)),
|
|
71
|
+
]),
|
|
72
|
+
], { termWidth: 14 }),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
function propertiesContent(properties) {
|
|
76
|
+
if (properties.length === 0) {
|
|
77
|
+
return [ui.p(ui.dim("(No properties defined)"))];
|
|
78
|
+
}
|
|
79
|
+
return [ui.ul(properties.map(propertyListItem))];
|
|
80
|
+
}
|
|
81
|
+
function propertyListItem(property) {
|
|
82
|
+
const badges = [];
|
|
83
|
+
if (property.visibility === "hidden")
|
|
84
|
+
badges.push(ui.dim("[hidden]"));
|
|
85
|
+
if (property.is_required)
|
|
86
|
+
badges.push(ui.bold("[required]"));
|
|
87
|
+
const header = [
|
|
88
|
+
ui.bold(property.name),
|
|
89
|
+
`(${ui.code(property.identifier)}):`,
|
|
90
|
+
ui.code(property.type),
|
|
91
|
+
...badges,
|
|
92
|
+
].join(" ");
|
|
93
|
+
const lines = [ui.p(header, false)];
|
|
94
|
+
if (property.description) {
|
|
95
|
+
lines.push(ui.p(ui.dim(property.description), false));
|
|
96
|
+
}
|
|
97
|
+
lines.push(...propertyDefinitionLines(property));
|
|
98
|
+
return ui.li(lines);
|
|
99
|
+
}
|
|
100
|
+
function propertyDefinitionLines(property) {
|
|
101
|
+
const { type, definition } = property;
|
|
102
|
+
if (type === "select" || type === "multi_select") {
|
|
103
|
+
const options = definition.options;
|
|
104
|
+
if (options && options.length > 0) {
|
|
105
|
+
return [
|
|
106
|
+
ui.p(`Options: ${options.map((o) => o.value).join(", ")}`, false),
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (type === "computed") {
|
|
111
|
+
const sql = definition.sql;
|
|
112
|
+
const outputType = definition.output_type;
|
|
113
|
+
const aggregation = definition.output_aggregation;
|
|
114
|
+
const lines = [];
|
|
115
|
+
if (outputType) {
|
|
116
|
+
const outputLine = aggregation
|
|
117
|
+
? `Output: ${ui.code(outputType)} ${ui.dim(`(aggregation: ${aggregation})`)}`
|
|
118
|
+
: `Output: ${ui.code(outputType)}`;
|
|
119
|
+
lines.push(ui.p(outputLine, false));
|
|
120
|
+
}
|
|
121
|
+
if (sql) {
|
|
122
|
+
const firstLine = sql.split("\n")[0];
|
|
123
|
+
const isLong = firstLine.length > 80 || sql.includes("\n");
|
|
124
|
+
const truncated = firstLine.length > 80 ? firstLine.slice(0, 80) : firstLine;
|
|
125
|
+
const display = isLong ? truncated + ui.dim(" …") : truncated;
|
|
126
|
+
lines.push(ui.p(`SQL: ${ui.code(display)}`, false));
|
|
127
|
+
}
|
|
128
|
+
return lines;
|
|
129
|
+
}
|
|
130
|
+
if (type === "file_matching_rule") {
|
|
131
|
+
const filePath = definition.file_path;
|
|
132
|
+
const ruleType = definition.rule_type;
|
|
133
|
+
const matchExpression = definition.match_expression;
|
|
134
|
+
return [
|
|
135
|
+
ui.p(`Rule type: ${ui.code(ruleType ?? "(none)")}`, false),
|
|
136
|
+
ui.p(`File path: ${ui.code(filePath ?? "(none)")}`, false),
|
|
137
|
+
ui.p(`Expression: ${ui.code(matchExpression ?? "(none)")}`, false),
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
if (type === "url") {
|
|
141
|
+
const cta = definition.call_to_action;
|
|
142
|
+
const ctaType = definition.call_to_action_type;
|
|
143
|
+
if (cta) {
|
|
144
|
+
return [ui.p(`CTA: ${ui.code(cta)} ${ui.dim(`(${ctaType})`)}`, false)];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (Object.keys(definition).length === 0) {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
return [ui.json(definition)];
|
|
151
|
+
}
|
|
152
|
+
function aliasesContent(aliases) {
|
|
153
|
+
if (Object.keys(aliases).length === 0) {
|
|
154
|
+
return [ui.p(ui.dim("(No aliases defined)"))];
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
return [ui.ul(Object.keys(aliases).map((aliasKey) => ui.li(aliasKey)))];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { entitiesCommand } from "./catalog/entities.js";
|
|
3
|
+
import { entityTypesCommand } from "./catalog/entityTypes.js";
|
|
4
|
+
export function catalogCommand() {
|
|
5
|
+
const catalog = new Command()
|
|
6
|
+
.name("catalog")
|
|
7
|
+
.description("Manage the Software Catalog");
|
|
8
|
+
catalog.addCommand(entitiesCommand());
|
|
9
|
+
catalog.addCommand(entityTypesCommand());
|
|
10
|
+
return catalog;
|
|
11
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Scorecard configuration
|
|
2
|
+
#
|
|
3
|
+
# Edit this file, then run:
|
|
4
|
+
#
|
|
5
|
+
# ```
|
|
6
|
+
# dx scorecards create --from-file <path>
|
|
7
|
+
# ```
|
|
8
|
+
#
|
|
9
|
+
# After the scorecard is created, make updates by running:
|
|
10
|
+
#
|
|
11
|
+
# ```
|
|
12
|
+
# dx scorecards update --id <id> --from-file <path>
|
|
13
|
+
# ```
|
|
14
|
+
|
|
15
|
+
name: ""
|
|
16
|
+
description: ""
|
|
17
|
+
published: false
|
|
18
|
+
|
|
19
|
+
# type: "LEVEL" (levels-based) or "POINTS" (points-based)
|
|
20
|
+
type: LEVEL
|
|
21
|
+
|
|
22
|
+
# Filter which entities this scorecard assesses.
|
|
23
|
+
# entity_filter_type: "entity_types" or "sql"
|
|
24
|
+
entity_filter_type: entity_types
|
|
25
|
+
# List of entity type identifiers to include (when entity_filter_type is "entity_types")
|
|
26
|
+
entity_filter_type_identifiers:
|
|
27
|
+
- service
|
|
28
|
+
# SQL filter expression (when entity_filter_type is "sql")
|
|
29
|
+
entity_filter_sql: ""
|
|
30
|
+
|
|
31
|
+
# User IDs of editors
|
|
32
|
+
editors: []
|
|
33
|
+
|
|
34
|
+
# Tags to apply to the scorecard
|
|
35
|
+
tags: []
|
|
36
|
+
|
|
37
|
+
# --- LEVEL-type fields (remove if using POINTS type) ---
|
|
38
|
+
empty_level_label: "Incomplete"
|
|
39
|
+
empty_level_color: "#cbd5e1"
|
|
40
|
+
levels:
|
|
41
|
+
- key: bronze
|
|
42
|
+
name: Bronze
|
|
43
|
+
color: "#fb923c"
|
|
44
|
+
rank: 1
|
|
45
|
+
- key: silver
|
|
46
|
+
name: Silver
|
|
47
|
+
color: "#9ca3af"
|
|
48
|
+
rank: 2
|
|
49
|
+
- key: gold
|
|
50
|
+
name: Gold
|
|
51
|
+
color: "#fbbf24"
|
|
52
|
+
rank: 3
|
|
53
|
+
|
|
54
|
+
# --- POINTS-type fields (remove if using LEVEL type) ---
|
|
55
|
+
# check_groups:
|
|
56
|
+
# - key: reliability
|
|
57
|
+
# name: Reliability
|
|
58
|
+
# ordering: 0
|
|
59
|
+
|
|
60
|
+
# Checks apply to both LEVEL and POINTS scorecards
|
|
61
|
+
checks:
|
|
62
|
+
- name: ""
|
|
63
|
+
description: ""
|
|
64
|
+
sql: |-
|
|
65
|
+
select 'PASS' as status
|
|
66
|
+
ordering: 0
|
|
67
|
+
published: false
|
|
68
|
+
estimated_dev_days: null
|
|
69
|
+
external_url: ""
|
|
70
|
+
# Entity filter: restrict which entities this check runs against
|
|
71
|
+
filter_sql: ""
|
|
72
|
+
filter_message: ""
|
|
73
|
+
# Output settings: capture a value from the check query
|
|
74
|
+
output_enabled: false
|
|
75
|
+
output_type: null
|
|
76
|
+
output_custom_options: null
|
|
77
|
+
output_aggregation: null
|
|
78
|
+
# For LEVEL type: reference a level key from the levels list above
|
|
79
|
+
scorecard_level_key: bronze
|
|
80
|
+
# For POINTS type: reference a check group key and assign points
|
|
81
|
+
# scorecard_check_group_key: reliability
|
|
82
|
+
# points: 10
|