@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
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
|
+
}
|
package/src/policies.ts
ADDED
|
@@ -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
|
+
}
|