herdr-plugin-amq 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +263 -0
- package/bin/herdr-amq.mjs +74 -0
- package/herdr-plugin.toml +57 -0
- package/package.json +50 -0
- package/skills/herdr-amq/SKILL.md +69 -0
- package/src/actions.mjs +573 -0
- package/src/blobs.mjs +348 -0
- package/src/board.mjs +760 -0
- package/src/bridge.mjs +373 -0
- package/src/briefs.mjs +201 -0
- package/src/config.mjs +146 -0
- package/src/herdr.mjs +215 -0
- package/src/index.mjs +4 -0
- package/src/markdown.mjs +167 -0
- package/src/panes.mjs +68 -0
- package/src/protocol.mjs +347 -0
- package/src/server.mjs +773 -0
- package/src/store.mjs +1063 -0
- package/src/web/app.js +3066 -0
- package/src/web/index.html +727 -0
- package/src/web/style.css +3842 -0
- package/src/worktrees.mjs +217 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* List all git worktrees in the target repository
|
|
7
|
+
*/
|
|
8
|
+
export function listWorktrees(repoRoot) {
|
|
9
|
+
if (!repoRoot || !fs.existsSync(repoRoot)) return [];
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const raw = execSync("git worktree list --porcelain", {
|
|
13
|
+
cwd: repoRoot,
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const worktrees = [];
|
|
19
|
+
let current = {};
|
|
20
|
+
|
|
21
|
+
const lines = raw.split(/\r?\n/);
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
if (!line.trim()) {
|
|
24
|
+
if (current.path) {
|
|
25
|
+
worktrees.push(current);
|
|
26
|
+
current = {};
|
|
27
|
+
}
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (line.startsWith("worktree ")) {
|
|
32
|
+
current.path = line.slice(9).trim();
|
|
33
|
+
} else if (line.startsWith("HEAD ")) {
|
|
34
|
+
current.head = line.slice(5).trim();
|
|
35
|
+
} else if (line.startsWith("branch ")) {
|
|
36
|
+
const fullBranch = line.slice(7).trim();
|
|
37
|
+
current.branch = fullBranch.replace(/^refs\/heads\//, "");
|
|
38
|
+
} else if (line.startsWith("bare")) {
|
|
39
|
+
current.isBare = true;
|
|
40
|
+
} else if (line.startsWith("detached")) {
|
|
41
|
+
current.isDetached = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (current.path) {
|
|
46
|
+
worktrees.push(current);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Associate worktree with agent handle if located in .worktrees/<handle>
|
|
50
|
+
return worktrees.map((wt) => {
|
|
51
|
+
const isMain = path.resolve(wt.path) === path.resolve(repoRoot);
|
|
52
|
+
let agent = null;
|
|
53
|
+
if (wt.path.includes(".worktrees")) {
|
|
54
|
+
agent = path.basename(wt.path);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
...wt,
|
|
58
|
+
isMain,
|
|
59
|
+
agent,
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
} catch (err) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create a new git worktree for an agent
|
|
69
|
+
*/
|
|
70
|
+
export function createWorktree(repoRoot, { handle, branch, baseBranch = "main" }) {
|
|
71
|
+
if (!repoRoot || !fs.existsSync(repoRoot)) {
|
|
72
|
+
return { ok: false, error: "Invalid repository root" };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const safeHandle = (handle || "agent").trim().toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
76
|
+
const branchName = branch ? branch.trim() : `agent/${safeHandle}`;
|
|
77
|
+
const targetDir = path.join(repoRoot, ".worktrees", safeHandle);
|
|
78
|
+
|
|
79
|
+
if (fs.existsSync(targetDir)) {
|
|
80
|
+
return { ok: false, error: `Worktree directory already exists: ${targetDir}` };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Ensure parent .worktrees dir exists
|
|
84
|
+
const worktreesParent = path.join(repoRoot, ".worktrees");
|
|
85
|
+
if (!fs.existsSync(worktreesParent)) {
|
|
86
|
+
fs.mkdirSync(worktreesParent, { recursive: true });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// Check if branch already exists
|
|
91
|
+
let branchExists = false;
|
|
92
|
+
try {
|
|
93
|
+
execSync(`git show-ref --verify --quiet refs/heads/${branchName}`, {
|
|
94
|
+
cwd: repoRoot,
|
|
95
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
96
|
+
});
|
|
97
|
+
branchExists = true;
|
|
98
|
+
} catch {}
|
|
99
|
+
|
|
100
|
+
const cmd = branchExists
|
|
101
|
+
? `git worktree add "${targetDir}" "${branchName}"`
|
|
102
|
+
: `git worktree add -b "${branchName}" "${targetDir}" "${baseBranch}"`;
|
|
103
|
+
|
|
104
|
+
execSync(cmd, {
|
|
105
|
+
cwd: repoRoot,
|
|
106
|
+
encoding: "utf8",
|
|
107
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
ok: true,
|
|
112
|
+
path: targetDir,
|
|
113
|
+
branch: branchName,
|
|
114
|
+
handle: safeHandle,
|
|
115
|
+
};
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return { ok: false, error: err.message || String(err) };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Remove an existing git worktree
|
|
123
|
+
*/
|
|
124
|
+
export function removeWorktree(repoRoot, { targetPath, force = false }) {
|
|
125
|
+
if (!repoRoot || !fs.existsSync(repoRoot)) {
|
|
126
|
+
return { ok: false, error: "Invalid repository root" };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!targetPath) {
|
|
130
|
+
return { ok: false, error: "Missing target worktree path" };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const resolved = path.resolve(targetPath);
|
|
134
|
+
const mainRepo = path.resolve(repoRoot);
|
|
135
|
+
|
|
136
|
+
if (resolved === mainRepo) {
|
|
137
|
+
return { ok: false, error: "Cannot remove the primary worktree" };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const flag = force ? "--force" : "";
|
|
142
|
+
execSync(`git worktree remove ${flag} "${resolved}"`, {
|
|
143
|
+
cwd: repoRoot,
|
|
144
|
+
encoding: "utf8",
|
|
145
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Prune stale worktree references
|
|
149
|
+
try {
|
|
150
|
+
execSync("git worktree prune", {
|
|
151
|
+
cwd: repoRoot,
|
|
152
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
153
|
+
});
|
|
154
|
+
} catch {}
|
|
155
|
+
|
|
156
|
+
return { ok: true, removed: resolved };
|
|
157
|
+
} catch (err) {
|
|
158
|
+
return { ok: false, error: err.message || String(err) };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Ensure an agent has a dedicated git worktree for safe concurrent execution.
|
|
164
|
+
* If the worktree already exists, returns its information.
|
|
165
|
+
* If not, creates it at .worktrees/<handle>.
|
|
166
|
+
*/
|
|
167
|
+
export function ensureAgentWorktree(repoRoot, handle, branch) {
|
|
168
|
+
if (!repoRoot || !fs.existsSync(repoRoot)) {
|
|
169
|
+
return { ok: false, error: "Invalid repository root" };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const safeHandle = (handle || "agent").trim().toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
173
|
+
const targetDir = path.join(repoRoot, ".worktrees", safeHandle);
|
|
174
|
+
|
|
175
|
+
// Check existing worktrees
|
|
176
|
+
const existing = listWorktrees(repoRoot);
|
|
177
|
+
const found = existing.find(
|
|
178
|
+
(wt) => path.resolve(wt.path) === path.resolve(targetDir) || wt.agent === safeHandle
|
|
179
|
+
);
|
|
180
|
+
if (found) {
|
|
181
|
+
return {
|
|
182
|
+
ok: true,
|
|
183
|
+
path: found.path,
|
|
184
|
+
branch: found.branch || `agent/${safeHandle}`,
|
|
185
|
+
handle: safeHandle,
|
|
186
|
+
existed: true,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// If directory exists on disk, treat as established
|
|
191
|
+
if (fs.existsSync(targetDir)) {
|
|
192
|
+
return {
|
|
193
|
+
ok: true,
|
|
194
|
+
path: targetDir,
|
|
195
|
+
branch: branch || `agent/${safeHandle}`,
|
|
196
|
+
handle: safeHandle,
|
|
197
|
+
existed: true,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Create new worktree
|
|
202
|
+
return createWorktree(repoRoot, {
|
|
203
|
+
handle: safeHandle,
|
|
204
|
+
branch: branch || `agent/${safeHandle}`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Ensure worktrees for an array of agent handles
|
|
210
|
+
*/
|
|
211
|
+
export function ensureAllWorktrees(repoRoot, handles = []) {
|
|
212
|
+
const results = [];
|
|
213
|
+
for (const h of handles) {
|
|
214
|
+
results.push(ensureAgentWorktree(repoRoot, h));
|
|
215
|
+
}
|
|
216
|
+
return results;
|
|
217
|
+
}
|