agentplane 0.1.6 → 0.1.7
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/assets/AGENTS.md +1 -1
- package/assets/agents/ORCHESTRATOR.json +1 -1
- package/assets/agents/UPGRADER.json +1 -1
- package/dist/cli/run-cli.d.ts.map +1 -1
- package/dist/cli/run-cli.js +22 -7
- package/dist/commands/branch/index.d.ts +60 -0
- package/dist/commands/branch/index.d.ts.map +1 -0
- package/dist/commands/branch/index.js +511 -0
- package/dist/commands/guard/index.d.ts +67 -0
- package/dist/commands/guard/index.d.ts.map +1 -0
- package/dist/commands/guard/index.js +367 -0
- package/dist/commands/hooks/index.d.ts +18 -0
- package/dist/commands/hooks/index.d.ts.map +1 -0
- package/dist/commands/hooks/index.js +290 -0
- package/dist/commands/pr/index.d.ts +46 -0
- package/dist/commands/pr/index.d.ts.map +1 -0
- package/dist/commands/pr/index.js +854 -0
- package/dist/commands/shared/git-diff.d.ts +9 -0
- package/dist/commands/shared/git-diff.d.ts.map +1 -0
- package/dist/commands/shared/git-diff.js +41 -0
- package/dist/commands/shared/git-ops.d.ts +24 -0
- package/dist/commands/shared/git-ops.d.ts.map +1 -0
- package/dist/commands/shared/git-ops.js +181 -0
- package/dist/commands/shared/git-worktree.d.ts +8 -0
- package/dist/commands/shared/git-worktree.d.ts.map +1 -0
- package/dist/commands/shared/git-worktree.js +48 -0
- package/dist/commands/shared/git.d.ts +4 -0
- package/dist/commands/shared/git.d.ts.map +1 -0
- package/dist/commands/shared/git.js +14 -0
- package/dist/commands/shared/path.d.ts +3 -0
- package/dist/commands/shared/path.d.ts.map +1 -0
- package/dist/commands/shared/path.js +14 -0
- package/dist/commands/shared/pr-meta.d.ts +21 -0
- package/dist/commands/shared/pr-meta.d.ts.map +1 -0
- package/dist/commands/shared/pr-meta.js +72 -0
- package/dist/commands/shared/task-backend.d.ts +15 -0
- package/dist/commands/shared/task-backend.d.ts.map +1 -0
- package/dist/commands/shared/task-backend.js +55 -0
- package/dist/commands/task/add.d.ts +8 -0
- package/dist/commands/task/add.d.ts.map +1 -0
- package/dist/commands/task/add.js +164 -0
- package/dist/commands/task/block.d.ts +19 -0
- package/dist/commands/task/block.d.ts.map +1 -0
- package/dist/commands/task/block.js +86 -0
- package/dist/commands/task/comment.d.ts +8 -0
- package/dist/commands/task/comment.d.ts.map +1 -0
- package/dist/commands/task/comment.js +29 -0
- package/dist/commands/task/doc.d.ts +17 -0
- package/dist/commands/task/doc.d.ts.map +1 -0
- package/dist/commands/task/doc.js +220 -0
- package/dist/commands/task/export.d.ts +5 -0
- package/dist/commands/task/export.d.ts.map +1 -0
- package/dist/commands/task/export.js +27 -0
- package/dist/commands/task/finish.d.ts +27 -0
- package/dist/commands/task/finish.d.ts.map +1 -0
- package/dist/commands/task/finish.js +131 -0
- package/dist/commands/task/index.d.ts +23 -0
- package/dist/commands/task/index.d.ts.map +1 -0
- package/dist/commands/task/index.js +22 -0
- package/dist/commands/task/lint.d.ts +5 -0
- package/dist/commands/task/lint.d.ts.map +1 -0
- package/dist/commands/task/lint.js +22 -0
- package/dist/commands/task/list.d.ts +11 -0
- package/dist/commands/task/list.d.ts.map +1 -0
- package/dist/commands/task/list.js +54 -0
- package/dist/commands/task/migrate.d.ts +6 -0
- package/dist/commands/task/migrate.d.ts.map +1 -0
- package/dist/commands/task/migrate.js +70 -0
- package/dist/commands/task/new.d.ts +8 -0
- package/dist/commands/task/new.d.ts.map +1 -0
- package/dist/commands/task/new.js +117 -0
- package/dist/commands/task/next.d.ts +6 -0
- package/dist/commands/task/next.d.ts.map +1 -0
- package/dist/commands/task/next.js +45 -0
- package/dist/commands/task/normalize.d.ts +6 -0
- package/dist/commands/task/normalize.d.ts.map +1 -0
- package/dist/commands/task/normalize.js +46 -0
- package/dist/commands/task/ready.d.ts +6 -0
- package/dist/commands/task/ready.d.ts.map +1 -0
- package/dist/commands/task/ready.js +57 -0
- package/dist/commands/task/scaffold.d.ts +8 -0
- package/dist/commands/task/scaffold.d.ts.map +1 -0
- package/dist/commands/task/scaffold.js +131 -0
- package/dist/commands/task/scrub.d.ts +8 -0
- package/dist/commands/task/scrub.d.ts.map +1 -0
- package/dist/commands/task/scrub.js +121 -0
- package/dist/commands/task/search.d.ts +7 -0
- package/dist/commands/task/search.d.ts.map +1 -0
- package/dist/commands/task/search.js +79 -0
- package/dist/commands/task/set-status.d.ts +19 -0
- package/dist/commands/task/set-status.d.ts.map +1 -0
- package/dist/commands/task/set-status.js +123 -0
- package/dist/commands/task/shared.d.ts +46 -0
- package/dist/commands/task/shared.d.ts.map +1 -0
- package/dist/commands/task/shared.js +283 -0
- package/dist/commands/task/show.d.ts +6 -0
- package/dist/commands/task/show.d.ts.map +1 -0
- package/dist/commands/task/show.js +35 -0
- package/dist/commands/task/start.d.ts +19 -0
- package/dist/commands/task/start.d.ts.map +1 -0
- package/dist/commands/task/start.js +109 -0
- package/dist/commands/task/update.d.ts +8 -0
- package/dist/commands/task/update.d.ts.map +1 -0
- package/dist/commands/task/update.js +144 -0
- package/dist/commands/task/verify.d.ts +14 -0
- package/dist/commands/task/verify.d.ts.map +1 -0
- package/dist/commands/task/verify.js +362 -0
- package/dist/commands/workflow.d.ts +5 -364
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +6 -4617
- package/package.json +2 -2
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { extractTaskSuffix, getStagedFiles, getUnstagedFiles, loadConfig, resolveProject, validateCommitSubject, } from "@agentplaneorg/core";
|
|
2
|
+
import { mapCoreError } from "../../cli/error-map.js";
|
|
3
|
+
import { invalidValueMessage, successMessage } from "../../cli/output.js";
|
|
4
|
+
import { CliError } from "../../shared/errors.js";
|
|
5
|
+
import { formatCommentBodyForCommit } from "../../shared/comment-format.js";
|
|
6
|
+
import { execFileAsync } from "../shared/git.js";
|
|
7
|
+
function pathIsUnder(candidate, prefix) {
|
|
8
|
+
if (prefix === "." || prefix === "")
|
|
9
|
+
return true;
|
|
10
|
+
if (candidate === prefix)
|
|
11
|
+
return true;
|
|
12
|
+
return candidate.startsWith(`${prefix}/`);
|
|
13
|
+
}
|
|
14
|
+
function normalizeAllowPrefix(prefix) {
|
|
15
|
+
return prefix.replace(/\/+$/, "");
|
|
16
|
+
}
|
|
17
|
+
export function suggestAllowPrefixes(paths) {
|
|
18
|
+
const out = new Set();
|
|
19
|
+
for (const filePath of paths) {
|
|
20
|
+
if (!filePath)
|
|
21
|
+
continue;
|
|
22
|
+
const idx = filePath.lastIndexOf("/");
|
|
23
|
+
if (idx <= 0)
|
|
24
|
+
out.add(filePath);
|
|
25
|
+
else
|
|
26
|
+
out.add(filePath.slice(0, idx));
|
|
27
|
+
}
|
|
28
|
+
return [...out].toSorted((a, b) => a.localeCompare(b));
|
|
29
|
+
}
|
|
30
|
+
export const GUARD_COMMIT_USAGE = "Usage: agentplane guard commit <task-id> -m <message> --allow <path> [--allow <path>...] [--auto-allow] [--allow-tasks] [--require-clean] [--quiet]";
|
|
31
|
+
export const GUARD_COMMIT_USAGE_EXAMPLE = 'agentplane guard commit 202602030608-F1Q8AB -m "✨ F1Q8AB update" --allow packages/agentplane';
|
|
32
|
+
export const COMMIT_USAGE = "Usage: agentplane commit <task-id> -m <message>";
|
|
33
|
+
export const COMMIT_USAGE_EXAMPLE = 'agentplane commit 202602030608-F1Q8AB -m "✨ F1Q8AB update"';
|
|
34
|
+
async function guardCommitCheck(opts) {
|
|
35
|
+
const resolved = await resolveProject({
|
|
36
|
+
cwd: opts.cwd,
|
|
37
|
+
rootOverride: opts.rootOverride ?? null,
|
|
38
|
+
});
|
|
39
|
+
const loaded = await loadConfig(resolved.agentplaneDir);
|
|
40
|
+
const policy = validateCommitSubject({
|
|
41
|
+
subject: opts.message,
|
|
42
|
+
taskId: opts.taskId,
|
|
43
|
+
genericTokens: loaded.config.commit.generic_tokens,
|
|
44
|
+
});
|
|
45
|
+
if (!policy.ok) {
|
|
46
|
+
throw new CliError({ exitCode: 5, code: "E_GIT", message: policy.errors.join("\n") });
|
|
47
|
+
}
|
|
48
|
+
const staged = await getStagedFiles({ cwd: opts.cwd, rootOverride: opts.rootOverride ?? null });
|
|
49
|
+
if (staged.length === 0) {
|
|
50
|
+
throw new CliError({
|
|
51
|
+
exitCode: 5,
|
|
52
|
+
code: "E_GIT",
|
|
53
|
+
message: "No staged files (git index empty)",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (opts.allow.length === 0) {
|
|
57
|
+
throw new CliError({
|
|
58
|
+
exitCode: 5,
|
|
59
|
+
code: "E_GIT",
|
|
60
|
+
message: "Provide at least one --allow <path> prefix",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const allow = opts.allow.map((prefix) => normalizeAllowPrefix(prefix));
|
|
64
|
+
const denied = new Set();
|
|
65
|
+
if (!opts.allowTasks)
|
|
66
|
+
denied.add(".agentplane/tasks.json");
|
|
67
|
+
if (opts.requireClean) {
|
|
68
|
+
const unstaged = await getUnstagedFiles({
|
|
69
|
+
cwd: opts.cwd,
|
|
70
|
+
rootOverride: opts.rootOverride ?? null,
|
|
71
|
+
});
|
|
72
|
+
if (unstaged.length > 0) {
|
|
73
|
+
throw new CliError({ exitCode: 5, code: "E_GIT", message: "Working tree is dirty" });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const filePath of staged) {
|
|
77
|
+
if (denied.has(filePath)) {
|
|
78
|
+
throw new CliError({
|
|
79
|
+
exitCode: 5,
|
|
80
|
+
code: "E_GIT",
|
|
81
|
+
message: `Staged file is forbidden by default: ${filePath} (use --allow-tasks to override)`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (!allow.some((prefix) => pathIsUnder(filePath, prefix))) {
|
|
85
|
+
throw new CliError({
|
|
86
|
+
exitCode: 5,
|
|
87
|
+
code: "E_GIT",
|
|
88
|
+
message: `Staged file is outside allowlist: ${filePath}`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export async function gitStatusChangedPaths(opts) {
|
|
94
|
+
const resolved = await resolveProject({
|
|
95
|
+
cwd: opts.cwd,
|
|
96
|
+
rootOverride: opts.rootOverride ?? null,
|
|
97
|
+
});
|
|
98
|
+
const { stdout } = await execFileAsync("git", ["status", "--porcelain", "-uall"], {
|
|
99
|
+
cwd: resolved.gitRoot,
|
|
100
|
+
});
|
|
101
|
+
const files = [];
|
|
102
|
+
for (const line of stdout.split("\n")) {
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
if (!trimmed)
|
|
105
|
+
continue;
|
|
106
|
+
const filePart = trimmed.slice(2).trim();
|
|
107
|
+
if (!filePart)
|
|
108
|
+
continue;
|
|
109
|
+
const name = filePart.includes("->") ? filePart.split("->").at(-1)?.trim() : filePart;
|
|
110
|
+
if (name)
|
|
111
|
+
files.push(name);
|
|
112
|
+
}
|
|
113
|
+
return files;
|
|
114
|
+
}
|
|
115
|
+
export async function ensureGitClean(opts) {
|
|
116
|
+
const staged = await getStagedFiles({ cwd: opts.cwd, rootOverride: opts.rootOverride ?? null });
|
|
117
|
+
if (staged.length > 0) {
|
|
118
|
+
throw new CliError({ exitCode: 5, code: "E_GIT", message: "Working tree has staged changes" });
|
|
119
|
+
}
|
|
120
|
+
const unstaged = await getUnstagedFiles({
|
|
121
|
+
cwd: opts.cwd,
|
|
122
|
+
rootOverride: opts.rootOverride ?? null,
|
|
123
|
+
});
|
|
124
|
+
if (unstaged.length > 0) {
|
|
125
|
+
throw new CliError({
|
|
126
|
+
exitCode: 5,
|
|
127
|
+
code: "E_GIT",
|
|
128
|
+
message: "Working tree has unstaged changes",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function stageAllowlist(opts) {
|
|
133
|
+
const resolved = await resolveProject({
|
|
134
|
+
cwd: opts.cwd,
|
|
135
|
+
rootOverride: opts.rootOverride ?? null,
|
|
136
|
+
});
|
|
137
|
+
const changed = await gitStatusChangedPaths({ cwd: opts.cwd, rootOverride: opts.rootOverride });
|
|
138
|
+
if (changed.length === 0) {
|
|
139
|
+
throw new CliError({
|
|
140
|
+
exitCode: 2,
|
|
141
|
+
code: "E_USAGE",
|
|
142
|
+
message: "No changes to stage (working tree clean)",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const allow = opts.allow.map((prefix) => normalizeAllowPrefix(prefix.trim().replace(/^\.?\//, "")));
|
|
146
|
+
const denied = new Set();
|
|
147
|
+
if (!opts.allowTasks)
|
|
148
|
+
denied.add(".agentplane/tasks.json");
|
|
149
|
+
const staged = [];
|
|
150
|
+
for (const filePath of changed) {
|
|
151
|
+
if (denied.has(filePath))
|
|
152
|
+
continue;
|
|
153
|
+
if (allow.some((prefix) => pathIsUnder(filePath, prefix))) {
|
|
154
|
+
staged.push(filePath);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const unique = [...new Set(staged)].toSorted((a, b) => a.localeCompare(b));
|
|
158
|
+
if (unique.length === 0) {
|
|
159
|
+
throw new CliError({
|
|
160
|
+
exitCode: 2,
|
|
161
|
+
code: "E_USAGE",
|
|
162
|
+
message: "No changes matched allowed prefixes (use --commit-auto-allow or update --commit-allow)",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
await execFileAsync("git", ["add", "--", ...unique], { cwd: resolved.gitRoot });
|
|
166
|
+
return unique;
|
|
167
|
+
}
|
|
168
|
+
function deriveCommitMessageFromComment(opts) {
|
|
169
|
+
const summary = (opts.formattedComment ?? formatCommentBodyForCommit(opts.body, opts.config))
|
|
170
|
+
.trim()
|
|
171
|
+
.replaceAll(/\s+/g, " ");
|
|
172
|
+
if (!summary) {
|
|
173
|
+
throw new CliError({
|
|
174
|
+
exitCode: 2,
|
|
175
|
+
code: "E_USAGE",
|
|
176
|
+
message: "Comment body is required to build a commit message from the task comment",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
const prefix = opts.emoji.trim();
|
|
180
|
+
if (!prefix) {
|
|
181
|
+
throw new CliError({
|
|
182
|
+
exitCode: 2,
|
|
183
|
+
code: "E_USAGE",
|
|
184
|
+
message: "Emoji prefix is required when deriving commit messages from task comments",
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const suffix = extractTaskSuffix(opts.taskId);
|
|
188
|
+
if (!suffix) {
|
|
189
|
+
throw new CliError({
|
|
190
|
+
exitCode: 2,
|
|
191
|
+
code: "E_USAGE",
|
|
192
|
+
message: invalidValueMessage("task id", opts.taskId, "valid task id"),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return `${prefix} ${suffix} ${summary}`;
|
|
196
|
+
}
|
|
197
|
+
export async function commitFromComment(opts) {
|
|
198
|
+
let allowPrefixes = opts.allow.map((prefix) => prefix.trim()).filter(Boolean);
|
|
199
|
+
if (opts.autoAllow && allowPrefixes.length === 0) {
|
|
200
|
+
const changed = await gitStatusChangedPaths({ cwd: opts.cwd, rootOverride: opts.rootOverride });
|
|
201
|
+
allowPrefixes = suggestAllowPrefixes(changed);
|
|
202
|
+
}
|
|
203
|
+
if (allowPrefixes.length === 0) {
|
|
204
|
+
throw new CliError({
|
|
205
|
+
exitCode: 2,
|
|
206
|
+
code: "E_USAGE",
|
|
207
|
+
message: "Provide at least one --commit-allow prefix or enable --commit-auto-allow",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
const staged = await stageAllowlist({
|
|
211
|
+
cwd: opts.cwd,
|
|
212
|
+
rootOverride: opts.rootOverride,
|
|
213
|
+
allow: allowPrefixes,
|
|
214
|
+
allowTasks: opts.allowTasks,
|
|
215
|
+
});
|
|
216
|
+
const message = deriveCommitMessageFromComment({
|
|
217
|
+
taskId: opts.taskId,
|
|
218
|
+
body: opts.commentBody,
|
|
219
|
+
emoji: opts.emoji,
|
|
220
|
+
formattedComment: opts.formattedComment,
|
|
221
|
+
config: opts.config,
|
|
222
|
+
});
|
|
223
|
+
await guardCommitCheck({
|
|
224
|
+
cwd: opts.cwd,
|
|
225
|
+
rootOverride: opts.rootOverride,
|
|
226
|
+
taskId: opts.taskId,
|
|
227
|
+
message,
|
|
228
|
+
allow: allowPrefixes,
|
|
229
|
+
allowTasks: opts.allowTasks,
|
|
230
|
+
requireClean: opts.requireClean,
|
|
231
|
+
quiet: opts.quiet,
|
|
232
|
+
});
|
|
233
|
+
const resolved = await resolveProject({
|
|
234
|
+
cwd: opts.cwd,
|
|
235
|
+
rootOverride: opts.rootOverride ?? null,
|
|
236
|
+
});
|
|
237
|
+
const env = {
|
|
238
|
+
...process.env,
|
|
239
|
+
AGENTPLANE_TASK_ID: opts.taskId,
|
|
240
|
+
AGENTPLANE_ALLOW_TASKS: opts.allowTasks ? "1" : "0",
|
|
241
|
+
AGENTPLANE_ALLOW_BASE: opts.allowTasks ? "1" : "0",
|
|
242
|
+
};
|
|
243
|
+
await execFileAsync("git", ["commit", "-m", message], { cwd: resolved.gitRoot, env });
|
|
244
|
+
const { stdout } = await execFileAsync("git", ["log", "-1", "--pretty=%H:%s"], {
|
|
245
|
+
cwd: resolved.gitRoot,
|
|
246
|
+
});
|
|
247
|
+
const trimmed = stdout.trim();
|
|
248
|
+
const [hash, subject] = trimmed.split(":", 2);
|
|
249
|
+
if (!opts.quiet) {
|
|
250
|
+
process.stdout.write(`${successMessage("committed", `${hash?.slice(0, 12) ?? ""} ${subject ?? ""}`.trim(), `staged=${staged.join(", ")}`)}\n`);
|
|
251
|
+
}
|
|
252
|
+
return { hash: hash ?? "", message: subject ?? "", staged };
|
|
253
|
+
}
|
|
254
|
+
export async function cmdGuardClean(opts) {
|
|
255
|
+
try {
|
|
256
|
+
const staged = await getStagedFiles({ cwd: opts.cwd, rootOverride: opts.rootOverride ?? null });
|
|
257
|
+
if (staged.length > 0) {
|
|
258
|
+
throw new CliError({
|
|
259
|
+
exitCode: 5,
|
|
260
|
+
code: "E_GIT",
|
|
261
|
+
message: "Staged files exist",
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
if (!opts.quiet) {
|
|
265
|
+
process.stdout.write(`${successMessage("index clean", undefined, "no staged files")}\n`);
|
|
266
|
+
}
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
if (err instanceof CliError)
|
|
271
|
+
throw err;
|
|
272
|
+
throw mapCoreError(err, { command: "guard clean", root: opts.rootOverride ?? null });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
export async function cmdGuardSuggestAllow(opts) {
|
|
276
|
+
try {
|
|
277
|
+
const staged = await getStagedFiles({ cwd: opts.cwd, rootOverride: opts.rootOverride ?? null });
|
|
278
|
+
if (staged.length === 0) {
|
|
279
|
+
throw new CliError({
|
|
280
|
+
exitCode: 2,
|
|
281
|
+
code: "E_USAGE",
|
|
282
|
+
message: "No staged files (git index empty)",
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
const prefixes = suggestAllowPrefixes(staged);
|
|
286
|
+
if (opts.format === "args") {
|
|
287
|
+
const args = prefixes.map((p) => `--allow ${p}`).join(" ");
|
|
288
|
+
process.stdout.write(`${args}\n`);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
for (const prefix of prefixes)
|
|
292
|
+
process.stdout.write(`${prefix}\n`);
|
|
293
|
+
}
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
throw mapCoreError(err, { command: "guard suggest-allow", root: opts.rootOverride ?? null });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
export async function cmdGuardCommit(opts) {
|
|
301
|
+
try {
|
|
302
|
+
await guardCommitCheck(opts);
|
|
303
|
+
if (!opts.quiet)
|
|
304
|
+
process.stdout.write("OK\n");
|
|
305
|
+
return 0;
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
if (err instanceof CliError)
|
|
309
|
+
throw err;
|
|
310
|
+
throw mapCoreError(err, { command: "guard commit", root: opts.rootOverride ?? null });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
export async function cmdCommit(opts) {
|
|
314
|
+
try {
|
|
315
|
+
let allow = opts.allow;
|
|
316
|
+
if (opts.autoAllow && allow.length === 0) {
|
|
317
|
+
const staged = await getStagedFiles({
|
|
318
|
+
cwd: opts.cwd,
|
|
319
|
+
rootOverride: opts.rootOverride ?? null,
|
|
320
|
+
});
|
|
321
|
+
const prefixes = suggestAllowPrefixes(staged);
|
|
322
|
+
if (prefixes.length === 0) {
|
|
323
|
+
throw new CliError({
|
|
324
|
+
exitCode: 5,
|
|
325
|
+
code: "E_GIT",
|
|
326
|
+
message: "No staged files (git index empty)",
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
allow = prefixes;
|
|
330
|
+
}
|
|
331
|
+
await guardCommitCheck({
|
|
332
|
+
cwd: opts.cwd,
|
|
333
|
+
rootOverride: opts.rootOverride,
|
|
334
|
+
taskId: opts.taskId,
|
|
335
|
+
message: opts.message,
|
|
336
|
+
allow,
|
|
337
|
+
allowTasks: opts.allowTasks,
|
|
338
|
+
requireClean: opts.requireClean,
|
|
339
|
+
quiet: opts.quiet,
|
|
340
|
+
});
|
|
341
|
+
const resolved = await resolveProject({
|
|
342
|
+
cwd: opts.cwd,
|
|
343
|
+
rootOverride: opts.rootOverride ?? null,
|
|
344
|
+
});
|
|
345
|
+
const env = {
|
|
346
|
+
...process.env,
|
|
347
|
+
AGENTPLANE_TASK_ID: opts.taskId,
|
|
348
|
+
AGENTPLANE_ALLOW_TASKS: opts.allowTasks ? "1" : "0",
|
|
349
|
+
AGENTPLANE_ALLOW_BASE: opts.allowBase ? "1" : "0",
|
|
350
|
+
};
|
|
351
|
+
await execFileAsync("git", ["commit", "-m", opts.message], { cwd: resolved.gitRoot, env });
|
|
352
|
+
if (!opts.quiet) {
|
|
353
|
+
const { stdout } = await execFileAsync("git", ["log", "-1", "--pretty=%H:%s"], {
|
|
354
|
+
cwd: resolved.gitRoot,
|
|
355
|
+
});
|
|
356
|
+
const trimmed = stdout.trim();
|
|
357
|
+
const [hash, subject] = trimmed.split(":", 2);
|
|
358
|
+
process.stdout.write(`${successMessage("committed", `${hash?.slice(0, 12) ?? ""} ${subject ?? ""}`.trim())}\n`);
|
|
359
|
+
}
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
if (err instanceof CliError)
|
|
364
|
+
throw err;
|
|
365
|
+
throw mapCoreError(err, { command: "commit", root: opts.rootOverride ?? null });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const HOOK_NAMES: readonly ["commit-msg", "pre-commit", "pre-push"];
|
|
2
|
+
export declare function cmdHooksInstall(opts: {
|
|
3
|
+
cwd: string;
|
|
4
|
+
rootOverride?: string;
|
|
5
|
+
quiet: boolean;
|
|
6
|
+
}): Promise<number>;
|
|
7
|
+
export declare function cmdHooksUninstall(opts: {
|
|
8
|
+
cwd: string;
|
|
9
|
+
rootOverride?: string;
|
|
10
|
+
quiet: boolean;
|
|
11
|
+
}): Promise<number>;
|
|
12
|
+
export declare function cmdHooksRun(opts: {
|
|
13
|
+
cwd: string;
|
|
14
|
+
rootOverride?: string;
|
|
15
|
+
hook: (typeof HOOK_NAMES)[number];
|
|
16
|
+
args: string[];
|
|
17
|
+
}): Promise<number>;
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/hooks/index.ts"],"names":[],"mappings":"AAeA,eAAO,MAAM,UAAU,mDAAoD,CAAC;AAqG5E,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CAmClB;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CA4BlB;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CA2HlB"}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getStagedFiles, loadConfig, resolveBaseBranch, resolveProject } from "@agentplaneorg/core";
|
|
4
|
+
import { loadTaskBackend } from "../../backends/task-backend.js";
|
|
5
|
+
import { mapBackendError, mapCoreError } from "../../cli/error-map.js";
|
|
6
|
+
import { fileExists } from "../../cli/fs-utils.js";
|
|
7
|
+
import { infoMessage, successMessage } from "../../cli/output.js";
|
|
8
|
+
import { CliError } from "../../shared/errors.js";
|
|
9
|
+
import { gitCurrentBranch, gitRevParse } from "../shared/git-ops.js";
|
|
10
|
+
import { isPathWithin } from "../shared/path.js";
|
|
11
|
+
const HOOK_MARKER = "agentplane-hook";
|
|
12
|
+
const SHIM_MARKER = "agentplane-hook-shim";
|
|
13
|
+
export const HOOK_NAMES = ["commit-msg", "pre-commit", "pre-push"];
|
|
14
|
+
async function resolveGitHooksDir(cwd) {
|
|
15
|
+
const repoRoot = await gitRevParse(cwd, ["--show-toplevel"]);
|
|
16
|
+
const commonDirRaw = await gitRevParse(cwd, ["--git-common-dir"]);
|
|
17
|
+
const hooksRaw = await gitRevParse(cwd, ["--git-path", "hooks"]);
|
|
18
|
+
const commonDir = path.resolve(path.isAbsolute(commonDirRaw) ? commonDirRaw : path.join(repoRoot, commonDirRaw));
|
|
19
|
+
const hooksDir = path.resolve(path.isAbsolute(hooksRaw) ? hooksRaw : path.join(repoRoot, hooksRaw));
|
|
20
|
+
const resolvedRoot = path.resolve(repoRoot);
|
|
21
|
+
if (!isPathWithin(resolvedRoot, hooksDir) && !isPathWithin(commonDir, hooksDir)) {
|
|
22
|
+
throw new CliError({
|
|
23
|
+
exitCode: 5,
|
|
24
|
+
code: "E_GIT",
|
|
25
|
+
message: [
|
|
26
|
+
"Refusing to manage git hooks outside the repository.",
|
|
27
|
+
`hooks_path=${hooksDir}`,
|
|
28
|
+
`repo_root=${resolvedRoot}`,
|
|
29
|
+
`common_dir=${commonDir}`,
|
|
30
|
+
"Fix:",
|
|
31
|
+
" 1) Use a repo-relative core.hooksPath (e.g., .git/hooks)",
|
|
32
|
+
" 2) Re-run `agentplane hooks install`",
|
|
33
|
+
].join("\n"),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return hooksDir;
|
|
37
|
+
}
|
|
38
|
+
async function fileIsManaged(filePath, marker) {
|
|
39
|
+
try {
|
|
40
|
+
const content = await readFile(filePath, "utf8");
|
|
41
|
+
return content.includes(marker);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function hookScriptText(hook) {
|
|
48
|
+
return [
|
|
49
|
+
"#!/usr/bin/env sh",
|
|
50
|
+
`# ${HOOK_MARKER} (do not edit)`,
|
|
51
|
+
"set -e",
|
|
52
|
+
"if ! command -v agentplane >/dev/null 2>&1; then",
|
|
53
|
+
' echo "agentplane hooks: agentplane not found in PATH" >&2',
|
|
54
|
+
" exit 1",
|
|
55
|
+
"fi",
|
|
56
|
+
"exec agentplane hooks run " + hook + ' "$@"',
|
|
57
|
+
"",
|
|
58
|
+
].join("\n");
|
|
59
|
+
}
|
|
60
|
+
function shimScriptText() {
|
|
61
|
+
return [
|
|
62
|
+
"#!/usr/bin/env sh",
|
|
63
|
+
`# ${SHIM_MARKER} (do not edit)`,
|
|
64
|
+
"set -e",
|
|
65
|
+
"if ! command -v agentplane >/dev/null 2>&1; then",
|
|
66
|
+
' echo "agentplane shim: agentplane not found in PATH" >&2',
|
|
67
|
+
" exit 1",
|
|
68
|
+
"fi",
|
|
69
|
+
'exec agentplane "$@"',
|
|
70
|
+
"",
|
|
71
|
+
].join("\n");
|
|
72
|
+
}
|
|
73
|
+
async function ensureShim(agentplaneDir, gitRoot) {
|
|
74
|
+
const shimDir = path.join(agentplaneDir, "bin");
|
|
75
|
+
const shimPath = path.join(shimDir, "agentplane");
|
|
76
|
+
await mkdir(shimDir, { recursive: true });
|
|
77
|
+
if (await fileExists(shimPath)) {
|
|
78
|
+
const managed = await fileIsManaged(shimPath, SHIM_MARKER);
|
|
79
|
+
if (!managed) {
|
|
80
|
+
throw new CliError({
|
|
81
|
+
exitCode: 5,
|
|
82
|
+
code: "E_GIT",
|
|
83
|
+
message: `Refusing to overwrite existing shim: ${path.relative(gitRoot, shimPath)}`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
await writeFile(shimPath, shimScriptText(), "utf8");
|
|
88
|
+
await chmod(shimPath, 0o755);
|
|
89
|
+
}
|
|
90
|
+
function readCommitSubject(message) {
|
|
91
|
+
for (const line of message.split("\n")) {
|
|
92
|
+
const trimmed = line.trim();
|
|
93
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
94
|
+
continue;
|
|
95
|
+
return trimmed;
|
|
96
|
+
}
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
function subjectHasSuffix(subject, suffixes) {
|
|
100
|
+
const lowered = subject.toLowerCase();
|
|
101
|
+
return suffixes.some((suffix) => suffix && lowered.includes(suffix.toLowerCase()));
|
|
102
|
+
}
|
|
103
|
+
export async function cmdHooksInstall(opts) {
|
|
104
|
+
try {
|
|
105
|
+
const resolved = await resolveProject({
|
|
106
|
+
cwd: opts.cwd,
|
|
107
|
+
rootOverride: opts.rootOverride ?? null,
|
|
108
|
+
});
|
|
109
|
+
const hooksDir = await resolveGitHooksDir(resolved.gitRoot);
|
|
110
|
+
await mkdir(hooksDir, { recursive: true });
|
|
111
|
+
await mkdir(resolved.agentplaneDir, { recursive: true });
|
|
112
|
+
await ensureShim(resolved.agentplaneDir, resolved.gitRoot);
|
|
113
|
+
for (const hook of HOOK_NAMES) {
|
|
114
|
+
const hookPath = path.join(hooksDir, hook);
|
|
115
|
+
if (await fileExists(hookPath)) {
|
|
116
|
+
const managed = await fileIsManaged(hookPath, HOOK_MARKER);
|
|
117
|
+
if (!managed) {
|
|
118
|
+
throw new CliError({
|
|
119
|
+
exitCode: 5,
|
|
120
|
+
code: "E_GIT",
|
|
121
|
+
message: `Refusing to overwrite existing hook: ${path.relative(resolved.gitRoot, hookPath)}`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
await writeFile(hookPath, hookScriptText(hook), "utf8");
|
|
126
|
+
await chmod(hookPath, 0o755);
|
|
127
|
+
}
|
|
128
|
+
if (!opts.quiet) {
|
|
129
|
+
process.stdout.write(`${path.relative(resolved.gitRoot, hooksDir)}\n`);
|
|
130
|
+
}
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
if (err instanceof CliError)
|
|
135
|
+
throw err;
|
|
136
|
+
throw mapCoreError(err, { command: "hooks install", root: opts.rootOverride ?? null });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export async function cmdHooksUninstall(opts) {
|
|
140
|
+
try {
|
|
141
|
+
const resolved = await resolveProject({
|
|
142
|
+
cwd: opts.cwd,
|
|
143
|
+
rootOverride: opts.rootOverride ?? null,
|
|
144
|
+
});
|
|
145
|
+
const hooksDir = await resolveGitHooksDir(resolved.gitRoot);
|
|
146
|
+
let removed = 0;
|
|
147
|
+
for (const hook of HOOK_NAMES) {
|
|
148
|
+
const hookPath = path.join(hooksDir, hook);
|
|
149
|
+
if (!(await fileExists(hookPath)))
|
|
150
|
+
continue;
|
|
151
|
+
const managed = await fileIsManaged(hookPath, HOOK_MARKER);
|
|
152
|
+
if (!managed)
|
|
153
|
+
continue;
|
|
154
|
+
await rm(hookPath, { force: true });
|
|
155
|
+
removed++;
|
|
156
|
+
}
|
|
157
|
+
if (!opts.quiet) {
|
|
158
|
+
process.stdout.write(removed > 0
|
|
159
|
+
? `${successMessage("removed hooks", undefined, `count=${removed}`)}\n`
|
|
160
|
+
: `${infoMessage("no agentplane hooks found")}\n`);
|
|
161
|
+
}
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
if (err instanceof CliError)
|
|
166
|
+
throw err;
|
|
167
|
+
throw mapCoreError(err, { command: "hooks uninstall", root: opts.rootOverride ?? null });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export async function cmdHooksRun(opts) {
|
|
171
|
+
try {
|
|
172
|
+
if (opts.hook === "commit-msg") {
|
|
173
|
+
const messagePath = opts.args[0];
|
|
174
|
+
if (!messagePath) {
|
|
175
|
+
throw new CliError({
|
|
176
|
+
exitCode: 2,
|
|
177
|
+
code: "E_USAGE",
|
|
178
|
+
message: "Missing commit message file path",
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
const raw = await readFile(messagePath, "utf8");
|
|
182
|
+
const subject = readCommitSubject(raw);
|
|
183
|
+
if (!subject) {
|
|
184
|
+
throw new CliError({
|
|
185
|
+
exitCode: 5,
|
|
186
|
+
code: "E_GIT",
|
|
187
|
+
message: "Commit message subject is empty",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const taskId = (process.env.AGENTPLANE_TASK_ID ?? "").trim();
|
|
191
|
+
if (taskId) {
|
|
192
|
+
const suffix = taskId.split("-").at(-1) ?? "";
|
|
193
|
+
if (!subject.includes(taskId) && (suffix.length === 0 || !subject.includes(suffix))) {
|
|
194
|
+
throw new CliError({
|
|
195
|
+
exitCode: 5,
|
|
196
|
+
code: "E_GIT",
|
|
197
|
+
message: "Commit subject must include task id or suffix",
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
const { backend } = await loadTaskBackend({
|
|
203
|
+
cwd: opts.cwd,
|
|
204
|
+
rootOverride: opts.rootOverride ?? null,
|
|
205
|
+
});
|
|
206
|
+
const tasks = await backend.listTasks();
|
|
207
|
+
const suffixes = tasks.map((task) => task.id.split("-").at(-1) ?? "").filter(Boolean);
|
|
208
|
+
if (suffixes.length === 0) {
|
|
209
|
+
throw new CliError({
|
|
210
|
+
exitCode: 5,
|
|
211
|
+
code: "E_GIT",
|
|
212
|
+
message: "No task IDs available to validate commit subject",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (!subjectHasSuffix(subject, suffixes)) {
|
|
216
|
+
throw new CliError({
|
|
217
|
+
exitCode: 5,
|
|
218
|
+
code: "E_GIT",
|
|
219
|
+
message: "Commit subject must mention a task suffix",
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
if (opts.hook === "pre-commit") {
|
|
225
|
+
const staged = await getStagedFiles({
|
|
226
|
+
cwd: opts.cwd,
|
|
227
|
+
rootOverride: opts.rootOverride ?? null,
|
|
228
|
+
});
|
|
229
|
+
if (staged.length === 0)
|
|
230
|
+
return 0;
|
|
231
|
+
const allowTasks = (process.env.AGENTPLANE_ALLOW_TASKS ?? "").trim() === "1";
|
|
232
|
+
const allowBase = (process.env.AGENTPLANE_ALLOW_BASE ?? "").trim() === "1";
|
|
233
|
+
const resolved = await resolveProject({
|
|
234
|
+
cwd: opts.cwd,
|
|
235
|
+
rootOverride: opts.rootOverride ?? null,
|
|
236
|
+
});
|
|
237
|
+
const loaded = await loadConfig(resolved.agentplaneDir);
|
|
238
|
+
const tasksPath = loaded.config.paths.tasks_path;
|
|
239
|
+
const tasksStaged = staged.includes(tasksPath);
|
|
240
|
+
const nonTasks = staged.filter((entry) => entry !== tasksPath);
|
|
241
|
+
if (tasksStaged && !allowTasks) {
|
|
242
|
+
throw new CliError({
|
|
243
|
+
exitCode: 5,
|
|
244
|
+
code: "E_GIT",
|
|
245
|
+
message: `${tasksPath} is protected by agentplane hooks (set AGENTPLANE_ALLOW_TASKS=1 to override)`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
if (loaded.config.workflow_mode === "branch_pr") {
|
|
249
|
+
const baseBranch = await resolveBaseBranch({
|
|
250
|
+
cwd: opts.cwd,
|
|
251
|
+
rootOverride: opts.rootOverride ?? null,
|
|
252
|
+
cliBaseOpt: null,
|
|
253
|
+
mode: loaded.config.workflow_mode,
|
|
254
|
+
});
|
|
255
|
+
if (!baseBranch) {
|
|
256
|
+
throw new CliError({
|
|
257
|
+
exitCode: 2,
|
|
258
|
+
code: "E_USAGE",
|
|
259
|
+
message: "Base branch could not be resolved (use `agentplane branch base set`).",
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
const currentBranch = await gitCurrentBranch(resolved.gitRoot);
|
|
263
|
+
if (tasksStaged && currentBranch !== baseBranch) {
|
|
264
|
+
throw new CliError({
|
|
265
|
+
exitCode: 5,
|
|
266
|
+
code: "E_GIT",
|
|
267
|
+
message: `${tasksPath} commits are allowed only on ${baseBranch} in branch_pr mode`,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (nonTasks.length > 0 && currentBranch === baseBranch && !allowBase) {
|
|
271
|
+
throw new CliError({
|
|
272
|
+
exitCode: 5,
|
|
273
|
+
code: "E_GIT",
|
|
274
|
+
message: `Code commits are forbidden on ${baseBranch} in branch_pr mode`,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
if (err instanceof CliError)
|
|
284
|
+
throw err;
|
|
285
|
+
throw mapBackendError(err, {
|
|
286
|
+
command: `hooks run ${opts.hook}`,
|
|
287
|
+
root: opts.rootOverride ?? null,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|