@sixb/cli-core 0.1.9 → 0.1.10

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 (59) hide show
  1. package/README.md +21 -16
  2. package/dist/api-client.d.ts +1 -0
  3. package/dist/api-client.d.ts.map +1 -1
  4. package/dist/bin/sixb +26 -0
  5. package/dist/commands/data.d.ts +5 -0
  6. package/dist/commands/data.d.ts.map +1 -0
  7. package/dist/commands/files.d.ts.map +1 -1
  8. package/dist/commands/index.d.ts +1 -1
  9. package/dist/commands/index.d.ts.map +1 -1
  10. package/dist/commands/memory.d.ts +3 -0
  11. package/dist/commands/memory.d.ts.map +1 -0
  12. package/dist/commands/metadata.d.ts +10 -4
  13. package/dist/commands/metadata.d.ts.map +1 -1
  14. package/dist/commands/runs.d.ts +8 -1
  15. package/dist/commands/runs.d.ts.map +1 -1
  16. package/dist/commands/shared.d.ts +2 -0
  17. package/dist/commands/shared.d.ts.map +1 -1
  18. package/dist/commands/system.d.ts +21 -0
  19. package/dist/commands/system.d.ts.map +1 -0
  20. package/dist/commands/users.d.ts.map +1 -1
  21. package/dist/generated/sixb.mjs +2547 -0
  22. package/dist/index.d.ts +8 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +290 -58
  25. package/dist/index.js.map +21 -17
  26. package/dist/main.d.ts +3 -0
  27. package/dist/main.d.ts.map +1 -0
  28. package/dist/memory.d.ts +21 -0
  29. package/dist/memory.d.ts.map +1 -0
  30. package/dist/policies.d.ts +9 -0
  31. package/dist/policies.d.ts.map +1 -1
  32. package/dist/run.d.ts +1 -0
  33. package/dist/run.d.ts.map +1 -1
  34. package/dist/sandbox-cli.d.ts +8 -0
  35. package/dist/sandbox-cli.d.ts.map +1 -0
  36. package/package.json +15 -1
  37. package/src/api-client.ts +70 -2
  38. package/src/bin/sixb +26 -0
  39. package/src/commands/actions.ts +3 -3
  40. package/src/commands/data.ts +90 -0
  41. package/src/commands/files.ts +27 -3
  42. package/src/commands/index.ts +16 -0
  43. package/src/commands/memory.ts +358 -0
  44. package/src/commands/metadata.ts +69 -29
  45. package/src/commands/models.ts +3 -3
  46. package/src/commands/ontology.ts +2 -2
  47. package/src/commands/runs.ts +22 -16
  48. package/src/commands/shared.ts +6 -0
  49. package/src/commands/system.ts +67 -0
  50. package/src/commands/telemetry.ts +2 -2
  51. package/src/commands/users.ts +22 -2
  52. package/src/commands/workflows.ts +3 -3
  53. package/src/generated/sixb.mjs +2547 -0
  54. package/src/index.ts +12 -3
  55. package/src/main.ts +39 -0
  56. package/src/memory.ts +30 -0
  57. package/src/policies.ts +3 -0
  58. package/src/run.ts +3 -2
  59. package/src/sandbox-cli.ts +18 -0
package/dist/main.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The Agent's memory as installed in its sandbox: the worker writes each repo under
3
+ * `$SIXB_MEMORY_DIR/<repo>/` with a manifest of what it loaded, and `sixb memory` saves the files
4
+ * that differ from it.
5
+ */
6
+ export type AgentMemoryRepo = "user" | "project";
7
+ /** Where the manifest is, relative to `$SIXB_CONTEXT_DIR`. */
8
+ export declare const AGENT_MEMORY_MANIFEST = "context/memory.json";
9
+ export declare const AGENT_MEMORY_ENTRY = "MEMORY.md";
10
+ /** What a repo starts from before its first save; it is saved with the first save. */
11
+ export declare const AGENT_MEMORY_STARTER = "# Memory\n\n";
12
+ /** What the local copy was loaded from: the revision and each file's SHA-256, by path. */
13
+ export interface AgentMemoryManifest {
14
+ readonly repos: Partial<Record<AgentMemoryRepo, AgentMemoryManifestRepo>>;
15
+ }
16
+ export interface AgentMemoryManifestRepo {
17
+ readonly revision: number;
18
+ readonly files: Readonly<Record<string, string>>;
19
+ }
20
+ export declare function agentMemoryFileHash(contents: string): string;
21
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,SAAS,CAAA;AAEhD,8DAA8D;AAC9D,eAAO,MAAM,qBAAqB,wBAAwB,CAAA;AAE1D,eAAO,MAAM,kBAAkB,cAAc,CAAA;AAE7C,sFAAsF;AACtF,eAAO,MAAM,oBAAoB,iBAAiB,CAAA;AAElD,0FAA0F;AAC1F,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,uBAAuB,CAAC,CAAC,CAAA;CAC1E;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACjD;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE5D"}
@@ -25,6 +25,15 @@ export declare const CLI_LIMITS: {
25
25
  readonly default: 20;
26
26
  readonly maximum: 200;
27
27
  };
