@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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +155 -0
  3. package/dist/api-client.d.ts +23 -0
  4. package/dist/api-client.d.ts.map +1 -0
  5. package/dist/arguments.d.ts +18 -0
  6. package/dist/arguments.d.ts.map +1 -0
  7. package/dist/commands/actions.d.ts +3 -0
  8. package/dist/commands/actions.d.ts.map +1 -0
  9. package/dist/commands/files.d.ts +3 -0
  10. package/dist/commands/files.d.ts.map +1 -0
  11. package/dist/commands/index.d.ts +6 -0
  12. package/dist/commands/index.d.ts.map +1 -0
  13. package/dist/commands/metadata.d.ts +18 -0
  14. package/dist/commands/metadata.d.ts.map +1 -0
  15. package/dist/commands/objects.d.ts +3 -0
  16. package/dist/commands/objects.d.ts.map +1 -0
  17. package/dist/commands/ontology.d.ts +3 -0
  18. package/dist/commands/ontology.d.ts.map +1 -0
  19. package/dist/commands/project.d.ts +3 -0
  20. package/dist/commands/project.d.ts.map +1 -0
  21. package/dist/commands/runs.d.ts +3 -0
  22. package/dist/commands/runs.d.ts.map +1 -0
  23. package/dist/commands/shared.d.ts +15 -0
  24. package/dist/commands/shared.d.ts.map +1 -0
  25. package/dist/commands/telemetry.d.ts +3 -0
  26. package/dist/commands/telemetry.d.ts.map +1 -0
  27. package/dist/commands/workflows.d.ts +3 -0
  28. package/dist/commands/workflows.d.ts.map +1 -0
  29. package/dist/graph.d.ts +10 -0
  30. package/dist/graph.d.ts.map +1 -0
  31. package/dist/index.d.ts +11 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +1400 -0
  34. package/dist/index.js.map +26 -0
  35. package/dist/output.d.ts +21 -0
  36. package/dist/output.d.ts.map +1 -0
  37. package/dist/policies.d.ts +38 -0
  38. package/dist/policies.d.ts.map +1 -0
  39. package/dist/run.d.ts +20 -0
  40. package/dist/run.d.ts.map +1 -0
  41. package/dist/tsconfig.json +5 -0
  42. package/package.json +53 -0
  43. package/src/api-client.ts +177 -0
  44. package/src/arguments.ts +130 -0
  45. package/src/commands/actions.ts +94 -0
  46. package/src/commands/files.ts +52 -0
  47. package/src/commands/index.ts +57 -0
  48. package/src/commands/metadata.ts +212 -0
  49. package/src/commands/objects.ts +291 -0
  50. package/src/commands/ontology.ts +50 -0
  51. package/src/commands/project.ts +13 -0
  52. package/src/commands/runs.ts +84 -0
  53. package/src/commands/shared.ts +97 -0
  54. package/src/commands/telemetry.ts +96 -0
  55. package/src/commands/workflows.ts +34 -0
  56. package/src/graph.ts +359 -0
  57. package/src/index.ts +24 -0
  58. package/src/output.ts +51 -0
  59. package/src/policies.ts +17 -0
  60. 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
+ }