@devosurf/tesser 0.1.0-alpha.2 → 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 +840 -223
- 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/config.ts +3 -1
- package/src/index.ts +226 -60
- package/src/inputs.test.ts +21 -0
- package/src/inputs.ts +64 -0
- package/src/login.test.ts +38 -0
- package/src/login.ts +109 -0
- 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
|
`);
|
|
@@ -807,6 +812,7 @@ var ApiClient = class {
|
|
|
807
812
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
808
813
|
import { homedir } from "node:os";
|
|
809
814
|
import { dirname, join } from "node:path";
|
|
815
|
+
var DEFAULT_INSTANCE_URL = "http://localhost:8377";
|
|
810
816
|
var CONFIG_PATH = join(
|
|
811
817
|
process.env["TESSER_CONFIG_DIR"] ?? join(homedir(), ".config", "tesser"),
|
|
812
818
|
"config.json"
|
|
@@ -848,13 +854,223 @@ function resolveTarget(opts) {
|
|
|
848
854
|
const projectRoot = findProjectRoot();
|
|
849
855
|
const manifest = projectRoot ? readLinkManifest(projectRoot) : null;
|
|
850
856
|
return {
|
|
851
|
-
url: opts.url ?? manifest?.instance ?? process.env["TESSER_URL"] ?? profile.url ??
|
|
857
|
+
url: opts.url ?? manifest?.instance ?? process.env["TESSER_URL"] ?? profile.url ?? DEFAULT_INSTANCE_URL,
|
|
852
858
|
token: opts.token ?? process.env["TESSER_TOKEN"] ?? profile.token,
|
|
853
859
|
project: manifest?.project,
|
|
854
860
|
projectRoot
|
|
855
861
|
};
|
|
856
862
|
}
|
|
857
863
|
|
|
864
|
+
// packages/cli/src/login.ts
|
|
865
|
+
import { createInterface } from "node:readline/promises";
|
|
866
|
+
import { stdin as input, stderr as output } from "node:process";
|
|
867
|
+
function resolveLoginInstance(cmdOpts, globalOpts) {
|
|
868
|
+
return cmdOpts.url ?? cmdOpts.instance ?? globalOpts.url ?? DEFAULT_INSTANCE_URL;
|
|
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
|
+
}
|
|
1073
|
+
|
|
858
1074
|
// packages/cli/src/project.ts
|
|
859
1075
|
import { mkdtempSync, existsSync as existsSync2, readdirSync, statSync } from "node:fs";
|
|
860
1076
|
import { tmpdir } from "node:os";
|
|
@@ -1068,22 +1284,109 @@ Then rerun: tesser deploy${opts.local ? " --local" : ""}`
|
|
|
1068
1284
|
throw new CliError(EXIT.ERROR, "timed out waiting for the deploy to settle", { last });
|
|
1069
1285
|
}
|
|
1070
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
|
+
|
|
1071
1374
|
// packages/cli/src/commands/dev.ts
|
|
1072
1375
|
import { randomBytes } from "node:crypto";
|
|
1073
1376
|
import { spawn as spawn2 } from "node:child_process";
|
|
1074
|
-
import { existsSync as
|
|
1075
|
-
import { join as
|
|
1377
|
+
import { existsSync as existsSync4, watch } from "node:fs";
|
|
1378
|
+
import { join as join4, dirname as dirname2 } from "node:path";
|
|
1076
1379
|
function findServerBin(start) {
|
|
1077
1380
|
const envBin = process.env["TESSER_SERVER_BIN"];
|
|
1078
|
-
if (envBin &&
|
|
1381
|
+
if (envBin && existsSync4(envBin)) {
|
|
1079
1382
|
return envBin.endsWith(".mjs") || envBin.endsWith(".js") ? [process.execPath, envBin] : [envBin];
|
|
1080
1383
|
}
|
|
1081
1384
|
let dir = start;
|
|
1082
1385
|
for (; ; ) {
|
|
1083
|
-
const entry =
|
|
1084
|
-
if (
|
|
1085
|
-
const shim =
|
|
1086
|
-
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];
|
|
1087
1390
|
const parent = dirname2(dir);
|
|
1088
1391
|
if (parent === dir) return null;
|
|
1089
1392
|
dir = parent;
|
|
@@ -1105,7 +1408,7 @@ async function dev(out, projectRoot, project, opts) {
|
|
|
1105
1408
|
env: {
|
|
1106
1409
|
...process.env,
|
|
1107
1410
|
PORT: String(port),
|
|
1108
|
-
TESSER_DATA_DIR:
|
|
1411
|
+
TESSER_DATA_DIR: join4(projectRoot, ".tesser"),
|
|
1109
1412
|
TESSER_BOOTSTRAP_TOKEN: token,
|
|
1110
1413
|
TESSER_BASE_URL: url,
|
|
1111
1414
|
DATABASE_URL: ""
|
|
@@ -1155,7 +1458,7 @@ async function dev(out, projectRoot, project, opts) {
|
|
|
1155
1458
|
await syncOnce();
|
|
1156
1459
|
if (opts.watch !== false) {
|
|
1157
1460
|
let timer = null;
|
|
1158
|
-
watch(
|
|
1461
|
+
watch(join4(projectRoot, "automations"), { recursive: true }, () => {
|
|
1159
1462
|
if (timer) clearTimeout(timer);
|
|
1160
1463
|
timer = setTimeout(() => {
|
|
1161
1464
|
out.log("change detected \u2014 redeploying\u2026");
|
|
@@ -1171,12 +1474,12 @@ async function dev(out, projectRoot, project, opts) {
|
|
|
1171
1474
|
}
|
|
1172
1475
|
|
|
1173
1476
|
// packages/cli/src/commands/init.ts
|
|
1174
|
-
import { existsSync as
|
|
1175
|
-
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";
|
|
1176
1479
|
|
|
1177
1480
|
// packages/cli/src/commands/project-docs.ts
|
|
1178
|
-
import { existsSync as
|
|
1179
|
-
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";
|
|
1180
1483
|
var TESSER_DEPENDENCIES = ["@devosurf/tesser-sdk", "@devosurf/tesser-connectors"];
|
|
1181
1484
|
var TESSER_DEV_DEPENDENCIES = ["@devosurf/tesser", "@devosurf/tesser-server", "@devosurf/tesser-testing"];
|
|
1182
1485
|
var GENERATED_DOC_PATHS = [
|
|
@@ -1207,13 +1510,13 @@ function projectPackageJson(name, version) {
|
|
|
1207
1510
|
};
|
|
1208
1511
|
}
|
|
1209
1512
|
function writeProjectAgentInstructions(root, projectName, version, opts) {
|
|
1210
|
-
const path =
|
|
1211
|
-
if (!opts.overwrite &&
|
|
1513
|
+
const path = join5(root, "AGENTS.md");
|
|
1514
|
+
if (!opts.overwrite && existsSync5(path)) return false;
|
|
1212
1515
|
writeFileSync2(path, projectAgentsMd(projectName, version));
|
|
1213
1516
|
return true;
|
|
1214
1517
|
}
|
|
1215
1518
|
function writeTesserGeneratedDocs(root, projectName, version) {
|
|
1216
|
-
const docsDir =
|
|
1519
|
+
const docsDir = join5(root, ".tesser", "docs");
|
|
1217
1520
|
mkdirSync2(docsDir, { recursive: true });
|
|
1218
1521
|
const docs = {
|
|
1219
1522
|
"manifest.json": JSON.stringify(
|
|
@@ -1234,7 +1537,7 @@ function writeTesserGeneratedDocs(root, projectName, version) {
|
|
|
1234
1537
|
"connectors.md": connectorsReferenceMd(version)
|
|
1235
1538
|
};
|
|
1236
1539
|
for (const [file, content] of Object.entries(docs)) {
|
|
1237
|
-
writeFileSync2(
|
|
1540
|
+
writeFileSync2(join5(docsDir, file), content);
|
|
1238
1541
|
}
|
|
1239
1542
|
return [...GENERATED_DOC_PATHS];
|
|
1240
1543
|
}
|
|
@@ -1250,7 +1553,7 @@ function tesserGitignore() {
|
|
|
1250
1553
|
`;
|
|
1251
1554
|
}
|
|
1252
1555
|
function ensureTesserDocsGitignore(root) {
|
|
1253
|
-
const path =
|
|
1556
|
+
const path = join5(root, ".gitignore");
|
|
1254
1557
|
const block = `
|
|
1255
1558
|
# Tesser local runtime state from \`tesser dev\`; keep generated agent docs committed.
|
|
1256
1559
|
.tesser/*
|
|
@@ -1258,21 +1561,21 @@ function ensureTesserDocsGitignore(root) {
|
|
|
1258
1561
|
!.tesser/docs/
|
|
1259
1562
|
!.tesser/docs/**
|
|
1260
1563
|
`;
|
|
1261
|
-
if (!
|
|
1564
|
+
if (!existsSync5(path)) {
|
|
1262
1565
|
writeFileSync2(path, tesserGitignore());
|
|
1263
1566
|
return true;
|
|
1264
1567
|
}
|
|
1265
|
-
const existing =
|
|
1568
|
+
const existing = readFileSync3(path, "utf8");
|
|
1266
1569
|
if (existing.includes("!.tesser/docs/**")) return false;
|
|
1267
1570
|
writeFileSync2(path, existing.replace(/\s*$/, "\n") + block);
|
|
1268
1571
|
return true;
|
|
1269
1572
|
}
|
|
1270
1573
|
function upgradeProject(out, project, version) {
|
|
1271
|
-
const packagePath =
|
|
1272
|
-
if (!
|
|
1574
|
+
const packagePath = join5(project.root, "package.json");
|
|
1575
|
+
if (!existsSync5(packagePath)) {
|
|
1273
1576
|
throw new CliError(EXIT.USAGE, "not inside a package-backed Tesser project (missing package.json)");
|
|
1274
1577
|
}
|
|
1275
|
-
const pkg = JSON.parse(
|
|
1578
|
+
const pkg = JSON.parse(readFileSync3(packagePath, "utf8"));
|
|
1276
1579
|
pinTesserPackages(pkg, version);
|
|
1277
1580
|
writeFileSync2(packagePath, JSON.stringify(pkg, null, 2) + "\n");
|
|
1278
1581
|
const docs = writeTesserGeneratedDocs(project.root, project.name, version);
|
|
@@ -1371,7 +1674,7 @@ Commit \`package.json\`, \`pnpm-lock.yaml\`, \`.tesser/docs/\`, and any Automati
|
|
|
1371
1674
|
function cliReferenceMd(version) {
|
|
1372
1675
|
return `# Tesser CLI reference
|
|
1373
1676
|
|
|
1374
|
-
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.
|
|
1375
1678
|
|
|
1376
1679
|
## Install and invoke
|
|
1377
1680
|
|
|
@@ -1390,7 +1693,7 @@ tesser init my-project --instance https://tesser.example.com
|
|
|
1390
1693
|
cd my-project
|
|
1391
1694
|
pnpm install # creates pnpm-lock.yaml; commit it
|
|
1392
1695
|
git init && git add -A && git commit -m init
|
|
1393
|
-
tesser login --
|
|
1696
|
+
printf '%s' "$TESSER_TOKEN" | tesser login --url https://tesser.example.com --token-stdin
|
|
1394
1697
|
tesser link --json # registers this Project on the Instance
|
|
1395
1698
|
tesser test --json
|
|
1396
1699
|
tesser build --json
|
|
@@ -1399,9 +1702,9 @@ tesser deploy --json
|
|
|
1399
1702
|
|
|
1400
1703
|
## Commands agents commonly use
|
|
1401
1704
|
|
|
1402
|
-
- \`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.
|
|
1403
1706
|
- \`tesser upgrade\` \u2014 pin Tesser packages to this CLI version and refresh \`.tesser/docs/\`. To target a version: \`npx @devosurf/tesser@<version> upgrade\`.
|
|
1404
|
-
- \`tesser login --
|
|
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.
|
|
1405
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.
|
|
1406
1709
|
- \`tesser status --json\` \u2014 instance health and deploy state.
|
|
1407
1710
|
- \`tesser test [--smoke-only] [--automation ID] --json\` \u2014 fast local tests plus generated smoke tests.
|
|
@@ -1410,13 +1713,17 @@ tesser deploy --json
|
|
|
1410
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\`.
|
|
1411
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.
|
|
1412
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.
|
|
1413
|
-
- \`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.
|
|
1414
1721
|
- \`tesser rollback <automation> --to <version> --json\` \u2014 alias re-point; no rebuild.
|
|
1415
1722
|
|
|
1416
1723
|
## Credential safety
|
|
1417
1724
|
|
|
1418
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.
|
|
1419
|
-
- 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.
|
|
1420
1727
|
- Treat connect links and status tokens as sensitive operational material.
|
|
1421
1728
|
|
|
1422
1729
|
## Deploy hygiene
|
|
@@ -1514,83 +1821,7 @@ For Connector calls, mock the Step result by Step name. Replay fixtures produced
|
|
|
1514
1821
|
`;
|
|
1515
1822
|
}
|
|
1516
1823
|
function connectorsReferenceMd(version) {
|
|
1517
|
-
return
|
|
1518
|
-
|
|
1519
|
-
Generated for \`@devosurf/tesser-connectors@${version}\`. A Connector is a typed integration; a Connection is an authed runtime instance injected by the Credential broker.
|
|
1520
|
-
|
|
1521
|
-
## Available connector imports
|
|
1522
|
-
|
|
1523
|
-
\`\`\`ts
|
|
1524
|
-
import {
|
|
1525
|
-
anthropic,
|
|
1526
|
-
claudeCode,
|
|
1527
|
-
github,
|
|
1528
|
-
gmail,
|
|
1529
|
-
googleCalendar,
|
|
1530
|
-
googleDocs,
|
|
1531
|
-
googleDrive,
|
|
1532
|
-
googleSheets,
|
|
1533
|
-
http,
|
|
1534
|
-
outlookMail,
|
|
1535
|
-
pi,
|
|
1536
|
-
resend,
|
|
1537
|
-
slack,
|
|
1538
|
-
} from "@devosurf/tesser-connectors";
|
|
1539
|
-
\`\`\`
|
|
1540
|
-
|
|
1541
|
-
Only import Connectors that the Automation declares in \`connections: { ... }\`.
|
|
1542
|
-
|
|
1543
|
-
## Pattern
|
|
1544
|
-
|
|
1545
|
-
\`\`\`ts
|
|
1546
|
-
import { defineAutomation, onSchedule } from "@devosurf/tesser-sdk";
|
|
1547
|
-
import { github, slack } from "@devosurf/tesser-connectors";
|
|
1548
|
-
import { z } from "zod";
|
|
1549
|
-
|
|
1550
|
-
export default defineAutomation({
|
|
1551
|
-
id: "digest",
|
|
1552
|
-
trigger: onSchedule({ cron: "0 9 * * *", tz: "UTC" }),
|
|
1553
|
-
connections: { github, slack },
|
|
1554
|
-
output: z.object({ posted: z.boolean(), count: z.number() }),
|
|
1555
|
-
|
|
1556
|
-
run: async (_input, ctx) => {
|
|
1557
|
-
const issues = await ctx.step("fetch-open-issues", () =>
|
|
1558
|
-
ctx.connections.github.issues.list({ state: "open", labels: ["bug"] }),
|
|
1559
|
-
);
|
|
1560
|
-
|
|
1561
|
-
if (issues.length === 0) return { posted: false, count: 0 };
|
|
1562
|
-
|
|
1563
|
-
await ctx.step("post-to-slack", () =>
|
|
1564
|
-
ctx.connections.slack.chat.postMessage({ channel: "#ops", text: \`\${issues.length} bugs\` }),
|
|
1565
|
-
);
|
|
1566
|
-
|
|
1567
|
-
return { posted: true, count: issues.length };
|
|
1568
|
-
},
|
|
1569
|
-
});
|
|
1570
|
-
\`\`\`
|
|
1571
|
-
|
|
1572
|
-
## Common Actions
|
|
1573
|
-
|
|
1574
|
-
- \`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.
|
|
1575
|
-
- \`ctx.connections.github.issues.list({ repo?, state?, labels?, limit? })\`, \`issues.create({ repo, title, body?, labels? })\`, \`issues.comment({ repo, number, body })\`, \`repos.get({ repo })\`.
|
|
1576
|
-
- \`ctx.connections.slack.chat.postMessage({ channel, text, threadTs? })\` and related Slack chat/conversation/user Actions.
|
|
1577
|
-
- \`ctx.connections.resend.emails.send(...)\`, Gmail/Outlook Mail and Google Calendar/Docs/Drive/Sheets Actions, Anthropic model Actions, Claude Code/Pi Harness-related Connectors: inspect package types or examples before using.
|
|
1578
|
-
|
|
1579
|
-
## Connector triggers
|
|
1580
|
-
|
|
1581
|
-
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.
|
|
1582
|
-
|
|
1583
|
-
\`\`\`ts
|
|
1584
|
-
trigger: github.triggers.issueOpened({ repo: "owner/repo" })
|
|
1585
|
-
\`\`\`
|
|
1586
|
-
|
|
1587
|
-
## Safety rules
|
|
1588
|
-
|
|
1589
|
-
- Connector Actions are not automatically durable. The Automation author must wrap every Action call in \`ctx.step\`.
|
|
1590
|
-
- The imported Connector is not a token-bearing client. The authed client exists only at \`ctx.connections.<name>\` inside \`run\`.
|
|
1591
|
-
- For bespoke APIs, combine \`http\` with declared \`secrets: { ... }\`; do not put API keys in source, tests, logs, CLI args, or committed env files.
|
|
1592
|
-
- 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.
|
|
1593
|
-
`;
|
|
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);
|
|
1594
1825
|
}
|
|
1595
1826
|
|
|
1596
1827
|
// packages/cli/src/commands/init.ts
|
|
@@ -1622,17 +1853,23 @@ function init(out, name, opts, version) {
|
|
|
1622
1853
|
if (!/^[a-z][a-z0-9-]{0,63}$/.test(name)) {
|
|
1623
1854
|
throw new CliError(EXIT.USAGE, "project name must be kebab-case");
|
|
1624
1855
|
}
|
|
1625
|
-
const root =
|
|
1626
|
-
if (
|
|
1627
|
-
|
|
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
|
+
}
|
|
1628
1865
|
}
|
|
1629
|
-
mkdirSync3(
|
|
1866
|
+
mkdirSync3(join6(root, "automations", "hello"), { recursive: true });
|
|
1630
1867
|
writeFileSync3(
|
|
1631
|
-
|
|
1868
|
+
join6(root, "tesser.json"),
|
|
1632
1869
|
JSON.stringify({ project: name, ...opts.instance !== void 0 ? { instance: opts.instance } : {} }, null, 2) + "\n"
|
|
1633
1870
|
);
|
|
1634
1871
|
writeFileSync3(
|
|
1635
|
-
|
|
1872
|
+
join6(root, "package.json"),
|
|
1636
1873
|
JSON.stringify(
|
|
1637
1874
|
projectPackageJson(name, version),
|
|
1638
1875
|
null,
|
|
@@ -1640,7 +1877,7 @@ function init(out, name, opts, version) {
|
|
|
1640
1877
|
) + "\n"
|
|
1641
1878
|
);
|
|
1642
1879
|
writeFileSync3(
|
|
1643
|
-
|
|
1880
|
+
join6(root, "tsconfig.json"),
|
|
1644
1881
|
JSON.stringify(
|
|
1645
1882
|
{
|
|
1646
1883
|
compilerOptions: {
|
|
@@ -1658,16 +1895,16 @@ function init(out, name, opts, version) {
|
|
|
1658
1895
|
) + "\n"
|
|
1659
1896
|
);
|
|
1660
1897
|
writeFileSync3(
|
|
1661
|
-
|
|
1898
|
+
join6(root, "vitest.config.ts"),
|
|
1662
1899
|
`import { defineConfig } from "vitest/config";
|
|
1663
1900
|
export default defineConfig({ test: { globals: true, include: ["automations/**/*.test.ts"] } });
|
|
1664
1901
|
`
|
|
1665
1902
|
);
|
|
1666
|
-
writeFileSync3(
|
|
1903
|
+
writeFileSync3(join6(root, ".gitignore"), tesserGitignore());
|
|
1667
1904
|
writeProjectAgentInstructions(root, name, version, { overwrite: true });
|
|
1668
1905
|
const docs = writeTesserGeneratedDocs(root, name, version);
|
|
1669
|
-
writeFileSync3(
|
|
1670
|
-
writeFileSync3(
|
|
1906
|
+
writeFileSync3(join6(root, "automations", "hello", "index.ts"), EXAMPLE_AUTOMATION);
|
|
1907
|
+
writeFileSync3(join6(root, "automations", "hello", "index.test.ts"), EXAMPLE_TEST);
|
|
1671
1908
|
const next = [
|
|
1672
1909
|
"cd " + name,
|
|
1673
1910
|
"pnpm install",
|
|
@@ -1688,18 +1925,18 @@ next:
|
|
|
1688
1925
|
}
|
|
1689
1926
|
|
|
1690
1927
|
// packages/cli/src/commands/replay.ts
|
|
1691
|
-
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, existsSync as
|
|
1692
|
-
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";
|
|
1693
1930
|
async function replay(out, api2, projectRoot, runId) {
|
|
1694
1931
|
const { replay: run } = await api2.get(`/runs/${runId}/replay`);
|
|
1695
|
-
const dir =
|
|
1696
|
-
if (!
|
|
1932
|
+
const dir = join7(projectRoot, "automations", run.automation_id);
|
|
1933
|
+
if (!existsSync7(dir)) {
|
|
1697
1934
|
throw new CliError(EXIT.NOT_FOUND, `automation directory not found locally: automations/${run.automation_id}`);
|
|
1698
1935
|
}
|
|
1699
1936
|
const shortId = run.id.slice(0, 8);
|
|
1700
|
-
const fixtureDir =
|
|
1937
|
+
const fixtureDir = join7(dir, "__replays__");
|
|
1701
1938
|
mkdirSync4(fixtureDir, { recursive: true });
|
|
1702
|
-
const fixturePath =
|
|
1939
|
+
const fixturePath = join7(fixtureDir, `${shortId}.replay.json`);
|
|
1703
1940
|
writeFileSync4(
|
|
1704
1941
|
fixturePath,
|
|
1705
1942
|
JSON.stringify(
|
|
@@ -1717,7 +1954,7 @@ async function replay(out, api2, projectRoot, runId) {
|
|
|
1717
1954
|
2
|
|
1718
1955
|
) + "\n"
|
|
1719
1956
|
);
|
|
1720
|
-
const testPath =
|
|
1957
|
+
const testPath = join7(dir, `replay-${shortId}.test.ts`);
|
|
1721
1958
|
writeFileSync4(
|
|
1722
1959
|
testPath,
|
|
1723
1960
|
`// Regression frozen from run ${run.id} (recorded status: ${run.status}).
|
|
@@ -1751,8 +1988,8 @@ run \`tesser test\` to execute it`
|
|
|
1751
1988
|
|
|
1752
1989
|
// packages/cli/src/commands/test.ts
|
|
1753
1990
|
import { execFile } from "node:child_process";
|
|
1754
|
-
import { existsSync as
|
|
1755
|
-
import { join as
|
|
1991
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
1992
|
+
import { join as join8 } from "node:path";
|
|
1756
1993
|
import { promisify } from "node:util";
|
|
1757
1994
|
|
|
1758
1995
|
// packages/testing/src/engine.ts
|
|
@@ -1971,7 +2208,7 @@ async function executeAutomation(def, opts = {}) {
|
|
|
1971
2208
|
`side effects must live inside a step (ADR-0002) \u2014 wrap the call: ctx.step("name", () => ctx.connections.${fullPath}(...))`
|
|
1972
2209
|
);
|
|
1973
2210
|
}
|
|
1974
|
-
const
|
|
2211
|
+
const input3 = await validateSchema(
|
|
1975
2212
|
actionDef.input,
|
|
1976
2213
|
rawInput ?? {},
|
|
1977
2214
|
`${fullPath} input`
|
|
@@ -1986,7 +2223,7 @@ async function executeAutomation(def, opts = {}) {
|
|
|
1986
2223
|
for (const seg of path) node = node?.[seg];
|
|
1987
2224
|
if (node !== void 0) {
|
|
1988
2225
|
return {
|
|
1989
|
-
value: typeof node === "function" ? await node(
|
|
2226
|
+
value: typeof node === "function" ? await node(input3) : node,
|
|
1990
2227
|
validate: false
|
|
1991
2228
|
};
|
|
1992
2229
|
}
|
|
@@ -1995,7 +2232,7 @@ async function executeAutomation(def, opts = {}) {
|
|
|
1995
2232
|
if (stepMock !== void 0) {
|
|
1996
2233
|
return {
|
|
1997
2234
|
value: typeof stepMock === "function" ? await stepMock(
|
|
1998
|
-
|
|
2235
|
+
input3,
|
|
1999
2236
|
{ action: actionPath, connection: connKey }
|
|
2000
2237
|
) : stepMock,
|
|
2001
2238
|
validate: false
|
|
@@ -2013,16 +2250,16 @@ async function executeAutomation(def, opts = {}) {
|
|
|
2013
2250
|
try {
|
|
2014
2251
|
const { value, validate } = await resolve();
|
|
2015
2252
|
const result = validate ? await validateSchema(actionDef.output, value, `${fullPath} sample output`) : value;
|
|
2016
|
-
const record = { args: [
|
|
2253
|
+
const record = { args: [input3], step: step.name, action: fullPath, result };
|
|
2017
2254
|
recordCall(spyFor(step.name), record);
|
|
2018
2255
|
recordCall(spyFor(fullPath), record);
|
|
2019
|
-
connectorCalls.push({ step: step.name, action: fullPath, input:
|
|
2256
|
+
connectorCalls.push({ step: step.name, action: fullPath, input: input3 });
|
|
2020
2257
|
return result;
|
|
2021
2258
|
} catch (err) {
|
|
2022
|
-
const record = { args: [
|
|
2259
|
+
const record = { args: [input3], step: step.name, action: fullPath, error: String(err) };
|
|
2023
2260
|
recordCall(spyFor(step.name), record);
|
|
2024
2261
|
recordCall(spyFor(fullPath), record);
|
|
2025
|
-
connectorCalls.push({ step: step.name, action: fullPath, input:
|
|
2262
|
+
connectorCalls.push({ step: step.name, action: fullPath, input: input3 });
|
|
2026
2263
|
throw err;
|
|
2027
2264
|
}
|
|
2028
2265
|
});
|
|
@@ -2187,7 +2424,7 @@ async function executeAutomation(def, opts = {}) {
|
|
|
2187
2424
|
recordCall(spyFor(`harness.${harnessKey}`), { args: [request], step: step.name, result });
|
|
2188
2425
|
return result;
|
|
2189
2426
|
});
|
|
2190
|
-
let
|
|
2427
|
+
let input2 = opts.input;
|
|
2191
2428
|
const trigger = def.trigger;
|
|
2192
2429
|
let inputSchema = def.input ?? (trigger.kind === "webhook" ? trigger.input : trigger.kind === "event" ? trigger.event?.schema : void 0);
|
|
2193
2430
|
if (trigger.kind === "connector" && trigger.connectorId !== void 0) {
|
|
@@ -2197,16 +2434,16 @@ async function executeAutomation(def, opts = {}) {
|
|
|
2197
2434
|
const decl = connector?.__connector.triggers?.[trigger.triggerId ?? ""];
|
|
2198
2435
|
if (decl) {
|
|
2199
2436
|
inputSchema = def.input ?? decl.output;
|
|
2200
|
-
if (
|
|
2201
|
-
|
|
2437
|
+
if (input2 === void 0) {
|
|
2438
|
+
input2 = connector?.__connector.samples?.[`trigger:${trigger.triggerId}`];
|
|
2202
2439
|
}
|
|
2203
2440
|
}
|
|
2204
2441
|
}
|
|
2205
|
-
if (
|
|
2206
|
-
|
|
2442
|
+
if (input2 === void 0 && inputSchema && trigger.kind !== "schedule") {
|
|
2443
|
+
input2 = await sampleFromSchema(inputSchema);
|
|
2207
2444
|
}
|
|
2208
|
-
if (inputSchema &&
|
|
2209
|
-
|
|
2445
|
+
if (inputSchema && input2 !== void 0) {
|
|
2446
|
+
input2 = await validateSchema(inputSchema, input2, `automation "${def.id}" input`);
|
|
2210
2447
|
}
|
|
2211
2448
|
const finish = (status, result, error) => {
|
|
2212
2449
|
const stepsByName = {};
|
|
@@ -2249,11 +2486,11 @@ async function executeAutomation(def, opts = {}) {
|
|
|
2249
2486
|
};
|
|
2250
2487
|
};
|
|
2251
2488
|
try {
|
|
2252
|
-
let
|
|
2489
|
+
let output2 = await def.run(input2, ctx);
|
|
2253
2490
|
if (def.output) {
|
|
2254
|
-
|
|
2491
|
+
output2 = await validateSchema(def.output, output2, `automation "${def.id}" output`);
|
|
2255
2492
|
}
|
|
2256
|
-
return finish("completed",
|
|
2493
|
+
return finish("completed", output2);
|
|
2257
2494
|
} catch (err) {
|
|
2258
2495
|
for (const item of [...undoStack].reverse()) {
|
|
2259
2496
|
try {
|
|
@@ -2328,7 +2565,7 @@ async function smokeModelScripts(def) {
|
|
|
2328
2565
|
}
|
|
2329
2566
|
|
|
2330
2567
|
// packages/testing/src/cassette.ts
|
|
2331
|
-
import { mkdirSync as mkdirSync5, readFileSync as
|
|
2568
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2332
2569
|
import { dirname as dirname3 } from "node:path";
|
|
2333
2570
|
import { createHash } from "node:crypto";
|
|
2334
2571
|
|
|
@@ -2337,9 +2574,9 @@ var exec = promisify(execFile);
|
|
|
2337
2574
|
function findVitest(projectRoot) {
|
|
2338
2575
|
let dir = projectRoot;
|
|
2339
2576
|
for (; ; ) {
|
|
2340
|
-
const bin =
|
|
2341
|
-
if (
|
|
2342
|
-
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, "..");
|
|
2343
2580
|
if (parent === dir) return null;
|
|
2344
2581
|
dir = parent;
|
|
2345
2582
|
}
|
|
@@ -2349,7 +2586,7 @@ async function runTests(out, projectRoot, opts) {
|
|
|
2349
2586
|
(a) => opts.filter === void 0 || a.automationId === opts.filter
|
|
2350
2587
|
);
|
|
2351
2588
|
if (automations.length === 0) {
|
|
2352
|
-
throw new CliError(EXIT.USAGE, `no automations found under ${
|
|
2589
|
+
throw new CliError(EXIT.USAGE, `no automations found under ${join8(projectRoot, "automations")}`);
|
|
2353
2590
|
}
|
|
2354
2591
|
const report = {
|
|
2355
2592
|
passed: true,
|
|
@@ -2436,13 +2673,309 @@ async function runTests(out, projectRoot, opts) {
|
|
|
2436
2673
|
process.exit(report.passed ? EXIT.OK : EXIT.TESTS_FAILED);
|
|
2437
2674
|
}
|
|
2438
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
|
+
|
|
2439
2961
|
// packages/cli/src/index.ts
|
|
2440
2962
|
var exec2 = promisify2(execFile2);
|
|
2441
2963
|
var program = new Command();
|
|
2442
|
-
var VERSION = JSON.parse(
|
|
2964
|
+
var VERSION = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
2443
2965
|
function setup() {
|
|
2444
2966
|
const opts = program.opts();
|
|
2445
|
-
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";
|
|
2446
2979
|
}
|
|
2447
2980
|
function target(opts) {
|
|
2448
2981
|
return resolveTarget({
|
|
@@ -2462,8 +2995,30 @@ function requireProject(opts) {
|
|
|
2462
2995
|
}
|
|
2463
2996
|
return { name: t.project, root: t.projectRoot };
|
|
2464
2997
|
}
|
|
2465
|
-
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")
|
|
2466
|
-
|
|
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) => {
|
|
2467
3022
|
const { out } = setup();
|
|
2468
3023
|
try {
|
|
2469
3024
|
init(out, name, cmdOpts, VERSION);
|
|
@@ -2479,18 +3034,17 @@ program.command("upgrade").description("pin Tesser packages to this CLI version
|
|
|
2479
3034
|
toExit(err, out);
|
|
2480
3035
|
}
|
|
2481
3036
|
});
|
|
2482
|
-
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) => {
|
|
2483
3038
|
const { out, opts } = setup();
|
|
2484
3039
|
try {
|
|
2485
|
-
const token = cmdOpts
|
|
2486
|
-
|
|
2487
|
-
const client = new ApiClient(cmdOpts.instance, token);
|
|
3040
|
+
const { instance, token } = await resolveLoginInputs(cmdOpts, opts);
|
|
3041
|
+
const client = new ApiClient(instance, token);
|
|
2488
3042
|
await client.get("/health");
|
|
2489
3043
|
const config = readConfig();
|
|
2490
|
-
config.profiles = { ...config.profiles, [cmdOpts.saveProfile]: { url:
|
|
3044
|
+
config.profiles = { ...config.profiles, [cmdOpts.saveProfile]: { url: instance, token } };
|
|
2491
3045
|
config.current = cmdOpts.saveProfile;
|
|
2492
3046
|
writeConfig(config);
|
|
2493
|
-
out.data({ profile: cmdOpts.saveProfile, url:
|
|
3047
|
+
out.data({ profile: cmdOpts.saveProfile, url: instance }, () => `logged in to ${instance} (profile "${cmdOpts.saveProfile}")`);
|
|
2494
3048
|
} catch (err) {
|
|
2495
3049
|
toExit(err, out);
|
|
2496
3050
|
}
|
|
@@ -2567,7 +3121,8 @@ program.command("dev").option("--port <port>", "port for the local instance", "8
|
|
|
2567
3121
|
const { out, opts } = setup();
|
|
2568
3122
|
try {
|
|
2569
3123
|
const { name, root } = requireProject(opts);
|
|
2570
|
-
|
|
3124
|
+
const port = parseIntOption(cmdOpts.port, "--port", { min: 1, max: 65535 });
|
|
3125
|
+
await dev(out, root, name, { port, ...cmdOpts.watch === false ? { watch: false } : {} });
|
|
2571
3126
|
} catch (err) {
|
|
2572
3127
|
toExit(err, out);
|
|
2573
3128
|
}
|
|
@@ -2589,6 +3144,23 @@ auth.command("pi").requiredOption("--connect <urlOrToken>", "Tesser /connect/<to
|
|
|
2589
3144
|
toExit(err, out);
|
|
2590
3145
|
}
|
|
2591
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
|
+
});
|
|
2592
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) => {
|
|
2593
3165
|
const { out, opts } = setup();
|
|
2594
3166
|
try {
|
|
@@ -2606,20 +3178,22 @@ program.command("connect").option("--wait", "poll until the human completes the
|
|
|
2606
3178
|
out.data({ url: null, requirements: [] }, () => "nothing missing \u2014 all requirements are satisfied");
|
|
2607
3179
|
return;
|
|
2608
3180
|
}
|
|
2609
|
-
|
|
3181
|
+
if (!cmdOpts.wait) {
|
|
3182
|
+
out.data(minted, () => `open in a browser to connect:
|
|
2610
3183
|
${minted.url}`);
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
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;
|
|
2620
3194
|
}
|
|
3195
|
+
if (status.status === "expired") throw new CliError(EXIT.HALTED_CREDENTIALS, "connect link expired");
|
|
2621
3196
|
}
|
|
2622
|
-
process.exit(EXIT.HALTED_CREDENTIALS);
|
|
2623
3197
|
} catch (err) {
|
|
2624
3198
|
toExit(err, out);
|
|
2625
3199
|
}
|
|
@@ -2647,7 +3221,7 @@ secrets.command("set").argument("<name>").option("--value-stdin", "read the valu
|
|
|
2647
3221
|
const value = Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
|
|
2648
3222
|
if (value.length === 0) throw new CliError(EXIT.USAGE, "empty value on stdin");
|
|
2649
3223
|
await api(opts).put(`/secrets/${encodeURIComponent(name)}`, { value });
|
|
2650
|
-
out.data({ set: name }, () => `secret "${name}" set \u2713`);
|
|
3224
|
+
out.data({ set: name, changed: true }, () => `secret "${name}" set \u2713`);
|
|
2651
3225
|
} catch (err) {
|
|
2652
3226
|
toExit(err, out);
|
|
2653
3227
|
}
|
|
@@ -2655,53 +3229,62 @@ secrets.command("set").argument("<name>").option("--value-stdin", "read the valu
|
|
|
2655
3229
|
secrets.command("rm").argument("<name>").action(async (name) => {
|
|
2656
3230
|
const { out, opts } = setup();
|
|
2657
3231
|
try {
|
|
2658
|
-
|
|
3232
|
+
const result = await api(opts).delete(`/secrets/${encodeURIComponent(name)}`);
|
|
3233
|
+
out.data({ ...result, changed: result.deleted });
|
|
2659
3234
|
} catch (err) {
|
|
2660
3235
|
toExit(err, out);
|
|
2661
3236
|
}
|
|
2662
3237
|
});
|
|
2663
3238
|
var runs = program.command("runs").description("inspect and drive runs");
|
|
2664
|
-
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) => {
|
|
2665
3240
|
const { out, opts } = setup();
|
|
2666
3241
|
try {
|
|
2667
3242
|
const { name } = requireProject(opts);
|
|
2668
|
-
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) });
|
|
2669
3246
|
if (cmdOpts.automation) params.set("automation", cmdOpts.automation);
|
|
2670
3247
|
if (cmdOpts.status) params.set("status", cmdOpts.status);
|
|
2671
3248
|
const data = await api(opts).get(`/runs?${params}`);
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
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
|
+
});
|
|
2676
3255
|
} catch (err) {
|
|
2677
3256
|
toExit(err, out);
|
|
2678
3257
|
}
|
|
2679
3258
|
});
|
|
2680
|
-
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) => {
|
|
2681
3260
|
const { out, opts } = setup();
|
|
2682
3261
|
try {
|
|
2683
|
-
|
|
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) })}`));
|
|
2684
3265
|
} catch (err) {
|
|
2685
3266
|
toExit(err, out);
|
|
2686
3267
|
}
|
|
2687
3268
|
});
|
|
2688
|
-
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) => {
|
|
2689
3270
|
const { out, opts } = setup();
|
|
2690
3271
|
try {
|
|
2691
3272
|
const { name } = requireProject(opts);
|
|
2692
3273
|
const body = { project: name, automation, env: cmdOpts.env };
|
|
2693
|
-
|
|
3274
|
+
const input2 = await resolveJsonInput({ literal: cmdOpts.input, file: cmdOpts.inputFile, label: "input" });
|
|
3275
|
+
if (input2 !== void 0) body["input"] = input2;
|
|
2694
3276
|
out.data(await api(opts).post("/runs", body));
|
|
2695
3277
|
} catch (err) {
|
|
2696
3278
|
toExit(err, out);
|
|
2697
3279
|
}
|
|
2698
3280
|
});
|
|
2699
|
-
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) => {
|
|
2700
3282
|
const { out, opts } = setup();
|
|
2701
3283
|
try {
|
|
3284
|
+
const payload = await resolveJsonInput({ literal: cmdOpts.payload, file: cmdOpts.payloadFile, label: "payload" });
|
|
2702
3285
|
out.data(
|
|
2703
3286
|
await api(opts).post(`/runs/${runId}/signals/${encodeURIComponent(name)}`, {
|
|
2704
|
-
...
|
|
3287
|
+
...payload !== void 0 ? { payload } : {}
|
|
2705
3288
|
})
|
|
2706
3289
|
);
|
|
2707
3290
|
} catch (err) {
|
|
@@ -2711,28 +3294,43 @@ runs.command("signal").argument("<runId>").argument("<name>").option("--payload
|
|
|
2711
3294
|
runs.command("cancel").argument("<runId>").action(async (runId) => {
|
|
2712
3295
|
const { out, opts } = setup();
|
|
2713
3296
|
try {
|
|
2714
|
-
|
|
3297
|
+
const result = await api(opts).post(`/runs/${runId}/cancel`);
|
|
3298
|
+
out.data({ ...result, changed: result.cancelled });
|
|
2715
3299
|
} catch (err) {
|
|
2716
3300
|
toExit(err, out);
|
|
2717
3301
|
}
|
|
2718
3302
|
});
|
|
2719
|
-
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) => {
|
|
2720
3304
|
const { out, opts } = setup();
|
|
2721
3305
|
try {
|
|
2722
3306
|
const client = api(opts);
|
|
2723
|
-
|
|
3307
|
+
const limit = parseIntOption(cmdOpts.limit, "--limit", { min: 1, max: 500 });
|
|
3308
|
+
let offset = parseIntOption(cmdOpts.offset, "--offset", { min: 0 });
|
|
3309
|
+
let finalDetail = null;
|
|
2724
3310
|
for (; ; ) {
|
|
2725
|
-
const detail = await client.get(`/runs/${runId}`);
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
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
|
+
}
|
|
2730
3320
|
return;
|
|
2731
3321
|
}
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
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}`);
|
|
2736
3334
|
return;
|
|
2737
3335
|
}
|
|
2738
3336
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
@@ -2754,13 +3352,22 @@ program.command("rollback").argument("<automation>").requiredOption("--to <versi
|
|
|
2754
3352
|
const { out, opts } = setup();
|
|
2755
3353
|
try {
|
|
2756
3354
|
const { name } = requireProject(opts);
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
);
|
|
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 });
|
|
2764
3371
|
} catch (err) {
|
|
2765
3372
|
toExit(err, out);
|
|
2766
3373
|
}
|
|
@@ -2780,7 +3387,17 @@ program.command("status").description("instance + project deploy status").action
|
|
|
2780
3387
|
}
|
|
2781
3388
|
});
|
|
2782
3389
|
program.parseAsync().catch((err) => {
|
|
2783
|
-
|
|
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
|
+
}
|
|
2784
3395
|
toExit(err, out);
|
|
2785
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
|
+
}
|
|
2786
3403
|
//# sourceMappingURL=index.js.map
|