28
+ /** The server's dataset bounds: 100 versions and 1,000 rows per page. */
29
+ readonly datasetVersions: {
30
+ readonly default: 20;
31
+ readonly maximum: 100;
32
+ };
33
+ readonly datasetRows: {
34
+ readonly default: 100;
35
+ readonly maximum: 1000;
36
+ };
28
37
  readonly inspect: {
29
38
  readonly depth: {
30
39
  readonly default: 2;
@@ -1 +1 @@
1
- {"version":3,"file":"policies.d.ts","sourceRoot":"","sources":["../src/policies.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,eAAO,MAAM,UAAU;;;;;;;;;IAGrB,+FAA+F;;;;;;;;;;;;IAI/F,qDAAqD;;;;;;;;;;;;;;;;;;;;CAQ7C,CAAA;AAEV,eAAO,MAAM,kBAAkB,EAAG,MAAe,CAAA;AACjD,eAAO,MAAM,uBAAuB,EAAG,WAAoB,CAAA;AAC3D,eAAO,MAAM,uBAAuB,EAAG,MAAe,CAAA"}
1
+ {"version":3,"file":"policies.d.ts","sourceRoot":"","sources":["../src/policies.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,eAAO,MAAM,UAAU;;;;;;;;;IAGrB,+FAA+F;;;;;;;;;;;;IAI/F,qDAAqD;;;;;IAErD,yEAAyE;;;;;;;;;;;;;;;;;;;;;;;;CASjE,CAAA;AAEV,eAAO,MAAM,kBAAkB,EAAG,MAAe,CAAA;AACjD,eAAO,MAAM,uBAAuB,EAAG,WAAoB,CAAA;AAC3D,eAAO,MAAM,uBAAuB,EAAG,MAAe,CAAA"}
package/dist/run.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { ApiClient } from "./api-client";
2
2
  export interface SandboxInstanceCliMode {
3
3
  readonly kind: "sandbox";
4
+ /** The sandbox's credentialed address for the API: the run's token is added on the way out. */
4
5
  readonly baseUrl: string;
5
6
  readonly runContextPath: string;
6
7
  }
package/dist/run.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAGxC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;CAChC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,MAAM,eAAe,GAAG,sBAAsB,GAAG,oBAAoB,CAAA;AAE3E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAA;CAC/B;AAED,wBAAsB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAG9E;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,eAAe,GAAG,SAAS,CAiBxE"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAGxC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;IACxB,+FAA+F;IAC/F,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;CAChC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,MAAM,eAAe,GAAG,sBAAsB,GAAG,oBAAoB,CAAA;AAE3E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAA;CAC/B;AAED,wBAAsB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAG9E;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,eAAe,GAAG,SAAS,CAiBxE"}
@@ -0,0 +1,8 @@
1
+ /** The `sixb` a sandbox installs: a shell launcher and the bundle it runs on Bun or Node. */
2
+ export interface SandboxCliAssets {
3
+ readonly launcher: string;
4
+ readonly artifact: Uint8Array;
5
+ }
6
+ /** Read from beside this module: `src/` in the workspace, `dist/` once built. */
7
+ export declare function loadSandboxCliAssets(): Promise<SandboxCliAssets>;
8
+ //# sourceMappingURL=sandbox-cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox-cli.d.ts","sourceRoot":"","sources":["../src/sandbox-cli.ts"],"names":[],"mappings":"AAEA,6FAA6F;AAC7F,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAA;CAC9B;AAID,iFAAiF;AACjF,wBAAgB,oBAAoB,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAMhE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sixb/cli-core",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Shared instance command runtime for Sixb CLIs",
5
5
  "keywords": [
6
6
  "sixb",
@@ -29,10 +29,24 @@
29
29
  },
30
30
  "scripts": {
31
31
  "build": "bun ../../scripts/build-package.ts",
32
+ "generate:sandbox-cli": "bun ./scripts/generate-sandbox-cli.ts",
33
+ "check:sandbox-cli": "bun ./scripts/generate-sandbox-cli.ts --check",
32
34
  "typecheck": "tsc --noEmit",
33
35
  "test": "bun test",
34
36
  "prepack": "bun run build"
35
37
  },
38
+ "sixbBuild": {
39
+ "assets": [
40
+ {
41
+ "from": "./src/bin",
42
+ "to": "./dist/bin"
43
+ },
44
+ {
45
+ "from": "./src/generated",
46
+ "to": "./dist/generated"
47
+ }
48
+ ]
49
+ },
36
50
  "devDependencies": {
37
51
  "@types/bun": "latest",
38
52
  "typescript": "^5.7.0"
package/src/api-client.ts CHANGED
@@ -1,21 +1,33 @@
1
1
  import { randomUUID } from "node:crypto"
2
2
  import { createWriteStream } from "node:fs"
3
3
  import { readFile, rename, rm } from "node:fs/promises"
4
+ import { request as httpRequest } from "node:http"
4
5
  import { basename, dirname, join } from "node:path"
5
6
  import { Readable } from "node:stream"
6
7
  import { pipeline } from "node:stream/promises"
7
8
  import type { ReadableStream as NodeReadableStream } from "node:stream/web"
8
9
  import { CliError, EXIT_API, fail } from "./output"
9
10
 
11
+ /**
12
+ * A base URL of `unix:<socket path>` reaches the API through a local socket, such as the one a
13
+ * sandbox's credential forwarder listens on: requests go to `http://localhost` over that socket.
14
+ */
15
+ const UNIX_BASE_URL_PREFIX = "unix:"
16
+
10
17
  export class ApiClient {
11
18
  readonly baseUrl: string
19
+ private readonly socketPath: string | undefined
12
20
  private readonly authorization: string | undefined
13
21
  private readonly missingBaseUrlMessage: string
14
22
  private readonly unavailableMessage: string
15
23
  private readonly unavailableHint: string
16
24
 
17
25
  constructor(options: ApiClientOptions) {
18
- this.baseUrl = options.baseUrl.replace(/\/+$/, "")
26
+ const baseUrl = options.baseUrl.replace(/\/+$/, "")
27
+ this.socketPath = baseUrl.startsWith(UNIX_BASE_URL_PREFIX)
28
+ ? baseUrl.slice(UNIX_BASE_URL_PREFIX.length)
29
+ : undefined
30
+ this.baseUrl = this.socketPath === undefined ? baseUrl : "http://localhost"
19
31
  this.authorization = options.authorization
20
32
  this.missingBaseUrlMessage = options.missingBaseUrlMessage
21
33
  this.unavailableMessage = options.unavailableMessage
@@ -107,7 +119,7 @@ export class ApiClient {
107
119
  try {
108
120
  const headers = new Headers(init.headers)
109
121
  if (this.authorization) headers.set("authorization", this.authorization)
110
- response = await fetch(url, { ...init, headers })
122
+ response = await send(url, { ...init, headers }, this.socketPath)
111
123
  } catch (error) {
112
124
  if (init.signal?.aborted) throw error
113
125
  throw new CliError(
@@ -193,3 +205,59 @@ function stringField(record: Readonly<Record<string, unknown>>, key: string): st
193
205
  const value = record[key]
194
206
  return typeof value === "string" ? value : undefined
195
207
  }
208
+
209
+ /** One request, over a Unix socket when the API is behind one. */
210
+ function send(url: URL, init: RequestInit, socketPath: string | undefined): Promise<Response> {
211
+ if (socketPath === undefined) return fetch(url, init)
212
+ // Bun's fetch speaks over a Unix socket itself; Node's does not, so it goes through node:http.
213
+ if (typeof Bun !== "undefined") return fetch(url, { ...init, unix: socketPath })
214
+ return sendOverNodeSocket(url, init, socketPath)
215
+ }
216
+
217
+ async function sendOverNodeSocket(
218
+ url: URL,
219
+ init: RequestInit,
220
+ socketPath: string
221
+ ): Promise<Response> {
222
+ // A Request settles the body and its content type (a form's boundary included) as fetch would.
223
+ const outgoing = new Request(url, init)
224
+ const body = outgoing.body ? Buffer.from(await outgoing.arrayBuffer()) : undefined
225
+ const headers: Record<string, string> = Object.fromEntries(outgoing.headers)
226
+ if (body) headers["content-length"] = String(body.byteLength)
227
+
228
+ return new Promise((resolve, reject) => {
229
+ const request = httpRequest(
230
+ {
231
+ socketPath,
232
+ method: outgoing.method,
233
+ path: `${url.pathname}${url.search}`,
234
+ headers: { ...headers, host: "localhost" },
235
+ ...(init.signal ? { signal: init.signal } : {}),
236
+ },
237
+ (response) => {
238
+ const responseHeaders = new Headers()
239
+ for (const [name, value] of Object.entries(response.headers)) {
240
+ for (const each of Array.isArray(value) ? value : value === undefined ? [] : [value]) {
241
+ responseHeaders.append(name, each)
242
+ }
243
+ }
244
+ const status = response.statusCode ?? 502
245
+ const empty = status === 204 || status === 304 || outgoing.method === "HEAD"
246
+ resolve(
247
+ new Response(empty ? null : nodeBody(response), {
248
+ status,
249
+ statusText: response.statusMessage ?? "",
250
+ headers: responseHeaders,
251
+ })
252
+ )
253
+ }
254
+ )
255
+ request.on("error", reject)
256
+ request.end(body)
257
+ })
258
+ }
259
+
260
+ /** The response body as a web stream: Node's and the global stream types differ only in typing. */
261
+ function nodeBody(response: Readable): ReadableStream<Uint8Array> {
262
+ return Readable.toWeb(response) as unknown as ReadableStream<Uint8Array>
263
+ }
package/src/bin/sixb ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env sh
2
+
3
+ set -eu
4
+
5
+ script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
6
+ artifact="$script_dir/../lib/sixb.mjs"
7
+ if [ ! -f "$artifact" ]; then
8
+ artifact="$script_dir/../generated/sixb.mjs"
9
+ fi
10
+ if [ ! -f "$artifact" ]; then
11
+ printf '%s\n' '{"error":{"code":"runtime_unavailable","message":"The Sixb CLI artifact is missing."}}' >&2
12
+ exit 2
13
+ fi
14
+
15
+ bun_path=$(command -v bun || true)
16
+ if [ -n "$bun_path" ]; then
17
+ exec "$bun_path" "$artifact" "$@"
18
+ fi
19
+
20
+ node_path=$(command -v node || true)
21
+ if [ -n "$node_path" ]; then
22
+ exec "$node_path" "$artifact" "$@"
23
+ fi
24
+
25
+ printf '%s\n' '{"error":{"code":"runtime_unavailable","message":"The Sixb CLI requires Bun or Node.js."}}' >&2
26
+ exit 2
@@ -3,7 +3,7 @@ import type { ApiClient } from "../api-client"
3
3
  import { isHelp, parseCommandArgs, requestsHelp } from "../arguments"
4
4
  import { CliError, EXIT_API, fail, writeJson, writeRequestResult, writeText } from "../output"
5
5
  import { GROUP_HELP } from "./metadata"
6
- import { asRecord, parseQueryOptions, readJson } from "./shared"
6
+ import { asRecord, idSegment, parseQueryOptions, readJson } from "./shared"
7
7
 
8
8
  /**
9
9
  * How long `actions request` waits for the run it requested. An agent's command is killed after 30
@@ -22,7 +22,7 @@ export async function actions(
22
22
  if (!sub || isHelp(sub) || requestsHelp(rest)) return writeText(GROUP_HELP.actions)
23
23
  if (sub === "get") {
24
24
  const { positionals } = parseCommandArgs(rest, {}, "actions get", 1)
25
- return writeJson(await api.get(`/api/actions/${encodeURIComponent(positionals[0] ?? "")}`))
25
+ return writeJson(await api.get(`/api/actions/${idSegment(positionals[0])}`))
26
26
  }
27
27
  if (sub === "list") {
28
28
  const options = parseQueryOptions(rest, { "--type": "objectTypeId" }, "actions list")
@@ -64,7 +64,7 @@ export async function actions(
64
64
  try {
65
65
  return writeRequestResult(
66
66
  await api.post(
67
- `/api/actions/${encodeURIComponent(actionId)}`,
67
+ `/api/actions/${idSegment(actionId)}`,
68
68
  {
69
69
  params,
70
70
  ...(subjectType && subjectId
@@ -0,0 +1,90 @@
1
+ import type { ApiClient } from "../api-client"
2
+ import {
3
+ integerInRange,
4
+ isHelp,
5
+ nonNegativeInteger,
6
+ parseCommandArgs,
7
+ requestsHelp,
8
+ } from "../arguments"
9
+ import { fail, writeJson, writeText } from "../output"
10
+ import { CLI_LIMITS } from "../policies"
11
+ import { GROUP_HELP } from "./metadata"
12
+ import { idSegment, type QueryOptions } from "./shared"
13
+
14
+ export async function datasets(api: ApiClient, args: readonly string[]): Promise<void> {
15
+ const [sub, ...rest] = args
16
+ if (!sub || isHelp(sub) || requestsHelp(rest)) return writeText(GROUP_HELP.datasets)
17
+
18
+ if (sub === "list") {
19
+ parseCommandArgs(rest, {}, "datasets list")
20
+ return writeJson(await api.get("/api/datasets"))
21
+ }
22
+
23
+ if (sub === "get") {
24
+ const { positionals } = parseCommandArgs(rest, {}, "datasets get", 1)
25
+ return writeJson(await api.get(`/api/datasets/${idSegment(positionals[0])}`))
26
+ }
27
+
28
+ if (sub === "versions") {
29
+ const { positionals, options } = parseCommandArgs(
30
+ rest,
31
+ { "--limit": "string" },
32
+ "datasets versions",
33
+ 1
34
+ )
35
+ const { default: fallback, maximum } = CLI_LIMITS.datasetVersions
36
+ const limit = integerInRange("--limit", options["--limit"] ?? String(fallback), 1, maximum)
37
+ return writeJson(
38
+ await api.get(`/api/datasets/${idSegment(positionals[0])}/versions`, { limit: String(limit) })
39
+ )
40
+ }
41
+
42
+ if (sub === "rows") {
43
+ const { positionals, options } = parseCommandArgs(
44
+ rest,
45
+ { "--version": "string", "--columns": "string", "--limit": "string", "--offset": "string" },
46
+ "datasets rows",
47
+ 1
48
+ )
49
+ const { default: fallback, maximum } = CLI_LIMITS.datasetRows
50
+ const query: QueryOptions = {
51
+ limit: String(integerInRange("--limit", options["--limit"] ?? String(fallback), 1, maximum)),
52
+ ...(options["--version"] === undefined ? {} : { versionId: options["--version"] }),
53
+ ...(options["--columns"] === undefined ? {} : { columns: columnList(options["--columns"]) }),
54
+ ...(options["--offset"] === undefined
55
+ ? {}
56
+ : { offset: String(nonNegativeInteger("--offset", options["--offset"])) }),
57
+ }
58
+ return writeJson(await api.get(`/api/datasets/${idSegment(positionals[0])}/rows`, query))
59
+ }
60
+
61
+ fail(`Unknown datasets command '${sub}'.`)
62
+ }
63
+
64
+ /** `syncs` and `pipelines`: the definitions the caller may run, with each one's latest run. */
65
+ export async function runnables(
66
+ api: ApiClient,
67
+ group: "syncs" | "pipelines",
68
+ args: readonly string[]
69
+ ): Promise<void> {
70
+ const [sub, ...rest] = args
71
+ if (!sub || isHelp(sub) || requestsHelp(rest)) return writeText(GROUP_HELP[group])
72
+
73
+ if (sub === "list") {
74
+ parseCommandArgs(rest, {}, `${group} list`)
75
+ return writeJson(await api.get(`/api/${group}`))
76
+ }
77
+
78
+ if (sub === "get") {
79
+ const { positionals } = parseCommandArgs(rest, {}, `${group} get`, 1)
80
+ return writeJson(await api.get(`/api/${group}/${idSegment(positionals[0])}`))
81
+ }
82
+
83
+ fail(`Unknown ${group} command '${sub}'.`)
84
+ }
85
+
86
+ function columnList(value: string): string {
87
+ const columns = value.split(",").map((column) => column.trim())
88
+ if (columns.some((column) => !column)) fail("--columns takes column names separated by commas.")
89
+ return columns.join(",")
90
+ }
@@ -3,6 +3,7 @@ import type { ApiClient } from "../api-client"
3
3
  import { isHelp, parseCommandArgs, requestsHelp, requireExact } from "../arguments"
4
4
  import { fail, writeJson, writeText } from "../output"
5
5
  import { GROUP_HELP } from "./metadata"
6
+ import { idSegment } from "./shared"
6
7
 
7
8
  export async function files(api: ApiClient, args: readonly string[]): Promise<void> {
8
9
  const [sub, ...rest] = args
@@ -30,19 +31,28 @@ export async function files(api: ApiClient, args: readonly string[]): Promise<vo
30
31
  [2, 3]
31
32
  )
32
33
  const context = positionals[0]
34
+ const output = options["--output"]
35
+ if (context === "url") {
36
+ // A content URL a manifest lists, such as an attachment's: fetched the way every other
37
+ // command reaches the API, so its credential is added wherever the CLI's address adds it.
38
+ requireExact(positionals, 2, "files download url requires exactly one content URL.")
39
+ const url = parseContentUrl(positionals[1] ?? "")
40
+ if (!output) fail("files download requires --output <local-path>.")
41
+ await api.download(url.pathname, output, Object.fromEntries(url.searchParams))
42
+ return writeJson({ downloaded: true, output })
43
+ }
33
44
  let route: string
34
45
  if (context === "object") {
35
46
  requireExact(positionals, 3, "files download object requires object type and primary id.")
36
47
  const type = positionals[1] ?? ""
37
48
  const id = positionals[2] ?? ""
38
- route = `/api/objects/${encodeURIComponent(type)}/${encodeURIComponent(id)}/files/content`
49
+ route = `/api/objects/${idSegment(type)}/${idSegment(id)}/files/content`
39
50
  } else if (context === "action-run" || context === "workflow-run") {
40
51
  requireExact(positionals, 2, `files download ${context} requires exactly one run id.`)
41
52
  const id = positionals[1] ?? ""
42
- route = `/api/${context}s/${encodeURIComponent(id)}/files/content`
53
+ route = `/api/${context}s/${idSegment(id)}/files/content`
43
54
  } else fail(`Unknown file download context '${context}'.`)
44
55
  const path = options["--path"]
45
- const output = options["--output"]
46
56
  if (!path) fail("files download requires --path <json-pointer>.")
47
57
  if (!output) fail("files download requires --output <local-path>.")
48
58
  await api.download(route, output, { path })
@@ -50,3 +60,17 @@ export async function files(api: ApiClient, args: readonly string[]): Promise<vo
50
60
  }
51
61
  fail(`Unknown files command '${sub}'.`)
52
62
  }
63
+
64
+ /** A Sixb API file content URL: only its path and query are used, never its origin. */
65
+ function parseContentUrl(value: string): URL {
66
+ let url: URL
67
+ try {
68
+ url = new URL(value)
69
+ } catch {
70
+ fail(`'${value}' is not a URL.`)
71
+ }
72
+ if (!url.pathname.startsWith("/api/") || !url.pathname.endsWith("/files/content")) {
73
+ fail(`'${value}' is not a Sixb file content URL.`)
74
+ }
75
+ return url
76
+ }
@@ -1,6 +1,7 @@
1
1
  import type { ApiClient } from "../api-client"
2
2
  import { fail } from "../output"
3
3
  import { actions } from "./actions"
4
+ import { datasets, runnables } from "./data"
4
5
  import { files } from "./files"
5
6
  import { models } from "./models"
6
7
  import { objects } from "./objects"
@@ -23,6 +24,11 @@ export const INSTANCE_COMMANDS = [
23
24
  "workflow-runs",
24
25
  "users",
25
26
  "models",
27
+ "datasets",
28
+ "syncs",
29
+ "sync-runs",
30
+ "pipelines",
31
+ "pipeline-runs",
26
32
  ] as const
27
33
 
28
34
  export type InstanceCommand = (typeof INSTANCE_COMMANDS)[number]
@@ -59,6 +65,16 @@ export async function dispatch(
59
65
  return users(api, args)
60
66
  case "models":
61
67
  return models(api, args)
68
+ case "datasets":
69
+ return datasets(api, args)
70
+ case "syncs":
71
+ return runnables(api, "syncs", args)
72
+ case "sync-runs":
73
+ return runs(api, "sync", args)
74
+ case "pipelines":
75
+ return runnables(api, "pipelines", args)
76
+ case "pipeline-runs":
77
+ return runs(api, "pipeline", args)
62
78
  default:
63
79
  fail(`Unknown command '${command}'. Run 'sixb --help'.`)
64
80
  }