@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devosurf/tesser",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.1",
4
4
  "description": "Tesser CLI — the machine-first interface for agents and humans.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -19,8 +19,8 @@
19
19
  "dependencies": {
20
20
  "commander": "^15.0.0",
21
21
  "esbuild": "^0.28.1",
22
- "@devosurf/tesser-sdk": "0.1.0-alpha.0",
23
- "@devosurf/tesser-testing": "0.1.0-alpha.0"
22
+ "@devosurf/tesser-sdk": "0.1.0-alpha.1",
23
+ "@devosurf/tesser-testing": "0.1.0-alpha.1"
24
24
  },
25
25
  "main": "./dist/index.js",
26
26
  "types": "./src/index.ts",
@@ -0,0 +1,87 @@
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, test, vi } from "vitest";
5
+ import type { Output } from "../output.js";
6
+ import { init } from "./init.js";
7
+ import { upgradeProject } from "./project-docs.js";
8
+
9
+ const roots: string[] = [];
10
+
11
+ function tempRoot(): string {
12
+ const root = mkdtempSync(join(tmpdir(), "tesser-cli-init-"));
13
+ roots.push(root);
14
+ return root;
15
+ }
16
+
17
+ function out(): Output {
18
+ return { data: vi.fn(), log: vi.fn(), fail: vi.fn() } as unknown as Output;
19
+ }
20
+
21
+ function readJson<T>(path: string): T {
22
+ return JSON.parse(readFileSync(path, "utf8")) as T;
23
+ }
24
+
25
+ afterEach(() => {
26
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
27
+ });
28
+
29
+ describe("tesser init", () => {
30
+ test("pins Tesser packages and writes committed agent docs", () => {
31
+ const parent = tempRoot();
32
+ init(out(), "agent-docs", { dir: parent, instance: "https://tesser.example.com" }, "0.2.0-beta.1");
33
+
34
+ const root = join(parent, "agent-docs");
35
+ const pkg = readJson<{
36
+ dependencies: Record<string, string>;
37
+ devDependencies: Record<string, string>;
38
+ }>(join(root, "package.json"));
39
+
40
+ expect(pkg.dependencies["@devosurf/tesser-sdk"]).toBe("0.2.0-beta.1");
41
+ expect(pkg.dependencies["@devosurf/tesser-connectors"]).toBe("0.2.0-beta.1");
42
+ expect(pkg.devDependencies["@devosurf/tesser"]).toBe("0.2.0-beta.1");
43
+ expect(pkg.devDependencies["@devosurf/tesser-server"]).toBe("0.2.0-beta.1");
44
+ expect(pkg.devDependencies["@devosurf/tesser-testing"]).toBe("0.2.0-beta.1");
45
+
46
+ expect(readFileSync(join(root, ".gitignore"), "utf8")).toContain("!.tesser/docs/**");
47
+ expect(readFileSync(join(root, "AGENTS.md"), "utf8")).toContain("./.tesser/docs/README.md");
48
+ expect(readFileSync(join(root, ".tesser", "docs", "sdk.md"), "utf8")).toContain("ctx.step");
49
+ expect(readFileSync(join(root, ".tesser", "docs", "cli.md"), "utf8")).toContain("pnpm-lock.yaml");
50
+
51
+ const manifest = readJson<{ tesserVersion: string; project: string }>(join(root, ".tesser", "docs", "manifest.json"));
52
+ expect(manifest).toMatchObject({ tesserVersion: "0.2.0-beta.1", project: "agent-docs" });
53
+ });
54
+ });
55
+
56
+ describe("tesser upgrade", () => {
57
+ test("updates pins and generated docs without overwriting AGENTS.md", () => {
58
+ const parent = tempRoot();
59
+ init(out(), "upgrade-me", { dir: parent }, "0.1.0-alpha.0");
60
+ const root = join(parent, "upgrade-me");
61
+ writeFileSync(join(root, "AGENTS.md"), "# Custom project policy\n");
62
+
63
+ const pkg = readJson<{
64
+ dependencies: Record<string, string>;
65
+ devDependencies: Record<string, string>;
66
+ }>(join(root, "package.json"));
67
+ pkg.dependencies["@devosurf/tesser-sdk"] = "latest";
68
+ pkg.devDependencies["@devosurf/tesser"] = "latest";
69
+ writeFileSync(join(root, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
70
+
71
+ upgradeProject(out(), { name: "upgrade-me", root }, "0.2.0-beta.1");
72
+
73
+ const upgraded = readJson<{
74
+ dependencies: Record<string, string>;
75
+ devDependencies: Record<string, string>;
76
+ }>(join(root, "package.json"));
77
+ expect(upgraded.dependencies["@devosurf/tesser-sdk"]).toBe("0.2.0-beta.1");
78
+ expect(upgraded.dependencies["@devosurf/tesser-connectors"]).toBe("0.2.0-beta.1");
79
+ expect(upgraded.devDependencies["@devosurf/tesser"]).toBe("0.2.0-beta.1");
80
+ expect(upgraded.devDependencies["@devosurf/tesser-server"]).toBe("0.2.0-beta.1");
81
+ expect(upgraded.devDependencies["@devosurf/tesser-testing"]).toBe("0.2.0-beta.1");
82
+
83
+ expect(readFileSync(join(root, "AGENTS.md"), "utf8")).toBe("# Custom project policy\n");
84
+ const manifest = readJson<{ tesserVersion: string }>(join(root, ".tesser", "docs", "manifest.json"));
85
+ expect(manifest.tesserVersion).toBe("0.2.0-beta.1");
86
+ });
87
+ });
@@ -5,6 +5,7 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
  import { EXIT } from "../exit-codes.js";
7
7
  import { CliError, Output } from "../output.js";
8
+ import { projectPackageJson, tesserGitignore, writeProjectAgentInstructions, writeTesserGeneratedDocs } from "./project-docs.js";
8
9
 
9
10
  const EXAMPLE_AUTOMATION = `import { defineAutomation, onWebhook } from "@devosurf/tesser-sdk";
10
11
  import { z } from "zod";
@@ -32,7 +33,7 @@ test("greets by name", async () => {
32
33
  });
33
34
  `;
34
35
 
35
- export function init(out: Output, name: string, opts: { dir?: string | undefined; instance?: string | undefined }): void {
36
+ export function init(out: Output, name: string, opts: { dir?: string | undefined; instance?: string | undefined }, version: string): void {
36
37
  if (!/^[a-z][a-z0-9-]{0,63}$/.test(name)) {
37
38
  throw new CliError(EXIT.USAGE, "project name must be kebab-case");
38
39
  }
@@ -49,20 +50,7 @@ export function init(out: Output, name: string, opts: { dir?: string | undefined
49
50
  writeFileSync(
50
51
  join(root, "package.json"),
51
52
  JSON.stringify(
52
- {
53
- name,
54
- private: true,
55
- type: "module",
56
- packageManager: "pnpm@9.12.0",
57
- scripts: { test: "tesser test", deploy: "tesser deploy", dev: "tesser dev" },
58
- dependencies: { "@devosurf/tesser-sdk": "latest", "@devosurf/tesser-connectors": "latest", zod: "^4" },
59
- devDependencies: {
60
- "@devosurf/tesser": "latest",
61
- "@devosurf/tesser-server": "latest",
62
- "@devosurf/tesser-testing": "latest",
63
- vitest: "^4",
64
- },
65
- },
53
+ projectPackageJson(name, version),
66
54
  null,
67
55
  2,
68
56
  ) + "\n",
@@ -89,13 +77,22 @@ export function init(out: Output, name: string, opts: { dir?: string | undefined
89
77
  join(root, "vitest.config.ts"),
90
78
  `import { defineConfig } from "vitest/config";\nexport default defineConfig({ test: { globals: true, include: ["automations/**/*.test.ts"] } });\n`,
91
79
  );
92
- writeFileSync(join(root, ".gitignore"), "node_modules/\n.tesser/\n.env\n");
80
+ writeFileSync(join(root, ".gitignore"), tesserGitignore());
81
+ writeProjectAgentInstructions(root, name, version, { overwrite: true });
82
+ const docs = writeTesserGeneratedDocs(root, name, version);
93
83
  writeFileSync(join(root, "automations", "hello", "index.ts"), EXAMPLE_AUTOMATION);
94
84
  writeFileSync(join(root, "automations", "hello", "index.test.ts"), EXAMPLE_TEST);
95
85
 
86
+ const next = [
87
+ "cd " + name,
88
+ "pnpm install",
89
+ "git init && git add -A && git commit -m init",
90
+ "tesser link",
91
+ "tesser test",
92
+ ];
96
93
  out.data(
97
- { created: root, next: ["cd " + name, "git init && git add -A && git commit -m init", "npm install (or pnpm)", "tesser link", "tesser test"] },
94
+ { created: root, tesserVersion: version, docs, next },
98
95
  () =>
99
- `created ${root}\nnext:\n cd ${name}\n git init && git add -A && git commit -m init\n pnpm install\n tesser link # register on your instance\n tesser test # green in milliseconds`,
96
+ `created ${root}\nnext:\n cd ${name}\n pnpm install # creates pnpm-lock.yaml; commit it\n git init && git add -A && git commit -m init\n tesser link # register on your instance\n tesser test # green in milliseconds`,
100
97
  );
101
98
  }
@@ -0,0 +1,155 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { EXIT } from "../exit-codes.js";
4
+ import { CliError, Output } from "../output.js";
5
+
6
+ export const TESSER_DEPENDENCIES = ["@devosurf/tesser-sdk", "@devosurf/tesser-connectors"] as const;
7
+ export const TESSER_DEV_DEPENDENCIES = ["@devosurf/tesser", "@devosurf/tesser-server", "@devosurf/tesser-testing"] as const;
8
+
9
+ export const GENERATED_DOC_PATHS = [
10
+ ".tesser/docs/manifest.json",
11
+ ".tesser/docs/README.md",
12
+ ".tesser/docs/cli.md",
13
+ ".tesser/docs/sdk.md",
14
+ ".tesser/docs/connectors.md",
15
+ ] as const;
16
+
17
+ interface JsonObject {
18
+ [key: string]: unknown;
19
+ }
20
+
21
+ export function projectPackageJson(name: string, version: string): JsonObject {
22
+ return {
23
+ name,
24
+ private: true,
25
+ type: "module",
26
+ packageManager: "pnpm@9.12.0",
27
+ scripts: { test: "tesser test", deploy: "tesser deploy", dev: "tesser dev" },
28
+ dependencies: {
29
+ "@devosurf/tesser-sdk": version,
30
+ "@devosurf/tesser-connectors": version,
31
+ zod: "^4",
32
+ },
33
+ devDependencies: {
34
+ "@devosurf/tesser": version,
35
+ "@devosurf/tesser-server": version,
36
+ "@devosurf/tesser-testing": version,
37
+ vitest: "^4",
38
+ },
39
+ };
40
+ }
41
+
42
+ export function writeProjectAgentInstructions(root: string, projectName: string, version: string, opts: { overwrite: boolean }): boolean {
43
+ const path = join(root, "AGENTS.md");
44
+ if (!opts.overwrite && existsSync(path)) return false;
45
+ writeFileSync(path, projectAgentsMd(projectName, version));
46
+ return true;
47
+ }
48
+
49
+ export function writeTesserGeneratedDocs(root: string, projectName: string, version: string): string[] {
50
+ const docsDir = join(root, ".tesser", "docs");
51
+ mkdirSync(docsDir, { recursive: true });
52
+ const docs: Record<string, string> = {
53
+ "manifest.json": JSON.stringify(
54
+ {
55
+ kind: "tesser-agent-docs",
56
+ schemaVersion: 1,
57
+ generatedBy: "@devosurf/tesser",
58
+ tesserVersion: version,
59
+ project: projectName,
60
+ generatedFiles: GENERATED_DOC_PATHS.filter((path) => path !== ".tesser/docs/manifest.json"),
61
+ },
62
+ null,
63
+ 2,
64
+ ) + "\n",
65
+ "README.md": docsReadmeMd(projectName, version),
66
+ "cli.md": cliReferenceMd(version),
67
+ "sdk.md": sdkReferenceMd(version),
68
+ "connectors.md": connectorsReferenceMd(version),
69
+ };
70
+ for (const [file, content] of Object.entries(docs)) {
71
+ writeFileSync(join(docsDir, file), content);
72
+ }
73
+ return [...GENERATED_DOC_PATHS];
74
+ }
75
+
76
+ export function tesserGitignore(): string {
77
+ return `node_modules/\n.env\n\n# Tesser local runtime state from \`tesser dev\`; keep generated agent docs committed.\n.tesser/*\n!.tesser/\n!.tesser/docs/\n!.tesser/docs/**\n`;
78
+ }
79
+
80
+ export function ensureTesserDocsGitignore(root: string): boolean {
81
+ const path = join(root, ".gitignore");
82
+ const block = `\n# Tesser local runtime state from \`tesser dev\`; keep generated agent docs committed.\n.tesser/*\n!.tesser/\n!.tesser/docs/\n!.tesser/docs/**\n`;
83
+ if (!existsSync(path)) {
84
+ writeFileSync(path, tesserGitignore());
85
+ return true;
86
+ }
87
+ const existing = readFileSync(path, "utf8");
88
+ if (existing.includes("!.tesser/docs/**")) return false;
89
+ writeFileSync(path, existing.replace(/\s*$/, "\n") + block);
90
+ return true;
91
+ }
92
+
93
+ export function upgradeProject(
94
+ out: Output,
95
+ project: { name: string; root: string },
96
+ version: string,
97
+ ): void {
98
+ const packagePath = join(project.root, "package.json");
99
+ if (!existsSync(packagePath)) {
100
+ throw new CliError(EXIT.USAGE, "not inside a package-backed Tesser project (missing package.json)");
101
+ }
102
+ const pkg = JSON.parse(readFileSync(packagePath, "utf8")) as JsonObject;
103
+ pinTesserPackages(pkg, version);
104
+ writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + "\n");
105
+
106
+ const docs = writeTesserGeneratedDocs(project.root, project.name, version);
107
+ const gitignoreUpdated = ensureTesserDocsGitignore(project.root);
108
+ const agentInstructionsCreated = writeProjectAgentInstructions(project.root, project.name, version, { overwrite: false });
109
+ const next = [
110
+ "pnpm install",
111
+ "tesser test --json",
112
+ "git add package.json pnpm-lock.yaml .gitignore AGENTS.md .tesser/docs && git commit -m \"upgrade tesser\"",
113
+ ];
114
+
115
+ out.data(
116
+ { upgraded: project.root, tesserVersion: version, docs, packageJson: "package.json", gitignoreUpdated, agentInstructionsCreated, next },
117
+ () =>
118
+ `upgraded ${project.root} to Tesser ${version}\nnext:\n pnpm install\n tesser test --json\n git add package.json pnpm-lock.yaml .gitignore AGENTS.md .tesser/docs && git commit -m "upgrade tesser"`,
119
+ );
120
+ }
121
+
122
+ function pinTesserPackages(pkg: JsonObject, version: string): void {
123
+ const dependencies = objectField(pkg, "dependencies");
124
+ for (const name of TESSER_DEPENDENCIES) dependencies[name] = version;
125
+ const devDependencies = objectField(pkg, "devDependencies");
126
+ for (const name of TESSER_DEV_DEPENDENCIES) devDependencies[name] = version;
127
+ }
128
+
129
+ function objectField(pkg: JsonObject, key: string): JsonObject {
130
+ const existing = pkg[key];
131
+ if (existing && typeof existing === "object" && !Array.isArray(existing)) return existing as JsonObject;
132
+ const next: JsonObject = {};
133
+ pkg[key] = next;
134
+ return next;
135
+ }
136
+
137
+ function projectAgentsMd(projectName: string, version: string): string {
138
+ return `# Working in this Tesser Project\n\nThis repository is a Tesser **Project**: one git repo of automations deployed to a Tesser **Instance**.\n\n## Read first\n- [Generated Tesser reference](./.tesser/docs/README.md) — generated for Tesser ${version}.\n- [CLI reference](./.tesser/docs/cli.md) — use \`--json\` for machine-readable output.\n- [SDK reference](./.tesser/docs/sdk.md) — authoring pattern, \`ctx.step\`, tests.\n- [Connector reference](./.tesser/docs/connectors.md) — safe Connection and Secret usage.\n\n## Project rules\n- Use Tesser terms: **Automation**, **Trigger**, **Step**, **Project**, **Instance**, **Connector**, **Connection**, **Secret**, **Credential broker**, **Replay**.\n- One Automation per directory under \`automations/\`; colocate \`*.test.ts\` with each Automation.\n- Every side effect and external call must be inside \`ctx.step("stable-name", async () => ...)\`.\n- Declare credentials statically with \`connections: { ... }\` and \`secrets: { ... }\`; never read or print secret values.\n- Run \`pnpm install\` after init/upgrade, commit \`pnpm-lock.yaml\`, and never commit \`node_modules/\`.\n- Before deploy, run \`tesser test --json\` (and usually \`tesser build --json\`).\n- Deploy from git. Commit generated Tesser docs under \`.tesser/docs/\`; local runtime state in other \`.tesser/*\` paths stays ignored.\n\n## Upgrade rule\nTo update this Project's pinned Tesser packages and generated references, run the target CLI version explicitly:\n\n\`\`\`bash\nnpx @devosurf/tesser@<version> upgrade\npnpm install\ntesser test --json\n\`\`\`\n\nProject name: \`${projectName}\`.\n`;
139
+ }
140
+
141
+ function docsReadmeMd(projectName: string, version: string): string {
142
+ return `# Tesser agent reference\n\nGenerated 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\`.\n\n## Start here\n1. Read \`AGENTS.md\` for project-specific policy.\n2. Use [cli.md](./cli.md) for command sequences and safe secret handling.\n3. Use [sdk.md](./sdk.md) before editing an Automation.\n4. Use [connectors.md](./connectors.md) before adding a Connection or Secret.\n\n## Version contract\nTesser packages in \`package.json\` should be pinned exactly to \`${version}\`:\n\n- \`@devosurf/tesser\`\n- \`@devosurf/tesser-sdk\`\n- \`@devosurf/tesser-connectors\`\n- \`@devosurf/tesser-testing\`\n- \`@devosurf/tesser-server\`\n\nTo upgrade, run the target CLI version so docs and packages move together:\n\n\`\`\`bash\nnpx @devosurf/tesser@<version> upgrade\npnpm install\ntesser test --json\n\`\`\`\n\nCommit \`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\`.\n`;
143
+ }
144
+
145
+ function cliReferenceMd(version: string): string {
146
+ return `# Tesser CLI reference\n\nGenerated 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.\n\n## Install and invoke\n\n\`\`\`bash\nnpm install -g @devosurf/tesser@${version}\ntesser --version\n\n# Or run an exact version without global install:\nnpx @devosurf/tesser@${version} --version\n\`\`\`\n\n## Common flow\n\n\`\`\`bash\ntesser init my-project --instance https://tesser.example.com\ncd my-project\npnpm install # creates pnpm-lock.yaml; commit it\ngit init && git add -A && git commit -m init\ntesser login --instance https://tesser.example.com --token "$TESSER_TOKEN"\ntesser link --json # registers this Project on the Instance\ntesser test --json\ntesser build --json\ntesser deploy --json\n\`\`\`\n\n## Commands agents commonly use\n\n- \`tesser init <name> [--dir DIR] [--instance URL]\` — scaffold a Project.\n- \`tesser upgrade\` — pin Tesser packages to this CLI version and refresh \`.tesser/docs/\`. To target a version: \`npx @devosurf/tesser@<version> upgrade\`.\n- \`tesser login --instance URL --token TOKEN\` — verify and store a profile. Prefer \`TESSER_TOKEN\` over pasting tokens into chat.\n- \`tesser link [--repo URL] --json\` — register the Project and print deploy-key/webhook setup data. CLI output must not include private keys or webhook secrets.\n- \`tesser status --json\` — instance health and deploy state.\n- \`tesser test [--smoke-only] [--automation ID] --json\` — fast local tests plus generated smoke tests.\n- \`tesser build --json\` — bundle and statically extract manifests. Use before deploy when changing requirements.\n- \`tesser dev [--port N] [--no-watch]\` — local Instance with embedded Postgres under ignored \`.tesser/*\` state.\n- \`tesser deploy [--ref REF] [--local] [--no-wait] --json\` — server-side build/test/promotion. Exit code 4 means halted on missing credentials; run \`tesser connect\`.\n- \`tesser connect [--wait] [--status TOKEN] --json\` — mint or poll the human connect link. The human supplies credentials in the browser; the agent only sees readiness.\n- \`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.\n- \`tesser runs list|show|trigger|signal|cancel --json\`; \`tesser logs <runId> [--follow]\`; \`tesser replay <runId>\`.\n- \`tesser rollback <automation> --to <version> --json\` — alias re-point; no rebuild.\n\n## Credential safety\n\n- 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.\n- Never pass raw secret values as CLI arguments. Use environment variables, profiles, connect links, OAuth, or \`--value-stdin\`.\n- Treat connect links and status tokens as sensitive operational material.\n\n## Deploy hygiene\n\n- Run \`pnpm install\` after init/upgrade and commit \`pnpm-lock.yaml\`; the Instance installs from lockfiles for reproducible builds.\n- Do not commit \`node_modules/\`, \`.env\`, or local runtime state in \`.tesser/*\` outside \`.tesser/docs/\`.\n- Git is the source of truth. Commit and push before expecting a normal Instance deploy to see changes.\n`;
147
+ }
148
+
149
+ function sdkReferenceMd(version: string): string {
150
+ return `# Tesser SDK reference\n\nGenerated for \`@devosurf/tesser-sdk@${version}\`. Author ordinary async TypeScript; Tesser durability comes only from Steps.\n\n## Minimal Automation\n\n\`\`\`ts\nimport { defineAutomation, onWebhook } from "@devosurf/tesser-sdk";\nimport { z } from "zod";\n\nexport default defineAutomation({\n id: "hello",\n trigger: onWebhook({ input: z.object({ name: z.string().default("world") }) }),\n output: z.object({ greeting: z.string() }),\n\n run: async (input, ctx) => {\n const greeting = await ctx.step("compose", async () => \`hello, \${input.name}!\`);\n return { greeting };\n },\n});\n\`\`\`\n\n## Non-negotiable rules\n\n- Use **Step**, not node/task/action, for durable checkpoints.\n- 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)\`.\n- Step names are stable API for the journal. Use short, descriptive names such as \`fetch-customer\`, \`post-to-slack\`, \`charge-card\`.\n- Step outputs are journaled and must be serializable. Return plain data, not functions, class instances, streams, sockets, or live handles.\n- An Automation declares credentials statically at the top level. Do not hide requirements inside \`run\`.\n- Do not access provider tokens directly. Runtime-injected \`ctx.connections.*\` and \`ctx.secrets.*\` are the only runtime credential surfaces.\n\n## Triggers\n\n\`\`\`ts\nimport { onWebhook, onSchedule, onEvent } from "@devosurf/tesser-sdk";\n\nconst webhook = onWebhook({ input: schema, respond: "sync" });\nconst schedule = onSchedule({ cron: "0 9 * * *", tz: "UTC" });\nconst event = onEvent(myEvent);\n\`\`\`\n\nConnector triggers are reached from the Connector import, for example \`github.triggers.issueOpened({ repo: "owner/repo" })\` when supported.\n\n## Connections and Secrets\n\n\`\`\`ts\nimport { defineAutomation, onWebhook, secret } from "@devosurf/tesser-sdk";\nimport { slack } from "@devosurf/tesser-connectors";\nimport { z } from "zod";\n\nexport default defineAutomation({\n id: "notify",\n trigger: onWebhook({ input: z.object({ text: z.string() }) }),\n connections: { slack },\n secrets: { signingKey: secret({ describe: "Shared HMAC signing key" }) },\n output: z.object({ ok: z.boolean() }),\n\n run: async (input, ctx) => {\n await ctx.step("post-message", () =>\n ctx.connections.slack.chat.postMessage({ channel: "#ops", text: input.text }),\n );\n return { ok: true };\n },\n});\n\`\`\`\n\nDeploy 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.\n\n## Tests\n\nColocate tests beside each Automation.\n\n\`\`\`ts\nimport { createTest } from "@devosurf/tesser-testing";\nimport automation from "./index";\n\ntest("greets by name", async () => {\n const t = createTest({ automation });\n const { result } = await t.run({ input: { name: "tesser" } });\n expect(result).toEqual({ greeting: "hello, tesser!" });\n});\n\`\`\`\n\nFor Connector calls, mock the Step result by Step name. Replay fixtures produced by \`tesser replay <runId>\` should become permanent regression tests.\n`;
151
+ }
152
+
153
+ function connectorsReferenceMd(version: string): string {
154
+ return `# Tesser Connector reference\n\nGenerated for \`@devosurf/tesser-connectors@${version}\`. A Connector is a typed integration; a Connection is an authed runtime instance injected by the Credential broker.\n\n## Available connector imports\n\n\`\`\`ts\nimport {\n anthropic,\n claudeCode,\n github,\n gmail,\n googleCalendar,\n googleDocs,\n googleDrive,\n googleSheets,\n http,\n pi,\n resend,\n slack,\n} from "@devosurf/tesser-connectors";\n\`\`\`\n\nOnly import Connectors that the Automation declares in \`connections: { ... }\`.\n\n## 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## Common Actions\n\n- \`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.\n- \`ctx.connections.github.issues.list({ repo?, state?, labels?, limit? })\`, \`issues.create({ repo, title, body?, labels? })\`, \`issues.comment({ repo, number, body })\`, \`repos.get({ repo })\`.\n- \`ctx.connections.slack.chat.postMessage({ channel, text, threadTs? })\` and related Slack chat/conversation/user Actions.\n- \`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.\n\n## Connector triggers\n\nSome 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.\n\n\`\`\`ts\ntrigger: github.triggers.issueOpened({ repo: "owner/repo" })\n\`\`\`\n\n## Safety rules\n\n- Connector Actions are not automatically durable. The Automation author must wrap every Action call in \`ctx.step\`.\n- The imported Connector is not a token-bearing client. The authed client exists only at \`ctx.connections.<name>\` inside \`run\`.\n- For bespoke APIs, combine \`http\` with declared \`secrets: { ... }\`; do not put API keys in source, tests, logs, CLI args, or committed env files.\n- 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.\n`;
155
+ }
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ import { authClaudeCode, authPi } from "./commands/auth.js";
16
16
  import { deploy } from "./commands/deploy.js";
17
17
  import { dev } from "./commands/dev.js";
18
18
  import { init } from "./commands/init.js";
19
+ import { upgradeProject } from "./commands/project-docs.js";
19
20
  import { replay } from "./commands/replay.js";
20
21
  import { runTests } from "./commands/test.js";
21
22
 
@@ -74,7 +75,19 @@ program
74
75
  .action((name: string, cmdOpts: { dir?: string; instance?: string }) => {
75
76
  const { out } = setup();
76
77
  try {
77
- init(out, name, cmdOpts);
78
+ init(out, name, cmdOpts, VERSION);
79
+ } catch (err) {
80
+ toExit(err, out);
81
+ }
82
+ });
83
+
84
+ program
85
+ .command("upgrade")
86
+ .description("pin Tesser packages to this CLI version and refresh generated Project docs")
87
+ .action(() => {
88
+ const { out, opts } = setup();
89
+ try {
90
+ upgradeProject(out, requireProject(opts), VERSION);
78
91
  } catch (err) {
79
92
  toExit(err, out);
80
93
  }