@sixb/cli-core 0.1.4
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 +155 -0
- package/dist/api-client.d.ts +23 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/arguments.d.ts +18 -0
- package/dist/arguments.d.ts.map +1 -0
- package/dist/commands/actions.d.ts +3 -0
- package/dist/commands/actions.d.ts.map +1 -0
- package/dist/commands/files.d.ts +3 -0
- package/dist/commands/files.d.ts.map +1 -0
- package/dist/commands/index.d.ts +6 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/metadata.d.ts +18 -0
- package/dist/commands/metadata.d.ts.map +1 -0
- package/dist/commands/objects.d.ts +3 -0
- package/dist/commands/objects.d.ts.map +1 -0
- package/dist/commands/ontology.d.ts +3 -0
- package/dist/commands/ontology.d.ts.map +1 -0
- package/dist/commands/project.d.ts +3 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/runs.d.ts +3 -0
- package/dist/commands/runs.d.ts.map +1 -0
- package/dist/commands/shared.d.ts +15 -0
- package/dist/commands/shared.d.ts.map +1 -0
- package/dist/commands/telemetry.d.ts +3 -0
- package/dist/commands/telemetry.d.ts.map +1 -0
- package/dist/commands/workflows.d.ts +3 -0
- package/dist/commands/workflows.d.ts.map +1 -0
- package/dist/graph.d.ts +10 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1400 -0
- package/dist/index.js.map +26 -0
- package/dist/output.d.ts +21 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/policies.d.ts +38 -0
- package/dist/policies.d.ts.map +1 -0
- package/dist/run.d.ts +20 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/tsconfig.json +5 -0
- package/package.json +53 -0
- package/src/api-client.ts +177 -0
- package/src/arguments.ts +130 -0
- package/src/commands/actions.ts +94 -0
- package/src/commands/files.ts +52 -0
- package/src/commands/index.ts +57 -0
- package/src/commands/metadata.ts +212 -0
- package/src/commands/objects.ts +291 -0
- package/src/commands/ontology.ts +50 -0
- package/src/commands/project.ts +13 -0
- package/src/commands/runs.ts +84 -0
- package/src/commands/shared.ts +97 -0
- package/src/commands/telemetry.ts +96 -0
- package/src/commands/workflows.ts +34 -0
- package/src/graph.ts +359 -0
- package/src/index.ts +24 -0
- package/src/output.ts +51 -0
- package/src/policies.ts +17 -0
- package/src/run.ts +46 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import type { ApiClient } from "../api-client"
|
|
2
|
+
import {
|
|
3
|
+
enumValue,
|
|
4
|
+
integerInRange,
|
|
5
|
+
isHelp,
|
|
6
|
+
parseCommandArgs,
|
|
7
|
+
requestsHelp,
|
|
8
|
+
requireOrderedRange,
|
|
9
|
+
rfc3339Value,
|
|
10
|
+
} from "../arguments"
|
|
11
|
+
import { inspectGraph } from "../graph"
|
|
12
|
+
import { fail, writeJson, writeText } from "../output"
|
|
13
|
+
import { CLI_LIMITS, DEFAULT_LIST_ORDER, DEFAULT_OBJECT_ORDER_BY } from "../policies"
|
|
14
|
+
import { FACETS_EXAMPLE, OBJECTS_HELP, QUERY_EXAMPLES, QUERY_HELP } from "./metadata"
|
|
15
|
+
import {
|
|
16
|
+
asRecord,
|
|
17
|
+
normalizeWindowOptions,
|
|
18
|
+
parseQueryOptions,
|
|
19
|
+
readJson,
|
|
20
|
+
singleFileOption,
|
|
21
|
+
} from "./shared"
|
|
22
|
+
|
|
23
|
+
export async function objects(api: ApiClient, args: readonly string[]): Promise<void> {
|
|
24
|
+
const [sub, ...rest] = args
|
|
25
|
+
if (!sub || isHelp(sub)) return writeText(OBJECTS_HELP)
|
|
26
|
+
switch (sub) {
|
|
27
|
+
case "inspect":
|
|
28
|
+
return objectsInspect(api, rest)
|
|
29
|
+
case "list":
|
|
30
|
+
return objectsList(api, rest)
|
|
31
|
+
case "get":
|
|
32
|
+
return objectsGet(api, rest)
|
|
33
|
+
case "search":
|
|
34
|
+
return objectsSearch(api, rest)
|
|
35
|
+
case "query":
|
|
36
|
+
return objectsQuery(api, rest)
|
|
37
|
+
case "count":
|
|
38
|
+
case "exists":
|
|
39
|
+
return objectsScalar(api, sub, rest)
|
|
40
|
+
case "facets":
|
|
41
|
+
return objectsFacets(api, rest)
|
|
42
|
+
case "links":
|
|
43
|
+
return objectsLinks(api, rest)
|
|
44
|
+
default:
|
|
45
|
+
fail(`Unknown objects command '${sub}'.`)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function objectsInspect(api: ApiClient, args: readonly string[]): Promise<void> {
|
|
50
|
+
if (requestsHelp(args)) return writeText(OBJECTS_HELP)
|
|
51
|
+
const { positionals, options } = parseCommandArgs(
|
|
52
|
+
args,
|
|
53
|
+
{
|
|
54
|
+
"--full": "boolean",
|
|
55
|
+
"--depth": "string",
|
|
56
|
+
"--max-objects": "string",
|
|
57
|
+
"--max-links": "string",
|
|
58
|
+
},
|
|
59
|
+
"objects inspect",
|
|
60
|
+
2
|
|
61
|
+
)
|
|
62
|
+
const objectTypeId = positionals[0] ?? ""
|
|
63
|
+
const primaryId = positionals[1] ?? ""
|
|
64
|
+
const depth = integerInRange(
|
|
65
|
+
"--depth",
|
|
66
|
+
options["--depth"] ?? String(CLI_LIMITS.inspect.depth.default),
|
|
67
|
+
0,
|
|
68
|
+
CLI_LIMITS.inspect.depth.maximum
|
|
69
|
+
)
|
|
70
|
+
const maxObjects = integerInRange(
|
|
71
|
+
"--max-objects",
|
|
72
|
+
options["--max-objects"] ?? String(CLI_LIMITS.inspect.objects.default),
|
|
73
|
+
1,
|
|
74
|
+
CLI_LIMITS.inspect.objects.maximum
|
|
75
|
+
)
|
|
76
|
+
const maxLinks = integerInRange(
|
|
77
|
+
"--max-links",
|
|
78
|
+
options["--max-links"] ?? String(CLI_LIMITS.inspect.links.default),
|
|
79
|
+
1,
|
|
80
|
+
CLI_LIMITS.inspect.links.maximum
|
|
81
|
+
)
|
|
82
|
+
const full = options["--full"] ?? false
|
|
83
|
+
writeJson(
|
|
84
|
+
await inspectGraph(api, objectTypeId, primaryId, {
|
|
85
|
+
depth,
|
|
86
|
+
maxObjects,
|
|
87
|
+
maxLinks,
|
|
88
|
+
full,
|
|
89
|
+
})
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function objectsList(api: ApiClient, args: readonly string[]): Promise<void> {
|
|
94
|
+
if (requestsHelp(args)) return writeText(OBJECTS_HELP)
|
|
95
|
+
const optionNames: Record<string, string> = {
|
|
96
|
+
"--type": "objectTypeId",
|
|
97
|
+
"--limit": "limit",
|
|
98
|
+
"--offset": "offset",
|
|
99
|
+
"--order-by": "orderBy",
|
|
100
|
+
"--order": "order",
|
|
101
|
+
"--id-prefix": "idPrefix",
|
|
102
|
+
"--id-suffix": "idSuffix",
|
|
103
|
+
"--created-after": "createdAfter",
|
|
104
|
+
"--created-before": "createdBefore",
|
|
105
|
+
"--updated-after": "updatedAfter",
|
|
106
|
+
"--updated-before": "updatedBefore",
|
|
107
|
+
}
|
|
108
|
+
const options = normalizeWindowOptions(parseQueryOptions(args, optionNames, "objects list"), {
|
|
109
|
+
defaultLimit: CLI_LIMITS.list.default,
|
|
110
|
+
maximumLimit: CLI_LIMITS.list.maximum,
|
|
111
|
+
defaultOrder: DEFAULT_LIST_ORDER,
|
|
112
|
+
offset: true,
|
|
113
|
+
})
|
|
114
|
+
options.orderBy = enumValue("--order-by", options.orderBy ?? DEFAULT_OBJECT_ORDER_BY, [
|
|
115
|
+
"createdAt",
|
|
116
|
+
"updatedAt",
|
|
117
|
+
"primaryId",
|
|
118
|
+
])
|
|
119
|
+
for (const [name, flag] of [
|
|
120
|
+
["createdAfter", "--created-after"],
|
|
121
|
+
["createdBefore", "--created-before"],
|
|
122
|
+
["updatedAfter", "--updated-after"],
|
|
123
|
+
["updatedBefore", "--updated-before"],
|
|
124
|
+
] as const) {
|
|
125
|
+
if (options[name] !== undefined) options[name] = rfc3339Value(flag, options[name])
|
|
126
|
+
}
|
|
127
|
+
requireOrderedRange(
|
|
128
|
+
"--created-after",
|
|
129
|
+
options.createdAfter,
|
|
130
|
+
"--created-before",
|
|
131
|
+
options.createdBefore
|
|
132
|
+
)
|
|
133
|
+
requireOrderedRange(
|
|
134
|
+
"--updated-after",
|
|
135
|
+
options.updatedAfter,
|
|
136
|
+
"--updated-before",
|
|
137
|
+
options.updatedBefore
|
|
138
|
+
)
|
|
139
|
+
writeJson(await api.get("/api/objects", options))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function objectsGet(api: ApiClient, args: readonly string[]): Promise<void> {
|
|
143
|
+
if (requestsHelp(args)) return writeText("Usage: sixb objects get <object-type> <primary-id>...")
|
|
144
|
+
const { positionals } = parseCommandArgs(args, {}, "objects get", [2, Number.POSITIVE_INFINITY])
|
|
145
|
+
const objectTypeId = positionals[0] ?? ""
|
|
146
|
+
writeJson(
|
|
147
|
+
await api.post("/api/objects/query", {
|
|
148
|
+
query: {
|
|
149
|
+
kind: "refs",
|
|
150
|
+
refs: positionals.slice(1).map((primaryId) => ({ objectTypeId, primaryId })),
|
|
151
|
+
},
|
|
152
|
+
includeTotal: false,
|
|
153
|
+
})
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function objectsSearch(api: ApiClient, args: readonly string[]): Promise<void> {
|
|
158
|
+
if (requestsHelp(args)) {
|
|
159
|
+
return writeText(`Usage: sixb objects search <text> [--limit <1-${CLI_LIMITS.search.maximum}>]`)
|
|
160
|
+
}
|
|
161
|
+
const { positionals, options } = parseCommandArgs(
|
|
162
|
+
args,
|
|
163
|
+
{ "--limit": "string" },
|
|
164
|
+
"objects search",
|
|
165
|
+
1
|
|
166
|
+
)
|
|
167
|
+
const limit = String(
|
|
168
|
+
integerInRange(
|
|
169
|
+
"--limit",
|
|
170
|
+
options["--limit"] ?? String(CLI_LIMITS.search.default),
|
|
171
|
+
1,
|
|
172
|
+
CLI_LIMITS.search.maximum
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
writeJson(await api.get("/api/objects/search", { q: positionals[0], limit }))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function objectsQuery(api: ApiClient, args: readonly string[]): Promise<void> {
|
|
179
|
+
if (requestsHelp(args)) return writeText(QUERY_HELP)
|
|
180
|
+
const { options } = parseCommandArgs(
|
|
181
|
+
args,
|
|
182
|
+
{
|
|
183
|
+
"--example": "string",
|
|
184
|
+
"--file": "string",
|
|
185
|
+
"--include-total": "boolean",
|
|
186
|
+
"--no-total": "boolean",
|
|
187
|
+
},
|
|
188
|
+
"objects query"
|
|
189
|
+
)
|
|
190
|
+
if (options["--include-total"] && options["--no-total"])
|
|
191
|
+
fail("--include-total and --no-total cannot be used together.")
|
|
192
|
+
if (options["--example"]) {
|
|
193
|
+
if (Object.keys(options).length !== 1) fail("--example cannot be combined with query options.")
|
|
194
|
+
const name = options["--example"]
|
|
195
|
+
if (name === "list") return writeText(Object.keys(QUERY_EXAMPLES).join(" "))
|
|
196
|
+
const example = QUERY_EXAMPLES[name]
|
|
197
|
+
if (!example) fail(`Unknown query example '${name}'. Run 'sixb objects query --example list'.`)
|
|
198
|
+
return writeText(example)
|
|
199
|
+
}
|
|
200
|
+
const source = options["--file"]
|
|
201
|
+
const includeTotal = options["--include-total"] ?? false
|
|
202
|
+
if (!source) fail("objects query requires --file <path|->.")
|
|
203
|
+
const input = await readJson(source)
|
|
204
|
+
const record = asRecord(input)
|
|
205
|
+
const body = Object.hasOwn(record, "query")
|
|
206
|
+
? { ...record, ...(Object.hasOwn(record, "includeTotal") ? {} : { includeTotal }) }
|
|
207
|
+
: { query: input, includeTotal }
|
|
208
|
+
writeJson(await api.post("/api/objects/query", body))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function objectsScalar(
|
|
212
|
+
api: ApiClient,
|
|
213
|
+
operation: "count" | "exists",
|
|
214
|
+
args: readonly string[]
|
|
215
|
+
): Promise<void> {
|
|
216
|
+
if (requestsHelp(args)) return writeText(`Usage: sixb objects ${operation} --file <path|->`)
|
|
217
|
+
const source = singleFileOption(args, `objects ${operation}`)
|
|
218
|
+
const input = await readJson(source)
|
|
219
|
+
const record = asRecord(input)
|
|
220
|
+
writeJson(
|
|
221
|
+
await api.post(`/api/objects/query/${operation}`, {
|
|
222
|
+
query: Object.hasOwn(record, "query") ? record.query : input,
|
|
223
|
+
})
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function objectsFacets(api: ApiClient, args: readonly string[]): Promise<void> {
|
|
228
|
+
if (requestsHelp(args)) {
|
|
229
|
+
return writeText(
|
|
230
|
+
"Usage: sixb objects facets --file <path|->\n sixb objects facets --example"
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
const { options } = parseCommandArgs(
|
|
234
|
+
args,
|
|
235
|
+
{ "--file": "string", "--example": "boolean" },
|
|
236
|
+
"objects facets"
|
|
237
|
+
)
|
|
238
|
+
if (options["--example"]) {
|
|
239
|
+
if (options["--file"]) fail("--example cannot be combined with --file.")
|
|
240
|
+
return writeText(FACETS_EXAMPLE)
|
|
241
|
+
}
|
|
242
|
+
if (!options["--file"]) fail("objects facets requires --file <path|->.")
|
|
243
|
+
const body = await readJson(options["--file"])
|
|
244
|
+
const record = asRecord(body)
|
|
245
|
+
if (!Object.hasOwn(record, "query") || !Object.hasOwn(record, "facets")) {
|
|
246
|
+
fail("objects facets input must contain query and facets.")
|
|
247
|
+
}
|
|
248
|
+
writeJson(await api.post("/api/objects/query/facets", body))
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function objectsLinks(api: ApiClient, args: readonly string[]): Promise<void> {
|
|
252
|
+
if (requestsHelp(args)) return writeText(OBJECTS_HELP)
|
|
253
|
+
const { positionals, options } = parseCommandArgs(
|
|
254
|
+
args,
|
|
255
|
+
{
|
|
256
|
+
"--link": "string",
|
|
257
|
+
"--direction": "string",
|
|
258
|
+
"--page-size": "string",
|
|
259
|
+
"--page-token": "string",
|
|
260
|
+
"--include-objects": "boolean",
|
|
261
|
+
},
|
|
262
|
+
"objects links",
|
|
263
|
+
2
|
|
264
|
+
)
|
|
265
|
+
const objectTypeId = positionals[0] ?? ""
|
|
266
|
+
const primaryId = positionals[1] ?? ""
|
|
267
|
+
const linkId = options["--link"]
|
|
268
|
+
const direction = enumValue("--direction", options["--direction"] ?? "both", [
|
|
269
|
+
"outgoing",
|
|
270
|
+
"incoming",
|
|
271
|
+
"both",
|
|
272
|
+
])
|
|
273
|
+
const pageSize = integerInRange(
|
|
274
|
+
"--page-size",
|
|
275
|
+
options["--page-size"] ?? String(CLI_LIMITS.linkPage.default),
|
|
276
|
+
1,
|
|
277
|
+
CLI_LIMITS.linkPage.maximum
|
|
278
|
+
)
|
|
279
|
+
const pageToken = options["--page-token"]
|
|
280
|
+
const includeObjects = options["--include-objects"] ?? false
|
|
281
|
+
writeJson(
|
|
282
|
+
await api.post("/api/objects/query/links", {
|
|
283
|
+
query: { kind: "refs", refs: [{ objectTypeId, primaryId }] },
|
|
284
|
+
direction,
|
|
285
|
+
includeObjects,
|
|
286
|
+
pageSize,
|
|
287
|
+
...(linkId ? { linkId } : {}),
|
|
288
|
+
...(pageToken ? { pageToken } : {}),
|
|
289
|
+
})
|
|
290
|
+
)
|
|
291
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ApiClient } from "../api-client"
|
|
2
|
+
import { isHelp, parseCommandArgs, requestsHelp } from "../arguments"
|
|
3
|
+
import { fail, writeJson, writeText } from "../output"
|
|
4
|
+
import { GROUP_HELP } from "./metadata"
|
|
5
|
+
import { asRecord, asRecords } from "./shared"
|
|
6
|
+
|
|
7
|
+
export async function ontology(api: ApiClient, args: readonly string[]): Promise<void> {
|
|
8
|
+
const [sub, ...rest] = args
|
|
9
|
+
if (!sub || isHelp(sub)) return writeText(GROUP_HELP.ontology)
|
|
10
|
+
if (sub === "list") {
|
|
11
|
+
if (requestsHelp(rest)) return writeText("Usage: sixb ontology list [--full]")
|
|
12
|
+
const { options } = parseCommandArgs(rest, { "--full": "boolean" }, "ontology list")
|
|
13
|
+
const full = options["--full"] ?? false
|
|
14
|
+
const value = await api.get("/api/object-types")
|
|
15
|
+
if (full) return writeJson(value)
|
|
16
|
+
if (!Array.isArray(value)) fail("The ontology API returned an invalid response.")
|
|
17
|
+
return writeJson(
|
|
18
|
+
value.map((entry) => {
|
|
19
|
+
const type = asRecord(entry)
|
|
20
|
+
const properties = asRecords(type.properties)
|
|
21
|
+
return {
|
|
22
|
+
id: type.id,
|
|
23
|
+
name: type.name,
|
|
24
|
+
description: type.description,
|
|
25
|
+
primaryPropertyId: properties.find((property) => property.primary === true)?.id,
|
|
26
|
+
links: asRecords(type.links).map(
|
|
27
|
+
({ id, name, description, targetObjectTypeId, cardinality }) => ({
|
|
28
|
+
id,
|
|
29
|
+
name,
|
|
30
|
+
...(description === undefined ? {} : { description }),
|
|
31
|
+
targetObjectTypeId,
|
|
32
|
+
cardinality,
|
|
33
|
+
})
|
|
34
|
+
),
|
|
35
|
+
actions: asRecords(type.actions).map(({ id, name, description }) => ({
|
|
36
|
+
id,
|
|
37
|
+
name,
|
|
38
|
+
...(description === undefined ? {} : { description }),
|
|
39
|
+
})),
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
if (sub === "get") {
|
|
45
|
+
if (requestsHelp(rest)) return writeText("Usage: sixb ontology get <object-type>")
|
|
46
|
+
const { positionals } = parseCommandArgs(rest, {}, "ontology get", 1)
|
|
47
|
+
return writeJson(await api.get(`/api/object-types/${encodeURIComponent(positionals[0] ?? "")}`))
|
|
48
|
+
}
|
|
49
|
+
fail(`Unknown ontology command '${sub}'.`)
|
|
50
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ApiClient } from "../api-client"
|
|
2
|
+
import { isHelp, parseCommandArgs, requestsHelp } from "../arguments"
|
|
3
|
+
import { fail, writeJson, writeText } from "../output"
|
|
4
|
+
import { GROUP_HELP } from "./metadata"
|
|
5
|
+
|
|
6
|
+
export async function project(api: ApiClient, args: readonly string[]): Promise<void> {
|
|
7
|
+
if (!args[0] || isHelp(args[0]) || (args[0] === "show" && requestsHelp(args.slice(1)))) {
|
|
8
|
+
return writeText(GROUP_HELP.project)
|
|
9
|
+
}
|
|
10
|
+
if (args[0] !== "show") fail(`Unknown project command '${args[0]}'.`)
|
|
11
|
+
parseCommandArgs(args.slice(1), {}, "project show")
|
|
12
|
+
writeJson(await api.get("/api/project"))
|
|
13
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { ApiClient } from "../api-client"
|
|
2
|
+
import {
|
|
3
|
+
enumValue,
|
|
4
|
+
isHelp,
|
|
5
|
+
parseCommandArgs,
|
|
6
|
+
requestsHelp,
|
|
7
|
+
requireOrderedRange,
|
|
8
|
+
rfc3339Value,
|
|
9
|
+
} from "../arguments"
|
|
10
|
+
import { fail, writeJson, writeText } from "../output"
|
|
11
|
+
import { CLI_LIMITS, DEFAULT_LIST_ORDER } from "../policies"
|
|
12
|
+
import { GROUP_HELP } from "./metadata"
|
|
13
|
+
import { normalizeWindowOptions, parseQueryOptions } from "./shared"
|
|
14
|
+
|
|
15
|
+
const ACTION_RUN_STATUSES = ["queued", "running", "succeeded", "failed", "cancelled"] as const
|
|
16
|
+
const WORKFLOW_RUN_STATUSES = [
|
|
17
|
+
"queued",
|
|
18
|
+
"running",
|
|
19
|
+
"waiting",
|
|
20
|
+
"succeeded",
|
|
21
|
+
"failed",
|
|
22
|
+
"cancelled",
|
|
23
|
+
] as const
|
|
24
|
+
|
|
25
|
+
export async function runs(
|
|
26
|
+
api: ApiClient,
|
|
27
|
+
kind: "action" | "workflow",
|
|
28
|
+
args: readonly string[]
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
const [sub, ...rest] = args
|
|
31
|
+
const group = `${kind}-runs` as "action-runs" | "workflow-runs"
|
|
32
|
+
if (!sub || isHelp(sub) || requestsHelp(rest)) return writeText(GROUP_HELP[group])
|
|
33
|
+
if (sub === "get") {
|
|
34
|
+
const { positionals } = parseCommandArgs(rest, {}, `${group} get`, 1)
|
|
35
|
+
return writeJson(await api.get(`/api/${group}/${encodeURIComponent(positionals[0] ?? "")}`))
|
|
36
|
+
}
|
|
37
|
+
if (sub === "list") {
|
|
38
|
+
const common = {
|
|
39
|
+
"--status": "status",
|
|
40
|
+
"--started-after": "startedAfter",
|
|
41
|
+
"--started-before": "startedBefore",
|
|
42
|
+
"--limit": "limit",
|
|
43
|
+
"--offset": "offset",
|
|
44
|
+
"--order": "order",
|
|
45
|
+
}
|
|
46
|
+
const action = {
|
|
47
|
+
...common,
|
|
48
|
+
"--action": "actionId",
|
|
49
|
+
"--type": "objectTypeId",
|
|
50
|
+
"--id": "primaryId",
|
|
51
|
+
}
|
|
52
|
+
const workflow = { ...common, "--workflow": "workflowId" }
|
|
53
|
+
const options = normalizeWindowOptions(
|
|
54
|
+
parseQueryOptions(rest, kind === "action" ? action : workflow, `${group} list`),
|
|
55
|
+
{
|
|
56
|
+
defaultLimit: CLI_LIMITS.list.default,
|
|
57
|
+
maximumLimit: CLI_LIMITS.list.maximum,
|
|
58
|
+
defaultOrder: DEFAULT_LIST_ORDER,
|
|
59
|
+
offset: true,
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
if (options.status !== undefined) {
|
|
63
|
+
options.status = enumValue(
|
|
64
|
+
"--status",
|
|
65
|
+
options.status,
|
|
66
|
+
kind === "action" ? ACTION_RUN_STATUSES : WORKFLOW_RUN_STATUSES
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
for (const [name, flag] of [
|
|
70
|
+
["startedAfter", "--started-after"],
|
|
71
|
+
["startedBefore", "--started-before"],
|
|
72
|
+
] as const) {
|
|
73
|
+
if (options[name] !== undefined) options[name] = rfc3339Value(flag, options[name])
|
|
74
|
+
}
|
|
75
|
+
requireOrderedRange(
|
|
76
|
+
"--started-after",
|
|
77
|
+
options.startedAfter,
|
|
78
|
+
"--started-before",
|
|
79
|
+
options.startedBefore
|
|
80
|
+
)
|
|
81
|
+
return writeJson(await api.get(`/api/${group}`, options))
|
|
82
|
+
}
|
|
83
|
+
fail(`Unknown ${group} command '${sub}'.`)
|
|
84
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises"
|
|
2
|
+
import { enumValue, integerInRange, nonNegativeInteger, parseCommandArgs } from "../arguments"
|
|
3
|
+
import { fail } from "../output"
|
|
4
|
+
|
|
5
|
+
export type QueryOptions = Record<string, string | undefined>
|
|
6
|
+
|
|
7
|
+
/** Parse flag/value pairs into API query parameters. */
|
|
8
|
+
|
|
9
|
+
export function parseQueryOptions(
|
|
10
|
+
args: readonly string[],
|
|
11
|
+
names: Readonly<Record<string, string>>,
|
|
12
|
+
command: string
|
|
13
|
+
): QueryOptions {
|
|
14
|
+
const kinds = Object.fromEntries(Object.keys(names).map((flag) => [flag, "string" as const]))
|
|
15
|
+
const { options } = parseCommandArgs(args, kinds, command)
|
|
16
|
+
const query: QueryOptions = {}
|
|
17
|
+
for (const [flag, value] of Object.entries(options)) {
|
|
18
|
+
const name = names[flag]
|
|
19
|
+
if (name) query[name] = value
|
|
20
|
+
}
|
|
21
|
+
return query
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function normalizeWindowOptions(
|
|
25
|
+
options: QueryOptions,
|
|
26
|
+
policy: {
|
|
27
|
+
readonly defaultLimit: number
|
|
28
|
+
readonly maximumLimit: number
|
|
29
|
+
readonly defaultOrder: "asc" | "desc"
|
|
30
|
+
readonly offset?: boolean
|
|
31
|
+
}
|
|
32
|
+
): QueryOptions {
|
|
33
|
+
const normalized: QueryOptions = {
|
|
34
|
+
...options,
|
|
35
|
+
limit: String(
|
|
36
|
+
integerInRange(
|
|
37
|
+
"--limit",
|
|
38
|
+
options.limit ?? String(policy.defaultLimit),
|
|
39
|
+
1,
|
|
40
|
+
policy.maximumLimit
|
|
41
|
+
)
|
|
42
|
+
),
|
|
43
|
+
order: enumValue("--order", options.order ?? policy.defaultOrder, ["asc", "desc"]),
|
|
44
|
+
}
|
|
45
|
+
if (policy.offset && options.offset !== undefined) {
|
|
46
|
+
normalized.offset = String(nonNegativeInteger("--offset", options.offset))
|
|
47
|
+
}
|
|
48
|
+
return normalized
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function singleFileOption(args: readonly string[], command: string): string {
|
|
52
|
+
const { options } = parseCommandArgs(args, { "--file": "string" }, command)
|
|
53
|
+
if (!options["--file"]) fail(`${command} requires --file <path|->.`)
|
|
54
|
+
return options["--file"]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function readJson(source: string): Promise<unknown> {
|
|
58
|
+
let text: string
|
|
59
|
+
try {
|
|
60
|
+
text = source === "-" ? await readStdin() : await readFile(source, "utf8")
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (isFileError(error, "ENOENT")) fail(`JSON file '${source}' does not exist.`)
|
|
63
|
+
throw error
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(text)
|
|
67
|
+
} catch {
|
|
68
|
+
fail(
|
|
69
|
+
source === "-"
|
|
70
|
+
? "Standard input is not valid JSON."
|
|
71
|
+
: `JSON file '${source}' is not valid JSON.`,
|
|
72
|
+
"invalid_json"
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function asRecord(value: unknown): Record<string, unknown> {
|
|
78
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
79
|
+
? (value as Record<string, unknown>)
|
|
80
|
+
: {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function asRecords(value: unknown): Record<string, unknown>[] {
|
|
84
|
+
return Array.isArray(value) ? value.map(asRecord) : []
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isFileError(error: unknown, code: string): error is NodeJS.ErrnoException {
|
|
88
|
+
return error instanceof Error && "code" in error && error.code === code
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function readStdin(): Promise<string> {
|
|
92
|
+
const chunks: Uint8Array[] = []
|
|
93
|
+
for await (const chunk of process.stdin) {
|
|
94
|
+
chunks.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk)
|
|
95
|
+
}
|
|
96
|
+
return new TextDecoder().decode(Buffer.concat(chunks))
|
|
97
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ApiClient } from "../api-client"
|
|
2
|
+
import {
|
|
3
|
+
enumValue,
|
|
4
|
+
integerInRange,
|
|
5
|
+
isHelp,
|
|
6
|
+
parseCommandArgs,
|
|
7
|
+
requestsHelp,
|
|
8
|
+
requireOrderedRange,
|
|
9
|
+
rfc3339Value,
|
|
10
|
+
} from "../arguments"
|
|
11
|
+
import { fail, writeJson, writeText } from "../output"
|
|
12
|
+
import { CLI_LIMITS, DEFAULT_TELEMETRY_ORDER } from "../policies"
|
|
13
|
+
import { GROUP_HELP } from "./metadata"
|
|
14
|
+
import { normalizeWindowOptions, readJson, singleFileOption } from "./shared"
|
|
15
|
+
|
|
16
|
+
export async function telemetry(api: ApiClient, args: readonly string[]): Promise<void> {
|
|
17
|
+
const [sub, ...rest] = args
|
|
18
|
+
if (!sub || isHelp(sub) || requestsHelp(rest)) return writeText(GROUP_HELP.telemetry)
|
|
19
|
+
if (sub === "latest") {
|
|
20
|
+
const { positionals } = parseCommandArgs(rest, {}, "telemetry latest", 3)
|
|
21
|
+
return writeJson(await api.get(telemetryPath(positionals, "latest")))
|
|
22
|
+
}
|
|
23
|
+
if (sub === "history") {
|
|
24
|
+
const { positionals, options } = parseCommandArgs(
|
|
25
|
+
rest,
|
|
26
|
+
{
|
|
27
|
+
"--from": "string",
|
|
28
|
+
"--to": "string",
|
|
29
|
+
"--limit": "string",
|
|
30
|
+
"--order": "string",
|
|
31
|
+
},
|
|
32
|
+
"telemetry history",
|
|
33
|
+
3
|
|
34
|
+
)
|
|
35
|
+
const query = normalizeWindowOptions(
|
|
36
|
+
{
|
|
37
|
+
from: options["--from"],
|
|
38
|
+
to: options["--to"],
|
|
39
|
+
limit: options["--limit"],
|
|
40
|
+
order: options["--order"],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
defaultLimit: CLI_LIMITS.telemetryHistory.default,
|
|
44
|
+
maximumLimit: CLI_LIMITS.telemetryHistory.maximum,
|
|
45
|
+
defaultOrder: DEFAULT_TELEMETRY_ORDER,
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
if (query.from !== undefined) query.from = rfc3339Value("--from", query.from)
|
|
49
|
+
if (query.to !== undefined) query.to = rfc3339Value("--to", query.to)
|
|
50
|
+
requireOrderedRange("--from", query.from, "--to", query.to)
|
|
51
|
+
return writeJson(await api.get(telemetryPath(positionals, "history"), query))
|
|
52
|
+
}
|
|
53
|
+
if (sub === "query") {
|
|
54
|
+
return writeJson(
|
|
55
|
+
await api.post(
|
|
56
|
+
"/api/telemetry/history",
|
|
57
|
+
normalizeTelemetryQueryInput(await readJson(singleFileOption(rest, "telemetry query")))
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
fail(`Unknown telemetry command '${sub}'.`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function telemetryPath(args: readonly string[], terminal: "latest" | "history"): string {
|
|
65
|
+
return `/api/objects/${encodeURIComponent(args[0] ?? "")}/${encodeURIComponent(args[1] ?? "")}/telemetry/${encodeURIComponent(args[2] ?? "")}/${terminal}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeTelemetryQueryInput(input: unknown): Record<string, unknown> {
|
|
69
|
+
if (Array.isArray(input) || typeof input !== "object" || input === null) {
|
|
70
|
+
fail("Telemetry query input must be a JSON object.")
|
|
71
|
+
}
|
|
72
|
+
const body = { ...input } as Record<string, unknown>
|
|
73
|
+
const rawLimit = body.limitPerSeries
|
|
74
|
+
if (rawLimit !== undefined && typeof rawLimit !== "number") {
|
|
75
|
+
fail("limitPerSeries must be a number.")
|
|
76
|
+
}
|
|
77
|
+
body.limitPerSeries = integerInRange(
|
|
78
|
+
"limitPerSeries",
|
|
79
|
+
String(rawLimit ?? CLI_LIMITS.telemetryHistory.default),
|
|
80
|
+
1,
|
|
81
|
+
CLI_LIMITS.telemetryHistory.maximum
|
|
82
|
+
)
|
|
83
|
+
const rawOrder = body.order
|
|
84
|
+
if (rawOrder !== undefined && typeof rawOrder !== "string") {
|
|
85
|
+
fail("order must be a string.")
|
|
86
|
+
}
|
|
87
|
+
body.order = enumValue("order", rawOrder ?? DEFAULT_TELEMETRY_ORDER, ["asc", "desc"])
|
|
88
|
+
for (const field of ["from", "to"] as const) {
|
|
89
|
+
const value = body[field]
|
|
90
|
+
if (value === undefined) continue
|
|
91
|
+
if (typeof value !== "string") fail(`${field} must be an RFC 3339 timestamp.`)
|
|
92
|
+
body[field] = rfc3339Value(field, value)
|
|
93
|
+
}
|
|
94
|
+
requireOrderedRange("from", body.from as string | undefined, "to", body.to as string | undefined)
|
|
95
|
+
return body
|
|
96
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ApiClient } from "../api-client"
|
|
2
|
+
import { isHelp, parseCommandArgs, requestsHelp } from "../arguments"
|
|
3
|
+
import { fail, writeJson, writeText } from "../output"
|
|
4
|
+
import { GROUP_HELP } from "./metadata"
|
|
5
|
+
import { readJson } from "./shared"
|
|
6
|
+
|
|
7
|
+
export async function workflows(api: ApiClient, args: readonly string[]): Promise<void> {
|
|
8
|
+
const [sub, ...rest] = args
|
|
9
|
+
if (!sub || isHelp(sub) || requestsHelp(rest)) return writeText(GROUP_HELP.workflows)
|
|
10
|
+
if (sub === "list") {
|
|
11
|
+
parseCommandArgs(rest, {}, "workflows list")
|
|
12
|
+
return writeJson(await api.get("/api/workflows"))
|
|
13
|
+
}
|
|
14
|
+
if (sub === "get") {
|
|
15
|
+
const {
|
|
16
|
+
positionals: [workflowId],
|
|
17
|
+
} = parseCommandArgs(rest, {}, "workflows get", 1)
|
|
18
|
+
return writeJson(await api.get(`/api/workflows/${encodeURIComponent(workflowId ?? "")}`))
|
|
19
|
+
}
|
|
20
|
+
if (sub === "start") {
|
|
21
|
+
const {
|
|
22
|
+
positionals: [workflowId],
|
|
23
|
+
options,
|
|
24
|
+
} = parseCommandArgs(rest, { "--file": "string" }, "workflows start", 1)
|
|
25
|
+
const input: unknown = options["--file"] ? await readJson(options["--file"]) : {}
|
|
26
|
+
if (Array.isArray(input) || typeof input !== "object" || input === null) {
|
|
27
|
+
fail("Workflow input must be a JSON object.")
|
|
28
|
+
}
|
|
29
|
+
return writeJson(
|
|
30
|
+
await api.post(`/api/workflows/${encodeURIComponent(workflowId ?? "")}/runs`, { input })
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
fail(`Unknown workflows command '${sub}'.`)
|
|
34
|
+
}
|