kalup

JSON output

The envelope/1 document every command prints with --json, field by field, and how to parse it.

Every Kalup command takes --json. With it, the command prints exactly one JSON document, envelope/1, to stdout and nothing else. Scripts, CI jobs and AI agents should always use it: the shape is versioned and fixed, while the human text may change.

The envelope

{
  "format": "envelope/1",
  "ok": true,
  "data": { "valid": true, "counts": { "errors": 0, "warnings": 0 } },
  "issues": []
}
FieldTypeAlways presentMeaning
format"envelope/1"YesThe version of this shape. It changes only additively inside envelope/1.
okbooleanYestrue when the command succeeded: exit 0, or exit 2 (differences found with --exit-code).
dataobjectNoThe command's result. Absent when the command failed before it had one. See What data holds.
issuesIssue[]YesErrors and warnings, possibly empty. On success, only warnings.

Issues

Every error and warning is one issue:

{
  "code": "E_UNKNOWN_TARGET",
  "message": "target 'staging' is not declared",
  "file": "kalup.config.ts",
  "line": 9,
  "configPath": "targets",
  "fix": "use one of sandbox, production, or declare targets.staging",
  "docs": "errors/E_UNKNOWN_TARGET.md"
}
FieldTypeAlways presentMeaning
codestringYesA stable code. E_ is an error, W_ a warning. Every code is listed in Errors.
messagestringYesOne sentence saying what is wrong. May change between versions; do not match on it.
filestringNoThe file, relative to the project root, when the issue is in one.
linenumberNoThe 1-based line in file.
configPathstringNoWhere in the config the issue is, as a dotted path: Company.properties.seatCount.fieldType, targets.sandbox.portalId.
fixstringNoWhat to do about it, in one line.
docsstringNoThe page for this code inside the installed package, relative to its docs/ folder. The same text is on Errors.
humanRequiredbooleanNotrue when only a person can resolve it, for example a key for the wrong portal. Such an issue comes with exit 4.

Warnings ride in issues even when ok is true. Tell them apart by the W_ prefix on code.

Where human text goes

Without --jsonWith --json
stdoutThe command's report, such as Config valid (0 errors, 0 warnings) or the IROne envelope/1 document
stderrOne line per issue: file:line: CODE: message (fix: ...) (docs: ...)Nothing from Kalup

--help and --version follow the same rule: with --json they are envelopes too, with data.usage and data.name, data.version, data.disclaimer.

What data holds

CommanddataShape
initThe new projecttarget, portalId, account, objects, scopes, files, and pull with the first pull's result (absent when that pull failed)
pullWhat was read and writtentarget, portalId, objects (a report per object in scope), files
pull --discoverWhat the scope leaves outtarget, portalId, objects (custom objects not in config), properties (per object, portal properties not in scope)
validateThe verdictvalid, counts.errors, counts.warnings
irThe IR documentirVersion, project, generator, resources, targets, tombstones. Absent with --check.
fmtThe files changedchanged: paths rewritten, or with --check, paths that would be
statusConfig and targetsconfig (validity and counts), targets (one entry per target with its checks)

Each command page documents its data in full.

When ok is false, data may still be present. validate always returns its verdict, status returns every target's checks even when one of them failed, and init keeps its result when only the first pull failed. ir, fmt and pull omit data on failure.

Parsing it robustly

  1. Read stdout as a whole and parse it as one JSON document. Do not read it line by line.
  2. Check format === "envelope/1" before you trust the rest.
  3. Branch on the process exit code first, then on ok. The exit code tells you what kind of outcome it is; ok alone cannot tell "invalid config" from "a person is needed".
  4. Match issues on code, never on message.
  5. Ignore fields you do not know. New optional fields may appear inside envelope/1.

A small example in Node:

import { execFileSync } from 'node:child_process'

function kalup(args: string[]) {
  try {
    const stdout = execFileSync('npx', ['kalup', ...args, '--json'], { encoding: 'utf8' })
    return { exitCode: 0, envelope: JSON.parse(stdout) }
  } catch (error) {
    // a non-zero exit still printed an envelope on stdout
    const e = error as { status: number; stdout: string }
    return { exitCode: e.status, envelope: JSON.parse(e.stdout) }
  }
}

const { exitCode, envelope } = kalup(['validate'])
if (exitCode === 3) {
  for (const issue of envelope.issues) console.log(`${issue.file}:${issue.line} ${issue.code} ${issue.fix ?? ''}`)
}

In a shell, with jq:

npx kalup validate --json | jq -r '.issues[] | select(.code | startswith("E_")) | "\(.file):\(.line) \(.code)"'

What it never contains

No envelope carries a key or a token, including in error messages: only the name of the variable a key is read from. Text that came from a portal is stripped of control characters and newlines, and capped in length, before it is printed. See Network and safety.

On this page