@shanepadgett/tau-agent 0.31.1 → 0.33.0

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.
@@ -1,301 +0,0 @@
1
- import { readFile, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
4
- import { createGitRunner, type GitRunner, loadRepoStatus } from "../../shared/git.ts";
5
- import { errorText } from "../../shared/text.ts";
6
- import { PublishActivityPanel, type PublishProgress } from "./publish-ui.ts";
7
-
8
- const PACKAGE_PATHS = ["packages/tui/package.json", "packages/agent/package.json"] as const;
9
- const LOCKFILE_PATH = "package-lock.json";
10
- const WORKFLOW_NAME = "publish.yml";
11
- const WORKFLOW_WAIT_MS = 10 * 60_000;
12
- const POLL_INTERVAL_MS = 5_000;
13
- const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
14
-
15
- type Bump = "patch" | "minor" | "major";
16
-
17
- interface PackageManifest {
18
- name: string;
19
- version: string;
20
- dependencies?: Record<string, string>;
21
- }
22
-
23
- interface WorkflowRun {
24
- databaseId: number;
25
- status: string;
26
- conclusion: string | null;
27
- url: string;
28
- }
29
-
30
- export default function publishExtension(pi: ExtensionAPI): void {
31
- pi.registerCommand("publish", {
32
- description: "Create a tagged release and monitor trusted npm publishing",
33
- handler: async (args, ctx) => {
34
- await ctx.waitForIdle();
35
- if (args.trim()) {
36
- ctx.ui.notify("Usage: /publish", "error");
37
- return;
38
- }
39
- if (!ctx.hasUI) {
40
- ctx.ui.notify("Publishing requires interactive confirmation.", "error");
41
- return;
42
- }
43
- try {
44
- await publish(pi, ctx);
45
- } catch (error) {
46
- pi.sendMessage(
47
- {
48
- customType: "tau.publish-failure",
49
- content: [
50
- "The /publish command failed with this error:",
51
- errorText(error),
52
- "Investigate the failure using read-only diagnostics. Determine the likely cause and recommend a solution to the user. Do not change files, git state, package state, or remote state, and do not retry publishing.",
53
- ].join("\n\n"),
54
- display: false,
55
- },
56
- { triggerTurn: true },
57
- );
58
- }
59
- },
60
- });
61
- }
62
-
63
- async function publish(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> {
64
- const git = createGitRunner(pi, ctx);
65
- const repo = await loadRepoStatus(git);
66
- if (!repo) throw new Error("No git repository found.");
67
- if (repo.fileCount > 0)
68
- throw new Error("Working tree is not clean. Commit or stash every change before publishing.");
69
- await run(pi, ctx, "gh", ["auth", "status"], repo.root, DEFAULT_COMMAND_TIMEOUT_MS);
70
- await run(pi, ctx, "npm", ["run", "check:package-sources"], repo.root, DEFAULT_COMMAND_TIMEOUT_MS);
71
-
72
- const manifests = await loadManifests(repo.root);
73
- const currentVersion = sharedVersion(manifests);
74
- const tag = await latestReleaseTag(git, repo.root);
75
- const bump = await recommendBump(git, repo.root, tag, currentVersion);
76
- const version = tag ? incrementVersion(currentVersion, bump) : currentVersion;
77
- const selectedBump = await ctx.ui.select(`Release ${version}`, [
78
- `Use recommended ${bump} bump`,
79
- "Choose a different bump",
80
- "Cancel",
81
- ]);
82
- if (!selectedBump || selectedBump === "Cancel") return;
83
-
84
- const finalBump =
85
- selectedBump === "Choose a different bump"
86
- ? await ctx.ui.select("Release type", ["patch", "minor", "major"])
87
- : bump;
88
- if (!finalBump) return;
89
- const finalVersion = tag ? incrementVersion(currentVersion, finalBump as Bump) : currentVersion;
90
- const releaseTag = `v${finalVersion}`;
91
- if (await tagExists(git, repo.root, releaseTag)) throw new Error(`Tag ${releaseTag} already exists.`);
92
-
93
- const confirmed = await ctx.ui.confirm(
94
- `Publish ${releaseTag}?`,
95
- `This will push ${releaseTag}. GitHub Actions will publish @shanepadgett/tau-tui and @shanepadgett/tau-agent to npm.`,
96
- );
97
- if (!confirmed) return;
98
-
99
- if (ctx.mode !== "tui") {
100
- await completePublish(pi, ctx, git, repo.root, manifests, tag, finalVersion, releaseTag);
101
- return;
102
- }
103
- const failure = await ctx.ui.custom<Error | undefined>((tui, theme, _keybindings, done) => {
104
- const progress = new PublishActivityPanel(tui, theme, releaseTag);
105
- void completePublish(pi, ctx, git, repo.root, manifests, tag, finalVersion, releaseTag, progress)
106
- .then(() => done(undefined))
107
- .catch((error) => done(error instanceof Error ? error : new Error(errorText(error))));
108
- return progress;
109
- });
110
- if (failure) throw failure;
111
- }
112
-
113
- async function completePublish(
114
- pi: ExtensionAPI,
115
- ctx: ExtensionCommandContext,
116
- git: GitRunner,
117
- root: string,
118
- manifests: readonly PackageManifest[],
119
- tag: string | undefined,
120
- version: string,
121
- releaseTag: string,
122
- progress: PublishProgress | undefined = undefined,
123
- ): Promise<void> {
124
- if (tag) {
125
- progress?.update("write package versions");
126
- await writeReleaseVersion(root, manifests, version);
127
- }
128
- progress?.update("npm pack --dry-run @shanepadgett/tau-tui");
129
- await run(
130
- pi,
131
- ctx,
132
- "npm",
133
- ["pack", "--dry-run", "--workspace", "@shanepadgett/tau-tui"],
134
- root,
135
- DEFAULT_COMMAND_TIMEOUT_MS,
136
- );
137
- progress?.update("npm pack --dry-run @shanepadgett/tau-agent");
138
- await run(
139
- pi,
140
- ctx,
141
- "npm",
142
- ["pack", "--dry-run", "--workspace", "@shanepadgett/tau-agent"],
143
- root,
144
- DEFAULT_COMMAND_TIMEOUT_MS,
145
- );
146
- if (tag) {
147
- progress?.update("git add release files");
148
- await git.run(["add", ...PACKAGE_PATHS, LOCKFILE_PATH], { cwd: root });
149
- progress?.update(`git commit chore(release): ${releaseTag}`);
150
- await git.run(["commit", "-m", `chore(release): ${releaseTag}`], { cwd: root });
151
- }
152
- progress?.update(`git tag ${releaseTag}`);
153
- await git.run(["tag", releaseTag], { cwd: root });
154
- progress?.update("git push origin HEAD");
155
- await git.run(["push", "origin", "HEAD"], { cwd: root, timeout: 120_000 });
156
- progress?.update(`git push origin ${releaseTag}`);
157
- await git.run(["push", "origin", releaseTag], { cwd: root, timeout: 120_000 });
158
- progress?.update("Waiting for GitHub Actions");
159
- await monitorWorkflow(pi, ctx, root, await git.run(["rev-parse", "HEAD"], { cwd: root }), releaseTag, progress);
160
- }
161
-
162
- async function loadManifests(root: string): Promise<PackageManifest[]> {
163
- return Promise.all(
164
- PACKAGE_PATHS.map(async (path) => JSON.parse(await readFile(join(root, path), "utf8")) as PackageManifest),
165
- );
166
- }
167
-
168
- function sharedVersion(manifests: readonly PackageManifest[]): string {
169
- const versions = new Set(manifests.map((manifest) => manifest.version));
170
- if (versions.size !== 1) throw new Error("Publishable packages must use the same version.");
171
- const version = manifests[0]?.version;
172
- if (!version || !/^\d+\.\d+\.\d+$/.test(version)) throw new Error("Package version must be stable semver.");
173
- return version;
174
- }
175
-
176
- async function latestReleaseTag(git: GitRunner, root: string): Promise<string | undefined> {
177
- const tags = await git.run(["tag", "--list", "v*", "--sort=-version:refname"], { cwd: root });
178
- return tags.split("\n").find((tag) => /^v\d+\.\d+\.\d+$/.test(tag));
179
- }
180
-
181
- async function recommendBump(
182
- git: GitRunner,
183
- root: string,
184
- tag: string | undefined,
185
- currentVersion: string,
186
- ): Promise<Bump> {
187
- const range = tag ? `${tag}..HEAD` : "HEAD";
188
- const commits = await git.run(["log", "--format=%B%x00", range], { cwd: root });
189
- if (/BREAKING CHANGE:|^[^\n]*!:/m.test(commits)) return currentVersion.startsWith("0.") ? "minor" : "major";
190
- if (/^feat(?:\([^\n]+\))?:/m.test(commits)) return "minor";
191
- return "patch";
192
- }
193
-
194
- function incrementVersion(version: string, bump: Bump): string {
195
- const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
196
- if (!match) throw new Error(`Invalid version: ${version}`);
197
- const major = Number(match[1]);
198
- const minor = Number(match[2]);
199
- const patch = Number(match[3]);
200
- if (bump === "major") return `${major + 1}.0.0`;
201
- if (bump === "minor") return `${major}.${minor + 1}.0`;
202
- return `${major}.${minor}.${patch + 1}`;
203
- }
204
-
205
- async function tagExists(git: GitRunner, root: string, tag: string): Promise<boolean> {
206
- return Boolean(await git.run(["tag", "--list", tag], { cwd: root }));
207
- }
208
-
209
- async function writeReleaseVersion(
210
- root: string,
211
- manifests: readonly PackageManifest[],
212
- version: string,
213
- ): Promise<void> {
214
- for (const [index, path] of PACKAGE_PATHS.entries()) {
215
- const manifest = manifests[index];
216
- if (!manifest) throw new Error(`Missing package manifest: ${path}`);
217
- manifest.version = version;
218
- if (manifest.name === "@shanepadgett/tau-agent" && manifest.dependencies)
219
- manifest.dependencies["@shanepadgett/tau-tui"] = version;
220
- await writeFile(join(root, path), `${JSON.stringify(manifest, null, "\t")}\n`);
221
- }
222
- const lockfile = JSON.parse(await readFile(join(root, LOCKFILE_PATH), "utf8")) as {
223
- packages: Record<string, PackageManifest>;
224
- };
225
- for (const path of PACKAGE_PATHS) {
226
- const lockfilePath = path.slice(0, -"/package.json".length);
227
- const manifest = lockfile.packages[lockfilePath];
228
- if (!manifest) throw new Error(`Missing lockfile package: ${lockfilePath}`);
229
- manifest.version = version;
230
- if (manifest.name === "@shanepadgett/tau-agent" && manifest.dependencies)
231
- manifest.dependencies["@shanepadgett/tau-tui"] = version;
232
- }
233
- await writeFile(join(root, LOCKFILE_PATH), `${JSON.stringify(lockfile, null, "\t")}\n`);
234
- }
235
-
236
- async function run(
237
- pi: ExtensionAPI,
238
- ctx: ExtensionCommandContext,
239
- command: string,
240
- args: string[],
241
- cwd: string,
242
- timeout: number,
243
- ): Promise<string> {
244
- const result = await pi.exec(command, args, { cwd, signal: ctx.signal, timeout });
245
- if (result.code === 0) return result.stdout.trim();
246
- throw new Error([result.stderr, result.stdout].filter(Boolean).join("\n").trim() || `${command} failed`);
247
- }
248
-
249
- async function monitorWorkflow(
250
- pi: ExtensionAPI,
251
- ctx: ExtensionCommandContext,
252
- root: string,
253
- sha: string,
254
- tag: string,
255
- progress: PublishProgress | undefined = undefined,
256
- ): Promise<void> {
257
- const deadline = Date.now() + WORKFLOW_WAIT_MS;
258
- while (Date.now() < deadline) {
259
- const output = await run(
260
- pi,
261
- ctx,
262
- "gh",
263
- [
264
- "run",
265
- "list",
266
- "--workflow",
267
- WORKFLOW_NAME,
268
- "--commit",
269
- sha,
270
- "--limit",
271
- "1",
272
- "--json",
273
- "databaseId,status,conclusion,url",
274
- ],
275
- root,
276
- DEFAULT_COMMAND_TIMEOUT_MS,
277
- );
278
- const workflowRun = (JSON.parse(output) as WorkflowRun[])[0];
279
- if (workflowRun) {
280
- progress?.update(`GitHub Actions: ${workflowRun.status}`, workflowRun.url);
281
- if (workflowRun.status === "completed") {
282
- if (workflowRun.conclusion === "success") {
283
- const version = tag.slice(1);
284
- ctx.ui.notify(
285
- [
286
- `Published ${tag}`,
287
- `GitHub Actions: ${workflowRun.url}`,
288
- `npm TUI: https://www.npmjs.com/package/@shanepadgett/tau-tui/v/${version}`,
289
- `npm Agent: https://www.npmjs.com/package/@shanepadgett/tau-agent/v/${version}`,
290
- ].join("\n"),
291
- "info",
292
- );
293
- return;
294
- }
295
- throw new Error(`GitHub Actions ${workflowRun.conclusion ?? "failed"}: ${workflowRun.url}`);
296
- }
297
- }
298
- await new Promise<void>((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
299
- }
300
- throw new Error(`Timed out waiting for GitHub Actions for ${tag}. Check the Actions tab.`);
301
- }
@@ -1,73 +0,0 @@
1
- import type { Theme } from "@earendil-works/pi-coding-agent";
2
- import { type Component, Text, type TUI } from "@earendil-works/pi-tui";
3
- import { ToolPanel } from "@shanepadgett/tau-tui";
4
-
5
- const RELEASE_STEPS = [
6
- "write package versions",
7
- "npm pack --dry-run @shanepadgett/tau-tui",
8
- "npm pack --dry-run @shanepadgett/tau-agent",
9
- "git add release files",
10
- "git commit",
11
- "git tag",
12
- "git push origin HEAD",
13
- "git push origin v",
14
- "Waiting for GitHub Actions",
15
- "GitHub Actions: publishing",
16
- ] as const;
17
-
18
- export interface PublishProgress {
19
- update(status: string, detail?: string): void;
20
- }
21
-
22
- export class PublishActivityPanel implements Component, PublishProgress {
23
- private readonly tui: TUI;
24
- private readonly theme: Theme;
25
- private readonly body = new Text("", 0, 0);
26
- private readonly panel: ToolPanel;
27
- private status = "Preparing release";
28
- private detail: string | undefined;
29
- private readonly completed: string[] = ["Release confirmed"];
30
-
31
- constructor(tui: TUI, theme: Theme, tag: string) {
32
- this.tui = tui;
33
- this.theme = theme;
34
- this.panel = new ToolPanel(theme, {
35
- title: `Publishing ${tag}`,
36
- secondary: "Editor is unavailable until this release finishes.",
37
- header: [theme.fg("muted", "GitHub Actions publishes through npm trusted publishing.")],
38
- body: this.body,
39
- footer: { kind: "hints", hints: [] },
40
- });
41
- this.renderBody();
42
- }
43
-
44
- update(status: string, detail: string | undefined = undefined): void {
45
- if (this.status !== "Preparing release" && this.status !== status) this.completed.push(this.status);
46
- this.status = status;
47
- this.detail = detail;
48
- this.renderBody();
49
- this.tui.requestRender();
50
- }
51
-
52
- render(width: number): string[] {
53
- return this.panel.render(width);
54
- }
55
-
56
- invalidate(): void {
57
- this.panel.invalidate();
58
- }
59
-
60
- private renderBody(): void {
61
- const upcoming = RELEASE_STEPS.filter(
62
- (step) => !this.completed.some((completed) => completed.startsWith(step)) && step !== this.status,
63
- );
64
- this.body.setText(
65
- [
66
- ...this.completed.map((step) => this.theme.fg("success", `✓ ${step}`)),
67
- this.theme.fg("accent", `… ${this.status}`),
68
- ...(this.detail ? [this.theme.fg("muted", this.detail)] : []),
69
- ...upcoming.map((step) => this.theme.fg("muted", `· ${step}`)),
70
- ].join("\n"),
71
- );
72
- }
73
- }