@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
package/src/graph.ts ADDED
@@ -0,0 +1,359 @@
1
+ import type { ApiClient } from "./api-client"
2
+ import { CliError, EXIT_API } from "./output"
3
+ import { CLI_LIMITS } from "./policies"
4
+
5
+ /** Build the bounded object graph returned by `objects inspect`. */
6
+
7
+ interface ObjectRef {
8
+ readonly objectTypeId: string
9
+ readonly primaryId: string
10
+ }
11
+
12
+ interface LocatedRef extends ObjectRef {
13
+ readonly distance: number
14
+ }
15
+
16
+ interface MaterializedObject extends ObjectRef {
17
+ readonly createdAt?: unknown
18
+ readonly updatedAt?: unknown
19
+ readonly [key: string]: unknown
20
+ }
21
+
22
+ interface PhysicalLink {
23
+ readonly sourceTypeId: string
24
+ readonly sourceId: string
25
+ readonly linkId: string
26
+ readonly targetTypeId: string
27
+ readonly targetId: string
28
+ readonly properties?: unknown
29
+ }
30
+
31
+ interface LinksResponse {
32
+ readonly objects: MaterializedObject[]
33
+ readonly links: Array<{
34
+ readonly source: ObjectRef
35
+ readonly linkId: string
36
+ readonly target: ObjectRef
37
+ readonly properties?: unknown
38
+ }>
39
+ readonly hasMore: boolean
40
+ readonly nextPageToken?: string
41
+ }
42
+
43
+ interface InspectGraphOptions {
44
+ readonly depth: number
45
+ readonly maxObjects: number
46
+ readonly maxLinks: number
47
+ readonly full: boolean
48
+ }
49
+
50
+ export async function inspectGraph(
51
+ api: ApiClient,
52
+ objectTypeId: string,
53
+ primaryId: string,
54
+ options: InspectGraphOptions
55
+ ): Promise<unknown> {
56
+ if (options.depth === 0) return inspectRoot(api, objectTypeId, primaryId, options)
57
+
58
+ let refs: LocatedRef[] = [{ objectTypeId, primaryId, distance: 0 }]
59
+ let frontier = refs
60
+ const objects = new Map<string, MaterializedObject>()
61
+ const links = new Map<string, PhysicalLink>()
62
+ let objectsTruncated = false
63
+ let linksTruncated = false
64
+ let pagesTruncated = false
65
+ let pagesRead = 0
66
+ let linksExamined = 0
67
+
68
+ for (let level = 0; level < options.depth && frontier.length > 0; level += 1) {
69
+ const levelObjects = new Map<string, MaterializedObject>()
70
+ const levelLinks = new Map<string, PhysicalLink>()
71
+ const seenPageTokens = new Set<string>()
72
+ let pageToken: string | undefined
73
+ let continuePaging = true
74
+
75
+ while (continuePaging) {
76
+ const remainingLinks = options.maxLinks - linksExamined
77
+ if (remainingLinks <= 0) {
78
+ linksTruncated = true
79
+ break
80
+ }
81
+ if (pagesRead >= CLI_LIMITS.inspect.maximumPages) {
82
+ pagesTruncated = true
83
+ break
84
+ }
85
+ const response = asLinksResponse(
86
+ await api.post("/api/objects/query/links", {
87
+ query: {
88
+ kind: "refs",
89
+ refs: frontier.map(({ objectTypeId: type, primaryId: id }) => ({
90
+ objectTypeId: type,
91
+ primaryId: id,
92
+ })),
93
+ },
94
+ direction: "both",
95
+ includeObjects: true,
96
+ pageSize: Math.min(CLI_LIMITS.linkPage.default, remainingLinks),
97
+ ...(pageToken ? { pageToken } : {}),
98
+ })
99
+ )
100
+ pagesRead += 1
101
+ const pageLinks = response.links.slice(0, remainingLinks)
102
+ linksExamined += pageLinks.length
103
+ linksTruncated ||= response.links.length > pageLinks.length
104
+ const relevantObjects = new Set(frontier.map(refKey))
105
+ for (const link of pageLinks) {
106
+ relevantObjects.add(refKey(link.source))
107
+ relevantObjects.add(refKey(link.target))
108
+ }
109
+ for (const object of response.objects) {
110
+ if (relevantObjects.has(refKey(object))) levelObjects.set(refKey(object), object)
111
+ }
112
+ for (const link of pageLinks) {
113
+ const physical = {
114
+ sourceTypeId: link.source.objectTypeId,
115
+ sourceId: link.source.primaryId,
116
+ linkId: link.linkId,
117
+ targetTypeId: link.target.objectTypeId,
118
+ targetId: link.target.primaryId,
119
+ ...(Object.hasOwn(link, "properties") ? { properties: link.properties } : {}),
120
+ }
121
+ levelLinks.set(linkKey(physical), physical)
122
+ }
123
+ if (!response.hasMore) {
124
+ continuePaging = false
125
+ continue
126
+ }
127
+ if (linksExamined >= options.maxLinks) {
128
+ linksTruncated = true
129
+ break
130
+ }
131
+ if (pagesRead >= CLI_LIMITS.inspect.maximumPages) {
132
+ pagesTruncated = true
133
+ break
134
+ }
135
+ if (!response.nextPageToken) {
136
+ invalidApiResponse("The object-links API reported another page without a nextPageToken.")
137
+ }
138
+ if (seenPageTokens.has(response.nextPageToken)) {
139
+ invalidApiResponse(
140
+ "The object-links API repeated a nextPageToken while inspecting the graph."
141
+ )
142
+ }
143
+ seenPageTokens.add(response.nextPageToken)
144
+ pageToken = response.nextPageToken
145
+ }
146
+
147
+ for (const [key, object] of levelObjects) objects.set(key, object)
148
+ for (const [key, link] of levelLinks) links.set(key, link)
149
+
150
+ const frontierKeys = new Set(frontier.map(refKey))
151
+ const candidates: LocatedRef[] = []
152
+ for (const link of levelLinks.values()) {
153
+ const source = { objectTypeId: link.sourceTypeId, primaryId: link.sourceId }
154
+ const target = { objectTypeId: link.targetTypeId, primaryId: link.targetId }
155
+ if (frontierKeys.has(refKey(source))) candidates.push({ ...target, distance: level + 1 })
156
+ else if (frontierKeys.has(refKey(target))) candidates.push({ ...source, distance: level + 1 })
157
+ }
158
+
159
+ const seen = new Set(refs.map(refKey))
160
+ const all = dedupeRefs([...refs, ...candidates])
161
+ objectsTruncated ||= all.length > options.maxObjects
162
+ refs = all.slice(0, options.maxObjects)
163
+ frontier = linksTruncated || pagesTruncated ? [] : refs.filter((ref) => !seen.has(refKey(ref)))
164
+ }
165
+
166
+ const kept = new Set(refs.map(refKey))
167
+ const filteredLinks = [...links.values()]
168
+ .filter(
169
+ (link) =>
170
+ kept.has(refKey({ objectTypeId: link.sourceTypeId, primaryId: link.sourceId })) &&
171
+ kept.has(refKey({ objectTypeId: link.targetTypeId, primaryId: link.targetId }))
172
+ )
173
+ .sort(compareLinks)
174
+ .map((link) =>
175
+ link.properties == null
176
+ ? {
177
+ sourceTypeId: link.sourceTypeId,
178
+ sourceId: link.sourceId,
179
+ linkId: link.linkId,
180
+ targetTypeId: link.targetTypeId,
181
+ targetId: link.targetId,
182
+ }
183
+ : link
184
+ )
185
+
186
+ const root = objects.get(refKey({ objectTypeId, primaryId }))
187
+ if (!root) objectNotFound(objectTypeId, primaryId)
188
+ const relatedObjects = refs
189
+ .filter((ref) => ref.distance > 0)
190
+ .flatMap((ref) => {
191
+ const object = objects.get(refKey(ref))
192
+ return object ? [{ ...object, distance: ref.distance }] : []
193
+ })
194
+
195
+ const objectTypes = options.full
196
+ ? await Promise.all(
197
+ [...new Set(refs.map((ref) => ref.objectTypeId))]
198
+ .sort((left, right) => left.localeCompare(right))
199
+ .map((typeId) => api.get(`/api/object-types/${encodeURIComponent(typeId)}`))
200
+ )
201
+ : undefined
202
+
203
+ return {
204
+ object: options.full ? root : compactObject(root),
205
+ relatedObjects: options.full ? relatedObjects : relatedObjects.map(compactObject),
206
+ links: filteredLinks,
207
+ graph: {
208
+ depth: options.depth,
209
+ maxObjects: options.maxObjects,
210
+ maxLinks: options.maxLinks,
211
+ maxPages: CLI_LIMITS.inspect.maximumPages,
212
+ objectCount: 1 + relatedObjects.length,
213
+ linkCount: filteredLinks.length,
214
+ pagesRead,
215
+ linksExamined,
216
+ truncated: objectsTruncated || linksTruncated || pagesTruncated,
217
+ truncation: {
218
+ objects: objectsTruncated,
219
+ links: linksTruncated,
220
+ pages: pagesTruncated,
221
+ },
222
+ },
223
+ ...(objectTypes ? { objectTypes } : {}),
224
+ }
225
+ }
226
+
227
+ async function inspectRoot(
228
+ api: ApiClient,
229
+ objectTypeId: string,
230
+ primaryId: string,
231
+ options: InspectGraphOptions
232
+ ): Promise<unknown> {
233
+ const response = await api.post("/api/objects/query", {
234
+ query: { kind: "refs", refs: [{ objectTypeId, primaryId }] },
235
+ includeTotal: false,
236
+ })
237
+ if (typeof response !== "object" || response === null || Array.isArray(response)) {
238
+ invalidApiResponse("The object query API returned an invalid response.")
239
+ }
240
+ const rows = (response as Record<string, unknown>).objects
241
+ if (!Array.isArray(rows)) invalidApiResponse("The object query API returned an invalid response.")
242
+ const root = rows.find(
243
+ (value): value is MaterializedObject =>
244
+ isMaterializedObject(value) &&
245
+ value.objectTypeId === objectTypeId &&
246
+ value.primaryId === primaryId
247
+ )
248
+ if (!root) objectNotFound(objectTypeId, primaryId)
249
+ const objectTypes = options.full
250
+ ? [await api.get(`/api/object-types/${encodeURIComponent(objectTypeId)}`)]
251
+ : undefined
252
+ return {
253
+ object: options.full ? root : compactObject(root),
254
+ relatedObjects: [],
255
+ links: [],
256
+ graph: {
257
+ depth: 0,
258
+ maxObjects: options.maxObjects,
259
+ maxLinks: options.maxLinks,
260
+ maxPages: CLI_LIMITS.inspect.maximumPages,
261
+ objectCount: 1,
262
+ linkCount: 0,
263
+ pagesRead: 0,
264
+ linksExamined: 0,
265
+ truncated: false,
266
+ truncation: { objects: false, links: false, pages: false },
267
+ },
268
+ ...(objectTypes ? { objectTypes } : {}),
269
+ }
270
+ }
271
+
272
+ function asLinksResponse(value: unknown): LinksResponse {
273
+ if (typeof value !== "object" || value === null || Array.isArray(value)) invalidLinksResponse()
274
+ const record = value as Record<string, unknown>
275
+ if (!Array.isArray(record.objects) || !Array.isArray(record.links)) invalidLinksResponse()
276
+ if (typeof record.hasMore !== "boolean") invalidLinksResponse()
277
+ if (record.nextPageToken !== undefined && typeof record.nextPageToken !== "string") {
278
+ invalidLinksResponse()
279
+ }
280
+ if (!record.objects.every(isMaterializedObject) || !record.links.every(isPhysicalLinkResponse)) {
281
+ invalidLinksResponse()
282
+ }
283
+ return record as unknown as LinksResponse
284
+ }
285
+
286
+ function invalidLinksResponse(): never {
287
+ invalidApiResponse("The object-links API returned an invalid response.")
288
+ }
289
+
290
+ function invalidApiResponse(message: string): never {
291
+ throw new CliError({ code: "invalid_api_response", message }, EXIT_API)
292
+ }
293
+
294
+ function objectNotFound(objectTypeId: string, primaryId: string): never {
295
+ throw new CliError(
296
+ { code: "not_found", message: `Object '${objectTypeId}/${primaryId}' was not found.` },
297
+ EXIT_API
298
+ )
299
+ }
300
+
301
+ function isMaterializedObject(value: unknown): value is MaterializedObject {
302
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false
303
+ const record = value as Record<string, unknown>
304
+ return typeof record.objectTypeId === "string" && typeof record.primaryId === "string"
305
+ }
306
+
307
+ function isPhysicalLinkResponse(value: unknown): value is LinksResponse["links"][number] {
308
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false
309
+ const record = value as Record<string, unknown>
310
+ return (
311
+ typeof record.linkId === "string" && isObjectRef(record.source) && isObjectRef(record.target)
312
+ )
313
+ }
314
+
315
+ function isObjectRef(value: unknown): value is ObjectRef {
316
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false
317
+ const record = value as Record<string, unknown>
318
+ return typeof record.objectTypeId === "string" && typeof record.primaryId === "string"
319
+ }
320
+
321
+ function compactObject<T extends MaterializedObject>(
322
+ object: T
323
+ ): Omit<T, "createdAt" | "updatedAt"> {
324
+ const { createdAt: _createdAt, updatedAt: _updatedAt, ...compact } = object
325
+ return compact
326
+ }
327
+
328
+ function dedupeRefs(values: LocatedRef[]): LocatedRef[] {
329
+ const unique = new Map<string, LocatedRef>()
330
+ for (const value of values) {
331
+ const key = refKey(value)
332
+ const current = unique.get(key)
333
+ if (!current || value.distance < current.distance) unique.set(key, value)
334
+ }
335
+ return [...unique.values()].sort(
336
+ (left, right) =>
337
+ left.distance - right.distance ||
338
+ left.objectTypeId.localeCompare(right.objectTypeId) ||
339
+ left.primaryId.localeCompare(right.primaryId)
340
+ )
341
+ }
342
+
343
+ function refKey(ref: ObjectRef): string {
344
+ return JSON.stringify([ref.objectTypeId, ref.primaryId])
345
+ }
346
+
347
+ function linkKey(link: PhysicalLink): string {
348
+ return JSON.stringify([
349
+ link.sourceTypeId,
350
+ link.sourceId,
351
+ link.linkId,
352
+ link.targetTypeId,
353
+ link.targetId,
354
+ ])
355
+ }
356
+
357
+ function compareLinks(left: PhysicalLink, right: PhysicalLink): number {
358
+ return linkKey(left).localeCompare(linkKey(right))
359
+ }
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ export type { ApiClientOptions } from "./api-client"
2
+ export { ApiClient } from "./api-client"
3
+ export { isHelp, requestsHelp } from "./arguments"
4
+ export type { InstanceCommand } from "./commands"
5
+ export { INSTANCE_COMMANDS, isInstanceCommand } from "./commands"
6
+ export { renderInstanceHelp } from "./commands/metadata"
7
+ export { asRecord, isFileError } from "./commands/shared"
8
+ export {
9
+ CliError,
10
+ EXIT_API,
11
+ EXIT_USAGE,
12
+ fail,
13
+ INSTANCE_CLI_VERSION,
14
+ reportError,
15
+ writeJson,
16
+ writeText,
17
+ } from "./output"
18
+ export type {
19
+ InstanceCliMode,
20
+ LocalInstanceCliMode,
21
+ RunInstanceCliInput,
22
+ SandboxInstanceCliMode,
23
+ } from "./run"
24
+ export { createInstanceApiClient, runInstanceCli } from "./run"
package/src/output.ts ADDED
@@ -0,0 +1,51 @@
1
+ /** Version of the stable JSON and command contract. */
2
+ export const INSTANCE_CLI_VERSION = "1"
3
+ export const EXIT_USAGE = 2
4
+ export const EXIT_API = 3
5
+
6
+ export interface CliErrorBody {
7
+ readonly code: string
8
+ readonly status?: number
9
+ readonly message: string
10
+ readonly hint?: string
11
+ readonly issues?: readonly unknown[]
12
+ }
13
+
14
+ export class CliError extends Error {
15
+ readonly body: CliErrorBody
16
+ readonly exitCode: number
17
+
18
+ constructor(body: CliErrorBody, exitCode = EXIT_USAGE) {
19
+ super(body.message)
20
+ this.name = "CliError"
21
+ this.body = body
22
+ this.exitCode = exitCode
23
+ }
24
+ }
25
+
26
+ export function fail(message: string, code = "invalid_arguments", hint?: string): never {
27
+ throw new CliError({ code, message, ...(hint ? { hint } : {}) })
28
+ }
29
+
30
+ export function writeJson(value: unknown): void {
31
+ process.stdout.write(`${JSON.stringify(value)}\n`)
32
+ }
33
+
34
+ export function writeText(value: string): void {
35
+ process.stdout.write(value.endsWith("\n") ? value : `${value}\n`)
36
+ }
37
+
38
+ export function reportError(error: unknown): number {
39
+ const cliError =
40
+ error instanceof CliError
41
+ ? error
42
+ : new CliError(
43
+ {
44
+ code: "internal_error",
45
+ message: error instanceof Error ? error.message : "The Sixb CLI failed unexpectedly.",
46
+ },
47
+ EXIT_API
48
+ )
49
+ process.stderr.write(`${JSON.stringify({ error: cliError.body })}\n`)
50
+ return cliError.exitCode
51
+ }
@@ -0,0 +1,17 @@
1
+ /** Stable safety and output bounds shared by local and sandbox modes. */
2
+ export const CLI_LIMITS = {
3
+ list: { default: 20, maximum: 1_000 },
4
+ search: { default: 20, maximum: 50 },
5
+ telemetryHistory: { default: 100, maximum: 1_000 },
6
+ linkPage: { default: 100, maximum: 1_000 },
7
+ inspect: {
8
+ depth: { default: 2, maximum: 3 },
9
+ objects: { default: 20, maximum: 100 },
10
+ links: { default: 50, maximum: 500 },
11
+ maximumPages: 10,
12
+ },
13
+ } as const
14
+
15
+ export const DEFAULT_LIST_ORDER = "desc" as const
16
+ export const DEFAULT_OBJECT_ORDER_BY = "updatedAt" as const
17
+ export const DEFAULT_TELEMETRY_ORDER = "desc" as const
package/src/run.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { ApiClient } from "./api-client"
2
+ import { dispatch } from "./commands"
3
+
4
+ export interface SandboxInstanceCliMode {
5
+ readonly kind: "sandbox"
6
+ readonly baseUrl: string
7
+ readonly runContextPath: string
8
+ }
9
+
10
+ export interface LocalInstanceCliMode {
11
+ readonly kind: "local"
12
+ readonly baseUrl: string
13
+ readonly token?: string
14
+ readonly profile?: string
15
+ }
16
+
17
+ export type InstanceCliMode = SandboxInstanceCliMode | LocalInstanceCliMode
18
+
19
+ export interface RunInstanceCliInput {
20
+ readonly args: readonly string[]
21
+ readonly mode: InstanceCliMode
22
+ }
23
+
24
+ export async function runInstanceCli(input: RunInstanceCliInput): Promise<void> {
25
+ const [command, ...args] = input.args
26
+ await dispatch(createInstanceApiClient(input.mode), command ?? "", args)
27
+ }
28
+
29
+ export function createInstanceApiClient(mode: InstanceCliMode): ApiClient {
30
+ return new ApiClient({
31
+ baseUrl: mode.baseUrl,
32
+ ...(mode.kind === "local" && mode.token ? { authorization: `Bearer ${mode.token}` } : {}),
33
+ missingBaseUrlMessage:
34
+ mode.kind === "sandbox"
35
+ ? "SIXB_API_BASE_URL is not set."
36
+ : "The selected Sixb profile has no API URL.",
37
+ unavailableMessage:
38
+ mode.kind === "sandbox"
39
+ ? "The Sixb API gateway could not be reached."
40
+ : "The Sixb API could not be reached.",
41
+ unavailableHint:
42
+ mode.kind === "sandbox"
43
+ ? "Run 'sixb doctor' to verify the sandbox runtime and gateway."
44
+ : "Run 'sixb status' to verify the current profile.",
45
+ })
46
+ }