@devosurf/tesser 0.1.0-alpha.3 → 0.1.0-alpha.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/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // packages/cli/src/index.ts
2
2
  import { execFile as execFile2 } from "node:child_process";
3
- import { readFileSync as readFileSync4 } from "node:fs";
3
+ import { readFileSync as readFileSync5 } from "node:fs";
4
4
  import { promisify as promisify2 } from "node:util";
5
- import { Command } from "commander";
5
+ import { Command, Option } from "commander";
6
6
 
7
7
  // packages/sdk/src/internal/codec.ts
8
8
  var NotSerializableError = class extends TypeError {
@@ -126,14 +126,14 @@ var UNIT_MS = {
126
126
  w: 6048e5
127
127
  };
128
128
  var PART = /(\d+(?:\.\d+)?)(ms|s|m|h|d|w)/g;
129
- function parseDuration(input, what = "duration") {
130
- if (typeof input === "number") {
131
- if (!Number.isFinite(input) || input < 0) {
132
- throw new TypeError(`${what}: expected a non-negative number of ms, got ${input}`);
129
+ function parseDuration(input2, what = "duration") {
130
+ if (typeof input2 === "number") {
131
+ if (!Number.isFinite(input2) || input2 < 0) {
132
+ throw new TypeError(`${what}: expected a non-negative number of ms, got ${input2}`);
133
133
  }
134
- return input;
134
+ return input2;
135
135
  }
136
- const s = input.trim();
136
+ const s = input2.trim();
137
137
  let total = 0;
138
138
  let matchedLen = 0;
139
139
  for (const m of s.matchAll(PART)) {
@@ -142,7 +142,7 @@ function parseDuration(input, what = "duration") {
142
142
  }
143
143
  if (matchedLen === 0 || matchedLen !== s.replace(/\s+/g, "").length) {
144
144
  throw new TypeError(
145
- `${what}: "${input}" is not a duration \u2014 use forms like "250ms", "30s", "5m", "1h30m", "2d"`
145
+ `${what}: "${input2}" is not a duration \u2014 use forms like "250ms", "30s", "5m", "1h30m", "2d"`
146
146
  );
147
147
  }
148
148
  return Math.round(total);
@@ -251,7 +251,7 @@ function buildConnectorClient(connector, invoke) {
251
251
  for (const [key, child] of Object.entries(tree)) {
252
252
  const childPath = [...path, key];
253
253
  if (isAction(child)) {
254
- node[key] = (input) => invoke(childPath, child, input);
254
+ node[key] = (input2) => invoke(childPath, child, input2);
255
255
  } else {
256
256
  node[key] = walk(child, childPath);
257
257
  }
@@ -469,7 +469,7 @@ function buildOperators(def, ctx, callModel) {
469
469
  const usage = { tokens: 0, outputTokens: 0 };
470
470
  const out = {};
471
471
  for (const [operatorKey, op] of Object.entries(def.operators ?? {})) {
472
- out[operatorKey] = (input) => executeOperator({ def, ctx, operatorKey, op, input, usage, callModel });
472
+ out[operatorKey] = (input2) => executeOperator({ def, ctx, operatorKey, op, input: input2, usage, callModel });
473
473
  }
474
474
  return Object.freeze(out);
475
475
  }
@@ -664,8 +664,8 @@ function buildHarnesses(def, _ctx, callHarness) {
664
664
  if (!request.output) throw new TerminalError(`harness.${harnessKey}: output schema is required`);
665
665
  const raw = await callHarness({ automationId: def.id, harnessKey, harness: h, request });
666
666
  const serial = toSerializable2(raw, `harness.${harnessKey} result`);
667
- const output = await validateSchema(request.output, serial.output, `harness.${harnessKey} output`);
668
- return { ...serial, output: toSerializable2(output, `harness.${harnessKey} output`) };
667
+ const output2 = await validateSchema(request.output, serial.output, `harness.${harnessKey} output`);
668
+ return { ...serial, output: toSerializable2(output2, `harness.${harnessKey} output`) };
669
669
  }
670
670
  };
671
671
  }
@@ -702,14 +702,18 @@ var EXIT = {
702
702
 
703
703
  // packages/cli/src/output.ts
704
704
  var Output = class {
705
- constructor(json) {
706
- this.json = json;
705
+ format;
706
+ constructor(formatOrJson) {
707
+ this.format = typeof formatOrJson === "boolean" ? formatOrJson ? "json" : "text" : formatOrJson;
708
+ }
709
+ get json() {
710
+ return this.format !== "text";
707
711
  }
708
- json;
709
712
  /** Emit the command's data result. `human` renders the no-JSON form. */
710
713
  data(value, human) {
711
714
  if (this.json) {
712
- process.stdout.write(JSON.stringify(value, null, 2) + "\n");
715
+ const pretty = this.format === "json";
716
+ process.stdout.write(JSON.stringify(value, null, pretty ? 2 : 0) + "\n");
713
717
  } else {
714
718
  process.stdout.write((human ? human(value) : JSON.stringify(value, null, 2)) + "\n");
715
719
  }
@@ -720,7 +724,8 @@ var Output = class {
720
724
  }
721
725
  fail(code, message, extra) {
722
726
  if (this.json) {
723
- process.stdout.write(JSON.stringify({ error: { code, message, ...extra } }, null, 2) + "\n");
727
+ const pretty = this.format === "json";
728
+ process.stdout.write(JSON.stringify({ error: { code, message, ...extra } }, null, pretty ? 2 : 0) + "\n");
724
729
  } else {
725
730
  process.stderr.write(`error: ${message}
726
731
  `);
@@ -857,9 +862,214 @@ function resolveTarget(opts) {
857
862
  }
858
863
 
859
864
  // packages/cli/src/login.ts
865
+ import { createInterface } from "node:readline/promises";
866
+ import { stdin as input, stderr as output } from "node:process";
860
867
  function resolveLoginInstance(cmdOpts, globalOpts) {
861
868
  return cmdOpts.url ?? cmdOpts.instance ?? globalOpts.url ?? DEFAULT_INSTANCE_URL;
862
869
  }
870
+ async function resolveLoginInputs(cmdOpts, globalOpts) {
871
+ if ((cmdOpts.token !== void 0 || globalOpts.token !== void 0) && cmdOpts.tokenStdin === true) {
872
+ throw new CliError(EXIT.USAGE, "use either --token/--token-stdin, not both");
873
+ }
874
+ const token = cmdOpts.token ?? globalOpts.token ?? (cmdOpts.tokenStdin === true ? await readTrimmedStdin("token") : void 0);
875
+ const instance = resolveLoginInstance(cmdOpts, globalOpts);
876
+ if (token !== void 0 && token.length > 0) return { instance, token };
877
+ if (isInteractive()) {
878
+ const promptedInstance = await promptLine("Instance URL", instance);
879
+ const promptedToken = await promptSecret("API token");
880
+ if (promptedToken.length === 0) throw new CliError(EXIT.USAGE, "empty API token");
881
+ return { instance: promptedInstance || instance, token: promptedToken };
882
+ }
883
+ throw new CliError(
884
+ EXIT.USAGE,
885
+ "missing API token \u2014 pass --token-stdin, set TESSER_TOKEN, or run `tesser login` in an interactive terminal"
886
+ );
887
+ }
888
+ function isInteractive() {
889
+ return input.isTTY === true && output.isTTY === true;
890
+ }
891
+ async function readTrimmedStdin(label) {
892
+ const chunks = [];
893
+ for await (const chunk of input) chunks.push(chunk);
894
+ const value = Buffer.concat(chunks).toString("utf8").trim();
895
+ if (value.length === 0) throw new CliError(EXIT.USAGE, `empty ${label} on stdin`);
896
+ return value;
897
+ }
898
+ async function promptLine(label, defaultValue) {
899
+ const rl = createInterface({ input, output });
900
+ try {
901
+ const suffix = defaultValue ? ` [${defaultValue}]` : "";
902
+ return (await rl.question(`${label}${suffix}: `)).trim() || defaultValue;
903
+ } finally {
904
+ rl.close();
905
+ }
906
+ }
907
+ async function promptSecret(label) {
908
+ if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") {
909
+ throw new CliError(EXIT.USAGE, "cannot prompt for a token without an interactive terminal; use --token-stdin");
910
+ }
911
+ output.write(`${label}: `);
912
+ input.setRawMode(true);
913
+ input.resume();
914
+ input.setEncoding("utf8");
915
+ return await new Promise((resolve, reject) => {
916
+ let value = "";
917
+ const cleanup = () => {
918
+ input.off("data", onData);
919
+ input.setRawMode(false);
920
+ output.write("\n");
921
+ };
922
+ const onData = (char) => {
923
+ if (char === "") {
924
+ cleanup();
925
+ reject(new CliError(EXIT.USAGE, "login cancelled"));
926
+ return;
927
+ }
928
+ if (char === "\r" || char === "\n") {
929
+ cleanup();
930
+ resolve(value.trim());
931
+ return;
932
+ }
933
+ if (char === "\x7F" || char === "\b") {
934
+ value = value.slice(0, -1);
935
+ return;
936
+ }
937
+ value += char;
938
+ };
939
+ input.on("data", onData);
940
+ });
941
+ }
942
+
943
+ // packages/cli/src/completion.ts
944
+ var COMMANDS = [
945
+ "schema",
946
+ "init",
947
+ "upgrade",
948
+ "login",
949
+ "link",
950
+ "status",
951
+ "test",
952
+ "build",
953
+ "dev",
954
+ "deploy",
955
+ "connect",
956
+ "auth",
957
+ "harness",
958
+ "secrets",
959
+ "runs",
960
+ "logs",
961
+ "replay",
962
+ "rollback",
963
+ "doctor",
964
+ "completion"
965
+ ];
966
+ var CHILDREN = {
967
+ auth: ["claude-code", "pi"],
968
+ harness: ["connect"],
969
+ "harness connect": ["claude-code", "pi"],
970
+ secrets: ["list", "set", "rm"],
971
+ runs: ["list", "show", "trigger", "signal", "cancel"]
972
+ };
973
+ function completionScript(shell) {
974
+ if (shell === "bash") return bashCompletion();
975
+ if (shell === "zsh") return zshCompletion();
976
+ throw new CliError(EXIT.USAGE, "completion shell must be bash or zsh");
977
+ }
978
+ function bashCompletion() {
979
+ const commands2 = COMMANDS.join(" ");
980
+ const cases = Object.entries(CHILDREN).map(([parent, children]) => ` "${parent}") COMPREPLY=( $(compgen -W "${children.join(" ")}" -- "$cur") ) ;;`).join("\n");
981
+ return `# bash completion for tesser
982
+ _tesser_completion() {
983
+ local cur prev words cword
984
+ _init_completion -n : || return
985
+ if [[ $cword -eq 1 ]]; then
986
+ COMPREPLY=( $(compgen -W "${commands2}" -- "$cur") )
987
+ return
988
+ fi
989
+ local parent="\${COMP_WORDS[1]}"
990
+ if [[ $cword -ge 3 ]]; then parent="\${COMP_WORDS[1]} \${COMP_WORDS[2]}"; fi
991
+ case "$parent" in
992
+ ${cases}
993
+ esac
994
+ }
995
+ complete -F _tesser_completion tesser
996
+ `;
997
+ }
998
+ function zshCompletion() {
999
+ const commandLines = COMMANDS.map((command) => ` '${command}: :->${command.replace(/ /g, "-")}'`).join(" \\\n");
1000
+ return `#compdef tesser
1001
+ _tesser() {
1002
+ local -a commands
1003
+ _arguments -C '(-h --help)'{-h,--help}'[display help]' '--json[machine output]' '(-o --output)'{-o,--output}'[output format]:format:(auto text json ndjson)' '1:command:->cmds' '*::arg:->args'
1004
+ case $state in
1005
+ cmds)
1006
+ _values 'tesser commands' ${commandLines}
1007
+ ;;
1008
+ args)
1009
+ case $words[1] in
1010
+ secrets) _values 'secrets commands' list set rm ;;
1011
+ runs) _values 'runs commands' list show trigger signal cancel ;;
1012
+ auth) _values 'auth commands' claude-code pi ;;
1013
+ harness) _values 'harness commands' connect ;;
1014
+ esac
1015
+ ;;
1016
+ esac
1017
+ }
1018
+ _tesser
1019
+ `;
1020
+ }
1021
+
1022
+ // packages/cli/src/inputs.ts
1023
+ import { readFile } from "node:fs/promises";
1024
+ function parseJsonLiteral(value, label) {
1025
+ try {
1026
+ return JSON.parse(value);
1027
+ } catch (cause) {
1028
+ const detail = cause instanceof Error ? cause.message : String(cause);
1029
+ throw new CliError(EXIT.USAGE, `invalid JSON for ${label}: ${detail}`);
1030
+ }
1031
+ }
1032
+ async function readTextInput(pathOrDash, label) {
1033
+ if (pathOrDash === "-") {
1034
+ const chunks = [];
1035
+ for await (const chunk of process.stdin) chunks.push(chunk);
1036
+ return Buffer.concat(chunks).toString("utf8");
1037
+ }
1038
+ try {
1039
+ return await readFile(pathOrDash, "utf8");
1040
+ } catch (cause) {
1041
+ const detail = cause instanceof Error ? cause.message : String(cause);
1042
+ throw new CliError(EXIT.USAGE, `cannot read ${label} file ${pathOrDash}: ${detail}`);
1043
+ }
1044
+ }
1045
+ async function resolveJsonInput(opts) {
1046
+ if (opts.literal !== void 0 && opts.file !== void 0) {
1047
+ throw new CliError(EXIT.USAGE, `use either --${opts.label} or --${opts.label}-file, not both`);
1048
+ }
1049
+ if (opts.literal !== void 0) return parseJsonLiteral(opts.literal, `--${opts.label}`);
1050
+ if (opts.file !== void 0) return parseJsonLiteral(await readTextInput(opts.file, `--${opts.label}`), `--${opts.label}-file`);
1051
+ return void 0;
1052
+ }
1053
+ function parseIntOption(value, label, opts = {}) {
1054
+ if (!/^\d+$/.test(value)) throw new CliError(EXIT.USAGE, `${label} must be an integer`);
1055
+ const parsed = Number(value);
1056
+ if (!Number.isSafeInteger(parsed)) throw new CliError(EXIT.USAGE, `${label} is too large`);
1057
+ if (opts.min !== void 0 && parsed < opts.min) throw new CliError(EXIT.USAGE, `${label} must be >= ${opts.min}`);
1058
+ if (opts.max !== void 0 && parsed > opts.max) throw new CliError(EXIT.USAGE, `${label} must be <= ${opts.max}`);
1059
+ return parsed;
1060
+ }
1061
+ function projectFields(items, fieldsCsv) {
1062
+ if (fieldsCsv === void 0 || fieldsCsv.trim() === "") return items;
1063
+ const fields = fieldsCsv.split(",").map((field) => field.trim()).filter(Boolean);
1064
+ if (fields.length === 0) return items;
1065
+ return items.map((item) => {
1066
+ const out = {};
1067
+ for (const field of fields) {
1068
+ if (field in item) out[field] = item[field];
1069
+ }
1070
+ return out;
1071
+ });
1072
+ }
863
1073
 
864
1074
  // packages/cli/src/project.ts
865
1075
  import { mkdtempSync, existsSync as existsSync2, readdirSync, statSync } from "node:fs";
@@ -1074,22 +1284,109 @@ Then rerun: tesser deploy${opts.local ? " --local" : ""}`
1074
1284
  throw new CliError(EXIT.ERROR, "timed out waiting for the deploy to settle", { last });
1075
1285
  }
1076
1286
 
1287
+ // packages/cli/src/commands/doctor.ts
1288
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
1289
+ import { join as join3 } from "node:path";
1290
+ async function doctor(out, opts) {
1291
+ const checks = [];
1292
+ checks.push({
1293
+ name: "project",
1294
+ ok: opts.target.projectRoot !== null,
1295
+ severity: opts.target.projectRoot ? "info" : "warning",
1296
+ message: opts.target.projectRoot ? `Project ${opts.target.project ?? "(unnamed)"} found` : "No tesser.json found from this directory",
1297
+ ...opts.target.projectRoot ? {} : { hint: "Run inside a Tesser Project, or create one with `tesser init <name>`." }
1298
+ });
1299
+ checks.push({
1300
+ name: "auth-token",
1301
+ ok: opts.target.token !== void 0,
1302
+ severity: opts.target.token ? "info" : "warning",
1303
+ message: opts.target.token ? "API token is configured" : "No API token configured",
1304
+ ...opts.target.token ? {} : { hint: "Run `tesser login` or set TESSER_TOKEN." }
1305
+ });
1306
+ if (opts.target.projectRoot) {
1307
+ const pkgPath = join3(opts.target.projectRoot, "package.json");
1308
+ const lockPath = join3(opts.target.projectRoot, "pnpm-lock.yaml");
1309
+ checks.push({
1310
+ name: "package-json",
1311
+ ok: existsSync3(pkgPath),
1312
+ severity: existsSync3(pkgPath) ? "info" : "warning",
1313
+ message: existsSync3(pkgPath) ? "package.json exists" : "package.json missing"
1314
+ });
1315
+ checks.push({
1316
+ name: "pnpm-lock",
1317
+ ok: existsSync3(lockPath),
1318
+ severity: existsSync3(lockPath) ? "info" : "warning",
1319
+ message: existsSync3(lockPath) ? "pnpm-lock.yaml exists" : "pnpm-lock.yaml missing",
1320
+ ...existsSync3(lockPath) ? {} : { hint: "Run `pnpm install` and commit pnpm-lock.yaml for reproducible Instance builds." }
1321
+ });
1322
+ if (existsSync3(pkgPath)) {
1323
+ try {
1324
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
1325
+ const pinned = pkg.dependencies?.["@devosurf/tesser-sdk"] ?? pkg.devDependencies?.["@devosurf/tesser"];
1326
+ checks.push({
1327
+ name: "tesser-pins",
1328
+ ok: typeof pinned === "string" && pinned.length > 0 && pinned !== "latest",
1329
+ severity: typeof pinned === "string" && pinned.length > 0 && pinned !== "latest" ? "info" : "warning",
1330
+ message: pinned ? `Tesser package pin detected (${pinned})` : "No Tesser package pin detected",
1331
+ ...pinned && pinned !== "latest" ? {} : { hint: "Run `tesser upgrade` with the target CLI version." }
1332
+ });
1333
+ } catch (cause) {
1334
+ checks.push({ name: "package-json-parse", ok: false, severity: "warning", message: `package.json is not valid JSON: ${String(cause)}` });
1335
+ }
1336
+ }
1337
+ }
1338
+ let network;
1339
+ if (opts.network && opts.target.token) {
1340
+ try {
1341
+ network = { ok: true, health: await opts.client.get("/health") };
1342
+ checks.push({ name: "instance-health", ok: true, severity: "info", message: `Instance reachable at ${opts.target.url}` });
1343
+ } catch (cause) {
1344
+ const error = cause instanceof Error ? cause.message : String(cause);
1345
+ network = { ok: false, error };
1346
+ checks.push({
1347
+ name: "instance-health",
1348
+ ok: false,
1349
+ severity: "error",
1350
+ message: error,
1351
+ hint: "Check --url/TESSER_URL and the API token, or rerun with --no-network for local checks only."
1352
+ });
1353
+ }
1354
+ }
1355
+ const report = {
1356
+ ok: checks.every((check) => check.severity !== "error" && check.ok),
1357
+ cliVersion: opts.version,
1358
+ node: process.version,
1359
+ cwd: process.cwd(),
1360
+ project: opts.target.project ?? null,
1361
+ projectRoot: opts.target.projectRoot,
1362
+ instance: opts.target.url,
1363
+ auth: { tokenPresent: opts.target.token !== void 0 },
1364
+ checks,
1365
+ ...network !== void 0 ? { network } : {}
1366
+ };
1367
+ out.data(report, (r) => {
1368
+ const lines = [`Tesser CLI ${r.cliVersion}`, `instance: ${r.instance}`, `project: ${r.project ?? "(none)"}`];
1369
+ for (const check of r.checks) lines.push(`${check.ok ? "ok" : check.severity}: ${check.name} \u2014 ${check.message}${check.hint ? ` (${check.hint})` : ""}`);
1370
+ return lines.join("\n");
1371
+ });
1372
+ }
1373
+
1077
1374
  // packages/cli/src/commands/dev.ts
1078
1375
  import { randomBytes } from "node:crypto";
1079
1376
  import { spawn as spawn2 } from "node:child_process";
1080
- import { existsSync as existsSync3, watch } from "node:fs";
1081
- import { join as join3, dirname as dirname2 } from "node:path";
1377
+ import { existsSync as existsSync4, watch } from "node:fs";
1378
+ import { join as join4, dirname as dirname2 } from "node:path";
1082
1379
  function findServerBin(start) {
1083
1380
  const envBin = process.env["TESSER_SERVER_BIN"];
1084
- if (envBin && existsSync3(envBin)) {
1381
+ if (envBin && existsSync4(envBin)) {
1085
1382
  return envBin.endsWith(".mjs") || envBin.endsWith(".js") ? [process.execPath, envBin] : [envBin];
1086
1383
  }
1087
1384
  let dir = start;
1088
1385
  for (; ; ) {
1089
- const entry = join3(dir, "node_modules", "@devosurf", "tesser-server", "bin", "tesser-server.mjs");
1090
- if (existsSync3(entry)) return [process.execPath, entry];
1091
- const shim = join3(dir, "node_modules", ".bin", "tesser-server");
1092
- if (existsSync3(shim)) return [shim];
1386
+ const entry = join4(dir, "node_modules", "@devosurf", "tesser-server", "bin", "tesser-server.mjs");
1387
+ if (existsSync4(entry)) return [process.execPath, entry];
1388
+ const shim = join4(dir, "node_modules", ".bin", "tesser-server");
1389
+ if (existsSync4(shim)) return [shim];
1093
1390
  const parent = dirname2(dir);
1094
1391
  if (parent === dir) return null;
1095
1392
  dir = parent;
@@ -1111,7 +1408,7 @@ async function dev(out, projectRoot, project, opts) {
1111
1408
  env: {
1112
1409
  ...process.env,
1113
1410
  PORT: String(port),
1114
- TESSER_DATA_DIR: join3(projectRoot, ".tesser"),
1411
+ TESSER_DATA_DIR: join4(projectRoot, ".tesser"),
1115
1412
  TESSER_BOOTSTRAP_TOKEN: token,
1116
1413
  TESSER_BASE_URL: url,
1117
1414
  DATABASE_URL: ""
@@ -1161,7 +1458,7 @@ async function dev(out, projectRoot, project, opts) {
1161
1458
  await syncOnce();
1162
1459
  if (opts.watch !== false) {
1163
1460
  let timer = null;
1164
- watch(join3(projectRoot, "automations"), { recursive: true }, () => {
1461
+ watch(join4(projectRoot, "automations"), { recursive: true }, () => {
1165
1462
  if (timer) clearTimeout(timer);
1166
1463
  timer = setTimeout(() => {
1167
1464
  out.log("change detected \u2014 redeploying\u2026");
@@ -1177,12 +1474,12 @@ async function dev(out, projectRoot, project, opts) {
1177
1474
  }
1178
1475
 
1179
1476
  // packages/cli/src/commands/init.ts
1180
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
1181
- import { join as join5 } from "node:path";
1477
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as readdirSync2, writeFileSync as writeFileSync3 } from "node:fs";
1478
+ import { join as join6 } from "node:path";
1182
1479
 
1183
1480
  // packages/cli/src/commands/project-docs.ts
1184
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
1185
- import { join as join4 } from "node:path";
1481
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
1482
+ import { join as join5 } from "node:path";
1186
1483
  var TESSER_DEPENDENCIES = ["@devosurf/tesser-sdk", "@devosurf/tesser-connectors"];
1187
1484
  var TESSER_DEV_DEPENDENCIES = ["@devosurf/tesser", "@devosurf/tesser-server", "@devosurf/tesser-testing"];
1188
1485
  var GENERATED_DOC_PATHS = [
@@ -1213,13 +1510,13 @@ function projectPackageJson(name, version) {
1213
1510
  };
1214
1511
  }
1215
1512
  function writeProjectAgentInstructions(root, projectName, version, opts) {
1216
- const path = join4(root, "AGENTS.md");
1217
- if (!opts.overwrite && existsSync4(path)) return false;
1513
+ const path = join5(root, "AGENTS.md");
1514
+ if (!opts.overwrite && existsSync5(path)) return false;
1218
1515
  writeFileSync2(path, projectAgentsMd(projectName, version));
1219
1516
  return true;
1220
1517
  }
1221
1518
  function writeTesserGeneratedDocs(root, projectName, version) {
1222
- const docsDir = join4(root, ".tesser", "docs");
1519
+ const docsDir = join5(root, ".tesser", "docs");
1223
1520
  mkdirSync2(docsDir, { recursive: true });
1224
1521
  const docs = {
1225
1522
  "manifest.json": JSON.stringify(
@@ -1240,7 +1537,7 @@ function writeTesserGeneratedDocs(root, projectName, version) {
1240
1537
  "connectors.md": connectorsReferenceMd(version)
1241
1538
  };
1242
1539
  for (const [file, content] of Object.entries(docs)) {
1243
- writeFileSync2(join4(docsDir, file), content);
1540
+ writeFileSync2(join5(docsDir, file), content);
1244
1541
  }
1245
1542
  return [...GENERATED_DOC_PATHS];
1246
1543
  }
@@ -1256,7 +1553,7 @@ function tesserGitignore() {
1256
1553
  `;
1257
1554
  }
1258
1555
  function ensureTesserDocsGitignore(root) {
1259
- const path = join4(root, ".gitignore");
1556
+ const path = join5(root, ".gitignore");
1260
1557
  const block = `
1261
1558
  # Tesser local runtime state from \`tesser dev\`; keep generated agent docs committed.
1262
1559
  .tesser/*
@@ -1264,21 +1561,21 @@ function ensureTesserDocsGitignore(root) {
1264
1561
  !.tesser/docs/
1265
1562
  !.tesser/docs/**
1266
1563
  `;
1267
- if (!existsSync4(path)) {
1564
+ if (!existsSync5(path)) {
1268
1565
  writeFileSync2(path, tesserGitignore());
1269
1566
  return true;
1270
1567
  }
1271
- const existing = readFileSync2(path, "utf8");
1568
+ const existing = readFileSync3(path, "utf8");
1272
1569
  if (existing.includes("!.tesser/docs/**")) return false;
1273
1570
  writeFileSync2(path, existing.replace(/\s*$/, "\n") + block);
1274
1571
  return true;
1275
1572
  }
1276
1573
  function upgradeProject(out, project, version) {
1277
- const packagePath = join4(project.root, "package.json");
1278
- if (!existsSync4(packagePath)) {
1574
+ const packagePath = join5(project.root, "package.json");
1575
+ if (!existsSync5(packagePath)) {
1279
1576
  throw new CliError(EXIT.USAGE, "not inside a package-backed Tesser project (missing package.json)");
1280
1577
  }
1281
- const pkg = JSON.parse(readFileSync2(packagePath, "utf8"));
1578
+ const pkg = JSON.parse(readFileSync3(packagePath, "utf8"));
1282
1579
  pinTesserPackages(pkg, version);
1283
1580
  writeFileSync2(packagePath, JSON.stringify(pkg, null, 2) + "\n");
1284
1581
  const docs = writeTesserGeneratedDocs(project.root, project.name, version);
@@ -1377,7 +1674,7 @@ Commit \`package.json\`, \`pnpm-lock.yaml\`, \`.tesser/docs/\`, and any Automati
1377
1674
  function cliReferenceMd(version) {
1378
1675
  return `# Tesser CLI reference
1379
1676
 
1380
- Generated for \`@devosurf/tesser@${version}\`. The CLI is the agent interface: prefer \`--json\` when output will drive follow-up actions. stdout is data; stderr is logs/progress.
1677
+ Generated for \`@devosurf/tesser@${version}\`. The CLI is the agent interface: prefer \`--output json\` (or \`--json\`) when output will drive follow-up actions. stdout is data; stderr is logs/progress. \`tesser schema\` prints the machine-readable command contract without auth or network.
1381
1678
 
1382
1679
  ## Install and invoke
1383
1680
 
@@ -1396,7 +1693,7 @@ tesser init my-project --instance https://tesser.example.com
1396
1693
  cd my-project
1397
1694
  pnpm install # creates pnpm-lock.yaml; commit it
1398
1695
  git init && git add -A && git commit -m init
1399
- tesser login --url https://tesser.example.com --token "$TESSER_TOKEN"
1696
+ printf '%s' "$TESSER_TOKEN" | tesser login --url https://tesser.example.com --token-stdin
1400
1697
  tesser link --json # registers this Project on the Instance
1401
1698
  tesser test --json
1402
1699
  tesser build --json
@@ -1405,9 +1702,9 @@ tesser deploy --json
1405
1702
 
1406
1703
  ## Commands agents commonly use
1407
1704
 
1408
- - \`tesser init <name> [--dir DIR] [--instance URL]\` \u2014 scaffold a Project.
1705
+ - \`tesser init <name> [--dir DIR] [--instance URL] [--force]\` \u2014 scaffold a Project; refuses non-empty directories unless forced.
1409
1706
  - \`tesser upgrade\` \u2014 pin Tesser packages to this CLI version and refresh \`.tesser/docs/\`. To target a version: \`npx @devosurf/tesser@<version> upgrade\`.
1410
- - \`tesser login --url URL --token TOKEN\` \u2014 verify and store a profile. Prefer \`TESSER_TOKEN\` over pasting tokens into chat.
1707
+ - \`printf '%s' "$TESSER_TOKEN" | tesser login --url URL --token-stdin\` \u2014 verify and store a profile. Humans may run \`tesser login\` and use masked prompts; avoid \`--token\` because argv can leak.
1411
1708
  - \`tesser link [--repo URL] --json\` \u2014 register the Project and print deploy-key/webhook setup data. CLI output must not include private keys or webhook secrets.
1412
1709
  - \`tesser status --json\` \u2014 instance health and deploy state.
1413
1710
  - \`tesser test [--smoke-only] [--automation ID] --json\` \u2014 fast local tests plus generated smoke tests.
@@ -1416,13 +1713,17 @@ tesser deploy --json
1416
1713
  - \`tesser deploy [--ref REF] [--local] [--no-wait] --json\` \u2014 server-side build/test/promotion. Exit code 4 means halted on missing credentials; run \`tesser connect\`.
1417
1714
  - \`tesser connect [--wait] [--status TOKEN] --json\` \u2014 mint or poll the human connect link. The human supplies credentials in the browser; the agent only sees readiness.
1418
1715
  - \`tesser secrets list --json\`, \`tesser secrets rm <name> --json\`, \`printenv MY_SECRET | tesser secrets set <name> --value-stdin --json\`. Never put secret values in argv.
1419
- - \`tesser runs list|show|trigger|signal|cancel --json\`; \`tesser logs <runId> [--follow]\`; \`tesser replay <runId>\`.
1716
+ - \`tesser schema --json\` \u2014 inspect commands, arguments, outputs, exit codes, and mutation markers.
1717
+ - \`tesser doctor --json\` \u2014 local Project/auth/package preflight; use \`--no-network\` for offline checks.
1718
+ - \`tesser completion bash|zsh\` \u2014 print shell completion scripts.
1719
+ - \`tesser runs list [--limit N] [--offset N] [--fields id,status] --json\`; \`tesser runs trigger <automation> --input-file - --json\`; \`tesser runs signal <runId> <name> --payload-file - --json\`; \`tesser runs show|cancel --json\`; \`tesser logs <runId> [--follow] [--limit N] [--offset N] [-o ndjson]\`; \`tesser replay <runId>\`.
1720
+ - \`tesser harness connect claude-code --connect CONNECT_URL\` and \`tesser harness connect pi --connect CONNECT_URL\` \u2014 preferred Harness credential namespace; legacy \`tesser auth ...\` aliases remain.
1420
1721
  - \`tesser rollback <automation> --to <version> --json\` \u2014 alias re-point; no rebuild.
1421
1722
 
1422
1723
  ## Credential safety
1423
1724
 
1424
1725
  - Do not ask the user to paste secrets into chat. Use masked secret prompts where your harness supports them, or ask the human to run a command locally.
1425
- - Never pass raw secret values as CLI arguments. Use environment variables, profiles, connect links, OAuth, or \`--value-stdin\`.
1726
+ - Never pass raw secret values as CLI arguments. Use masked prompts, environment variables, profiles, connect links, OAuth, \`--token-stdin\`, \`--value-stdin\`, or \`--*-file -\` stdin lanes.
1426
1727
  - Treat connect links and status tokens as sensitive operational material.
1427
1728
 
1428
1729
  ## Deploy hygiene
@@ -1520,83 +1821,7 @@ For Connector calls, mock the Step result by Step name. Replay fixtures produced
1520
1821
  `;
1521
1822
  }
1522
1823
  function connectorsReferenceMd(version) {
1523
- return `# Tesser Connector reference
1524
-
1525
- Generated for \`@devosurf/tesser-connectors@${version}\`. A Connector is a typed integration; a Connection is an authed runtime instance injected by the Credential broker.
1526
-
1527
- ## Available connector imports
1528
-
1529
- \`\`\`ts
1530
- import {
1531
- anthropic,
1532
- claudeCode,
1533
- github,
1534
- gmail,
1535
- googleCalendar,
1536
- googleDocs,
1537
- googleDrive,
1538
- googleSheets,
1539
- http,
1540
- outlookMail,
1541
- pi,
1542
- resend,
1543
- slack,
1544
- } from "@devosurf/tesser-connectors";
1545
- \`\`\`
1546
-
1547
- Only import Connectors that the Automation declares in \`connections: { ... }\`.
1548
-
1549
- ## Pattern
1550
-
1551
- \`\`\`ts
1552
- import { defineAutomation, onSchedule } from "@devosurf/tesser-sdk";
1553
- import { github, slack } from "@devosurf/tesser-connectors";
1554
- import { z } from "zod";
1555
-
1556
- export default defineAutomation({
1557
- id: "digest",
1558
- trigger: onSchedule({ cron: "0 9 * * *", tz: "UTC" }),
1559
- connections: { github, slack },
1560
- output: z.object({ posted: z.boolean(), count: z.number() }),
1561
-
1562
- run: async (_input, ctx) => {
1563
- const issues = await ctx.step("fetch-open-issues", () =>
1564
- ctx.connections.github.issues.list({ state: "open", labels: ["bug"] }),
1565
- );
1566
-
1567
- if (issues.length === 0) return { posted: false, count: 0 };
1568
-
1569
- await ctx.step("post-to-slack", () =>
1570
- ctx.connections.slack.chat.postMessage({ channel: "#ops", text: \`\${issues.length} bugs\` }),
1571
- );
1572
-
1573
- return { posted: true, count: issues.length };
1574
- },
1575
- });
1576
- \`\`\`
1577
-
1578
- ## Common Actions
1579
-
1580
- - \`ctx.connections.http.get({ url, headers?, query? })\` and \`ctx.connections.http.request({ method, url, headers?, query?, body?, bodyText? })\`. Generic writes are not retry-safe; still wrap them in a Step.
1581
- - \`ctx.connections.github.issues.list({ repo?, state?, labels?, limit? })\`, \`issues.create({ repo, title, body?, labels? })\`, \`issues.comment({ repo, number, body })\`, \`repos.get({ repo })\`.
1582
- - \`ctx.connections.slack.chat.postMessage({ channel, text, threadTs? })\` and related Slack chat/conversation/user Actions.
1583
- - \`ctx.connections.resend.emails.send(...)\`, Gmail and Outlook Mail Actions (Outlook accepts optional \`mailbox\` for shared mailbox \`/users/{userPrincipalName}\` access), Google Calendar/Docs/Drive/Sheets Actions, Anthropic model Actions, Claude Code/Pi Harness-related Connectors: inspect package types or examples before using.
1584
-
1585
- ## Connector triggers
1586
-
1587
- Some Connectors expose typed triggers, e.g. GitHub issue triggers, Slack event triggers, Gmail/Outlook mailbox poll triggers. Use the Connector's \`triggers\` constructors; do not hand-roll webhook delivery unless no Connector exists.
1588
-
1589
- \`\`\`ts
1590
- trigger: github.triggers.issueOpened({ repo: "owner/repo" })
1591
- \`\`\`
1592
-
1593
- ## Safety rules
1594
-
1595
- - Connector Actions are not automatically durable. The Automation author must wrap every Action call in \`ctx.step\`.
1596
- - The imported Connector is not a token-bearing client. The authed client exists only at \`ctx.connections.<name>\` inside \`run\`.
1597
- - For bespoke APIs, combine \`http\` with declared \`secrets: { ... }\`; do not put API keys in source, tests, logs, CLI args, or committed env files.
1598
- - If deploy halts on a missing Connection, surface \`tesser connect\` output to the human. The agent must not complete OAuth or paste raw secrets itself.
1599
- `;
1824
+ return '# Tesser Connector reference\n\nGenerated for `@devosurf/tesser-connectors@__VERSION__`. This file is intentionally a source map, not a duplicated API reference. The installed Connector package is the authoritative version-pinned reference. Do not rely on training data, memory, or hand-written summaries.\n\n## Hard rule\n\nDo not guess Connector APIs from memory, training data, old examples, or web search. After `pnpm install`, inspect the installed package in `node_modules/@devosurf/tesser-connectors`.\n\n## Source-of-truth order\n\n1. Project policy: `AGENTS.md` and `.tesser/docs/connectors.md`.\n2. Connector inventory: `node_modules/@devosurf/tesser-connectors/manifest.json`.\n3. Import names: `node_modules/@devosurf/tesser-connectors/index.ts`.\n4. Exact input/output schemas, samples, triggers, provider quirks: `node_modules/@devosurf/tesser-connectors/<connector-id>/index.ts`.\n5. Provider OAuth facts: `node_modules/@devosurf/tesser-connectors/providers/*.ts` and `catalog/index.ts`.\n\nNever edit `node_modules`. Read it, then edit the Automation under `automations/<id>/` and validate with `tesser test --json` / `tesser build --json`.\n\n## Quick inspection commands\n\nRun `pnpm install` first if `node_modules` is absent.\n\n```bash\n# Barrel export names: use these names in `import { ... } from "@devosurf/tesser-connectors"`.\nsed -n \'1,180p\' node_modules/@devosurf/tesser-connectors/index.ts\n\n# Installed connector inventory with actions/triggers.\nnode - <<\'NODE\'\nconst fs = require(\'node:fs\');\nconst manifest = JSON.parse(fs.readFileSync(\'node_modules/@devosurf/tesser-connectors/manifest.json\', \'utf8\'));\nfor (const c of manifest.connectors) {\n console.log("");\n console.log(`${c.id}: ${c.describe}`);\n const auth = Object.keys(c.auth ?? {});\n if (auth.length) console.log(` auth: ${auth.join(\', \')}`);\n if (c.actions?.length) console.log(` actions: ${c.actions.map((a) => `${a.path} [${a.safety}${a.retrySafe ? \', retry-safe\' : \'\'}]`).join(\', \')}`);\n if (c.triggers?.length) console.log(` triggers: ${c.triggers.map((t) => `${t.id} [${t.strategy}]`).join(\', \')}`);\n}\nNODE\n\n# Exact schemas for one Connector, including Zod input/output objects.\nsed -n \'1,260p\' node_modules/@devosurf/tesser-connectors/outlook-mail/index.ts\n```\n\n## Authoring pattern\n\n```ts\nimport { defineAutomation, onSchedule } from "@devosurf/tesser-sdk";\nimport { github, slack } from "@devosurf/tesser-connectors";\nimport { z } from "zod";\n\nexport default defineAutomation({\n id: "digest",\n trigger: onSchedule({ cron: "0 9 * * *", tz: "UTC" }),\n connections: { github, slack },\n output: z.object({ posted: z.boolean(), count: z.number() }),\n\n run: async (_input, ctx) => {\n const issues = await ctx.step("fetch-open-issues", () =>\n ctx.connections.github.issues.list({ state: "open", labels: ["bug"] }),\n );\n\n if (issues.length === 0) return { posted: false, count: 0 };\n\n await ctx.step("post-to-slack", () =>\n ctx.connections.slack.chat.postMessage({ channel: "#ops", text: `${issues.length} bugs` }),\n );\n\n return { posted: true, count: issues.length };\n },\n});\n```\n\n## Rules that matter more than the Connector API\n\n- A Connector import is not an authed client. It is a typed requirement declaration.\n- Runtime calls go through `ctx.connections.<connection-key>` inside `run`.\n- Every Connector Action call must be inside `ctx.step(...)`.\n- Declare raw credentials separately with `secrets: { name: secret({ describe: "..." }) }`.\n- The Credential broker injects values at runtime; agents must never read or print tokens/secrets.\n- Deploy halts on missing Connections or Secrets; a human completes the connect link.\n\n## Connector ids currently shipped in this package\n\nRead `manifest.json` for the authoritative list. Current ids include core HTTP/email/repo/chat/calendar/document/spreadsheet/model/harness Connectors such as `http`, `github`, `slack`, `resend`, `gmail`, `outlook-mail`, Google Connectors, and Harness/model Connectors. Do not hardcode this list in Automations or docs; inspect the installed manifest.\n'.replaceAll("__VERSION__", version);
1600
1825
  }
1601
1826
 
1602
1827
  // packages/cli/src/commands/init.ts
@@ -1628,17 +1853,23 @@ function init(out, name, opts, version) {
1628
1853
  if (!/^[a-z][a-z0-9-]{0,63}$/.test(name)) {
1629
1854
  throw new CliError(EXIT.USAGE, "project name must be kebab-case");
1630
1855
  }
1631
- const root = join5(opts.dir ?? process.cwd(), name);
1632
- if (existsSync5(join5(root, "tesser.json"))) {
1633
- throw new CliError(EXIT.CONFLICT, `${root} is already a Tesser project`);
1856
+ const root = join6(opts.dir ?? process.cwd(), name);
1857
+ if (existsSync6(root) && !opts.force) {
1858
+ if (existsSync6(join6(root, "tesser.json"))) {
1859
+ throw new CliError(EXIT.CONFLICT, `${root} is already a Tesser project`);
1860
+ }
1861
+ const entries = readdirSync2(root).filter((entry) => entry !== ".DS_Store");
1862
+ if (entries.length > 0) {
1863
+ throw new CliError(EXIT.CONFLICT, `${root} is not empty \u2014 choose a new name or pass --force to write into it`);
1864
+ }
1634
1865
  }
1635
- mkdirSync3(join5(root, "automations", "hello"), { recursive: true });
1866
+ mkdirSync3(join6(root, "automations", "hello"), { recursive: true });
1636
1867
  writeFileSync3(
1637
- join5(root, "tesser.json"),
1868
+ join6(root, "tesser.json"),
1638
1869
  JSON.stringify({ project: name, ...opts.instance !== void 0 ? { instance: opts.instance } : {} }, null, 2) + "\n"
1639
1870
  );
1640
1871
  writeFileSync3(
1641
- join5(root, "package.json"),
1872
+ join6(root, "package.json"),
1642
1873
  JSON.stringify(
1643
1874
  projectPackageJson(name, version),
1644
1875
  null,
@@ -1646,7 +1877,7 @@ function init(out, name, opts, version) {
1646
1877
  ) + "\n"
1647
1878
  );
1648
1879
  writeFileSync3(
1649
- join5(root, "tsconfig.json"),
1880
+ join6(root, "tsconfig.json"),
1650
1881
  JSON.stringify(
1651
1882
  {
1652
1883
  compilerOptions: {
@@ -1664,16 +1895,16 @@ function init(out, name, opts, version) {
1664
1895
  ) + "\n"
1665
1896
  );
1666
1897
  writeFileSync3(
1667
- join5(root, "vitest.config.ts"),
1898
+ join6(root, "vitest.config.ts"),
1668
1899
  `import { defineConfig } from "vitest/config";
1669
1900
  export default defineConfig({ test: { globals: true, include: ["automations/**/*.test.ts"] } });
1670
1901
  `
1671
1902
  );
1672
- writeFileSync3(join5(root, ".gitignore"), tesserGitignore());
1903
+ writeFileSync3(join6(root, ".gitignore"), tesserGitignore());
1673
1904
  writeProjectAgentInstructions(root, name, version, { overwrite: true });
1674
1905
  const docs = writeTesserGeneratedDocs(root, name, version);
1675
- writeFileSync3(join5(root, "automations", "hello", "index.ts"), EXAMPLE_AUTOMATION);
1676
- writeFileSync3(join5(root, "automations", "hello", "index.test.ts"), EXAMPLE_TEST);
1906
+ writeFileSync3(join6(root, "automations", "hello", "index.ts"), EXAMPLE_AUTOMATION);
1907
+ writeFileSync3(join6(root, "automations", "hello", "index.test.ts"), EXAMPLE_TEST);
1677
1908
  const next = [
1678
1909
  "cd " + name,
1679
1910
  "pnpm install",
@@ -1694,18 +1925,18 @@ next:
1694
1925
  }
1695
1926
 
1696
1927
  // packages/cli/src/commands/replay.ts
1697
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "node:fs";
1698
- import { join as join6 } from "node:path";
1928
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, existsSync as existsSync7 } from "node:fs";
1929
+ import { join as join7 } from "node:path";
1699
1930
  async function replay(out, api2, projectRoot, runId) {
1700
1931
  const { replay: run } = await api2.get(`/runs/${runId}/replay`);
1701
- const dir = join6(projectRoot, "automations", run.automation_id);
1702
- if (!existsSync6(dir)) {
1932
+ const dir = join7(projectRoot, "automations", run.automation_id);
1933
+ if (!existsSync7(dir)) {
1703
1934
  throw new CliError(EXIT.NOT_FOUND, `automation directory not found locally: automations/${run.automation_id}`);
1704
1935
  }
1705
1936
  const shortId = run.id.slice(0, 8);
1706
- const fixtureDir = join6(dir, "__replays__");
1937
+ const fixtureDir = join7(dir, "__replays__");
1707
1938
  mkdirSync4(fixtureDir, { recursive: true });
1708
- const fixturePath = join6(fixtureDir, `${shortId}.replay.json`);
1939
+ const fixturePath = join7(fixtureDir, `${shortId}.replay.json`);
1709
1940
  writeFileSync4(
1710
1941
  fixturePath,
1711
1942
  JSON.stringify(
@@ -1723,7 +1954,7 @@ async function replay(out, api2, projectRoot, runId) {
1723
1954
  2
1724
1955
  ) + "\n"
1725
1956
  );
1726
- const testPath = join6(dir, `replay-${shortId}.test.ts`);
1957
+ const testPath = join7(dir, `replay-${shortId}.test.ts`);
1727
1958
  writeFileSync4(
1728
1959
  testPath,
1729
1960
  `// Regression frozen from run ${run.id} (recorded status: ${run.status}).
@@ -1757,8 +1988,8 @@ run \`tesser test\` to execute it`
1757
1988
 
1758
1989
  // packages/cli/src/commands/test.ts
1759
1990
  import { execFile } from "node:child_process";
1760
- import { existsSync as existsSync7 } from "node:fs";
1761
- import { join as join7 } from "node:path";
1991
+ import { existsSync as existsSync8 } from "node:fs";
1992
+ import { join as join8 } from "node:path";
1762
1993
  import { promisify } from "node:util";
1763
1994
 
1764
1995
  // packages/testing/src/engine.ts
@@ -1977,7 +2208,7 @@ async function executeAutomation(def, opts = {}) {
1977
2208
  `side effects must live inside a step (ADR-0002) \u2014 wrap the call: ctx.step("name", () => ctx.connections.${fullPath}(...))`
1978
2209
  );
1979
2210
  }
1980
- const input2 = await validateSchema(
2211
+ const input3 = await validateSchema(
1981
2212
  actionDef.input,
1982
2213
  rawInput ?? {},
1983
2214
  `${fullPath} input`
@@ -1992,7 +2223,7 @@ async function executeAutomation(def, opts = {}) {
1992
2223
  for (const seg of path) node = node?.[seg];
1993
2224
  if (node !== void 0) {
1994
2225
  return {
1995
- value: typeof node === "function" ? await node(input2) : node,
2226
+ value: typeof node === "function" ? await node(input3) : node,
1996
2227
  validate: false
1997
2228
  };
1998
2229
  }
@@ -2001,7 +2232,7 @@ async function executeAutomation(def, opts = {}) {
2001
2232
  if (stepMock !== void 0) {
2002
2233
  return {
2003
2234
  value: typeof stepMock === "function" ? await stepMock(
2004
- input2,
2235
+ input3,
2005
2236
  { action: actionPath, connection: connKey }
2006
2237
  ) : stepMock,
2007
2238
  validate: false
@@ -2019,16 +2250,16 @@ async function executeAutomation(def, opts = {}) {
2019
2250
  try {
2020
2251
  const { value, validate } = await resolve();
2021
2252
  const result = validate ? await validateSchema(actionDef.output, value, `${fullPath} sample output`) : value;
2022
- const record = { args: [input2], step: step.name, action: fullPath, result };
2253
+ const record = { args: [input3], step: step.name, action: fullPath, result };
2023
2254
  recordCall(spyFor(step.name), record);
2024
2255
  recordCall(spyFor(fullPath), record);
2025
- connectorCalls.push({ step: step.name, action: fullPath, input: input2 });
2256
+ connectorCalls.push({ step: step.name, action: fullPath, input: input3 });
2026
2257
  return result;
2027
2258
  } catch (err) {
2028
- const record = { args: [input2], step: step.name, action: fullPath, error: String(err) };
2259
+ const record = { args: [input3], step: step.name, action: fullPath, error: String(err) };
2029
2260
  recordCall(spyFor(step.name), record);
2030
2261
  recordCall(spyFor(fullPath), record);
2031
- connectorCalls.push({ step: step.name, action: fullPath, input: input2 });
2262
+ connectorCalls.push({ step: step.name, action: fullPath, input: input3 });
2032
2263
  throw err;
2033
2264
  }
2034
2265
  });
@@ -2193,7 +2424,7 @@ async function executeAutomation(def, opts = {}) {
2193
2424
  recordCall(spyFor(`harness.${harnessKey}`), { args: [request], step: step.name, result });
2194
2425
  return result;
2195
2426
  });
2196
- let input = opts.input;
2427
+ let input2 = opts.input;
2197
2428
  const trigger = def.trigger;
2198
2429
  let inputSchema = def.input ?? (trigger.kind === "webhook" ? trigger.input : trigger.kind === "event" ? trigger.event?.schema : void 0);
2199
2430
  if (trigger.kind === "connector" && trigger.connectorId !== void 0) {
@@ -2203,16 +2434,16 @@ async function executeAutomation(def, opts = {}) {
2203
2434
  const decl = connector?.__connector.triggers?.[trigger.triggerId ?? ""];
2204
2435
  if (decl) {
2205
2436
  inputSchema = def.input ?? decl.output;
2206
- if (input === void 0) {
2207
- input = connector?.__connector.samples?.[`trigger:${trigger.triggerId}`];
2437
+ if (input2 === void 0) {
2438
+ input2 = connector?.__connector.samples?.[`trigger:${trigger.triggerId}`];
2208
2439
  }
2209
2440
  }
2210
2441
  }
2211
- if (input === void 0 && inputSchema && trigger.kind !== "schedule") {
2212
- input = await sampleFromSchema(inputSchema);
2442
+ if (input2 === void 0 && inputSchema && trigger.kind !== "schedule") {
2443
+ input2 = await sampleFromSchema(inputSchema);
2213
2444
  }
2214
- if (inputSchema && input !== void 0) {
2215
- input = await validateSchema(inputSchema, input, `automation "${def.id}" input`);
2445
+ if (inputSchema && input2 !== void 0) {
2446
+ input2 = await validateSchema(inputSchema, input2, `automation "${def.id}" input`);
2216
2447
  }
2217
2448
  const finish = (status, result, error) => {
2218
2449
  const stepsByName = {};
@@ -2255,11 +2486,11 @@ async function executeAutomation(def, opts = {}) {
2255
2486
  };
2256
2487
  };
2257
2488
  try {
2258
- let output = await def.run(input, ctx);
2489
+ let output2 = await def.run(input2, ctx);
2259
2490
  if (def.output) {
2260
- output = await validateSchema(def.output, output, `automation "${def.id}" output`);
2491
+ output2 = await validateSchema(def.output, output2, `automation "${def.id}" output`);
2261
2492
  }
2262
- return finish("completed", output);
2493
+ return finish("completed", output2);
2263
2494
  } catch (err) {
2264
2495
  for (const item of [...undoStack].reverse()) {
2265
2496
  try {
@@ -2334,7 +2565,7 @@ async function smokeModelScripts(def) {
2334
2565
  }
2335
2566
 
2336
2567
  // packages/testing/src/cassette.ts
2337
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "node:fs";
2568
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
2338
2569
  import { dirname as dirname3 } from "node:path";
2339
2570
  import { createHash } from "node:crypto";
2340
2571
 
@@ -2343,9 +2574,9 @@ var exec = promisify(execFile);
2343
2574
  function findVitest(projectRoot) {
2344
2575
  let dir = projectRoot;
2345
2576
  for (; ; ) {
2346
- const bin = join7(dir, "node_modules", ".bin", process.platform === "win32" ? "vitest.cmd" : "vitest");
2347
- if (existsSync7(bin)) return bin;
2348
- const parent = join7(dir, "..");
2577
+ const bin = join8(dir, "node_modules", ".bin", process.platform === "win32" ? "vitest.cmd" : "vitest");
2578
+ if (existsSync8(bin)) return bin;
2579
+ const parent = join8(dir, "..");
2349
2580
  if (parent === dir) return null;
2350
2581
  dir = parent;
2351
2582
  }
@@ -2355,7 +2586,7 @@ async function runTests(out, projectRoot, opts) {
2355
2586
  (a) => opts.filter === void 0 || a.automationId === opts.filter
2356
2587
  );
2357
2588
  if (automations.length === 0) {
2358
- throw new CliError(EXIT.USAGE, `no automations found under ${join7(projectRoot, "automations")}`);
2589
+ throw new CliError(EXIT.USAGE, `no automations found under ${join8(projectRoot, "automations")}`);
2359
2590
  }
2360
2591
  const report = {
2361
2592
  passed: true,
@@ -2442,13 +2673,309 @@ async function runTests(out, projectRoot, opts) {
2442
2673
  process.exit(report.passed ? EXIT.OK : EXIT.TESTS_FAILED);
2443
2674
  }
2444
2675
 
2676
+ // packages/cli/src/schema.ts
2677
+ var errorRows = [
2678
+ ["error", EXIT.ERROR, true, "Unexpected internal error"],
2679
+ ["usage", EXIT.USAGE, false, "Bad arguments or missing Project context"],
2680
+ ["tests_failed", EXIT.TESTS_FAILED, false, "Local validation failed"],
2681
+ ["halted_credentials", EXIT.HALTED_CREDENTIALS, false, "Deploy/connect halted on missing credentials"],
2682
+ ["auth", EXIT.AUTH, false, "Cannot reach or authenticate against the Instance"],
2683
+ ["not_found", EXIT.NOT_FOUND, false, "Requested resource does not exist"],
2684
+ ["conflict", EXIT.CONFLICT, false, "Operation conflicts with current state"],
2685
+ ["deploy_failed", EXIT.DEPLOY_FAILED, false, "Build, test gate, or deploy failed"]
2686
+ ];
2687
+ var commands = [
2688
+ {
2689
+ name: "schema",
2690
+ description: "Emit the machine-readable CLI contract without auth, config, or network",
2691
+ mutating: false,
2692
+ args: [{ name: "command", type: "string[]", required: false }],
2693
+ output_fields: ["clispec", "name", "version", "global_args", "commands", "errors", "outcomes"]
2694
+ },
2695
+ {
2696
+ name: "completion",
2697
+ description: "Print a shell completion script",
2698
+ mutating: false,
2699
+ args: [{ name: "shell", type: "string", enum: ["bash", "zsh"], required: true }],
2700
+ output_fields: []
2701
+ },
2702
+ {
2703
+ name: "doctor",
2704
+ description: "Agent preflight for local Project, auth, pins, and optional Instance health",
2705
+ mutating: false,
2706
+ args: [{ name: "--no-network", type: "boolean", required: false, default: false }],
2707
+ output_fields: ["ok", "cliVersion", "node", "cwd", "project", "projectRoot", "instance", "auth", "checks", "network"]
2708
+ },
2709
+ {
2710
+ name: "init",
2711
+ description: "Scaffold a new Project",
2712
+ mutating: true,
2713
+ args: [
2714
+ { name: "name", type: "string", required: true },
2715
+ { name: "--dir", type: "string", required: false },
2716
+ { name: "--instance", type: "string", required: false },
2717
+ { name: "--force", type: "boolean", required: false, default: false }
2718
+ ],
2719
+ output_fields: ["created", "tesserVersion", "docs", "next"]
2720
+ },
2721
+ {
2722
+ name: "upgrade",
2723
+ description: "Pin Tesser packages to this CLI version and refresh generated Project docs",
2724
+ mutating: true,
2725
+ args: [],
2726
+ output_fields: ["upgraded", "tesserVersion", "docs", "next"]
2727
+ },
2728
+ {
2729
+ name: "login",
2730
+ description: "Verify and store Instance API credentials in a local profile",
2731
+ mutating: true,
2732
+ args: [
2733
+ { name: "--url", type: "string", required: false, default: "http://localhost:8377" },
2734
+ { name: "--token-stdin", type: "boolean", required: false, default: false },
2735
+ { name: "--token", type: "string", required: false, secret: true, deprecated: "Prefer --token-stdin" },
2736
+ { name: "--save-profile", type: "string", required: false, default: "default" }
2737
+ ],
2738
+ output_fields: ["profile", "url"]
2739
+ },
2740
+ {
2741
+ name: "link",
2742
+ description: "Register this Project on the Instance and wire git-sync",
2743
+ mutating: true,
2744
+ args: [{ name: "--repo", type: "string", required: false }],
2745
+ output_fields: ["id", "name", "repoUrl", "deployKeyPublic", "webhookSetupUrl"]
2746
+ },
2747
+ {
2748
+ name: "status",
2749
+ description: "Instance and Project deploy status",
2750
+ mutating: false,
2751
+ args: [],
2752
+ output_fields: ["instance", "health", "status", "project", "deploy"]
2753
+ },
2754
+ {
2755
+ name: "test",
2756
+ description: "Run colocated tests plus generated smoke tests",
2757
+ mutating: false,
2758
+ args: [
2759
+ { name: "--smoke-only", type: "boolean", required: false, default: false },
2760
+ { name: "--automation", type: "string", required: false }
2761
+ ],
2762
+ output_fields: ["passed", "colocated", "smoke", "failures"]
2763
+ },
2764
+ {
2765
+ name: "build",
2766
+ description: "Build and statically extract automation manifests locally",
2767
+ mutating: false,
2768
+ args: [],
2769
+ output_fields: ["automations"]
2770
+ },
2771
+ {
2772
+ name: "dev",
2773
+ description: "Run a local Instance with embedded Postgres and deploy-on-change",
2774
+ mutating: true,
2775
+ args: [
2776
+ { name: "--port", type: "integer", required: false, default: 8377 },
2777
+ { name: "--no-watch", type: "boolean", required: false, default: false }
2778
+ ],
2779
+ output_fields: []
2780
+ },
2781
+ {
2782
+ name: "deploy",
2783
+ description: "Sync git or a local tree to the Instance and promote on green",
2784
+ mutating: true,
2785
+ args: [
2786
+ { name: "--ref", type: "string", required: false },
2787
+ { name: "--local", type: "boolean", required: false, default: false },
2788
+ { name: "--no-wait", type: "boolean", required: false, default: false }
2789
+ ],
2790
+ output_fields: ["status", "report", "live"]
2791
+ },
2792
+ {
2793
+ name: "harness connect claude-code",
2794
+ description: "Connect Claude Code as a brokered Harness (alias of auth claude-code)",
2795
+ mutating: true,
2796
+ args: [
2797
+ { name: "--connect", type: "string", required: true },
2798
+ { name: "--mode", type: "string", enum: ["subscription", "apiKey"], required: false, default: "subscription" },
2799
+ { name: "--token-stdin", type: "boolean", required: false, default: false, secret: true },
2800
+ { name: "--from-env", type: "string", required: false, secret: true },
2801
+ { name: "--scope", type: "string", enum: ["workspace", "per_user"], required: false, default: "workspace" },
2802
+ { name: "--end-user-id", type: "string", required: false },
2803
+ { name: "--bin", type: "string", required: false, default: "claude" }
2804
+ ],
2805
+ output_fields: ["connector", "mode", "connected"]
2806
+ },
2807
+ {
2808
+ name: "harness connect pi",
2809
+ description: "Connect Pi as a brokered Harness (alias of auth pi)",
2810
+ mutating: true,
2811
+ args: [
2812
+ { name: "--connect", type: "string", required: true },
2813
+ { name: "--mode", type: "string", enum: ["anthropicOAuth", "anthropicApiKey"], required: false, default: "anthropicOAuth" },
2814
+ { name: "--token-stdin", type: "boolean", required: false, default: false, secret: true },
2815
+ { name: "--from-env", type: "string", required: false, secret: true },
2816
+ { name: "--scope", type: "string", enum: ["workspace", "per_user"], required: false, default: "workspace" },
2817
+ { name: "--end-user-id", type: "string", required: false }
2818
+ ],
2819
+ output_fields: ["connector", "mode", "connected"]
2820
+ },
2821
+ {
2822
+ name: "connect",
2823
+ description: "Mint or poll the human connect link for missing credentials",
2824
+ mutating: true,
2825
+ args: [
2826
+ { name: "--wait", type: "boolean", required: false, default: false },
2827
+ { name: "--status", type: "string", required: false }
2828
+ ],
2829
+ output_fields: ["url", "token", "requirements", "status"]
2830
+ },
2831
+ {
2832
+ name: "secrets list",
2833
+ description: "List Secret names only",
2834
+ mutating: false,
2835
+ args: [],
2836
+ output_fields: ["secrets"]
2837
+ },
2838
+ {
2839
+ name: "secrets set",
2840
+ description: "Set a Secret from stdin; never accepts the value on argv",
2841
+ mutating: true,
2842
+ args: [
2843
+ { name: "name", type: "string", required: true },
2844
+ { name: "--value-stdin", type: "boolean", required: true, secret: true }
2845
+ ],
2846
+ output_fields: ["set", "changed"]
2847
+ },
2848
+ {
2849
+ name: "secrets rm",
2850
+ description: "Delete a Secret by name",
2851
+ mutating: true,
2852
+ args: [{ name: "name", type: "string", required: true }],
2853
+ output_fields: ["deleted", "changed"]
2854
+ },
2855
+ {
2856
+ name: "runs list",
2857
+ description: "List runs with bounded output",
2858
+ mutating: false,
2859
+ args: [
2860
+ { name: "--automation", type: "string", required: false },
2861
+ { name: "--status", type: "string", required: false },
2862
+ { name: "--limit", type: "integer", required: false, default: 25 },
2863
+ { name: "--offset", type: "integer", required: false, default: 0 },
2864
+ { name: "--fields", type: "string", required: false }
2865
+ ],
2866
+ output_fields: ["runs", "total", "limit", "offset", "truncated"]
2867
+ },
2868
+ {
2869
+ name: "runs show",
2870
+ description: "Show a run with bounded log output",
2871
+ mutating: false,
2872
+ args: [
2873
+ { name: "runId", type: "string", required: true },
2874
+ { name: "--log-limit", type: "integer", required: false, default: 100 },
2875
+ { name: "--log-offset", type: "integer", required: false, default: 0 }
2876
+ ],
2877
+ output_fields: ["run", "steps", "logs", "logsTotal", "logsLimit", "logsOffset", "logsTruncated"]
2878
+ },
2879
+ {
2880
+ name: "runs trigger",
2881
+ description: "Manually trigger an Automation",
2882
+ mutating: true,
2883
+ args: [
2884
+ { name: "automation", type: "string", required: true },
2885
+ { name: "--input", type: "json", required: false },
2886
+ { name: "--input-file", type: "path|-", required: false },
2887
+ { name: "--env", type: "string", required: false, default: "production" }
2888
+ ],
2889
+ output_fields: ["runId"]
2890
+ },
2891
+ {
2892
+ name: "runs signal",
2893
+ description: "Deliver a Signal to a suspended run",
2894
+ mutating: true,
2895
+ args: [
2896
+ { name: "runId", type: "string", required: true },
2897
+ { name: "name", type: "string", required: true },
2898
+ { name: "--payload", type: "json", required: false },
2899
+ { name: "--payload-file", type: "path|-", required: false }
2900
+ ],
2901
+ output_fields: ["delivered"]
2902
+ },
2903
+ {
2904
+ name: "runs cancel",
2905
+ description: "Cancel a queued or suspended run",
2906
+ mutating: true,
2907
+ args: [{ name: "runId", type: "string", required: true }],
2908
+ output_fields: ["cancelled", "changed"]
2909
+ },
2910
+ {
2911
+ name: "logs",
2912
+ description: "Print bounded run logs or follow until settle",
2913
+ mutating: false,
2914
+ args: [
2915
+ { name: "runId", type: "string", required: true },
2916
+ { name: "--follow", type: "boolean", required: false, default: false },
2917
+ { name: "--limit", type: "integer", required: false, default: 100 },
2918
+ { name: "--offset", type: "integer", required: false, default: 0 }
2919
+ ],
2920
+ output_fields: ["run", "logs", "logsTotal", "logsLimit", "logsOffset", "logsTruncated"]
2921
+ },
2922
+ {
2923
+ name: "replay",
2924
+ description: "Freeze a real run as a regression fixture and test",
2925
+ mutating: true,
2926
+ args: [{ name: "runId", type: "string", required: true }],
2927
+ output_fields: ["runId", "fixture", "test"]
2928
+ },
2929
+ {
2930
+ name: "rollback",
2931
+ description: "Re-point an Automation alias to a prior immutable version",
2932
+ mutating: true,
2933
+ args: [
2934
+ { name: "automation", type: "string", required: true },
2935
+ { name: "--to", type: "integer", required: true },
2936
+ { name: "--env", type: "string", required: false, default: "production" }
2937
+ ],
2938
+ output_fields: ["rolledBack", "automation", "env", "toVersion", "changed"]
2939
+ }
2940
+ ];
2941
+ function cliSchema(version, commandPrefix) {
2942
+ const filtered = commandPrefix ? commands.filter((command) => command.name === commandPrefix || command.name.startsWith(`${commandPrefix} `)) : commands;
2943
+ return {
2944
+ clispec: "0.2",
2945
+ name: "tesser",
2946
+ version,
2947
+ description: "Code-first, agent-native automation CLI",
2948
+ global_args: [
2949
+ { name: "--json", type: "boolean", default: false, description: "Alias for --output json" },
2950
+ { name: "--output", short: "-o", type: "string", enum: ["auto", "text", "json", "ndjson"], default: "auto" },
2951
+ { name: "--url", type: "string", required: false },
2952
+ { name: "--token", type: "string", required: false, secret: true },
2953
+ { name: "--profile", type: "string", required: false }
2954
+ ],
2955
+ commands: filtered,
2956
+ errors: errorRows.map(([kind, exitCode, retryable, description]) => ({ kind, exit_code: exitCode, retryable, description })),
2957
+ outcomes: []
2958
+ };
2959
+ }
2960
+
2445
2961
  // packages/cli/src/index.ts
2446
2962
  var exec2 = promisify2(execFile2);
2447
2963
  var program = new Command();
2448
- var VERSION = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8")).version;
2964
+ var VERSION = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8")).version;
2449
2965
  function setup() {
2450
2966
  const opts = program.opts();
2451
- return { out: new Output(opts.json ?? false), opts };
2967
+ return { out: new Output(resolveOutputFormat(opts)), opts };
2968
+ }
2969
+ function resolveOutputFormat(opts) {
2970
+ if (opts.json) return "json";
2971
+ const requested = opts.output ?? "auto";
2972
+ if (requested === "auto") return process.stdout.isTTY ? "text" : "json";
2973
+ return requested;
2974
+ }
2975
+ function isJsonRequestedFromArgv() {
2976
+ const idx = process.argv.findIndex((arg) => arg === "--output" || arg === "-o");
2977
+ const explicitOutput = idx >= 0 ? process.argv[idx + 1] : void 0;
2978
+ return process.argv.includes("--json") || explicitOutput === "json" || explicitOutput === "ndjson";
2452
2979
  }
2453
2980
  function target(opts) {
2454
2981
  return resolveTarget({
@@ -2468,8 +2995,30 @@ function requireProject(opts) {
2468
2995
  }
2469
2996
  return { name: t.project, root: t.projectRoot };
2470
2997
  }
2471
- program.name("tesser").version(VERSION).description("Code-first, agent-native automation. stdout = data, stderr = logs; --json everywhere.").option("--json", "machine output: one JSON document on stdout").option("--url <url>", "instance URL (overrides tesser.json / profile)").option("--token <token>", "API token (overrides TESSER_TOKEN / profile)").option("--profile <name>", "config profile");
2472
- program.command("init").argument("<name>", "project name (kebab-case)").option("--dir <dir>", "parent directory").option("--instance <url>", "instance URL to write into tesser.json").description("scaffold a new Project (one repo of automations)").action((name, cmdOpts) => {
2998
+ program.name("tesser").version(VERSION).description("Code-first, agent-native automation. stdout = data, stderr = logs; --json everywhere.").option("--json", "machine output: one JSON document on stdout (alias for --output json)").addOption(new Option("-o, --output <format>", "output format").choices(["auto", "text", "json", "ndjson"]).default("auto")).option("--url <url>", "instance URL (overrides tesser.json / profile)").option("--token <token>", "API token (overrides TESSER_TOKEN / profile)").option("--profile <name>", "config profile").exitOverride().configureOutput({ outputError: () => {
2999
+ } }).action(() => {
3000
+ const out = new Output(resolveOutputFormat(program.opts()));
3001
+ if (out.json) out.fail(EXIT.USAGE, "missing command");
3002
+ program.outputHelp({ error: true });
3003
+ process.exit(EXIT.USAGE);
3004
+ });
3005
+ program.command("completion").argument("<shell>", "bash or zsh").description("print shell completion script").action((shell) => {
3006
+ const { out } = setup();
3007
+ try {
3008
+ out.data(completionScript(shell), (script) => script.replace(/\n$/, ""));
3009
+ } catch (err) {
3010
+ toExit(err, out);
3011
+ }
3012
+ });
3013
+ program.command("schema").argument("[command...]", "optional command subtree, e.g. runs list").description("emit the machine-readable CLI contract (no auth, config, or network needed)").action((parts) => {
3014
+ const { out } = setup();
3015
+ try {
3016
+ out.data(cliSchema(VERSION, parts.length > 0 ? parts.join(" ") : void 0));
3017
+ } catch (err) {
3018
+ toExit(err, out);
3019
+ }
3020
+ });
3021
+ program.command("init").argument("<name>", "project name (kebab-case)").option("--dir <dir>", "parent directory").option("--instance <url>", "instance URL to write into tesser.json").option("--force", "write into an existing non-empty directory").description("scaffold a new Project (one repo of automations)").action((name, cmdOpts) => {
2473
3022
  const { out } = setup();
2474
3023
  try {
2475
3024
  init(out, name, cmdOpts, VERSION);
@@ -2485,12 +3034,10 @@ program.command("upgrade").description("pin Tesser packages to this CLI version
2485
3034
  toExit(err, out);
2486
3035
  }
2487
3036
  });
2488
- program.command("login").option("--token <token>", "API token from the instance (printed at first boot)").option("--url <url>", `instance URL (defaults to ${DEFAULT_INSTANCE_URL})`).option("--instance <url>", "deprecated alias for --url").option("--save-profile <name>", "profile name", "default").description("store instance credentials in ~/.config/tesser (or use env TESSER_TOKEN)").action(async (cmdOpts) => {
3037
+ program.command("login").option("--token <token>", "API token from the instance (discouraged: argv can leak; prefer --token-stdin)").option("--token-stdin", "read the API token from stdin").option("--url <url>", `instance URL (defaults to ${DEFAULT_INSTANCE_URL})`).option("--instance <url>", "deprecated alias for --url").option("--save-profile <name>", "profile name", "default").description("store instance credentials in ~/.config/tesser (or use env TESSER_TOKEN)").action(async (cmdOpts) => {
2489
3038
  const { out, opts } = setup();
2490
3039
  try {
2491
- const token = cmdOpts.token ?? opts.token;
2492
- if (!token) throw new CliError(EXIT.USAGE, "missing required option '--token <token>'");
2493
- const instance = resolveLoginInstance(cmdOpts, opts);
3040
+ const { instance, token } = await resolveLoginInputs(cmdOpts, opts);
2494
3041
  const client = new ApiClient(instance, token);
2495
3042
  await client.get("/health");
2496
3043
  const config = readConfig();
@@ -2574,7 +3121,8 @@ program.command("dev").option("--port <port>", "port for the local instance", "8
2574
3121
  const { out, opts } = setup();
2575
3122
  try {
2576
3123
  const { name, root } = requireProject(opts);
2577
- await dev(out, root, name, { port: Number(cmdOpts.port), ...cmdOpts.watch === false ? { watch: false } : {} });
3124
+ const port = parseIntOption(cmdOpts.port, "--port", { min: 1, max: 65535 });
3125
+ await dev(out, root, name, { port, ...cmdOpts.watch === false ? { watch: false } : {} });
2578
3126
  } catch (err) {
2579
3127
  toExit(err, out);
2580
3128
  }
@@ -2596,6 +3144,23 @@ auth.command("pi").requiredOption("--connect <urlOrToken>", "Tesser /connect/<to
2596
3144
  toExit(err, out);
2597
3145
  }
2598
3146
  });
3147
+ var harnessConnect = program.command("harness").description("brokered Harness helpers").command("connect").description("connect a Harness credential through a Tesser connect link");
3148
+ harnessConnect.command("claude-code").requiredOption("--connect <urlOrToken>", "Tesser /connect/<token> URL or token").option("--mode <mode>", "subscription or apiKey", "subscription").option("--token-stdin", "read token from stdin instead of running claude setup-token").option("--from-env <name>", "read token from an environment variable").option("--scope <scope>", "workspace or per_user", "workspace").option("--end-user-id <id>", "end-user id for per_user connections").option("--bin <path>", "claude binary", "claude").description("connect Claude Code as a brokered Harness; alias of `tesser auth claude-code`").action(async (cmdOpts) => {
3149
+ const { out, opts } = setup();
3150
+ try {
3151
+ await authClaudeCode(out, target(opts).url, cmdOpts);
3152
+ } catch (err) {
3153
+ toExit(err, out);
3154
+ }
3155
+ });
3156
+ harnessConnect.command("pi").requiredOption("--connect <urlOrToken>", "Tesser /connect/<token> URL or token").option("--mode <mode>", "anthropicOAuth or anthropicApiKey", "anthropicOAuth").option("--token-stdin", "read token from stdin").option("--from-env <name>", "read token from an environment variable").option("--scope <scope>", "workspace or per_user", "workspace").option("--end-user-id <id>", "end-user id for per_user connections").description("connect Pi as a brokered Harness; alias of `tesser auth pi`").action(async (cmdOpts) => {
3157
+ const { out, opts } = setup();
3158
+ try {
3159
+ await authPi(out, target(opts).url, cmdOpts);
3160
+ } catch (err) {
3161
+ toExit(err, out);
3162
+ }
3163
+ });
2599
3164
  program.command("connect").option("--wait", "poll until the human completes the link").option("--status <token>", "check an existing connect link").description("mint a connect link for missing credentials; the human completes it in a browser (ADR-0005)").action(async (cmdOpts) => {
2600
3165
  const { out, opts } = setup();
2601
3166
  try {
@@ -2613,20 +3178,22 @@ program.command("connect").option("--wait", "poll until the human completes the
2613
3178
  out.data({ url: null, requirements: [] }, () => "nothing missing \u2014 all requirements are satisfied");
2614
3179
  return;
2615
3180
  }
2616
- out.data(minted, () => `open in a browser to connect:
3181
+ if (!cmdOpts.wait) {
3182
+ out.data(minted, () => `open in a browser to connect:
2617
3183
  ${minted.url}`);
2618
- if (cmdOpts.wait) {
2619
- for (; ; ) {
2620
- await new Promise((r) => setTimeout(r, 2e3));
2621
- const status = await client.get(`/connect-links/${minted.token}/status`);
2622
- if (status.status === "completed") {
2623
- out.log("connect link completed \u2713");
2624
- return;
2625
- }
2626
- if (status.status === "expired") throw new CliError(EXIT.HALTED_CREDENTIALS, "connect link expired");
3184
+ process.exit(EXIT.HALTED_CREDENTIALS);
3185
+ }
3186
+ out.log(`open in a browser to connect:
3187
+ ${minted.url}`);
3188
+ for (; ; ) {
3189
+ await new Promise((r) => setTimeout(r, 2e3));
3190
+ const status = await client.get(`/connect-links/${minted.token}/status`);
3191
+ if (status.status === "completed") {
3192
+ out.data({ ...minted, status: "completed" }, () => "connect link completed \u2713");
3193
+ return;
2627
3194
  }
3195
+ if (status.status === "expired") throw new CliError(EXIT.HALTED_CREDENTIALS, "connect link expired");
2628
3196
  }
2629
- process.exit(EXIT.HALTED_CREDENTIALS);
2630
3197
  } catch (err) {
2631
3198
  toExit(err, out);
2632
3199
  }
@@ -2654,7 +3221,7 @@ secrets.command("set").argument("<name>").option("--value-stdin", "read the valu
2654
3221
  const value = Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
2655
3222
  if (value.length === 0) throw new CliError(EXIT.USAGE, "empty value on stdin");
2656
3223
  await api(opts).put(`/secrets/${encodeURIComponent(name)}`, { value });
2657
- out.data({ set: name }, () => `secret "${name}" set \u2713`);
3224
+ out.data({ set: name, changed: true }, () => `secret "${name}" set \u2713`);
2658
3225
  } catch (err) {
2659
3226
  toExit(err, out);
2660
3227
  }
@@ -2662,53 +3229,62 @@ secrets.command("set").argument("<name>").option("--value-stdin", "read the valu
2662
3229
  secrets.command("rm").argument("<name>").action(async (name) => {
2663
3230
  const { out, opts } = setup();
2664
3231
  try {
2665
- out.data(await api(opts).delete(`/secrets/${encodeURIComponent(name)}`));
3232
+ const result = await api(opts).delete(`/secrets/${encodeURIComponent(name)}`);
3233
+ out.data({ ...result, changed: result.deleted });
2666
3234
  } catch (err) {
2667
3235
  toExit(err, out);
2668
3236
  }
2669
3237
  });
2670
3238
  var runs = program.command("runs").description("inspect and drive runs");
2671
- runs.command("list").option("--automation <id>").option("--status <status>").option("--limit <n>", "max rows", "25").action(async (cmdOpts) => {
3239
+ runs.command("list").option("--automation <id>").option("--status <status>").option("--limit <n>", "max rows", "25").option("--offset <n>", "rows to skip", "0").option("--fields <csv>", "comma-separated fields to include in each run").action(async (cmdOpts) => {
2672
3240
  const { out, opts } = setup();
2673
3241
  try {
2674
3242
  const { name } = requireProject(opts);
2675
- const params = new URLSearchParams({ project: name, limit: cmdOpts.limit });
3243
+ const limit = parseIntOption(cmdOpts.limit, "--limit", { min: 1, max: 200 });
3244
+ const offset = parseIntOption(cmdOpts.offset, "--offset", { min: 0 });
3245
+ const params = new URLSearchParams({ project: name, limit: String(limit), offset: String(offset) });
2676
3246
  if (cmdOpts.automation) params.set("automation", cmdOpts.automation);
2677
3247
  if (cmdOpts.status) params.set("status", cmdOpts.status);
2678
3248
  const data = await api(opts).get(`/runs?${params}`);
2679
- out.data(
2680
- data,
2681
- (d) => d.runs.map((r) => `${r.id} ${r.automation_id} ${r.status} (${r.trigger_kind}) ${r.created_at}`).join("\n") || "(no runs)"
2682
- );
3249
+ const projected = { ...data, runs: projectFields(data.runs, cmdOpts.fields) };
3250
+ out.data(projected, (d) => {
3251
+ const rows = d.runs.map((r) => `${r.id ?? "?"} ${r.automation_id ?? "?"} ${r.status ?? "?"} (${r.trigger_kind ?? "?"}) ${r.created_at ?? "?"}`).join("\n") || "(no runs)";
3252
+ return d.truncated ? `${rows}
3253
+ (showing ${d.runs.length} of ${d.total ?? "?"}; use --offset for more)` : rows;
3254
+ });
2683
3255
  } catch (err) {
2684
3256
  toExit(err, out);
2685
3257
  }
2686
3258
  });
2687
- runs.command("show").argument("<runId>").action(async (runId) => {
3259
+ runs.command("show").argument("<runId>").option("--log-limit <n>", "max log rows", "100").option("--log-offset <n>", "log rows to skip", "0").action(async (runId, cmdOpts) => {
2688
3260
  const { out, opts } = setup();
2689
3261
  try {
2690
- out.data(await api(opts).get(`/runs/${runId}`));
3262
+ const logLimit = parseIntOption(cmdOpts.logLimit, "--log-limit", { min: 1, max: 500 });
3263
+ const logOffset = parseIntOption(cmdOpts.logOffset, "--log-offset", { min: 0 });
3264
+ out.data(await api(opts).get(`/runs/${runId}?${new URLSearchParams({ logLimit: String(logLimit), logOffset: String(logOffset) })}`));
2691
3265
  } catch (err) {
2692
3266
  toExit(err, out);
2693
3267
  }
2694
3268
  });
2695
- runs.command("trigger").argument("<automation>").option("--input <json>", "input payload as JSON").option("--env <env>", "environment", "production").action(async (automation, cmdOpts) => {
3269
+ runs.command("trigger").argument("<automation>").option("--input <json>", "input payload as JSON").option("--input-file <path>", "read input JSON from a file, or '-' for stdin").option("--env <env>", "environment", "production").action(async (automation, cmdOpts) => {
2696
3270
  const { out, opts } = setup();
2697
3271
  try {
2698
3272
  const { name } = requireProject(opts);
2699
3273
  const body = { project: name, automation, env: cmdOpts.env };
2700
- if (cmdOpts.input !== void 0) body["input"] = JSON.parse(cmdOpts.input);
3274
+ const input2 = await resolveJsonInput({ literal: cmdOpts.input, file: cmdOpts.inputFile, label: "input" });
3275
+ if (input2 !== void 0) body["input"] = input2;
2701
3276
  out.data(await api(opts).post("/runs", body));
2702
3277
  } catch (err) {
2703
3278
  toExit(err, out);
2704
3279
  }
2705
3280
  });
2706
- runs.command("signal").argument("<runId>").argument("<name>").option("--payload <json>", "signal payload as JSON").action(async (runId, name, cmdOpts) => {
3281
+ runs.command("signal").argument("<runId>").argument("<name>").option("--payload <json>", "signal payload as JSON").option("--payload-file <path>", "read signal payload JSON from a file, or '-' for stdin").action(async (runId, name, cmdOpts) => {
2707
3282
  const { out, opts } = setup();
2708
3283
  try {
3284
+ const payload = await resolveJsonInput({ literal: cmdOpts.payload, file: cmdOpts.payloadFile, label: "payload" });
2709
3285
  out.data(
2710
3286
  await api(opts).post(`/runs/${runId}/signals/${encodeURIComponent(name)}`, {
2711
- ...cmdOpts.payload !== void 0 ? { payload: JSON.parse(cmdOpts.payload) } : {}
3287
+ ...payload !== void 0 ? { payload } : {}
2712
3288
  })
2713
3289
  );
2714
3290
  } catch (err) {
@@ -2718,28 +3294,43 @@ runs.command("signal").argument("<runId>").argument("<name>").option("--payload
2718
3294
  runs.command("cancel").argument("<runId>").action(async (runId) => {
2719
3295
  const { out, opts } = setup();
2720
3296
  try {
2721
- out.data(await api(opts).post(`/runs/${runId}/cancel`));
3297
+ const result = await api(opts).post(`/runs/${runId}/cancel`);
3298
+ out.data({ ...result, changed: result.cancelled });
2722
3299
  } catch (err) {
2723
3300
  toExit(err, out);
2724
3301
  }
2725
3302
  });
2726
- program.command("logs").argument("<runId>").option("--follow", "poll until the run settles").description("step logs for one run").action(async (runId, cmdOpts) => {
3303
+ program.command("logs").argument("<runId>").option("--follow", "poll until the run settles").option("--limit <n>", "max log rows per page", "100").option("--offset <n>", "log rows to skip", "0").description("step logs for one run").action(async (runId, cmdOpts) => {
2727
3304
  const { out, opts } = setup();
2728
3305
  try {
2729
3306
  const client = api(opts);
2730
- let printed = 0;
3307
+ const limit = parseIntOption(cmdOpts.limit, "--limit", { min: 1, max: 500 });
3308
+ let offset = parseIntOption(cmdOpts.offset, "--offset", { min: 0 });
3309
+ let finalDetail = null;
2731
3310
  for (; ; ) {
2732
- const detail = await client.get(`/runs/${runId}`);
2733
- const fresh = detail.logs.slice(printed);
2734
- printed = detail.logs.length;
2735
- if (out.json && !cmdOpts.follow) {
2736
- out.data(detail);
3311
+ const detail = await client.get(`/runs/${runId}?${new URLSearchParams({ logLimit: String(limit), logOffset: String(offset) })}`);
3312
+ finalDetail = detail;
3313
+ if (!cmdOpts.follow) {
3314
+ if (out.json) out.data(detail);
3315
+ else {
3316
+ for (const l of detail.logs) process.stdout.write(`[${l.level}]${l.step ? ` (${l.step})` : ""} ${l.msg}
3317
+ `);
3318
+ if (detail.logsTruncated) out.log(`showing ${detail.logs.length} of ${detail.logsTotal ?? "?"}; use --offset for more`);
3319
+ }
2737
3320
  return;
2738
3321
  }
2739
- for (const l of fresh) out.log(`[${l.level}]${l.step ? ` (${l.step})` : ""} ${l.msg}`);
2740
- if (!cmdOpts.follow || ["completed", "failed", "cancelled"].includes(detail.run.status)) {
2741
- if (!out.json) out.log(`run ${detail.run.status}`);
2742
- else out.data(detail);
3322
+ if (out.format === "ndjson") {
3323
+ for (const l of detail.logs) process.stdout.write(JSON.stringify({ log: l }) + "\n");
3324
+ } else if (!out.json) {
3325
+ for (const l of detail.logs) process.stdout.write(`[${l.level}]${l.step ? ` (${l.step})` : ""} ${l.msg}
3326
+ `);
3327
+ }
3328
+ offset += detail.logs.length;
3329
+ if (detail.logsTruncated) continue;
3330
+ if (["completed", "failed", "cancelled"].includes(detail.run.status)) {
3331
+ if (out.format === "ndjson") process.stdout.write(JSON.stringify({ run: detail.run }) + "\n");
3332
+ else if (out.json) out.data(finalDetail);
3333
+ else out.log(`run ${detail.run.status}`);
2743
3334
  return;
2744
3335
  }
2745
3336
  await new Promise((r) => setTimeout(r, 1e3));
@@ -2761,13 +3352,22 @@ program.command("rollback").argument("<automation>").requiredOption("--to <versi
2761
3352
  const { out, opts } = setup();
2762
3353
  try {
2763
3354
  const { name } = requireProject(opts);
2764
- out.data(
2765
- await api(opts).post(`/projects/${name}/rollback`, {
2766
- automation,
2767
- toVersion: Number(cmdOpts.to),
2768
- env: cmdOpts.env
2769
- })
2770
- );
3355
+ const toVersion = parseIntOption(cmdOpts.to, "--to", { min: 1 });
3356
+ const result = await api(opts).post(`/projects/${name}/rollback`, {
3357
+ automation,
3358
+ toVersion,
3359
+ env: cmdOpts.env
3360
+ });
3361
+ out.data({ ...result, changed: true });
3362
+ } catch (err) {
3363
+ toExit(err, out);
3364
+ }
3365
+ });
3366
+ program.command("doctor").option("--no-network", "skip Instance health checks").description("agent preflight: local Project, auth, pins, and optional Instance health").action(async (cmdOpts) => {
3367
+ const { out, opts } = setup();
3368
+ try {
3369
+ const resolved = target(opts);
3370
+ await doctor(out, { version: VERSION, target: resolved, client: new ApiClient(resolved.url, resolved.token), network: cmdOpts.network !== false });
2771
3371
  } catch (err) {
2772
3372
  toExit(err, out);
2773
3373
  }
@@ -2787,7 +3387,17 @@ program.command("status").description("instance + project deploy status").action
2787
3387
  }
2788
3388
  });
2789
3389
  program.parseAsync().catch((err) => {
2790
- const out = new Output(process.argv.includes("--json"));
3390
+ if (isCommanderHelp(err)) process.exit(err.exitCode ?? EXIT.OK);
3391
+ const out = new Output(isJsonRequestedFromArgv() ? "json" : process.stdout.isTTY ? "text" : "json");
3392
+ if (isCommanderUsageError(err)) {
3393
+ out.fail(EXIT.USAGE, err.message.replace(/^error: /, ""));
3394
+ }
2791
3395
  toExit(err, out);
2792
3396
  });
3397
+ function isCommanderHelp(err) {
3398
+ return typeof err === "object" && err !== null && err.code === "commander.helpDisplayed";
3399
+ }
3400
+ function isCommanderUsageError(err) {
3401
+ return typeof err === "object" && err !== null && typeof err.code === "string" && err.code.startsWith("commander.") && typeof err.message === "string";
3402
+ }
2793
3403
  //# sourceMappingURL=index.js.map