@devosurf/tesser 0.1.0-alpha.0 → 0.1.0-alpha.2
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 +3 -0
- package/dist/index.js +479 -54
- package/dist/index.js.map +4 -4
- package/package.json +3 -3
- package/src/commands/init.test.ts +87 -0
- package/src/commands/init.ts +15 -18
- package/src/commands/project-docs.ts +155 -0
- package/src/index.ts +14 -1
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ The interface both your agent and you drive everything through. Machine-first (A
|
|
|
10
10
|
# auth & link (agent lane: set TESSER_TOKEN and skip all prompts)
|
|
11
11
|
tesser login --instance URL --token tsk_… # verify + store a profile
|
|
12
12
|
tesser init <name> # scaffold a Project (one repo of automations)
|
|
13
|
+
tesser upgrade # pin Tesser packages to this CLI version + refresh .tesser/docs
|
|
13
14
|
tesser link [--repo URL] # register the Project on the instance
|
|
14
15
|
tesser status # instance health + deploy state
|
|
15
16
|
|
|
@@ -35,6 +36,8 @@ tesser logs <runId> [--follow]
|
|
|
35
36
|
Auth resolution: `--token` > `TESSER_TOKEN` > profile (`~/.config/tesser/config.json`).
|
|
36
37
|
Instance resolution: `--url` > `tesser.json` > `TESSER_URL` > profile > `http://localhost:8377`.
|
|
37
38
|
|
|
39
|
+
`init` pins all Tesser packages to the CLI's exact version and writes committed agent reference docs under `.tesser/docs/` plus a user-editable `AGENTS.md`. During alpha/beta, upgrade with the target CLI version so package pins and docs move together: `npx @devosurf/tesser@<version> upgrade`.
|
|
40
|
+
|
|
38
41
|
`tesser dev` spawns the separately-installed `tesser-server` binary (process boundary —
|
|
39
42
|
this Apache package never links the AGPL server). `tesser test` runs the project's own
|
|
40
43
|
test runner behind the scenes plus auto-smoke for untested automations; no third-party
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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 readFileSync4 } from "node:fs";
|
|
4
4
|
import { promisify as promisify2 } from "node:util";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
@@ -1171,8 +1171,429 @@ async function dev(out, projectRoot, project, opts) {
|
|
|
1171
1171
|
}
|
|
1172
1172
|
|
|
1173
1173
|
// packages/cli/src/commands/init.ts
|
|
1174
|
-
import { existsSync as
|
|
1174
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1175
|
+
import { join as join5 } from "node:path";
|
|
1176
|
+
|
|
1177
|
+
// packages/cli/src/commands/project-docs.ts
|
|
1178
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1175
1179
|
import { join as join4 } from "node:path";
|
|
1180
|
+
var TESSER_DEPENDENCIES = ["@devosurf/tesser-sdk", "@devosurf/tesser-connectors"];
|
|
1181
|
+
var TESSER_DEV_DEPENDENCIES = ["@devosurf/tesser", "@devosurf/tesser-server", "@devosurf/tesser-testing"];
|
|
1182
|
+
var GENERATED_DOC_PATHS = [
|
|
1183
|
+
".tesser/docs/manifest.json",
|
|
1184
|
+
".tesser/docs/README.md",
|
|
1185
|
+
".tesser/docs/cli.md",
|
|
1186
|
+
".tesser/docs/sdk.md",
|
|
1187
|
+
".tesser/docs/connectors.md"
|
|
1188
|
+
];
|
|
1189
|
+
function projectPackageJson(name, version) {
|
|
1190
|
+
return {
|
|
1191
|
+
name,
|
|
1192
|
+
private: true,
|
|
1193
|
+
type: "module",
|
|
1194
|
+
packageManager: "pnpm@9.12.0",
|
|
1195
|
+
scripts: { test: "tesser test", deploy: "tesser deploy", dev: "tesser dev" },
|
|
1196
|
+
dependencies: {
|
|
1197
|
+
"@devosurf/tesser-sdk": version,
|
|
1198
|
+
"@devosurf/tesser-connectors": version,
|
|
1199
|
+
zod: "^4"
|
|
1200
|
+
},
|
|
1201
|
+
devDependencies: {
|
|
1202
|
+
"@devosurf/tesser": version,
|
|
1203
|
+
"@devosurf/tesser-server": version,
|
|
1204
|
+
"@devosurf/tesser-testing": version,
|
|
1205
|
+
vitest: "^4"
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
function writeProjectAgentInstructions(root, projectName, version, opts) {
|
|
1210
|
+
const path = join4(root, "AGENTS.md");
|
|
1211
|
+
if (!opts.overwrite && existsSync4(path)) return false;
|
|
1212
|
+
writeFileSync2(path, projectAgentsMd(projectName, version));
|
|
1213
|
+
return true;
|
|
1214
|
+
}
|
|
1215
|
+
function writeTesserGeneratedDocs(root, projectName, version) {
|
|
1216
|
+
const docsDir = join4(root, ".tesser", "docs");
|
|
1217
|
+
mkdirSync2(docsDir, { recursive: true });
|
|
1218
|
+
const docs = {
|
|
1219
|
+
"manifest.json": JSON.stringify(
|
|
1220
|
+
{
|
|
1221
|
+
kind: "tesser-agent-docs",
|
|
1222
|
+
schemaVersion: 1,
|
|
1223
|
+
generatedBy: "@devosurf/tesser",
|
|
1224
|
+
tesserVersion: version,
|
|
1225
|
+
project: projectName,
|
|
1226
|
+
generatedFiles: GENERATED_DOC_PATHS.filter((path) => path !== ".tesser/docs/manifest.json")
|
|
1227
|
+
},
|
|
1228
|
+
null,
|
|
1229
|
+
2
|
|
1230
|
+
) + "\n",
|
|
1231
|
+
"README.md": docsReadmeMd(projectName, version),
|
|
1232
|
+
"cli.md": cliReferenceMd(version),
|
|
1233
|
+
"sdk.md": sdkReferenceMd(version),
|
|
1234
|
+
"connectors.md": connectorsReferenceMd(version)
|
|
1235
|
+
};
|
|
1236
|
+
for (const [file, content] of Object.entries(docs)) {
|
|
1237
|
+
writeFileSync2(join4(docsDir, file), content);
|
|
1238
|
+
}
|
|
1239
|
+
return [...GENERATED_DOC_PATHS];
|
|
1240
|
+
}
|
|
1241
|
+
function tesserGitignore() {
|
|
1242
|
+
return `node_modules/
|
|
1243
|
+
.env
|
|
1244
|
+
|
|
1245
|
+
# Tesser local runtime state from \`tesser dev\`; keep generated agent docs committed.
|
|
1246
|
+
.tesser/*
|
|
1247
|
+
!.tesser/
|
|
1248
|
+
!.tesser/docs/
|
|
1249
|
+
!.tesser/docs/**
|
|
1250
|
+
`;
|
|
1251
|
+
}
|
|
1252
|
+
function ensureTesserDocsGitignore(root) {
|
|
1253
|
+
const path = join4(root, ".gitignore");
|
|
1254
|
+
const block = `
|
|
1255
|
+
# Tesser local runtime state from \`tesser dev\`; keep generated agent docs committed.
|
|
1256
|
+
.tesser/*
|
|
1257
|
+
!.tesser/
|
|
1258
|
+
!.tesser/docs/
|
|
1259
|
+
!.tesser/docs/**
|
|
1260
|
+
`;
|
|
1261
|
+
if (!existsSync4(path)) {
|
|
1262
|
+
writeFileSync2(path, tesserGitignore());
|
|
1263
|
+
return true;
|
|
1264
|
+
}
|
|
1265
|
+
const existing = readFileSync2(path, "utf8");
|
|
1266
|
+
if (existing.includes("!.tesser/docs/**")) return false;
|
|
1267
|
+
writeFileSync2(path, existing.replace(/\s*$/, "\n") + block);
|
|
1268
|
+
return true;
|
|
1269
|
+
}
|
|
1270
|
+
function upgradeProject(out, project, version) {
|
|
1271
|
+
const packagePath = join4(project.root, "package.json");
|
|
1272
|
+
if (!existsSync4(packagePath)) {
|
|
1273
|
+
throw new CliError(EXIT.USAGE, "not inside a package-backed Tesser project (missing package.json)");
|
|
1274
|
+
}
|
|
1275
|
+
const pkg = JSON.parse(readFileSync2(packagePath, "utf8"));
|
|
1276
|
+
pinTesserPackages(pkg, version);
|
|
1277
|
+
writeFileSync2(packagePath, JSON.stringify(pkg, null, 2) + "\n");
|
|
1278
|
+
const docs = writeTesserGeneratedDocs(project.root, project.name, version);
|
|
1279
|
+
const gitignoreUpdated = ensureTesserDocsGitignore(project.root);
|
|
1280
|
+
const agentInstructionsCreated = writeProjectAgentInstructions(project.root, project.name, version, { overwrite: false });
|
|
1281
|
+
const next = [
|
|
1282
|
+
"pnpm install",
|
|
1283
|
+
"tesser test --json",
|
|
1284
|
+
'git add package.json pnpm-lock.yaml .gitignore AGENTS.md .tesser/docs && git commit -m "upgrade tesser"'
|
|
1285
|
+
];
|
|
1286
|
+
out.data(
|
|
1287
|
+
{ upgraded: project.root, tesserVersion: version, docs, packageJson: "package.json", gitignoreUpdated, agentInstructionsCreated, next },
|
|
1288
|
+
() => `upgraded ${project.root} to Tesser ${version}
|
|
1289
|
+
next:
|
|
1290
|
+
pnpm install
|
|
1291
|
+
tesser test --json
|
|
1292
|
+
git add package.json pnpm-lock.yaml .gitignore AGENTS.md .tesser/docs && git commit -m "upgrade tesser"`
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
function pinTesserPackages(pkg, version) {
|
|
1296
|
+
const dependencies = objectField(pkg, "dependencies");
|
|
1297
|
+
for (const name of TESSER_DEPENDENCIES) dependencies[name] = version;
|
|
1298
|
+
const devDependencies = objectField(pkg, "devDependencies");
|
|
1299
|
+
for (const name of TESSER_DEV_DEPENDENCIES) devDependencies[name] = version;
|
|
1300
|
+
}
|
|
1301
|
+
function objectField(pkg, key) {
|
|
1302
|
+
const existing = pkg[key];
|
|
1303
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) return existing;
|
|
1304
|
+
const next = {};
|
|
1305
|
+
pkg[key] = next;
|
|
1306
|
+
return next;
|
|
1307
|
+
}
|
|
1308
|
+
function projectAgentsMd(projectName, version) {
|
|
1309
|
+
return `# Working in this Tesser Project
|
|
1310
|
+
|
|
1311
|
+
This repository is a Tesser **Project**: one git repo of automations deployed to a Tesser **Instance**.
|
|
1312
|
+
|
|
1313
|
+
## Read first
|
|
1314
|
+
- [Generated Tesser reference](./.tesser/docs/README.md) \u2014 generated for Tesser ${version}.
|
|
1315
|
+
- [CLI reference](./.tesser/docs/cli.md) \u2014 use \`--json\` for machine-readable output.
|
|
1316
|
+
- [SDK reference](./.tesser/docs/sdk.md) \u2014 authoring pattern, \`ctx.step\`, tests.
|
|
1317
|
+
- [Connector reference](./.tesser/docs/connectors.md) \u2014 safe Connection and Secret usage.
|
|
1318
|
+
|
|
1319
|
+
## Project rules
|
|
1320
|
+
- Use Tesser terms: **Automation**, **Trigger**, **Step**, **Project**, **Instance**, **Connector**, **Connection**, **Secret**, **Credential broker**, **Replay**.
|
|
1321
|
+
- One Automation per directory under \`automations/\`; colocate \`*.test.ts\` with each Automation.
|
|
1322
|
+
- Every side effect and external call must be inside \`ctx.step("stable-name", async () => ...)\`.
|
|
1323
|
+
- Declare credentials statically with \`connections: { ... }\` and \`secrets: { ... }\`; never read or print secret values.
|
|
1324
|
+
- Run \`pnpm install\` after init/upgrade, commit \`pnpm-lock.yaml\`, and never commit \`node_modules/\`.
|
|
1325
|
+
- Before deploy, run \`tesser test --json\` (and usually \`tesser build --json\`).
|
|
1326
|
+
- Deploy from git. Commit generated Tesser docs under \`.tesser/docs/\`; local runtime state in other \`.tesser/*\` paths stays ignored.
|
|
1327
|
+
|
|
1328
|
+
## Upgrade rule
|
|
1329
|
+
To update this Project's pinned Tesser packages and generated references, run the target CLI version explicitly:
|
|
1330
|
+
|
|
1331
|
+
\`\`\`bash
|
|
1332
|
+
npx @devosurf/tesser@<version> upgrade
|
|
1333
|
+
pnpm install
|
|
1334
|
+
tesser test --json
|
|
1335
|
+
\`\`\`
|
|
1336
|
+
|
|
1337
|
+
Project name: \`${projectName}\`.
|
|
1338
|
+
`;
|
|
1339
|
+
}
|
|
1340
|
+
function docsReadmeMd(projectName, version) {
|
|
1341
|
+
return `# Tesser agent reference
|
|
1342
|
+
|
|
1343
|
+
Generated for Project \`${projectName}\` by \`@devosurf/tesser@${version}\`. Treat this directory as generated/vendor reference: do not hand-edit files under \`.tesser/docs/\`; customize local policy in \`AGENTS.md\`.
|
|
1344
|
+
|
|
1345
|
+
## Start here
|
|
1346
|
+
1. Read \`AGENTS.md\` for project-specific policy.
|
|
1347
|
+
2. Use [cli.md](./cli.md) for command sequences and safe secret handling.
|
|
1348
|
+
3. Use [sdk.md](./sdk.md) before editing an Automation.
|
|
1349
|
+
4. Use [connectors.md](./connectors.md) before adding a Connection or Secret.
|
|
1350
|
+
|
|
1351
|
+
## Version contract
|
|
1352
|
+
Tesser packages in \`package.json\` should be pinned exactly to \`${version}\`:
|
|
1353
|
+
|
|
1354
|
+
- \`@devosurf/tesser\`
|
|
1355
|
+
- \`@devosurf/tesser-sdk\`
|
|
1356
|
+
- \`@devosurf/tesser-connectors\`
|
|
1357
|
+
- \`@devosurf/tesser-testing\`
|
|
1358
|
+
- \`@devosurf/tesser-server\`
|
|
1359
|
+
|
|
1360
|
+
To upgrade, run the target CLI version so docs and packages move together:
|
|
1361
|
+
|
|
1362
|
+
\`\`\`bash
|
|
1363
|
+
npx @devosurf/tesser@<version> upgrade
|
|
1364
|
+
pnpm install
|
|
1365
|
+
tesser test --json
|
|
1366
|
+
\`\`\`
|
|
1367
|
+
|
|
1368
|
+
Commit \`package.json\`, \`pnpm-lock.yaml\`, \`.tesser/docs/\`, and any Automation/test changes together. Do not commit \`node_modules/\` or local runtime files such as \`.tesser/master.key\`.
|
|
1369
|
+
`;
|
|
1370
|
+
}
|
|
1371
|
+
function cliReferenceMd(version) {
|
|
1372
|
+
return `# Tesser CLI reference
|
|
1373
|
+
|
|
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.
|
|
1375
|
+
|
|
1376
|
+
## Install and invoke
|
|
1377
|
+
|
|
1378
|
+
\`\`\`bash
|
|
1379
|
+
npm install -g @devosurf/tesser@${version}
|
|
1380
|
+
tesser --version
|
|
1381
|
+
|
|
1382
|
+
# Or run an exact version without global install:
|
|
1383
|
+
npx @devosurf/tesser@${version} --version
|
|
1384
|
+
\`\`\`
|
|
1385
|
+
|
|
1386
|
+
## Common flow
|
|
1387
|
+
|
|
1388
|
+
\`\`\`bash
|
|
1389
|
+
tesser init my-project --instance https://tesser.example.com
|
|
1390
|
+
cd my-project
|
|
1391
|
+
pnpm install # creates pnpm-lock.yaml; commit it
|
|
1392
|
+
git init && git add -A && git commit -m init
|
|
1393
|
+
tesser login --instance https://tesser.example.com --token "$TESSER_TOKEN"
|
|
1394
|
+
tesser link --json # registers this Project on the Instance
|
|
1395
|
+
tesser test --json
|
|
1396
|
+
tesser build --json
|
|
1397
|
+
tesser deploy --json
|
|
1398
|
+
\`\`\`
|
|
1399
|
+
|
|
1400
|
+
## Commands agents commonly use
|
|
1401
|
+
|
|
1402
|
+
- \`tesser init <name> [--dir DIR] [--instance URL]\` \u2014 scaffold a Project.
|
|
1403
|
+
- \`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 --instance URL --token TOKEN\` \u2014 verify and store a profile. Prefer \`TESSER_TOKEN\` over pasting tokens into chat.
|
|
1405
|
+
- \`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
|
+
- \`tesser status --json\` \u2014 instance health and deploy state.
|
|
1407
|
+
- \`tesser test [--smoke-only] [--automation ID] --json\` \u2014 fast local tests plus generated smoke tests.
|
|
1408
|
+
- \`tesser build --json\` \u2014 bundle and statically extract manifests. Use before deploy when changing requirements.
|
|
1409
|
+
- \`tesser dev [--port N] [--no-watch]\` \u2014 local Instance with embedded Postgres under ignored \`.tesser/*\` state.
|
|
1410
|
+
- \`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
|
+
- \`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
|
+
- \`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 runs list|show|trigger|signal|cancel --json\`; \`tesser logs <runId> [--follow]\`; \`tesser replay <runId>\`.
|
|
1414
|
+
- \`tesser rollback <automation> --to <version> --json\` \u2014 alias re-point; no rebuild.
|
|
1415
|
+
|
|
1416
|
+
## Credential safety
|
|
1417
|
+
|
|
1418
|
+
- 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, or \`--value-stdin\`.
|
|
1420
|
+
- Treat connect links and status tokens as sensitive operational material.
|
|
1421
|
+
|
|
1422
|
+
## Deploy hygiene
|
|
1423
|
+
|
|
1424
|
+
- Run \`pnpm install\` after init/upgrade and commit \`pnpm-lock.yaml\`; the Instance installs from lockfiles for reproducible builds.
|
|
1425
|
+
- Do not commit \`node_modules/\`, \`.env\`, or local runtime state in \`.tesser/*\` outside \`.tesser/docs/\`.
|
|
1426
|
+
- Git is the source of truth. Commit and push before expecting a normal Instance deploy to see changes.
|
|
1427
|
+
`;
|
|
1428
|
+
}
|
|
1429
|
+
function sdkReferenceMd(version) {
|
|
1430
|
+
return `# Tesser SDK reference
|
|
1431
|
+
|
|
1432
|
+
Generated for \`@devosurf/tesser-sdk@${version}\`. Author ordinary async TypeScript; Tesser durability comes only from Steps.
|
|
1433
|
+
|
|
1434
|
+
## Minimal Automation
|
|
1435
|
+
|
|
1436
|
+
\`\`\`ts
|
|
1437
|
+
import { defineAutomation, onWebhook } from "@devosurf/tesser-sdk";
|
|
1438
|
+
import { z } from "zod";
|
|
1439
|
+
|
|
1440
|
+
export default defineAutomation({
|
|
1441
|
+
id: "hello",
|
|
1442
|
+
trigger: onWebhook({ input: z.object({ name: z.string().default("world") }) }),
|
|
1443
|
+
output: z.object({ greeting: z.string() }),
|
|
1444
|
+
|
|
1445
|
+
run: async (input, ctx) => {
|
|
1446
|
+
const greeting = await ctx.step("compose", async () => \`hello, \${input.name}!\`);
|
|
1447
|
+
return { greeting };
|
|
1448
|
+
},
|
|
1449
|
+
});
|
|
1450
|
+
\`\`\`
|
|
1451
|
+
|
|
1452
|
+
## Non-negotiable rules
|
|
1453
|
+
|
|
1454
|
+
- Use **Step**, not node/task/action, for durable checkpoints.
|
|
1455
|
+
- Every side effect, external I/O call, Connector Action, bespoke HTTP request, file/network operation, or secret-dependent operation must happen inside \`ctx.step(name, fn)\`.
|
|
1456
|
+
- Step names are stable API for the journal. Use short, descriptive names such as \`fetch-customer\`, \`post-to-slack\`, \`charge-card\`.
|
|
1457
|
+
- Step outputs are journaled and must be serializable. Return plain data, not functions, class instances, streams, sockets, or live handles.
|
|
1458
|
+
- An Automation declares credentials statically at the top level. Do not hide requirements inside \`run\`.
|
|
1459
|
+
- Do not access provider tokens directly. Runtime-injected \`ctx.connections.*\` and \`ctx.secrets.*\` are the only runtime credential surfaces.
|
|
1460
|
+
|
|
1461
|
+
## Triggers
|
|
1462
|
+
|
|
1463
|
+
\`\`\`ts
|
|
1464
|
+
import { onWebhook, onSchedule, onEvent } from "@devosurf/tesser-sdk";
|
|
1465
|
+
|
|
1466
|
+
const webhook = onWebhook({ input: schema, respond: "sync" });
|
|
1467
|
+
const schedule = onSchedule({ cron: "0 9 * * *", tz: "UTC" });
|
|
1468
|
+
const event = onEvent(myEvent);
|
|
1469
|
+
\`\`\`
|
|
1470
|
+
|
|
1471
|
+
Connector triggers are reached from the Connector import, for example \`github.triggers.issueOpened({ repo: "owner/repo" })\` when supported.
|
|
1472
|
+
|
|
1473
|
+
## Connections and Secrets
|
|
1474
|
+
|
|
1475
|
+
\`\`\`ts
|
|
1476
|
+
import { defineAutomation, onWebhook, secret } from "@devosurf/tesser-sdk";
|
|
1477
|
+
import { slack } from "@devosurf/tesser-connectors";
|
|
1478
|
+
import { z } from "zod";
|
|
1479
|
+
|
|
1480
|
+
export default defineAutomation({
|
|
1481
|
+
id: "notify",
|
|
1482
|
+
trigger: onWebhook({ input: z.object({ text: z.string() }) }),
|
|
1483
|
+
connections: { slack },
|
|
1484
|
+
secrets: { signingKey: secret({ describe: "Shared HMAC signing key" }) },
|
|
1485
|
+
output: z.object({ ok: z.boolean() }),
|
|
1486
|
+
|
|
1487
|
+
run: async (input, ctx) => {
|
|
1488
|
+
await ctx.step("post-message", () =>
|
|
1489
|
+
ctx.connections.slack.chat.postMessage({ channel: "#ops", text: input.text }),
|
|
1490
|
+
);
|
|
1491
|
+
return { ok: true };
|
|
1492
|
+
},
|
|
1493
|
+
});
|
|
1494
|
+
\`\`\`
|
|
1495
|
+
|
|
1496
|
+
Deploy halts if required Connections or Secrets are missing. The agent should surface the connect link to a human and poll status; it must not collect the secret value itself.
|
|
1497
|
+
|
|
1498
|
+
## Tests
|
|
1499
|
+
|
|
1500
|
+
Colocate tests beside each Automation.
|
|
1501
|
+
|
|
1502
|
+
\`\`\`ts
|
|
1503
|
+
import { createTest } from "@devosurf/tesser-testing";
|
|
1504
|
+
import automation from "./index";
|
|
1505
|
+
|
|
1506
|
+
test("greets by name", async () => {
|
|
1507
|
+
const t = createTest({ automation });
|
|
1508
|
+
const { result } = await t.run({ input: { name: "tesser" } });
|
|
1509
|
+
expect(result).toEqual({ greeting: "hello, tesser!" });
|
|
1510
|
+
});
|
|
1511
|
+
\`\`\`
|
|
1512
|
+
|
|
1513
|
+
For Connector calls, mock the Step result by Step name. Replay fixtures produced by \`tesser replay <runId>\` should become permanent regression tests.
|
|
1514
|
+
`;
|
|
1515
|
+
}
|
|
1516
|
+
function connectorsReferenceMd(version) {
|
|
1517
|
+
return `# Tesser Connector reference
|
|
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
|
+
`;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// packages/cli/src/commands/init.ts
|
|
1176
1597
|
var EXAMPLE_AUTOMATION = `import { defineAutomation, onWebhook } from "@devosurf/tesser-sdk";
|
|
1177
1598
|
import { z } from "zod";
|
|
1178
1599
|
|
|
@@ -1197,42 +1618,29 @@ test("greets by name", async () => {
|
|
|
1197
1618
|
expect(result).toEqual({ greeting: "hello, tesser!" });
|
|
1198
1619
|
});
|
|
1199
1620
|
`;
|
|
1200
|
-
function init(out, name, opts) {
|
|
1621
|
+
function init(out, name, opts, version) {
|
|
1201
1622
|
if (!/^[a-z][a-z0-9-]{0,63}$/.test(name)) {
|
|
1202
1623
|
throw new CliError(EXIT.USAGE, "project name must be kebab-case");
|
|
1203
1624
|
}
|
|
1204
|
-
const root =
|
|
1205
|
-
if (
|
|
1625
|
+
const root = join5(opts.dir ?? process.cwd(), name);
|
|
1626
|
+
if (existsSync5(join5(root, "tesser.json"))) {
|
|
1206
1627
|
throw new CliError(EXIT.CONFLICT, `${root} is already a Tesser project`);
|
|
1207
1628
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1629
|
+
mkdirSync3(join5(root, "automations", "hello"), { recursive: true });
|
|
1630
|
+
writeFileSync3(
|
|
1631
|
+
join5(root, "tesser.json"),
|
|
1211
1632
|
JSON.stringify({ project: name, ...opts.instance !== void 0 ? { instance: opts.instance } : {} }, null, 2) + "\n"
|
|
1212
1633
|
);
|
|
1213
|
-
|
|
1214
|
-
|
|
1634
|
+
writeFileSync3(
|
|
1635
|
+
join5(root, "package.json"),
|
|
1215
1636
|
JSON.stringify(
|
|
1216
|
-
|
|
1217
|
-
name,
|
|
1218
|
-
private: true,
|
|
1219
|
-
type: "module",
|
|
1220
|
-
packageManager: "pnpm@9.12.0",
|
|
1221
|
-
scripts: { test: "tesser test", deploy: "tesser deploy", dev: "tesser dev" },
|
|
1222
|
-
dependencies: { "@devosurf/tesser-sdk": "latest", "@devosurf/tesser-connectors": "latest", zod: "^4" },
|
|
1223
|
-
devDependencies: {
|
|
1224
|
-
"@devosurf/tesser": "latest",
|
|
1225
|
-
"@devosurf/tesser-server": "latest",
|
|
1226
|
-
"@devosurf/tesser-testing": "latest",
|
|
1227
|
-
vitest: "^4"
|
|
1228
|
-
}
|
|
1229
|
-
},
|
|
1637
|
+
projectPackageJson(name, version),
|
|
1230
1638
|
null,
|
|
1231
1639
|
2
|
|
1232
1640
|
) + "\n"
|
|
1233
1641
|
);
|
|
1234
|
-
|
|
1235
|
-
|
|
1642
|
+
writeFileSync3(
|
|
1643
|
+
join5(root, "tsconfig.json"),
|
|
1236
1644
|
JSON.stringify(
|
|
1237
1645
|
{
|
|
1238
1646
|
compilerOptions: {
|
|
@@ -1249,41 +1657,50 @@ function init(out, name, opts) {
|
|
|
1249
1657
|
2
|
|
1250
1658
|
) + "\n"
|
|
1251
1659
|
);
|
|
1252
|
-
|
|
1253
|
-
|
|
1660
|
+
writeFileSync3(
|
|
1661
|
+
join5(root, "vitest.config.ts"),
|
|
1254
1662
|
`import { defineConfig } from "vitest/config";
|
|
1255
1663
|
export default defineConfig({ test: { globals: true, include: ["automations/**/*.test.ts"] } });
|
|
1256
1664
|
`
|
|
1257
1665
|
);
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1666
|
+
writeFileSync3(join5(root, ".gitignore"), tesserGitignore());
|
|
1667
|
+
writeProjectAgentInstructions(root, name, version, { overwrite: true });
|
|
1668
|
+
const docs = writeTesserGeneratedDocs(root, name, version);
|
|
1669
|
+
writeFileSync3(join5(root, "automations", "hello", "index.ts"), EXAMPLE_AUTOMATION);
|
|
1670
|
+
writeFileSync3(join5(root, "automations", "hello", "index.test.ts"), EXAMPLE_TEST);
|
|
1671
|
+
const next = [
|
|
1672
|
+
"cd " + name,
|
|
1673
|
+
"pnpm install",
|
|
1674
|
+
"git init && git add -A && git commit -m init",
|
|
1675
|
+
"tesser link",
|
|
1676
|
+
"tesser test"
|
|
1677
|
+
];
|
|
1261
1678
|
out.data(
|
|
1262
|
-
{ created: root,
|
|
1679
|
+
{ created: root, tesserVersion: version, docs, next },
|
|
1263
1680
|
() => `created ${root}
|
|
1264
1681
|
next:
|
|
1265
1682
|
cd ${name}
|
|
1683
|
+
pnpm install # creates pnpm-lock.yaml; commit it
|
|
1266
1684
|
git init && git add -A && git commit -m init
|
|
1267
|
-
|
|
1268
|
-
tesser
|
|
1269
|
-
tesser test # green in milliseconds`
|
|
1685
|
+
tesser link # register on your instance
|
|
1686
|
+
tesser test # green in milliseconds`
|
|
1270
1687
|
);
|
|
1271
1688
|
}
|
|
1272
1689
|
|
|
1273
1690
|
// packages/cli/src/commands/replay.ts
|
|
1274
|
-
import { mkdirSync as
|
|
1275
|
-
import { join as
|
|
1691
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "node:fs";
|
|
1692
|
+
import { join as join6 } from "node:path";
|
|
1276
1693
|
async function replay(out, api2, projectRoot, runId) {
|
|
1277
1694
|
const { replay: run } = await api2.get(`/runs/${runId}/replay`);
|
|
1278
|
-
const dir =
|
|
1279
|
-
if (!
|
|
1695
|
+
const dir = join6(projectRoot, "automations", run.automation_id);
|
|
1696
|
+
if (!existsSync6(dir)) {
|
|
1280
1697
|
throw new CliError(EXIT.NOT_FOUND, `automation directory not found locally: automations/${run.automation_id}`);
|
|
1281
1698
|
}
|
|
1282
1699
|
const shortId = run.id.slice(0, 8);
|
|
1283
|
-
const fixtureDir =
|
|
1284
|
-
|
|
1285
|
-
const fixturePath =
|
|
1286
|
-
|
|
1700
|
+
const fixtureDir = join6(dir, "__replays__");
|
|
1701
|
+
mkdirSync4(fixtureDir, { recursive: true });
|
|
1702
|
+
const fixturePath = join6(fixtureDir, `${shortId}.replay.json`);
|
|
1703
|
+
writeFileSync4(
|
|
1287
1704
|
fixturePath,
|
|
1288
1705
|
JSON.stringify(
|
|
1289
1706
|
{
|
|
@@ -1300,8 +1717,8 @@ async function replay(out, api2, projectRoot, runId) {
|
|
|
1300
1717
|
2
|
|
1301
1718
|
) + "\n"
|
|
1302
1719
|
);
|
|
1303
|
-
const testPath =
|
|
1304
|
-
|
|
1720
|
+
const testPath = join6(dir, `replay-${shortId}.test.ts`);
|
|
1721
|
+
writeFileSync4(
|
|
1305
1722
|
testPath,
|
|
1306
1723
|
`// Regression frozen from run ${run.id} (recorded status: ${run.status}).
|
|
1307
1724
|
// Generated by \`tesser replay\` \u2014 adjust the final assertion once the bug is fixed.
|
|
@@ -1334,8 +1751,8 @@ run \`tesser test\` to execute it`
|
|
|
1334
1751
|
|
|
1335
1752
|
// packages/cli/src/commands/test.ts
|
|
1336
1753
|
import { execFile } from "node:child_process";
|
|
1337
|
-
import { existsSync as
|
|
1338
|
-
import { join as
|
|
1754
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
1755
|
+
import { join as join7 } from "node:path";
|
|
1339
1756
|
import { promisify } from "node:util";
|
|
1340
1757
|
|
|
1341
1758
|
// packages/testing/src/engine.ts
|
|
@@ -1911,7 +2328,7 @@ async function smokeModelScripts(def) {
|
|
|
1911
2328
|
}
|
|
1912
2329
|
|
|
1913
2330
|
// packages/testing/src/cassette.ts
|
|
1914
|
-
import { mkdirSync as
|
|
2331
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1915
2332
|
import { dirname as dirname3 } from "node:path";
|
|
1916
2333
|
import { createHash } from "node:crypto";
|
|
1917
2334
|
|
|
@@ -1920,9 +2337,9 @@ var exec = promisify(execFile);
|
|
|
1920
2337
|
function findVitest(projectRoot) {
|
|
1921
2338
|
let dir = projectRoot;
|
|
1922
2339
|
for (; ; ) {
|
|
1923
|
-
const bin =
|
|
1924
|
-
if (
|
|
1925
|
-
const parent =
|
|
2340
|
+
const bin = join7(dir, "node_modules", ".bin", process.platform === "win32" ? "vitest.cmd" : "vitest");
|
|
2341
|
+
if (existsSync7(bin)) return bin;
|
|
2342
|
+
const parent = join7(dir, "..");
|
|
1926
2343
|
if (parent === dir) return null;
|
|
1927
2344
|
dir = parent;
|
|
1928
2345
|
}
|
|
@@ -1932,7 +2349,7 @@ async function runTests(out, projectRoot, opts) {
|
|
|
1932
2349
|
(a) => opts.filter === void 0 || a.automationId === opts.filter
|
|
1933
2350
|
);
|
|
1934
2351
|
if (automations.length === 0) {
|
|
1935
|
-
throw new CliError(EXIT.USAGE, `no automations found under ${
|
|
2352
|
+
throw new CliError(EXIT.USAGE, `no automations found under ${join7(projectRoot, "automations")}`);
|
|
1936
2353
|
}
|
|
1937
2354
|
const report = {
|
|
1938
2355
|
passed: true,
|
|
@@ -2022,7 +2439,7 @@ async function runTests(out, projectRoot, opts) {
|
|
|
2022
2439
|
// packages/cli/src/index.ts
|
|
2023
2440
|
var exec2 = promisify2(execFile2);
|
|
2024
2441
|
var program = new Command();
|
|
2025
|
-
var VERSION = JSON.parse(
|
|
2442
|
+
var VERSION = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
2026
2443
|
function setup() {
|
|
2027
2444
|
const opts = program.opts();
|
|
2028
2445
|
return { out: new Output(opts.json ?? false), opts };
|
|
@@ -2049,7 +2466,15 @@ program.name("tesser").version(VERSION).description("Code-first, agent-native au
|
|
|
2049
2466
|
program.command("init").argument("<name>", "project name (kebab-case)").option("--dir <dir>", "parent directory").option("--instance <url>", "instance URL to write into tesser.json").description("scaffold a new Project (one repo of automations)").action((name, cmdOpts) => {
|
|
2050
2467
|
const { out } = setup();
|
|
2051
2468
|
try {
|
|
2052
|
-
init(out, name, cmdOpts);
|
|
2469
|
+
init(out, name, cmdOpts, VERSION);
|
|
2470
|
+
} catch (err) {
|
|
2471
|
+
toExit(err, out);
|
|
2472
|
+
}
|
|
2473
|
+
});
|
|
2474
|
+
program.command("upgrade").description("pin Tesser packages to this CLI version and refresh generated Project docs").action(() => {
|
|
2475
|
+
const { out, opts } = setup();
|
|
2476
|
+
try {
|
|
2477
|
+
upgradeProject(out, requireProject(opts), VERSION);
|
|
2053
2478
|
} catch (err) {
|
|
2054
2479
|
toExit(err, out);
|
|
2055
2480
|
}
|