@devosurf/tesser 0.1.0-alpha.0 → 0.1.0-alpha.1
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 +478 -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,428 @@ 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
|
+
pi,
|
|
1535
|
+
resend,
|
|
1536
|
+
slack,
|
|
1537
|
+
} from "@devosurf/tesser-connectors";
|
|
1538
|
+
\`\`\`
|
|
1539
|
+
|
|
1540
|
+
Only import Connectors that the Automation declares in \`connections: { ... }\`.
|
|
1541
|
+
|
|
1542
|
+
## Pattern
|
|
1543
|
+
|
|
1544
|
+
\`\`\`ts
|
|
1545
|
+
import { defineAutomation, onSchedule } from "@devosurf/tesser-sdk";
|
|
1546
|
+
import { github, slack } from "@devosurf/tesser-connectors";
|
|
1547
|
+
import { z } from "zod";
|
|
1548
|
+
|
|
1549
|
+
export default defineAutomation({
|
|
1550
|
+
id: "digest",
|
|
1551
|
+
trigger: onSchedule({ cron: "0 9 * * *", tz: "UTC" }),
|
|
1552
|
+
connections: { github, slack },
|
|
1553
|
+
output: z.object({ posted: z.boolean(), count: z.number() }),
|
|
1554
|
+
|
|
1555
|
+
run: async (_input, ctx) => {
|
|
1556
|
+
const issues = await ctx.step("fetch-open-issues", () =>
|
|
1557
|
+
ctx.connections.github.issues.list({ state: "open", labels: ["bug"] }),
|
|
1558
|
+
);
|
|
1559
|
+
|
|
1560
|
+
if (issues.length === 0) return { posted: false, count: 0 };
|
|
1561
|
+
|
|
1562
|
+
await ctx.step("post-to-slack", () =>
|
|
1563
|
+
ctx.connections.slack.chat.postMessage({ channel: "#ops", text: \`\${issues.length} bugs\` }),
|
|
1564
|
+
);
|
|
1565
|
+
|
|
1566
|
+
return { posted: true, count: issues.length };
|
|
1567
|
+
},
|
|
1568
|
+
});
|
|
1569
|
+
\`\`\`
|
|
1570
|
+
|
|
1571
|
+
## Common Actions
|
|
1572
|
+
|
|
1573
|
+
- \`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.
|
|
1574
|
+
- \`ctx.connections.github.issues.list({ repo?, state?, labels?, limit? })\`, \`issues.create({ repo, title, body?, labels? })\`, \`issues.comment({ repo, number, body })\`, \`repos.get({ repo })\`.
|
|
1575
|
+
- \`ctx.connections.slack.chat.postMessage({ channel, text, threadTs? })\` and related Slack chat/conversation/user Actions.
|
|
1576
|
+
- \`ctx.connections.resend.emails.send(...)\`, Gmail/Google Calendar/Docs/Drive/Sheets Actions, Anthropic model Actions, Claude Code/Pi Harness-related Connectors: inspect package types or examples before using.
|
|
1577
|
+
|
|
1578
|
+
## Connector triggers
|
|
1579
|
+
|
|
1580
|
+
Some Connectors expose typed triggers, e.g. GitHub issue triggers and Slack event triggers. Use the Connector's \`triggers\` constructors; do not hand-roll webhook delivery unless no Connector exists.
|
|
1581
|
+
|
|
1582
|
+
\`\`\`ts
|
|
1583
|
+
trigger: github.triggers.issueOpened({ repo: "owner/repo" })
|
|
1584
|
+
\`\`\`
|
|
1585
|
+
|
|
1586
|
+
## Safety rules
|
|
1587
|
+
|
|
1588
|
+
- Connector Actions are not automatically durable. The Automation author must wrap every Action call in \`ctx.step\`.
|
|
1589
|
+
- The imported Connector is not a token-bearing client. The authed client exists only at \`ctx.connections.<name>\` inside \`run\`.
|
|
1590
|
+
- For bespoke APIs, combine \`http\` with declared \`secrets: { ... }\`; do not put API keys in source, tests, logs, CLI args, or committed env files.
|
|
1591
|
+
- 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.
|
|
1592
|
+
`;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// packages/cli/src/commands/init.ts
|
|
1176
1596
|
var EXAMPLE_AUTOMATION = `import { defineAutomation, onWebhook } from "@devosurf/tesser-sdk";
|
|
1177
1597
|
import { z } from "zod";
|
|
1178
1598
|
|
|
@@ -1197,42 +1617,29 @@ test("greets by name", async () => {
|
|
|
1197
1617
|
expect(result).toEqual({ greeting: "hello, tesser!" });
|
|
1198
1618
|
});
|
|
1199
1619
|
`;
|
|
1200
|
-
function init(out, name, opts) {
|
|
1620
|
+
function init(out, name, opts, version) {
|
|
1201
1621
|
if (!/^[a-z][a-z0-9-]{0,63}$/.test(name)) {
|
|
1202
1622
|
throw new CliError(EXIT.USAGE, "project name must be kebab-case");
|
|
1203
1623
|
}
|
|
1204
|
-
const root =
|
|
1205
|
-
if (
|
|
1624
|
+
const root = join5(opts.dir ?? process.cwd(), name);
|
|
1625
|
+
if (existsSync5(join5(root, "tesser.json"))) {
|
|
1206
1626
|
throw new CliError(EXIT.CONFLICT, `${root} is already a Tesser project`);
|
|
1207
1627
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1628
|
+
mkdirSync3(join5(root, "automations", "hello"), { recursive: true });
|
|
1629
|
+
writeFileSync3(
|
|
1630
|
+
join5(root, "tesser.json"),
|
|
1211
1631
|
JSON.stringify({ project: name, ...opts.instance !== void 0 ? { instance: opts.instance } : {} }, null, 2) + "\n"
|
|
1212
1632
|
);
|
|
1213
|
-
|
|
1214
|
-
|
|
1633
|
+
writeFileSync3(
|
|
1634
|
+
join5(root, "package.json"),
|
|
1215
1635
|
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
|
-
},
|
|
1636
|
+
projectPackageJson(name, version),
|
|
1230
1637
|
null,
|
|
1231
1638
|
2
|
|
1232
1639
|
) + "\n"
|
|
1233
1640
|
);
|
|
1234
|
-
|
|
1235
|
-
|
|
1641
|
+
writeFileSync3(
|
|
1642
|
+
join5(root, "tsconfig.json"),
|
|
1236
1643
|
JSON.stringify(
|
|
1237
1644
|
{
|
|
1238
1645
|
compilerOptions: {
|
|
@@ -1249,41 +1656,50 @@ function init(out, name, opts) {
|
|
|
1249
1656
|
2
|
|
1250
1657
|
) + "\n"
|
|
1251
1658
|
);
|
|
1252
|
-
|
|
1253
|
-
|
|
1659
|
+
writeFileSync3(
|
|
1660
|
+
join5(root, "vitest.config.ts"),
|
|
1254
1661
|
`import { defineConfig } from "vitest/config";
|
|
1255
1662
|
export default defineConfig({ test: { globals: true, include: ["automations/**/*.test.ts"] } });
|
|
1256
1663
|
`
|
|
1257
1664
|
);
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1665
|
+
writeFileSync3(join5(root, ".gitignore"), tesserGitignore());
|
|
1666
|
+
writeProjectAgentInstructions(root, name, version, { overwrite: true });
|
|
1667
|
+
const docs = writeTesserGeneratedDocs(root, name, version);
|
|
1668
|
+
writeFileSync3(join5(root, "automations", "hello", "index.ts"), EXAMPLE_AUTOMATION);
|
|
1669
|
+
writeFileSync3(join5(root, "automations", "hello", "index.test.ts"), EXAMPLE_TEST);
|
|
1670
|
+
const next = [
|
|
1671
|
+
"cd " + name,
|
|
1672
|
+
"pnpm install",
|
|
1673
|
+
"git init && git add -A && git commit -m init",
|
|
1674
|
+
"tesser link",
|
|
1675
|
+
"tesser test"
|
|
1676
|
+
];
|
|
1261
1677
|
out.data(
|
|
1262
|
-
{ created: root,
|
|
1678
|
+
{ created: root, tesserVersion: version, docs, next },
|
|
1263
1679
|
() => `created ${root}
|
|
1264
1680
|
next:
|
|
1265
1681
|
cd ${name}
|
|
1682
|
+
pnpm install # creates pnpm-lock.yaml; commit it
|
|
1266
1683
|
git init && git add -A && git commit -m init
|
|
1267
|
-
|
|
1268
|
-
tesser
|
|
1269
|
-
tesser test # green in milliseconds`
|
|
1684
|
+
tesser link # register on your instance
|
|
1685
|
+
tesser test # green in milliseconds`
|
|
1270
1686
|
);
|
|
1271
1687
|
}
|
|
1272
1688
|
|
|
1273
1689
|
// packages/cli/src/commands/replay.ts
|
|
1274
|
-
import { mkdirSync as
|
|
1275
|
-
import { join as
|
|
1690
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "node:fs";
|
|
1691
|
+
import { join as join6 } from "node:path";
|
|
1276
1692
|
async function replay(out, api2, projectRoot, runId) {
|
|
1277
1693
|
const { replay: run } = await api2.get(`/runs/${runId}/replay`);
|
|
1278
|
-
const dir =
|
|
1279
|
-
if (!
|
|
1694
|
+
const dir = join6(projectRoot, "automations", run.automation_id);
|
|
1695
|
+
if (!existsSync6(dir)) {
|
|
1280
1696
|
throw new CliError(EXIT.NOT_FOUND, `automation directory not found locally: automations/${run.automation_id}`);
|
|
1281
1697
|
}
|
|
1282
1698
|
const shortId = run.id.slice(0, 8);
|
|
1283
|
-
const fixtureDir =
|
|
1284
|
-
|
|
1285
|
-
const fixturePath =
|
|
1286
|
-
|
|
1699
|
+
const fixtureDir = join6(dir, "__replays__");
|
|
1700
|
+
mkdirSync4(fixtureDir, { recursive: true });
|
|
1701
|
+
const fixturePath = join6(fixtureDir, `${shortId}.replay.json`);
|
|
1702
|
+
writeFileSync4(
|
|
1287
1703
|
fixturePath,
|
|
1288
1704
|
JSON.stringify(
|
|
1289
1705
|
{
|
|
@@ -1300,8 +1716,8 @@ async function replay(out, api2, projectRoot, runId) {
|
|
|
1300
1716
|
2
|
|
1301
1717
|
) + "\n"
|
|
1302
1718
|
);
|
|
1303
|
-
const testPath =
|
|
1304
|
-
|
|
1719
|
+
const testPath = join6(dir, `replay-${shortId}.test.ts`);
|
|
1720
|
+
writeFileSync4(
|
|
1305
1721
|
testPath,
|
|
1306
1722
|
`// Regression frozen from run ${run.id} (recorded status: ${run.status}).
|
|
1307
1723
|
// Generated by \`tesser replay\` \u2014 adjust the final assertion once the bug is fixed.
|
|
@@ -1334,8 +1750,8 @@ run \`tesser test\` to execute it`
|
|
|
1334
1750
|
|
|
1335
1751
|
// packages/cli/src/commands/test.ts
|
|
1336
1752
|
import { execFile } from "node:child_process";
|
|
1337
|
-
import { existsSync as
|
|
1338
|
-
import { join as
|
|
1753
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
1754
|
+
import { join as join7 } from "node:path";
|
|
1339
1755
|
import { promisify } from "node:util";
|
|
1340
1756
|
|
|
1341
1757
|
// packages/testing/src/engine.ts
|
|
@@ -1911,7 +2327,7 @@ async function smokeModelScripts(def) {
|
|
|
1911
2327
|
}
|
|
1912
2328
|
|
|
1913
2329
|
// packages/testing/src/cassette.ts
|
|
1914
|
-
import { mkdirSync as
|
|
2330
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1915
2331
|
import { dirname as dirname3 } from "node:path";
|
|
1916
2332
|
import { createHash } from "node:crypto";
|
|
1917
2333
|
|
|
@@ -1920,9 +2336,9 @@ var exec = promisify(execFile);
|
|
|
1920
2336
|
function findVitest(projectRoot) {
|
|
1921
2337
|
let dir = projectRoot;
|
|
1922
2338
|
for (; ; ) {
|
|
1923
|
-
const bin =
|
|
1924
|
-
if (
|
|
1925
|
-
const parent =
|
|
2339
|
+
const bin = join7(dir, "node_modules", ".bin", process.platform === "win32" ? "vitest.cmd" : "vitest");
|
|
2340
|
+
if (existsSync7(bin)) return bin;
|
|
2341
|
+
const parent = join7(dir, "..");
|
|
1926
2342
|
if (parent === dir) return null;
|
|
1927
2343
|
dir = parent;
|
|
1928
2344
|
}
|
|
@@ -1932,7 +2348,7 @@ async function runTests(out, projectRoot, opts) {
|
|
|
1932
2348
|
(a) => opts.filter === void 0 || a.automationId === opts.filter
|
|
1933
2349
|
);
|
|
1934
2350
|
if (automations.length === 0) {
|
|
1935
|
-
throw new CliError(EXIT.USAGE, `no automations found under ${
|
|
2351
|
+
throw new CliError(EXIT.USAGE, `no automations found under ${join7(projectRoot, "automations")}`);
|
|
1936
2352
|
}
|
|
1937
2353
|
const report = {
|
|
1938
2354
|
passed: true,
|
|
@@ -2022,7 +2438,7 @@ async function runTests(out, projectRoot, opts) {
|
|
|
2022
2438
|
// packages/cli/src/index.ts
|
|
2023
2439
|
var exec2 = promisify2(execFile2);
|
|
2024
2440
|
var program = new Command();
|
|
2025
|
-
var VERSION = JSON.parse(
|
|
2441
|
+
var VERSION = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
2026
2442
|
function setup() {
|
|
2027
2443
|
const opts = program.opts();
|
|
2028
2444
|
return { out: new Output(opts.json ?? false), opts };
|
|
@@ -2049,7 +2465,15 @@ program.name("tesser").version(VERSION).description("Code-first, agent-native au
|
|
|
2049
2465
|
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
2466
|
const { out } = setup();
|
|
2051
2467
|
try {
|
|
2052
|
-
init(out, name, cmdOpts);
|
|
2468
|
+
init(out, name, cmdOpts, VERSION);
|
|
2469
|
+
} catch (err) {
|
|
2470
|
+
toExit(err, out);
|
|
2471
|
+
}
|
|
2472
|
+
});
|
|
2473
|
+
program.command("upgrade").description("pin Tesser packages to this CLI version and refresh generated Project docs").action(() => {
|
|
2474
|
+
const { out, opts } = setup();
|
|
2475
|
+
try {
|
|
2476
|
+
upgradeProject(out, requireProject(opts), VERSION);
|
|
2053
2477
|
} catch (err) {
|
|
2054
2478
|
toExit(err, out);
|
|
2055
2479
|
}
|