@omercnet/paseo-beads 0.1.0-next.72.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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/client/beads-view.ts +155 -0
- package/client/paseo-beads.tsx +1222 -0
- package/client/web.ts +15 -0
- package/docs/images/paseo-beads-compact.png +0 -0
- package/docs/images/paseo-beads-wide.png +0 -0
- package/index.client.tsx +39 -0
- package/index.server.ts +9 -0
- package/package.json +70 -0
- package/paseo-plugin.json +6 -0
- package/server/beads.ts +324 -0
- package/shared/beads.ts +89 -0
package/client/web.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
|
|
3
|
+
declare const document: {
|
|
4
|
+
getElementById(id: string): { focus(): void } | null;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function focusWebElement(id: string): boolean {
|
|
8
|
+
if (Platform.OS !== "web") return false;
|
|
9
|
+
|
|
10
|
+
const element = document.getElementById(id);
|
|
11
|
+
if (!element) return false;
|
|
12
|
+
|
|
13
|
+
element.focus();
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
Binary file
|
|
Binary file
|
package/index.client.tsx
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { PluginClientContext } from "@getpaseo/plugin/client";
|
|
2
|
+
import { PaseoBeads } from "./client/paseo-beads";
|
|
3
|
+
|
|
4
|
+
export default function contribute(client: PluginClientContext) {
|
|
5
|
+
const removePanel = client.addWorkspacePanel({
|
|
6
|
+
id: "beads",
|
|
7
|
+
title: "Beads",
|
|
8
|
+
icon: "CircleDot",
|
|
9
|
+
context: "workspace",
|
|
10
|
+
locations: ["workspace", "explorer"],
|
|
11
|
+
Component: PaseoBeads,
|
|
12
|
+
});
|
|
13
|
+
const removeWorkspaceCommand = client.addCommandCenterItem({
|
|
14
|
+
id: "open-beads",
|
|
15
|
+
title: "Open Beads",
|
|
16
|
+
icon: "CircleDot",
|
|
17
|
+
keywords: ["beads", "issues", "dependencies", "ready", "work queue"],
|
|
18
|
+
context: "workspace",
|
|
19
|
+
onSelect({ openPanel }) {
|
|
20
|
+
openPanel("beads");
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
const removeAgentCommand = client.addCommandCenterItem({
|
|
24
|
+
id: "open-beads-agent",
|
|
25
|
+
title: "Open Beads",
|
|
26
|
+
icon: "CircleDot",
|
|
27
|
+
keywords: ["beads", "issues", "dependencies", "ready", "work queue"],
|
|
28
|
+
context: "agent",
|
|
29
|
+
onSelect({ openPanel }) {
|
|
30
|
+
openPanel("beads");
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return () => {
|
|
35
|
+
removeAgentCommand();
|
|
36
|
+
removeWorkspaceCommand();
|
|
37
|
+
removePanel();
|
|
38
|
+
};
|
|
39
|
+
}
|
package/index.server.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
2
|
+
import { handleGetWorkspaceBead, handleGetWorkspaceBeads } from "./server/beads";
|
|
3
|
+
import { getWorkspaceBead, getWorkspaceBeads } from "./shared/beads";
|
|
4
|
+
|
|
5
|
+
export default function contribute(server: PluginServerContext) {
|
|
6
|
+
server.handle(getWorkspaceBeads, handleGetWorkspaceBeads);
|
|
7
|
+
server.handle(getWorkspaceBead, handleGetWorkspaceBead);
|
|
8
|
+
return () => {};
|
|
9
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@omercnet/paseo-beads",
|
|
3
|
+
"version": "0.1.0-next.72.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A dependency-aware Beads work queue for each Paseo workspace.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Omer Cohen",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/omercnet/paseo-plugins.git",
|
|
11
|
+
"directory": "paseo-beads"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/omercnet/paseo-plugins/tree/main/paseo-beads#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/omercnet/paseo-plugins/issues"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"packageManager": "npm@11.19.1",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=24"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"paseo",
|
|
26
|
+
"paseo-plugin",
|
|
27
|
+
"beads",
|
|
28
|
+
"local-first",
|
|
29
|
+
"issue-tracking",
|
|
30
|
+
"coding-agents"
|
|
31
|
+
],
|
|
32
|
+
"files": [
|
|
33
|
+
"CHANGELOG.md",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"README.md",
|
|
36
|
+
"index.client.tsx",
|
|
37
|
+
"index.server.ts",
|
|
38
|
+
"client",
|
|
39
|
+
"server",
|
|
40
|
+
"shared",
|
|
41
|
+
"docs",
|
|
42
|
+
"paseo-plugin.json"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"check": "biome check .",
|
|
46
|
+
"check:write": "biome check --write .",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:coverage": "vitest run --coverage --coverage.reporter=text --coverage.reporter=lcov --coverage.thresholds.lines=90 --coverage.thresholds.functions=85",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"package:release": "node scripts/package-release.ts",
|
|
51
|
+
"verify:package": "node scripts/verify-package.ts"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@biomejs/biome": "^2.5.10",
|
|
55
|
+
"@getpaseo/cli": "0.8.0",
|
|
56
|
+
"@getpaseo/client": "0.8.0",
|
|
57
|
+
"@getpaseo/plugin": "0.8.0",
|
|
58
|
+
"@getpaseo/protocol": "0.8.0",
|
|
59
|
+
"@tanstack/react-query": "^5.102.3",
|
|
60
|
+
"@types/node": "^24.5.2",
|
|
61
|
+
"@types/react": "~19.2.0",
|
|
62
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
63
|
+
"fflate": "^0.8.3",
|
|
64
|
+
"react": "19.1.0",
|
|
65
|
+
"react-native": "0.81.5",
|
|
66
|
+
"typescript": "^7.0.0",
|
|
67
|
+
"vitest": "^5.0.0",
|
|
68
|
+
"zod": "^4.4.3"
|
|
69
|
+
}
|
|
70
|
+
}
|
package/server/beads.ts
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import type { RpcInput, RpcOutput } from "@getpaseo/plugin";
|
|
4
|
+
import type { PluginHandlerContext } from "@getpaseo/plugin/server";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import {
|
|
7
|
+
BEADS_LIMITS,
|
|
8
|
+
type BeadDetail,
|
|
9
|
+
BeadDetailSchema,
|
|
10
|
+
type BeadSummary,
|
|
11
|
+
BeadSummarySchema,
|
|
12
|
+
BeadsSnapshotSchema,
|
|
13
|
+
type getWorkspaceBead,
|
|
14
|
+
type getWorkspaceBeads,
|
|
15
|
+
} from "../shared/beads";
|
|
16
|
+
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
18
|
+
const COMMAND_TIMEOUT_MS = 10_000;
|
|
19
|
+
const MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
20
|
+
const MAX_DIAGNOSTIC_CHARS = 512;
|
|
21
|
+
|
|
22
|
+
const rawIdentifierSchema = z.string().max(BEADS_LIMITS.identifier);
|
|
23
|
+
const rawTitleSchema = z.string().max(BEADS_LIMITS.title);
|
|
24
|
+
const rawStatusOrTypeSchema = z.string().max(BEADS_LIMITS.statusOrType);
|
|
25
|
+
const rawTimestampSchema = z.string().max(BEADS_LIMITS.timestamp).datetime();
|
|
26
|
+
|
|
27
|
+
const RawBeadSummarySchema = z.object({
|
|
28
|
+
id: rawIdentifierSchema,
|
|
29
|
+
title: rawTitleSchema,
|
|
30
|
+
status: rawStatusOrTypeSchema,
|
|
31
|
+
priority: z.number().int().min(0).max(4),
|
|
32
|
+
issue_type: rawStatusOrTypeSchema,
|
|
33
|
+
assignee: z.string().max(BEADS_LIMITS.assignee).nullable().optional(),
|
|
34
|
+
labels: z
|
|
35
|
+
.array(z.string().max(BEADS_LIMITS.label))
|
|
36
|
+
.max(BEADS_LIMITS.labels)
|
|
37
|
+
.nullable()
|
|
38
|
+
.optional(),
|
|
39
|
+
parent: rawIdentifierSchema.nullable().optional(),
|
|
40
|
+
updated_at: rawTimestampSchema.nullable().optional(),
|
|
41
|
+
dependency_count: z.number().int().nonnegative().nullable().optional(),
|
|
42
|
+
dependent_count: z.number().int().nonnegative().nullable().optional(),
|
|
43
|
+
comment_count: z.number().int().nonnegative().nullable().optional(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const RawBeadDependencySchema = z.object({
|
|
47
|
+
id: rawIdentifierSchema,
|
|
48
|
+
title: rawTitleSchema,
|
|
49
|
+
status: rawStatusOrTypeSchema,
|
|
50
|
+
issue_type: rawStatusOrTypeSchema,
|
|
51
|
+
dependency_type: rawStatusOrTypeSchema,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const RawBeadDetailSchema = RawBeadSummarySchema.extend({
|
|
55
|
+
description: z.string().max(BEADS_LIMITS.body).nullable().optional(),
|
|
56
|
+
acceptance_criteria: z.string().max(BEADS_LIMITS.body).nullable().optional(),
|
|
57
|
+
design: z.string().max(BEADS_LIMITS.body).nullable().optional(),
|
|
58
|
+
notes: z.string().max(BEADS_LIMITS.body).nullable().optional(),
|
|
59
|
+
dependencies: z
|
|
60
|
+
.array(RawBeadDependencySchema)
|
|
61
|
+
.max(BEADS_LIMITS.relationships)
|
|
62
|
+
.nullable()
|
|
63
|
+
.optional(),
|
|
64
|
+
dependents: z
|
|
65
|
+
.array(RawBeadDependencySchema)
|
|
66
|
+
.max(BEADS_LIMITS.relationships)
|
|
67
|
+
.nullable()
|
|
68
|
+
.optional(),
|
|
69
|
+
comments: z.array(z.unknown()).max(BEADS_LIMITS.relationships).nullable().optional(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const RawIssueListSchema = z.array(RawBeadSummarySchema).max(BEADS_LIMITS.rawIssueList);
|
|
73
|
+
const RawDetailListSchema = z.array(RawBeadDetailSchema).max(BEADS_LIMITS.rawIssueList);
|
|
74
|
+
const RawReadyIssueListSchema = z
|
|
75
|
+
.array(z.object({ id: rawIdentifierSchema }))
|
|
76
|
+
.max(BEADS_LIMITS.globalReadyIds);
|
|
77
|
+
const JsonEnvelopeSchema = z.object({
|
|
78
|
+
schema_version: z.number().int(),
|
|
79
|
+
data: z.unknown(),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
type RawBeadSummary = z.infer<typeof RawBeadSummarySchema>;
|
|
83
|
+
type RawBeadDetail = z.infer<typeof RawBeadDetailSchema>;
|
|
84
|
+
type CommandError = Error & {
|
|
85
|
+
code?: string | number;
|
|
86
|
+
killed?: boolean;
|
|
87
|
+
signal?: string | null;
|
|
88
|
+
stdout?: string | Buffer;
|
|
89
|
+
stderr?: string | Buffer;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
type BdCommandRunner = (
|
|
93
|
+
command: string,
|
|
94
|
+
args: string[],
|
|
95
|
+
options: {
|
|
96
|
+
encoding: "utf8";
|
|
97
|
+
timeout: number;
|
|
98
|
+
maxBuffer: number;
|
|
99
|
+
},
|
|
100
|
+
) => Promise<{ stdout: string }>;
|
|
101
|
+
|
|
102
|
+
const defaultBdCommandRunner: BdCommandRunner = async (command, args, options) =>
|
|
103
|
+
execFileAsync(command, args, options);
|
|
104
|
+
|
|
105
|
+
function parseJson(stdout: string): unknown {
|
|
106
|
+
let value: unknown;
|
|
107
|
+
try {
|
|
108
|
+
value = JSON.parse(stdout);
|
|
109
|
+
} catch {
|
|
110
|
+
const error = new Error("Beads returned invalid JSON.") as CommandError;
|
|
111
|
+
error.stdout = stdout;
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const hasEnvelopeData =
|
|
116
|
+
typeof value === "object" && value !== null && Object.hasOwn(value, "data");
|
|
117
|
+
const envelope = hasEnvelopeData
|
|
118
|
+
? JsonEnvelopeSchema.safeParse(value)
|
|
119
|
+
: { success: false as const };
|
|
120
|
+
return envelope.success ? envelope.data.data : value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function runBd(
|
|
124
|
+
directory: string,
|
|
125
|
+
args: readonly string[],
|
|
126
|
+
runner: BdCommandRunner,
|
|
127
|
+
): Promise<unknown> {
|
|
128
|
+
const { stdout } = await runner("bd", ["--readonly", "-C", directory, ...args], {
|
|
129
|
+
encoding: "utf8",
|
|
130
|
+
timeout: COMMAND_TIMEOUT_MS,
|
|
131
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
132
|
+
});
|
|
133
|
+
return parseJson(stdout);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function loadReadyIds(
|
|
137
|
+
directory: string,
|
|
138
|
+
runner: BdCommandRunner,
|
|
139
|
+
): Promise<ReadonlySet<string>> {
|
|
140
|
+
const result = await runBd(directory, ["list", "--ready", "--json", "--limit", "0"], runner);
|
|
141
|
+
return new Set(RawReadyIssueListSchema.parse(result).map(({ id }) => id));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function errorText(error: unknown): string {
|
|
145
|
+
if (!(error instanceof Error)) {
|
|
146
|
+
return typeof error === "string"
|
|
147
|
+
? error.slice(0, MAX_DIAGNOSTIC_CHARS).replace(/\s+/g, " ").trim() || "Unknown failure"
|
|
148
|
+
: "Unknown failure";
|
|
149
|
+
}
|
|
150
|
+
const commandError = error as CommandError;
|
|
151
|
+
const output = commandError.stderr || commandError.stdout || commandError.message;
|
|
152
|
+
const text = Buffer.isBuffer(output)
|
|
153
|
+
? output.subarray(0, MAX_DIAGNOSTIC_CHARS).toString("utf8")
|
|
154
|
+
: output.slice(0, MAX_DIAGNOSTIC_CHARS);
|
|
155
|
+
return text.replace(/\s+/g, " ").trim() || "Unknown failure";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isBdUnavailable(error: unknown): boolean {
|
|
159
|
+
return (error as CommandError | undefined)?.code === "ENOENT";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isNotInitialized(error: unknown): boolean {
|
|
163
|
+
return /no beads project found/i.test(errorText(error));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function unexpectedFailure(action: string, error: unknown): Error {
|
|
167
|
+
const commandError = error as CommandError | undefined;
|
|
168
|
+
if (commandError?.killed || commandError?.signal === "SIGTERM") {
|
|
169
|
+
return new Error(`Unable to ${action}: Beads timed out after 10 seconds.`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
console.error(`[paseo-beads] Unable to ${action}: ${errorText(error)}`);
|
|
173
|
+
return new Error(`Unable to ${action}. Check Paseo plugin logs for details.`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function resolveWorkspaceDirectory(
|
|
177
|
+
workspaceId: string,
|
|
178
|
+
context: PluginHandlerContext,
|
|
179
|
+
): Promise<string> {
|
|
180
|
+
const workspace = await context.paseo.workspaces.ref(workspaceId).refresh();
|
|
181
|
+
if (!workspace?.workspaceDirectory) throw new Error("Workspace not found.");
|
|
182
|
+
return workspace.workspaceDirectory;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeSummary(raw: RawBeadSummary, readyIds: ReadonlySet<string>): BeadSummary {
|
|
186
|
+
const isReady = readyIds.has(raw.id);
|
|
187
|
+
return BeadSummarySchema.parse({
|
|
188
|
+
id: raw.id,
|
|
189
|
+
title: raw.title,
|
|
190
|
+
status: raw.status,
|
|
191
|
+
priority: raw.priority,
|
|
192
|
+
issueType: raw.issue_type,
|
|
193
|
+
assignee: raw.assignee ?? null,
|
|
194
|
+
labels: raw.labels ?? [],
|
|
195
|
+
parent: raw.parent ?? null,
|
|
196
|
+
updatedAt: raw.updated_at ?? null,
|
|
197
|
+
dependencyCount: raw.dependency_count ?? 0,
|
|
198
|
+
dependentCount: raw.dependent_count ?? 0,
|
|
199
|
+
commentCount: raw.comment_count ?? 0,
|
|
200
|
+
isReady,
|
|
201
|
+
isBlocked: raw.status === "blocked" || (raw.status === "open" && !isReady),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function normalizeDetail(raw: RawBeadDetail, readyIds: ReadonlySet<string>): BeadDetail {
|
|
206
|
+
const isReady = readyIds.has(raw.id);
|
|
207
|
+
const dependencies = (raw.dependencies ?? []).map((dependency) => ({
|
|
208
|
+
id: dependency.id,
|
|
209
|
+
title: dependency.title,
|
|
210
|
+
status: dependency.status,
|
|
211
|
+
issueType: dependency.issue_type,
|
|
212
|
+
dependencyType: dependency.dependency_type,
|
|
213
|
+
}));
|
|
214
|
+
const dependents = (raw.dependents ?? []).map((dependent) => ({
|
|
215
|
+
id: dependent.id,
|
|
216
|
+
title: dependent.title,
|
|
217
|
+
status: dependent.status,
|
|
218
|
+
issueType: dependent.issue_type,
|
|
219
|
+
dependencyType: dependent.dependency_type,
|
|
220
|
+
}));
|
|
221
|
+
|
|
222
|
+
return BeadDetailSchema.parse({
|
|
223
|
+
id: raw.id,
|
|
224
|
+
title: raw.title,
|
|
225
|
+
status: raw.status,
|
|
226
|
+
priority: raw.priority,
|
|
227
|
+
issueType: raw.issue_type,
|
|
228
|
+
assignee: raw.assignee ?? null,
|
|
229
|
+
labels: raw.labels ?? [],
|
|
230
|
+
parent: raw.parent ?? null,
|
|
231
|
+
updatedAt: raw.updated_at ?? null,
|
|
232
|
+
dependencyCount: raw.dependency_count ?? dependencies.length,
|
|
233
|
+
dependentCount: raw.dependent_count ?? dependents.length,
|
|
234
|
+
commentCount: raw.comment_count ?? raw.comments?.length ?? 0,
|
|
235
|
+
isReady,
|
|
236
|
+
isBlocked: raw.status === "blocked" || (raw.status === "open" && !isReady),
|
|
237
|
+
description: raw.description ?? null,
|
|
238
|
+
acceptanceCriteria: raw.acceptance_criteria ?? null,
|
|
239
|
+
design: raw.design ?? null,
|
|
240
|
+
notes: raw.notes ?? null,
|
|
241
|
+
dependencies,
|
|
242
|
+
dependents,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function unavailableSnapshot(state: "not_initialized" | "bd_unavailable") {
|
|
247
|
+
return BeadsSnapshotSchema.parse({
|
|
248
|
+
state,
|
|
249
|
+
issues: [],
|
|
250
|
+
truncated: false,
|
|
251
|
+
refreshedAt: new Date().toISOString(),
|
|
252
|
+
message:
|
|
253
|
+
state === "bd_unavailable"
|
|
254
|
+
? "The bd CLI is not available on this Paseo host."
|
|
255
|
+
: "Beads is not initialized for this workspace.",
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function handleGetWorkspaceBeads(
|
|
260
|
+
{ workspaceId }: RpcInput<typeof getWorkspaceBeads>,
|
|
261
|
+
context: PluginHandlerContext,
|
|
262
|
+
runner: BdCommandRunner = defaultBdCommandRunner,
|
|
263
|
+
): Promise<RpcOutput<typeof getWorkspaceBeads>> {
|
|
264
|
+
const directory = await resolveWorkspaceDirectory(workspaceId, context);
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const [listJson, readyIds] = await Promise.all([
|
|
268
|
+
runBd(
|
|
269
|
+
directory,
|
|
270
|
+
["list", "--json", "--sort", "priority", "--limit", String(BEADS_LIMITS.rawIssueList)],
|
|
271
|
+
runner,
|
|
272
|
+
),
|
|
273
|
+
loadReadyIds(directory, runner),
|
|
274
|
+
]);
|
|
275
|
+
const listed = RawIssueListSchema.parse(listJson);
|
|
276
|
+
const truncated = listed.length > BEADS_LIMITS.issueList;
|
|
277
|
+
const displayed = listed.slice(0, BEADS_LIMITS.issueList);
|
|
278
|
+
const issues = displayed.map((issue) => normalizeSummary(issue, readyIds));
|
|
279
|
+
return BeadsSnapshotSchema.parse({
|
|
280
|
+
state: "ready",
|
|
281
|
+
issues,
|
|
282
|
+
truncated,
|
|
283
|
+
refreshedAt: new Date().toISOString(),
|
|
284
|
+
message: null,
|
|
285
|
+
});
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (isBdUnavailable(error)) return unavailableSnapshot("bd_unavailable");
|
|
288
|
+
if (isNotInitialized(error)) return unavailableSnapshot("not_initialized");
|
|
289
|
+
throw unexpectedFailure("load Beads issues", error);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export async function handleGetWorkspaceBead(
|
|
294
|
+
{ workspaceId, issueId }: RpcInput<typeof getWorkspaceBead>,
|
|
295
|
+
context: PluginHandlerContext,
|
|
296
|
+
runner: BdCommandRunner = defaultBdCommandRunner,
|
|
297
|
+
): Promise<RpcOutput<typeof getWorkspaceBead>> {
|
|
298
|
+
const directory = await resolveWorkspaceDirectory(workspaceId, context);
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const [showJson, readyIds] = await Promise.all([
|
|
302
|
+
runBd(directory, ["show", issueId, "--json", "--include-dependents"], runner),
|
|
303
|
+
loadReadyIds(directory, runner),
|
|
304
|
+
]);
|
|
305
|
+
const issues = RawDetailListSchema.parse(showJson);
|
|
306
|
+
if (!issues[0]) return { detail: null };
|
|
307
|
+
return { detail: normalizeDetail(issues[0], readyIds) };
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (
|
|
310
|
+
/no issues? found matching(?: the provided IDs|\s+"[^"]+")|issue .* not found/i.test(
|
|
311
|
+
errorText(error),
|
|
312
|
+
)
|
|
313
|
+
) {
|
|
314
|
+
return { detail: null };
|
|
315
|
+
}
|
|
316
|
+
if (isBdUnavailable(error)) {
|
|
317
|
+
throw new Error("The bd CLI is not available on this Paseo host.");
|
|
318
|
+
}
|
|
319
|
+
if (isNotInitialized(error)) {
|
|
320
|
+
throw new Error("Beads is not initialized for this workspace.");
|
|
321
|
+
}
|
|
322
|
+
throw unexpectedFailure("load the Beads issue", error);
|
|
323
|
+
}
|
|
324
|
+
}
|
package/shared/beads.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { defineRpc } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const BEADS_LIMITS = {
|
|
5
|
+
issueList: 500,
|
|
6
|
+
rawIssueList: 501,
|
|
7
|
+
globalReadyIds: 25_000,
|
|
8
|
+
identifier: 256,
|
|
9
|
+
title: 4_096,
|
|
10
|
+
statusOrType: 128,
|
|
11
|
+
assignee: 512,
|
|
12
|
+
label: 256,
|
|
13
|
+
labels: 128,
|
|
14
|
+
body: 262_144,
|
|
15
|
+
relationships: 2_000,
|
|
16
|
+
publicMessage: 512,
|
|
17
|
+
timestamp: 64,
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
const identifierSchema = z.string().max(BEADS_LIMITS.identifier);
|
|
21
|
+
const titleSchema = z.string().max(BEADS_LIMITS.title);
|
|
22
|
+
const statusOrTypeSchema = z.string().max(BEADS_LIMITS.statusOrType);
|
|
23
|
+
const timestampSchema = z.string().max(BEADS_LIMITS.timestamp).datetime();
|
|
24
|
+
|
|
25
|
+
export const BeadDependencySchema = z.object({
|
|
26
|
+
id: identifierSchema,
|
|
27
|
+
title: titleSchema,
|
|
28
|
+
status: statusOrTypeSchema,
|
|
29
|
+
issueType: statusOrTypeSchema,
|
|
30
|
+
dependencyType: statusOrTypeSchema,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const BeadSummarySchema = z.object({
|
|
34
|
+
id: identifierSchema,
|
|
35
|
+
title: titleSchema,
|
|
36
|
+
status: statusOrTypeSchema,
|
|
37
|
+
priority: z.number().int().min(0).max(4),
|
|
38
|
+
issueType: statusOrTypeSchema,
|
|
39
|
+
assignee: z.string().max(BEADS_LIMITS.assignee).nullable(),
|
|
40
|
+
labels: z.array(z.string().max(BEADS_LIMITS.label)).max(BEADS_LIMITS.labels),
|
|
41
|
+
parent: identifierSchema.nullable(),
|
|
42
|
+
updatedAt: timestampSchema.nullable(),
|
|
43
|
+
dependencyCount: z.number().int().nonnegative(),
|
|
44
|
+
dependentCount: z.number().int().nonnegative(),
|
|
45
|
+
commentCount: z.number().int().nonnegative(),
|
|
46
|
+
isReady: z.boolean(),
|
|
47
|
+
isBlocked: z.boolean(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const BeadDetailSchema = BeadSummarySchema.extend({
|
|
51
|
+
description: z.string().max(BEADS_LIMITS.body).nullable(),
|
|
52
|
+
acceptanceCriteria: z.string().max(BEADS_LIMITS.body).nullable(),
|
|
53
|
+
design: z.string().max(BEADS_LIMITS.body).nullable(),
|
|
54
|
+
notes: z.string().max(BEADS_LIMITS.body).nullable(),
|
|
55
|
+
dependencies: z.array(BeadDependencySchema).max(BEADS_LIMITS.relationships),
|
|
56
|
+
dependents: z.array(BeadDependencySchema).max(BEADS_LIMITS.relationships),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const BeadsSnapshotSchema = z.object({
|
|
60
|
+
state: z.enum(["ready", "not_initialized", "bd_unavailable"]),
|
|
61
|
+
issues: z.array(BeadSummarySchema).max(BEADS_LIMITS.issueList),
|
|
62
|
+
truncated: z.boolean(),
|
|
63
|
+
refreshedAt: timestampSchema,
|
|
64
|
+
message: z.string().max(BEADS_LIMITS.publicMessage).nullable(),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export type BeadSummary = z.infer<typeof BeadSummarySchema>;
|
|
68
|
+
export type BeadDetail = z.infer<typeof BeadDetailSchema>;
|
|
69
|
+
export type BeadsSnapshot = z.infer<typeof BeadsSnapshotSchema>;
|
|
70
|
+
|
|
71
|
+
const workspaceInput = z.object({
|
|
72
|
+
workspaceId: identifierSchema.min(1),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const getWorkspaceBeads = defineRpc({
|
|
76
|
+
name: "paseo-beads.get-workspace-beads",
|
|
77
|
+
input: workspaceInput,
|
|
78
|
+
output: BeadsSnapshotSchema,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export const getWorkspaceBead = defineRpc({
|
|
82
|
+
name: "paseo-beads.get-workspace-bead",
|
|
83
|
+
input: workspaceInput.extend({
|
|
84
|
+
issueId: identifierSchema
|
|
85
|
+
.min(1)
|
|
86
|
+
.refine((value) => !value.startsWith("-"), "Issue ID must not start with a hyphen"),
|
|
87
|
+
}),
|
|
88
|
+
output: z.object({ detail: BeadDetailSchema.nullable() }),
|
|
89
|
+
});
|