@datumos/datum-cli 0.0.0 → 0.589.0

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/cli/datum.js ADDED
@@ -0,0 +1,814 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `datum` — the official CLI of the Datum API (docs/cli.md).
4
+ *
5
+ * Every API command here is DERIVED: the tree, the flags and every help
6
+ * page come from the OpenAPI contract (server/src/openapi/spec.ts), which
7
+ * CLAUDE.md requires to change in the same commit as any API change — so
8
+ * the CLI and its --help update themselves with the API. The ONLY
9
+ * hand-written commands are the ones that are not API operations: the
10
+ * CLI's own (`help`, `spec`, `commands`), the person sign-in
11
+ * (`login`, `logout`, `whoami`, `mcp serve` — docs/developer-access.md),
12
+ * which talk to the identity provider and to Claude Code, not to the API,
13
+ * and `artifacts publish <dir>` (#589), which is an API command spanning a
14
+ * directory walk and several calls, so no single derived one could be it,
15
+ * and `outputs download <id>` (#2680) — metadata from /api, the bytes from
16
+ * the origin's /outputs route.
17
+ *
18
+ * Auth model: an API key (DATUM_API_KEY / --api-key; panel: Settings →
19
+ * API keys) rides the public surface (/public/api) — the key pins the
20
+ * actor server-side; /api/secrets, /api/config and /api/api-keys stay
21
+ * blocked (docs/public-api.md). Without a key, the sign-in from
22
+ * `datum login` (a person's Entra access token) rides /api with the
23
+ * person's own role (the panel's — #1817). --env pins the target deployment
24
+ * variant on the Ring Accelerator via x-oa-variant.
25
+ */
26
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
27
+ import path from "node:path";
28
+ import { fileURLToPath } from "node:url";
29
+ import { openApiSpec } from "../openapi/spec.js";
30
+ import { CREDENTIAL_PROCESS_ENV, CredentialProcessError, readCredentialProcess } from "./credentialProcess.js";
31
+ /** The 409 code of a write that waits for the person's approval — the
32
+ * server's `APPROVAL_REQUIRED_CODE` (personAgent.interfaces.ts; the package
33
+ * ships only cli/ and openapi/, a test pins the two equal). */
34
+ export const APPROVAL_REQUIRED_CODE = "approval_required";
35
+ /** Exit code of a write that waits for the person's approval (409
36
+ * `approval_required`, #2719) — the same number datum-tool gives a
37
+ * pending approval. */
38
+ export const EXIT_APPROVAL_REQUIRED = 4;
39
+ import { buildCommandTree, matchCommand, } from "./commandTree.js";
40
+ import { BUILTIN_COMMANDS, createStyle, renderBuiltinHelp, renderCommandHelp, renderCommandList, renderGroupHelp, renderRootHelp, } from "./help.js";
41
+ import { buildRequest, executeRequest, MAX_PRINTED_RESPONSE_BYTES, spillNotice, spillResponse, toCurl, UsageError, } from "./invoke.js";
42
+ import { runArtifactsPublish } from "./artifactsPublish.js";
43
+ import { runOutputsDownload } from "./outputsDownload.js";
44
+ import { interruptedBySignal, runPreviewDown, runPreviewTrace, runPreviewUp, runPreviewsList } from "./previewCommand.js";
45
+ import { daemonReadsPairTokenEnv, maskedTitle, pairTokenOutOfArgs, SATELLITE_PAIR_TOKEN_ENV, SATELLITE_PAIR_TOKEN_ENV_SINCE } from "./satellitePairToken.js";
46
+ import { runA2a } from "./a2aCommand.js";
47
+ import { runLogin, runLogout, runWhoami } from "./authCommands.js";
48
+ import { CredentialStore } from "./credentials.js";
49
+ import { LoginFlowError, NotLoggedInError, ensureAccessToken, spaceBaseUrl } from "./entraLogin.js";
50
+ import { runMcpServe } from "./mcpServe.js";
51
+ import { runSpaceManifestInit, runSpaceManifestPull, runSpaceManifestPush, runSpaceManifestSchema, runSpaceManifestValidate, } from "./spaceManifestCommand.js";
52
+ import { ENV_DELEGATED, ENV_LAUNCHER_VERSION, preflight, runProcess, updatePaths, } from "./selfUpdate.js";
53
+ import { runUpdate } from "./updateCommand.js";
54
+ const spec = openApiSpec;
55
+ /** Global flags and their arity; everything else belongs to the command. */
56
+ const GLOBAL_VALUE_FLAGS = new Set([
57
+ "base-url",
58
+ "api-key",
59
+ "env",
60
+ "data",
61
+ "fields",
62
+ "space",
63
+ ]);
64
+ const GLOBAL_BOOL_FLAGS = new Set(["curl", "raw", "full", "help", "version"]);
65
+ /** First pass: pull global flags out of argv, keep the rest in order. */
66
+ function extractGlobals(argv) {
67
+ const values = new Map();
68
+ const bools = new Set();
69
+ const rest = [];
70
+ for (let i = 0; i < argv.length; i++) {
71
+ const token = argv[i];
72
+ if (token === "-h") {
73
+ bools.add("help");
74
+ continue;
75
+ }
76
+ if (token.startsWith("--")) {
77
+ const eq = token.indexOf("=");
78
+ const name = eq === -1 ? token.slice(2) : token.slice(2, eq);
79
+ if (GLOBAL_BOOL_FLAGS.has(name)) {
80
+ bools.add(name);
81
+ continue;
82
+ }
83
+ if (GLOBAL_VALUE_FLAGS.has(name)) {
84
+ const value = eq === -1 ? argv[++i] : token.slice(eq + 1);
85
+ if (value === undefined) {
86
+ throw new UsageError(`--${name} needs a value`);
87
+ }
88
+ values.set(name, value);
89
+ continue;
90
+ }
91
+ }
92
+ rest.push(token);
93
+ }
94
+ return { globals: { values, bools }, rest };
95
+ }
96
+ /**
97
+ * Second pass, with the command known: command flags consume a value
98
+ * unless their schema says boolean; bare tokens are positionals.
99
+ */
100
+ function parseCommandArgs(tokens, boolFlags) {
101
+ const flags = new Map();
102
+ const positionals = [];
103
+ for (let i = 0; i < tokens.length; i++) {
104
+ const token = tokens[i];
105
+ if (!token.startsWith("--")) {
106
+ positionals.push(token);
107
+ continue;
108
+ }
109
+ const eq = token.indexOf("=");
110
+ const name = eq === -1 ? token.slice(2) : token.slice(2, eq);
111
+ let value;
112
+ if (eq !== -1) {
113
+ value = token.slice(eq + 1);
114
+ }
115
+ else if (boolFlags.has(name)) {
116
+ value = "";
117
+ }
118
+ else {
119
+ const next = tokens[++i];
120
+ if (next === undefined) {
121
+ throw new UsageError(`--${name} needs a value`);
122
+ }
123
+ value = next;
124
+ }
125
+ const list = flags.get(name) ?? [];
126
+ list.push(value);
127
+ flags.set(name, list);
128
+ }
129
+ return { positionals, flags };
130
+ }
131
+ /** PROD/STAGE/TEST are canonical uppercase; personal variants pass verbatim. */
132
+ function normalizeVariant(value) {
133
+ if (!value)
134
+ return undefined;
135
+ return /^(prod|stage|test)$/i.test(value) ? value.toUpperCase() : value;
136
+ }
137
+ /**
138
+ * `datum satellite …` (#504): hands the arguments to the installed
139
+ * `@datum/datum-satellite` binary (`satellite`) with inherited stdio, so the
140
+ * one command Account → Computers prints works as printed. The daemon is
141
+ * deliberately NOT bundled here — it runs on machines the panel never
142
+ * touches and carries its own version and contract (satellite/CHANGELOG.md).
143
+ */
144
+ /** The hand-written verbs of `space-manifest` (#1809, #1824) — `list`/`set` stay derived. */
145
+ const SPACE_MANIFEST_DIR_VERBS = new Set(["pull", "push", "init", "validate", "schema"]);
146
+ /** `datum preview <verb>` (#2928) — `datum previews` lists. */
147
+ const PREVIEW_VERBS = new Set(["up", "down", "trace"]);
148
+ const SPACE_MANIFEST_DIR_FLAGS = {
149
+ pull: { bools: [], values: [] },
150
+ push: { bools: ["dry-run", "prune", "expect-empty"], values: ["markdown"] },
151
+ init: { bools: ["refresh"], values: [] },
152
+ validate: { bools: [], values: [] },
153
+ schema: { bools: [], values: [] },
154
+ };
155
+ /**
156
+ * `datum space-manifest pull|push|init|validate <dir>` and `schema [kind]`:
157
+ * one positional (the directory; the kind, optional, for schema) and the
158
+ * verb's own flags; `--space` / `DATUM_SPACE` (a global) picks the space
159
+ * host for the calls and names the space for init, `--raw` prints the
160
+ * server's answer of `push` as JSON instead of the Markdown plan.
161
+ */
162
+ async function runSpaceManifestDir(verb, tokens, space, raw, resolveConfig) {
163
+ const io = { out: process.stdout, err: process.stderr };
164
+ const known = SPACE_MANIFEST_DIR_FLAGS[verb];
165
+ let args;
166
+ try {
167
+ args = parseCommandArgs(tokens, new Set(known.bools));
168
+ }
169
+ catch (err) {
170
+ if (err instanceof UsageError) {
171
+ console.error(`datum: ${err.message}`);
172
+ return 2;
173
+ }
174
+ throw err;
175
+ }
176
+ const allowed = [...known.bools, ...known.values];
177
+ const unknown = [...args.flags.keys()].find((f) => !allowed.includes(f));
178
+ if (unknown !== undefined) {
179
+ console.error(`datum: unknown flag --${unknown}${allowed.length ? `. Known flags: ${allowed.map((f) => `--${f}`).join(", ")}.` : " — this command takes none."}`);
180
+ return 2;
181
+ }
182
+ if (verb === "schema") {
183
+ if (args.positionals.length > 1) {
184
+ console.error("datum: expected at most 1 argument ([kind]) — see: datum space-manifest schema --help");
185
+ return 2;
186
+ }
187
+ const config = await resolveConfig();
188
+ if (!config)
189
+ return 2;
190
+ const kind = args.positionals[0];
191
+ return runSpaceManifestSchema(kind !== undefined ? { kind } : {}, config, io);
192
+ }
193
+ if (args.positionals.length !== 1) {
194
+ console.error(`datum: expected 1 argument (<dir>) — see: datum space-manifest ${verb} --help`);
195
+ return 2;
196
+ }
197
+ const dir = args.positionals[0];
198
+ if (verb === "init")
199
+ return runSpaceManifestInit({ dir, space, refresh: args.flags.has("refresh") }, io);
200
+ const config = await resolveConfig();
201
+ if (!config)
202
+ return 2;
203
+ if (verb === "pull")
204
+ return runSpaceManifestPull({ dir }, config, io);
205
+ if (verb === "validate")
206
+ return runSpaceManifestValidate({ dir }, config, io);
207
+ const markdown = args.flags.get("markdown")?.at(-1);
208
+ return runSpaceManifestPush({
209
+ dir,
210
+ dryRun: args.flags.has("dry-run"),
211
+ prune: args.flags.has("prune"),
212
+ expectEmpty: args.flags.has("expect-empty"),
213
+ ...(markdown !== undefined ? { markdown } : {}),
214
+ json: raw,
215
+ }, config, io);
216
+ }
217
+ async function runSatellitePassthrough(given) {
218
+ const { spawn, spawnSync } = await import("node:child_process");
219
+ // The one-time pairing token travels in the daemon's environment, never on a command line (#2948) —
220
+ // when the installed daemon reads it (0.21.2+); an older one still gets the flag, and is told to upgrade.
221
+ const out = pairTokenOutOfArgs(given);
222
+ let args = given;
223
+ let env = process.env;
224
+ if (out.token) {
225
+ process.title = maskedTitle(process.argv, out.token);
226
+ const probe = spawnSync("satellite", ["--version"], { encoding: "utf8", timeout: 10_000 });
227
+ if (daemonReadsPairTokenEnv(probe.stdout ?? "")) {
228
+ args = out.args;
229
+ env = { ...process.env, [SATELLITE_PAIR_TOKEN_ENV]: out.token };
230
+ }
231
+ else if (!probe.error) {
232
+ console.error(`datum: this satellite daemon is older than ${SATELLITE_PAIR_TOKEN_ENV_SINCE} — the pairing token goes on its command line (visible in ps until it pairs). Upgrade: npm i -g @datum/datum-satellite`);
233
+ }
234
+ }
235
+ return new Promise((resolve) => {
236
+ const child = spawn("satellite", args, { stdio: "inherit", env });
237
+ child.on("error", (err) => {
238
+ if (err.code === "ENOENT") {
239
+ console.error("datum: the satellite daemon is not installed on this machine — install it first:\n" +
240
+ " npm i -g @datum/datum-satellite (from your organisation's npm registry)\n" +
241
+ "then re-run the command.");
242
+ resolve(2);
243
+ return;
244
+ }
245
+ console.error(`datum: cannot start the satellite daemon: ${err.message}`);
246
+ resolve(1);
247
+ });
248
+ child.on("close", (code, signal) => resolve(code ?? (signal ? 130 : 1)));
249
+ });
250
+ }
251
+ function appVersion() {
252
+ const componentDir = path.dirname(fileURLToPath(import.meta.url));
253
+ // ../package.json: the published npm package; ../../../package.json: the
254
+ // repo root when running from server/src or server/dist.
255
+ for (const rel of ["../package.json", "../../../package.json"]) {
256
+ try {
257
+ const pkg = JSON.parse(readFileSync(path.resolve(componentDir, rel), "utf8"));
258
+ if (pkg.version)
259
+ return pkg.version;
260
+ }
261
+ catch {
262
+ // try the next candidate
263
+ }
264
+ }
265
+ return "unknown";
266
+ }
267
+ function updateDeps() {
268
+ return { fetch, run: runProcess, nodePath: process.execPath, now: Date.now };
269
+ }
270
+ /**
271
+ * The server the launcher's update check asks: the same resolution as the
272
+ * commands (flag, env, the sign-in's default) — but NO localhost fallback,
273
+ * a machine that configured no server has nothing to ask.
274
+ */
275
+ function configuredBaseUrl(argv) {
276
+ for (let i = 0; i < argv.length; i++) {
277
+ const t = argv[i];
278
+ if (t === "--base-url")
279
+ return argv[i + 1] ?? null;
280
+ if (t.startsWith("--base-url="))
281
+ return t.slice("--base-url=".length);
282
+ }
283
+ return (process.env.DATUM_BASE_URL ??
284
+ process.env.MCA_BASE_URL ??
285
+ new CredentialStore().defaultBaseUrl() ??
286
+ null);
287
+ }
288
+ /**
289
+ * The binary's entry: the self-update preflight (selfUpdate.ts) may hand the
290
+ * process over to a newer installed version — same argv, inherited stdio,
291
+ * its exit code — before main() runs here at all.
292
+ */
293
+ export async function runBin(argv) {
294
+ const entryFile = fileURLToPath(import.meta.url);
295
+ const pre = await preflight({
296
+ argv,
297
+ env: process.env,
298
+ entryDir: path.dirname(entryFile),
299
+ entryFile,
300
+ ownVersion: appVersion(),
301
+ baseUrl: configuredBaseUrl(argv),
302
+ paths: updatePaths(),
303
+ deps: updateDeps(),
304
+ err: process.stderr,
305
+ });
306
+ if (pre.delegate) {
307
+ const { spawnSync } = await import("node:child_process");
308
+ const run = spawnSync(process.execPath, [pre.delegate, ...argv], {
309
+ stdio: "inherit",
310
+ env: { ...process.env, [ENV_DELEGATED]: "1", [ENV_LAUNCHER_VERSION]: appVersion() },
311
+ });
312
+ if (run.error) {
313
+ console.error(`datum: cannot start ${pre.delegate} (${run.error.message}) — running the installed package instead`);
314
+ return main(argv);
315
+ }
316
+ return run.status ?? (run.signal ? 130 : 1);
317
+ }
318
+ return main(argv);
319
+ }
320
+ export async function main(argv) {
321
+ const style = createStyle(process.stdout.isTTY === true);
322
+ const commands = buildCommandTree(spec);
323
+ let globals;
324
+ let rest;
325
+ try {
326
+ ({ globals, rest } = extractGlobals(argv));
327
+ }
328
+ catch (err) {
329
+ if (err instanceof UsageError) {
330
+ console.error(`datum: ${err.message}`);
331
+ return 2;
332
+ }
333
+ throw err;
334
+ }
335
+ if (globals.bools.has("version")) {
336
+ console.log(`datum ${appVersion()} (API contract ${spec.info.version})`);
337
+ return 0;
338
+ }
339
+ // Built-ins that are about the CLI itself, not an API operation.
340
+ if (rest[0] === "help") {
341
+ rest = rest.slice(1);
342
+ globals.bools.add("help");
343
+ }
344
+ if (rest[0] === "spec") {
345
+ // The largest fixed dump this CLI owns (over a megabyte), so it obeys
346
+ // the same rule as an oversized response: parked in a file unless the
347
+ // caller asked for it on stdout (#646).
348
+ const rendered = JSON.stringify(spec, null, 2);
349
+ const bytes = Buffer.byteLength(rendered);
350
+ if (!globals.bools.has("full") && bytes > MAX_PRINTED_RESPONSE_BYTES) {
351
+ const file = await spillResponse(rendered);
352
+ process.stdout.write(spillNotice(bytes, file, spec, {
353
+ what: "the OpenAPI spec",
354
+ hint: "re-run with --full",
355
+ }));
356
+ return 0;
357
+ }
358
+ process.stdout.write(`${rendered}\n`);
359
+ return 0;
360
+ }
361
+ if (rest[0] === "commands") {
362
+ console.log(renderCommandList(commands, style));
363
+ return 0;
364
+ }
365
+ // The person sign-in (docs/developer-access.md) — the CLI's own, not API
366
+ // operations. Base URL resolution here also honours the stored default
367
+ // (`datum login` remembers where it signed in).
368
+ const store = new CredentialStore();
369
+ const orgBaseUrl = globals.values.get("base-url") ??
370
+ process.env.DATUM_BASE_URL ??
371
+ process.env.MCA_BASE_URL ??
372
+ store.defaultBaseUrl() ??
373
+ "http://localhost:3001";
374
+ const space = globals.values.get("space") ?? process.env.DATUM_SPACE;
375
+ // MCA_* fallbacks: pre-rename shells keep working; DATUM_* always wins.
376
+ // Resolved here, before the sign-in commands, because `whoami`/`login` must
377
+ // tell the person which credential API commands would actually use (#343):
378
+ // a key ALWAYS wins over the stored sign-in (docs/cli.md "Auth").
379
+ // A credential PROCESS (#2719) sits between the flag and the env key:
380
+ // on a personal agent's machine it hands out the person's own short-lived
381
+ // token. Run lazily — only once a command needs a credential.
382
+ const credentialProcess = globals.values.has("api-key") ? "" : (process.env[CREDENTIAL_PROCESS_ENV]?.trim() ?? "");
383
+ let apiKeySource = globals.values.has("api-key")
384
+ ? "--api-key"
385
+ : credentialProcess
386
+ ? CREDENTIAL_PROCESS_ENV
387
+ : process.env.DATUM_API_KEY
388
+ ? "DATUM_API_KEY"
389
+ : process.env.MCA_API_KEY
390
+ ? "MCA_API_KEY"
391
+ : undefined;
392
+ let apiKey = globals.values.get("api-key") ??
393
+ (credentialProcess ? undefined : (process.env.DATUM_API_KEY ?? process.env.MCA_API_KEY));
394
+ const authIo = { out: process.stdout, err: process.stderr, store, apiKeySource, apiKey };
395
+ /** Runs the credential process once (null = reported, exit 2). */
396
+ const loadProcessCredential = () => {
397
+ if (!credentialProcess || apiKey)
398
+ return true;
399
+ try {
400
+ apiKey = readCredentialProcess(credentialProcess).token;
401
+ authIo.apiKey = apiKey;
402
+ return true;
403
+ }
404
+ catch (err) {
405
+ if (!(err instanceof CredentialProcessError))
406
+ throw err;
407
+ console.error(`datum: ${err.message}`);
408
+ apiKeySource = undefined;
409
+ return false;
410
+ }
411
+ };
412
+ /**
413
+ * The credential and the surface every API call rides — shared by the
414
+ * derived commands and by the hand-written `artifacts publish`, which is
415
+ * an API command too (it just spans several calls). Resolved lazily,
416
+ * because group help and the sign-in commands must not demand a
417
+ * credential. Returns null after reporting a usage problem.
418
+ */
419
+ const resolveConfig = async () => {
420
+ // --curl renders a placeholder: no reason to ask for a real token.
421
+ if (!globals.bools.has("curl") && !loadProcessCredential())
422
+ return null;
423
+ const variant = normalizeVariant(globals.values.get("env") ?? process.env.DATUM_ENV ?? process.env.MCA_ENV);
424
+ // No key → the person's sign-in, refreshed when about to expire. A
425
+ // missing/expired sign-in is reported as usage (2), like a missing key.
426
+ let bearerToken;
427
+ if (!apiKey) {
428
+ try {
429
+ bearerToken = await ensureAccessToken(orgBaseUrl, store);
430
+ }
431
+ catch (err) {
432
+ if (err instanceof NotLoggedInError || err instanceof LoginFlowError) {
433
+ // --curl renders a placeholder anyway — no sign-in needed for that.
434
+ if (!globals.bools.has("curl")) {
435
+ console.error(`datum: ${err.message}`);
436
+ return null;
437
+ }
438
+ }
439
+ else {
440
+ throw err;
441
+ }
442
+ }
443
+ }
444
+ let baseUrl = orgBaseUrl;
445
+ if (space) {
446
+ try {
447
+ // --space addresses the space's own host (tenant by host, like the panel).
448
+ baseUrl = spaceBaseUrl(orgBaseUrl, space);
449
+ }
450
+ catch (err) {
451
+ if (err instanceof UsageError) {
452
+ console.error(`datum: ${err.message}`);
453
+ return null;
454
+ }
455
+ throw err;
456
+ }
457
+ }
458
+ return {
459
+ baseUrl,
460
+ ...(apiKey ? { apiKey } : {}),
461
+ ...(bearerToken ? { bearerToken } : {}),
462
+ ...(variant ? { variant } : {}),
463
+ // --curl only renders the request (with a placeholder credential).
464
+ ...(globals.bools.has("curl") ? { allowMissingKey: true } : {}),
465
+ };
466
+ };
467
+ /**
468
+ * `datum artifacts publish <dir>` is hand-written (#589), but the spec
469
+ * also DERIVES `artifacts publish plan` and `artifacts publish commit`
470
+ * from the two-step transport endpoints. A third word that names one of
471
+ * those derived commands therefore belongs to the tree; anything else is
472
+ * the directory to publish. (A directory literally called `plan` needs
473
+ * `./plan`.)
474
+ */
475
+ const derivedUnderArtifactsPublish = (words) => words[0] === "artifacts" &&
476
+ words[1] === "publish" &&
477
+ commands.some((c) => c.name === `artifacts publish ${words[2] ?? ""}`);
478
+ // A directory that happens to be called `plan` or `commit` loses to the
479
+ // derived command — say how to reach it instead of leaving the person
480
+ // with "missing required flag --files" (#589 e2e).
481
+ if (derivedUnderArtifactsPublish(rest) && rest[2] && existsSync(rest[2]) && statSync(rest[2]).isDirectory()) {
482
+ console.error(`datum: \`${rest[2]}\` is also the derived command \`artifacts publish ${rest[2]}\` — to publish the directory, write \`./${rest[2]}\``);
483
+ }
484
+ // The hand-written commands' --help (and `datum help <cmd>`) must print
485
+ // usage — never start a device-code flow or block on stdio waiting for MCP
486
+ // frames, which is what falling through into the handler did.
487
+ const builtin = derivedUnderArtifactsPublish(rest)
488
+ ? undefined
489
+ : BUILTIN_COMMANDS.find((c) => c.tokens.every((t, i) => rest[i] === t));
490
+ if (builtin && globals.bools.has("help")) {
491
+ process.stdout.write(`${renderBuiltinHelp(builtin, style)}\n`);
492
+ return 0;
493
+ }
494
+ if (rest[0] === "update") {
495
+ // Self-update (#1512, selfUpdate.ts): the CLI's own, not an API operation.
496
+ return runUpdate({
497
+ args: rest.slice(1),
498
+ baseUrl: orgBaseUrl,
499
+ ownVersion: appVersion(),
500
+ entryDir: path.dirname(fileURLToPath(import.meta.url)),
501
+ env: process.env,
502
+ paths: updatePaths(),
503
+ deps: updateDeps(),
504
+ }, { out: process.stdout, err: process.stderr });
505
+ }
506
+ if (rest[0] === "login")
507
+ return runLogin(orgBaseUrl, authIo);
508
+ if (rest[0] === "logout")
509
+ return runLogout(orgBaseUrl, authIo);
510
+ if (rest[0] === "whoami")
511
+ return loadProcessCredential() ? runWhoami(orgBaseUrl, space, authIo) : 2;
512
+ if (rest[0] === "a2a")
513
+ return loadProcessCredential() ? runA2a(orgBaseUrl, space, rest.slice(1), authIo) : 2;
514
+ if (rest[0] === "mcp" && rest[1] === "serve") {
515
+ return runMcpServe({ baseUrl: orgBaseUrl, space, store, version: appVersion() });
516
+ }
517
+ if (rest[0] === "mcp") {
518
+ // `datum mcp` / `datum mcp --help` / `datum mcp start`: the only verb is
519
+ // `serve` — say so instead of the API tree's "Unknown command: mcp".
520
+ const serve = BUILTIN_COMMANDS.find((c) => c.tokens[1] === "serve");
521
+ console.error(`datum: did you mean \`datum mcp serve\`? (the stdio MCP server for Claude Code)`);
522
+ process.stdout.write(`${renderBuiltinHelp(serve, style)}\n`);
523
+ return globals.bools.has("help") ? 0 : 2;
524
+ }
525
+ if (rest[0] === "satellite") {
526
+ // `datum satellite …` (#504): the daemon lives in its own package (it
527
+ // runs on machines the panel never touches); this CLI only hands the
528
+ // arguments to the installed binary so the one command the Account page
529
+ // shows works as printed.
530
+ return runSatellitePassthrough(rest.slice(1));
531
+ }
532
+ if (rest[0] === "artifacts" && rest[1] === "publish" && !derivedUnderArtifactsPublish(rest)) {
533
+ // Publishing a page from outside a run (#589): one command, but a walk
534
+ // and up to N+2 HTTP calls — the derived tree cannot express it, so
535
+ // POST /api/artifacts/publish is `x-cli: hidden` and this owns the name.
536
+ const config = await resolveConfig();
537
+ if (!config)
538
+ return 2;
539
+ let args;
540
+ try {
541
+ args = parseCommandArgs(rest.slice(2), new Set());
542
+ }
543
+ catch (err) {
544
+ if (err instanceof UsageError) {
545
+ console.error(`datum: ${err.message}`);
546
+ return 2;
547
+ }
548
+ throw err;
549
+ }
550
+ const flag = (name) => args.flags.get(name)?.at(-1);
551
+ const unknown = [...args.flags.keys()].filter((f) => !["key", "title", "description", "entry"].includes(f));
552
+ if (unknown.length > 0) {
553
+ console.error(`datum: unknown flag --${unknown[0]}. Known flags: --key, --title, --description, --entry.`);
554
+ return 2;
555
+ }
556
+ if (args.positionals.length !== 1) {
557
+ console.error("datum: expected 1 argument (<dir>) — see: datum artifacts publish --help");
558
+ return 2;
559
+ }
560
+ return runArtifactsPublish({
561
+ dir: args.positionals[0],
562
+ ...(flag("key") !== undefined ? { key: flag("key") } : {}),
563
+ ...(flag("title") !== undefined ? { title: flag("title") } : {}),
564
+ ...(flag("description") !== undefined ? { description: flag("description") } : {}),
565
+ ...(flag("entry") !== undefined ? { entry: flag("entry") } : {}),
566
+ }, config, { out: process.stdout, err: process.stderr });
567
+ }
568
+ if (rest[0] === "previews" || (rest[0] === "preview" && PREVIEW_VERBS.has(rest[1] ?? ""))) {
569
+ // The preview gateway from a terminal (#2928): a foreground loop and a
570
+ // follow over /api/acc-previews — nothing the derived tree can express.
571
+ const config = await resolveConfig();
572
+ if (!config)
573
+ return 2;
574
+ const verb = rest[0] === "previews" ? "list" : rest[1];
575
+ let args;
576
+ try {
577
+ args = parseCommandArgs(rest.slice(rest[0] === "previews" ? 1 : 2), new Set(["follow", "json"]));
578
+ }
579
+ catch (err) {
580
+ if (err instanceof UsageError) {
581
+ console.error(`datum: ${err.message}`);
582
+ return 2;
583
+ }
584
+ throw err;
585
+ }
586
+ const flag = (name) => args.flags.get(name)?.at(-1);
587
+ const known = { list: ["json"], up: ["port", "vhost", "machine"], down: [], trace: ["follow", "limit"] };
588
+ const unknown = [...args.flags.keys()].find((f) => !known[verb].includes(f));
589
+ if (unknown) {
590
+ console.error(`datum: unknown flag --${unknown} — see: datum ${rest[0] === "previews" ? "previews" : `preview ${verb}`} --help`);
591
+ return 2;
592
+ }
593
+ // Ctrl+C ends the foreground verbs cleanly (up closes its preview); the others keep the default.
594
+ const foreground = verb === "up" || (verb === "trace" && args.flags.has("follow"));
595
+ const io = {
596
+ out: process.stdout,
597
+ err: process.stderr,
598
+ // A foreground verb outlives one access token: refresh before each call.
599
+ freshToken: () => ensureAccessToken(orgBaseUrl, store),
600
+ ...(foreground ? { interrupted: interruptedBySignal() } : {}),
601
+ };
602
+ if (verb === "list")
603
+ return runPreviewsList({ json: args.flags.has("json") }, config, io);
604
+ if (verb === "up") {
605
+ const port = Number(flag("port"));
606
+ if (!Number.isInteger(port) || port < 1 || port > 65_535) {
607
+ console.error("datum: --port <1-65535> is required — the port your app listens on (127.0.0.1)");
608
+ return 2;
609
+ }
610
+ return runPreviewUp({ port, ...(flag("vhost") ? { vhost: flag("vhost") } : {}), ...(flag("machine") ? { machine: flag("machine") } : {}) }, config, io);
611
+ }
612
+ if (args.positionals.length !== 1) {
613
+ console.error(`datum: expected 1 argument (<id>, from datum previews) — see: datum preview ${verb} --help`);
614
+ return 2;
615
+ }
616
+ const id = args.positionals[0];
617
+ if (verb === "down")
618
+ return runPreviewDown({ id }, config, io);
619
+ const limit = flag("limit") !== undefined ? Number(flag("limit")) : undefined;
620
+ return runPreviewTrace({ id, follow: args.flags.has("follow"), ...(limit !== undefined && Number.isInteger(limit) ? { limit } : {}) }, config, io);
621
+ }
622
+ if (rest[0] === "outputs" && rest[1] === "download") {
623
+ // An output's bytes (#2680): metadata from /api, the file from the
624
+ // origin's /outputs route — two calls the derived tree cannot express.
625
+ const config = await resolveConfig();
626
+ if (!config)
627
+ return 2;
628
+ let args;
629
+ try {
630
+ args = parseCommandArgs(rest.slice(2), new Set());
631
+ }
632
+ catch (err) {
633
+ if (err instanceof UsageError) {
634
+ console.error(`datum: ${err.message}`);
635
+ return 2;
636
+ }
637
+ throw err;
638
+ }
639
+ const unknown = [...args.flags.keys()].filter((f) => f !== "out");
640
+ if (unknown.length > 0) {
641
+ console.error(`datum: unknown flag --${unknown[0]}. Known flags: --out.`);
642
+ return 2;
643
+ }
644
+ if (args.positionals.length !== 1) {
645
+ console.error("datum: expected 1 argument (<id>) — see: datum outputs download --help");
646
+ return 2;
647
+ }
648
+ const out = args.flags.get("out")?.at(-1);
649
+ return runOutputsDownload({ id: args.positionals[0], ...(out !== undefined ? { out } : {}) }, config, { out: process.stdout, err: process.stderr });
650
+ }
651
+ // The derived `space-manifest schema list` and `space-manifest validate
652
+ // --spec …` (the raw endpoints) stay reachable: the hand-written verb
653
+ // takes a kind / a directory, never the derived command's own words.
654
+ const derivedSpaceManifest = (rest[1] === "schema" && rest[2] === "list") ||
655
+ (rest[1] === "validate" &&
656
+ (globals.values.has("data") || rest.slice(2).some((t) => t === "--spec" || t.startsWith("--spec="))));
657
+ if (rest[0] === "space-manifest" && SPACE_MANIFEST_DIR_VERBS.has(rest[1] ?? "") && !derivedSpaceManifest) {
658
+ // A space's configuration as files (#1809): each verb walks a directory
659
+ // around one call of the derived `space-manifest list|set`.
660
+ return runSpaceManifestDir(rest[1], rest.slice(2), space, globals.bools.has("raw"), resolveConfig);
661
+ }
662
+ if (rest.length === 0) {
663
+ console.log(renderRootHelp(spec, commands, style));
664
+ return 0;
665
+ }
666
+ const words = [];
667
+ for (const token of rest) {
668
+ if (token.startsWith("--"))
669
+ break;
670
+ words.push(token);
671
+ }
672
+ const match = matchCommand(commands, words);
673
+ if (!match) {
674
+ console.log(renderGroupHelp(words, commands, style));
675
+ // A valid group listing is help (0); an unknown word is an error (2).
676
+ const prefix = words.join(" ");
677
+ const known = commands.some((c) => c.name.startsWith(`${prefix} `));
678
+ return known ? 0 : 2;
679
+ }
680
+ const { command } = match;
681
+ if (globals.bools.has("help")) {
682
+ console.log(renderCommandHelp(command, style));
683
+ return 0;
684
+ }
685
+ // Bare group ("datum cases") — words matched nothing longer but the exact
686
+ // match needs its positionals/flags; a SHORTER match than the words given
687
+ // means trailing words are positionals, which is the normal call path.
688
+ const boolFlags = new Set([...command.queryFlags, ...command.bodyFlags]
689
+ .filter((f) => f.type === "boolean")
690
+ .map((f) => f.flag));
691
+ const commandTokens = rest.slice(command.name.split(" ").length);
692
+ const config = await resolveConfig();
693
+ if (!config)
694
+ return 2;
695
+ try {
696
+ const args = parseCommandArgs(commandTokens, boolFlags);
697
+ if (globals.values.has("data")) {
698
+ args.body = globals.values.get("data");
699
+ }
700
+ const request = buildRequest(command, args, config);
701
+ if (globals.bools.has("curl")) {
702
+ console.log(toCurl(request, { full: globals.bools.has("full") }));
703
+ return 0;
704
+ }
705
+ const result = await executeRequest(request, {
706
+ raw: globals.bools.has("raw"),
707
+ full: globals.bools.has("full"),
708
+ ...(globals.values.has("fields")
709
+ ? {
710
+ fields: globals.values
711
+ .get("fields")
712
+ .split(",")
713
+ .map((f) => f.trim())
714
+ .filter(Boolean),
715
+ }
716
+ : {}),
717
+ out: process.stdout,
718
+ });
719
+ if (!result.ok) {
720
+ const detail = result.detail ? ` — ${result.detail}` : "";
721
+ console.error(`datum: HTTP ${result.status}${detail} (${command.method} ${request.url})`);
722
+ if (result.status === 401 && !result.detail) {
723
+ console.error(config.apiKey
724
+ ? "datum: the API key was rejected — check DATUM_API_KEY / --api-key (panel: Settings → API keys)"
725
+ : "datum: the sign-in was rejected — run `datum login` again");
726
+ }
727
+ // A write waiting for the person's approval (#2719) is not a failure
728
+ // of the call: its own exit code, like datum-tool's pending_approval.
729
+ if (result.code === APPROVAL_REQUIRED_CODE)
730
+ return EXIT_APPROVAL_REQUIRED;
731
+ return 1;
732
+ }
733
+ return 0;
734
+ }
735
+ catch (err) {
736
+ if (err instanceof UsageError) {
737
+ console.error(`datum: ${err.message}`);
738
+ return 2;
739
+ }
740
+ console.error(`datum: ${describeNetworkError(err, config.baseUrl)}`);
741
+ return 1;
742
+ }
743
+ }
744
+ /** fetch() failures arrive as TypeError with a coded `cause` — translate. */
745
+ function describeNetworkError(err, baseUrl) {
746
+ const cause = err.cause;
747
+ switch (cause?.code) {
748
+ case "ENOTFOUND":
749
+ case "EAI_AGAIN":
750
+ return `cannot resolve the host in ${baseUrl} — check --base-url / DATUM_BASE_URL (and VPN/DNS)`;
751
+ case "ECONNREFUSED":
752
+ return `connection refused by ${baseUrl} — is the server running there?`;
753
+ case "ETIMEDOUT":
754
+ case "UND_ERR_CONNECT_TIMEOUT":
755
+ return `connection to ${baseUrl} timed out — network, VPN or firewall?`;
756
+ case "CERT_HAS_EXPIRED":
757
+ case "DEPTH_ZERO_SELF_SIGNED_CERT":
758
+ case "UNABLE_TO_VERIFY_LEAF_SIGNATURE":
759
+ return `TLS certificate problem at ${baseUrl}: ${cause.code}`;
760
+ default: {
761
+ const message = cause?.message ?? (err instanceof Error ? err.message : String(err));
762
+ return `request to ${baseUrl} failed — ${message}`;
763
+ }
764
+ }
765
+ }
766
+ /** The script name as the OS passed it — both separators, because Windows
767
+ * hands `C:\…\cli\datum.js` (#1816: the old `/`-only check let the CLI exit
768
+ * 0 without a word on every Windows machine). */
769
+ export function scriptBaseName(argv1) {
770
+ return argv1.split(/[\\/]/).pop() ?? "";
771
+ }
772
+ /**
773
+ * Whether this module runs as the `datum` binary (not imported by tests):
774
+ * the script node started IS this file (realpath — the npm bin shims
775
+ * `datum` and `mca` on macOS/Linux are symlinks to it), or its name is
776
+ * `datum`, `datum.js` or `datum.ts` on either separator. `unrecognised` =
777
+ * a bin shim's name that matched neither — the caller says so out loud
778
+ * instead of exiting 0 in silence.
779
+ */
780
+ export function cliEntryOf(argv1, moduleFile, realpath) {
781
+ if (!argv1)
782
+ return "imported";
783
+ try {
784
+ if (realpath(argv1) === realpath(moduleFile))
785
+ return "run";
786
+ }
787
+ catch {
788
+ // an argv[1] that is not a file (a REPL, -e) — the name decides
789
+ }
790
+ const name = scriptBaseName(argv1);
791
+ if (/^datum(\.(ts|js))?$/i.test(name))
792
+ return "run";
793
+ return /^(datum|mca)(\.(cmd|ps1|exe))?$/i.test(name) ? "unrecognised" : "imported";
794
+ }
795
+ // Invoked as a binary — directly (…/datum.ts, …\datum.js) or through the
796
+ // npm bin shim (a symlink named `datum`; datum.cmd / datum.ps1 on Windows).
797
+ // NOTE: exitCode, never process.exit() — exit() drops buffered stdout on
798
+ // pipes, truncating large JSON responses mid-stream (seen at ~64 kB during
799
+ // skill testing).
800
+ const cliEntry = cliEntryOf(process.argv[1], fileURLToPath(import.meta.url), realpathSync);
801
+ if (cliEntry === "unrecognised") {
802
+ console.error(`datum: started as ${process.argv[1]} but did not recognise it as the CLI entry — nothing ran. ` +
803
+ "Please report this to the Datum team with the path above.");
804
+ process.exitCode = 1;
805
+ }
806
+ if (cliEntry === "run") {
807
+ runBin(process.argv.slice(2)).then((code) => {
808
+ process.exitCode = code;
809
+ }, (err) => {
810
+ console.error(`datum: ${err instanceof Error ? err.message : err}`);
811
+ process.exitCode = 1;
812
+ });
813
+ }
814
+ //# sourceMappingURL=datum.js.map