@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/README.md +15 -7
- package/dist/index.js +830 -220
- package/dist/index.js.map +4 -4
- package/package.json +3 -3
- package/src/commands/doctor.test.ts +39 -0
- package/src/commands/doctor.ts +117 -0
- package/src/commands/init.test.ts +14 -1
- package/src/commands/init.ts +10 -4
- package/src/commands/project-docs.ts +6 -2
- package/src/completion.test.ts +14 -0
- package/src/completion.ts +92 -0
- package/src/index.ts +220 -57
- package/src/inputs.test.ts +21 -0
- package/src/inputs.ts +64 -0
- package/src/login.test.ts +19 -1
- package/src/login.ts +92 -1
- package/src/output.ts +15 -3
- package/src/schema.test.ts +26 -0
- package/src/schema.ts +289 -0
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
|
|
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(
|
|
130
|
-
if (typeof
|
|
131
|
-
if (!Number.isFinite(
|
|
132
|
-
throw new TypeError(`${what}: expected a non-negative number of ms, got ${
|
|
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
|
|
134
|
+
return input2;
|
|
135
135
|
}
|
|
136
|
-
const s =
|
|
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}: "${
|
|
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] = (
|
|
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] = (
|
|
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
|
|
668
|
-
return { ...serial, output: toSerializable2(
|
|
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
|
-
|
|
706
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1081
|
-
import { join as
|
|
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 &&
|
|
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 =
|
|
1090
|
-
if (
|
|
1091
|
-
const shim =
|
|
1092
|
-
if (
|
|
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:
|
|
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(
|
|
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
|
|
1181
|
-
import { join as
|
|
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
|
|
1185
|
-
import { join as
|
|
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 =
|
|
1217
|
-
if (!opts.overwrite &&
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 (!
|
|
1564
|
+
if (!existsSync5(path)) {
|
|
1268
1565
|
writeFileSync2(path, tesserGitignore());
|
|
1269
1566
|
return true;
|
|
1270
1567
|
}
|
|
1271
|
-
const existing =
|
|
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 =
|
|
1278
|
-
if (!
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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 =
|
|
1632
|
-
if (
|
|
1633
|
-
|
|
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(
|
|
1866
|
+
mkdirSync3(join6(root, "automations", "hello"), { recursive: true });
|
|
1636
1867
|
writeFileSync3(
|
|
1637
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
1903
|
+
writeFileSync3(join6(root, ".gitignore"), tesserGitignore());
|
|
1673
1904
|
writeProjectAgentInstructions(root, name, version, { overwrite: true });
|
|
1674
1905
|
const docs = writeTesserGeneratedDocs(root, name, version);
|
|
1675
|
-
writeFileSync3(
|
|
1676
|
-
writeFileSync3(
|
|
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
|
|
1698
|
-
import { join as
|
|
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 =
|
|
1702
|
-
if (!
|
|
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 =
|
|
1937
|
+
const fixtureDir = join7(dir, "__replays__");
|
|
1707
1938
|
mkdirSync4(fixtureDir, { recursive: true });
|
|
1708
|
-
const fixturePath =
|
|
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 =
|
|
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
|
|
1761
|
-
import { join as
|
|
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
|
|
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(
|
|
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
|
-
|
|
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: [
|
|
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:
|
|
2256
|
+
connectorCalls.push({ step: step.name, action: fullPath, input: input3 });
|
|
2026
2257
|
return result;
|
|
2027
2258
|
} catch (err) {
|
|
2028
|
-
const record = { args: [
|
|
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:
|
|
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
|
|
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 (
|
|
2207
|
-
|
|
2437
|
+
if (input2 === void 0) {
|
|
2438
|
+
input2 = connector?.__connector.samples?.[`trigger:${trigger.triggerId}`];
|
|
2208
2439
|
}
|
|
2209
2440
|
}
|
|
2210
2441
|
}
|
|
2211
|
-
if (
|
|
2212
|
-
|
|
2442
|
+
if (input2 === void 0 && inputSchema && trigger.kind !== "schedule") {
|
|
2443
|
+
input2 = await sampleFromSchema(inputSchema);
|
|
2213
2444
|
}
|
|
2214
|
-
if (inputSchema &&
|
|
2215
|
-
|
|
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
|
|
2489
|
+
let output2 = await def.run(input2, ctx);
|
|
2259
2490
|
if (def.output) {
|
|
2260
|
-
|
|
2491
|
+
output2 = await validateSchema(def.output, output2, `automation "${def.id}" output`);
|
|
2261
2492
|
}
|
|
2262
|
-
return finish("completed",
|
|
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
|
|
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 =
|
|
2347
|
-
if (
|
|
2348
|
-
const parent =
|
|
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 ${
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3181
|
+
if (!cmdOpts.wait) {
|
|
3182
|
+
out.data(minted, () => `open in a browser to connect:
|
|
2617
3183
|
${minted.url}`);
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
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
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
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
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
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
|
-
|
|
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
|