@codebyplan/cli 2.0.2 → 2.2.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.
- package/README.md +11 -0
- package/dist/cli.js +437 -101
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,6 +71,17 @@ claude mcp add codebyplan -e CODEBYPLAN_API_KEY=your_key_here -- npx -y @codebyp
|
|
|
71
71
|
|------|-------------|
|
|
72
72
|
| `health_check` | Check server version, API connectivity, and latency |
|
|
73
73
|
|
|
74
|
+
### Settings File Model
|
|
75
|
+
|
|
76
|
+
The `sync_claude_files` tool writes a `settings.json` file to the project's `.claude/` directory. This file contains shared settings (hooks, statusLine, attribution, permissions.deny/ask/additionalDirectories) merged from global and repo-specific scopes in the database.
|
|
77
|
+
|
|
78
|
+
A separate `settings.local.json` file (not managed by sync) holds local-only configuration: `permissions.allow` and MCP server definitions. Claude Code merges both files at runtime, with `settings.local.json` taking precedence.
|
|
79
|
+
|
|
80
|
+
| File | Managed By | Contains |
|
|
81
|
+
|------|------------|----------|
|
|
82
|
+
| `settings.json` | `sync_claude_files` (synced from DB) | Shared settings: hooks, statusLine, attribution, permissions.deny/ask/additionalDirectories |
|
|
83
|
+
| `settings.local.json` | User (local-only) | Machine-specific: permissions.allow, MCP server config |
|
|
84
|
+
|
|
74
85
|
## Environment Variables
|
|
75
86
|
|
|
76
87
|
| Variable | Required | Description |
|
package/dist/cli.js
CHANGED
|
@@ -37,7 +37,7 @@ var VERSION, PACKAGE_NAME;
|
|
|
37
37
|
var init_version = __esm({
|
|
38
38
|
"src/lib/version.ts"() {
|
|
39
39
|
"use strict";
|
|
40
|
-
VERSION = "2.0
|
|
40
|
+
VERSION = "2.2.0";
|
|
41
41
|
PACKAGE_NAME = "@codebyplan/cli";
|
|
42
42
|
}
|
|
43
43
|
});
|
|
@@ -384,7 +384,36 @@ function mergeSettings(template, local) {
|
|
|
384
384
|
}
|
|
385
385
|
return merged;
|
|
386
386
|
}
|
|
387
|
-
|
|
387
|
+
function mergeGlobalAndRepoSettings(global, repo) {
|
|
388
|
+
const merged = { ...global, ...repo };
|
|
389
|
+
const globalPerms = global.permissions && typeof global.permissions === "object" ? global.permissions : {};
|
|
390
|
+
const repoPerms = repo.permissions && typeof repo.permissions === "object" ? repo.permissions : {};
|
|
391
|
+
if (Object.keys(globalPerms).length > 0 || Object.keys(repoPerms).length > 0) {
|
|
392
|
+
const mergedPerms = { ...globalPerms, ...repoPerms };
|
|
393
|
+
for (const key of ARRAY_PERMISSION_KEYS) {
|
|
394
|
+
const globalArr = Array.isArray(globalPerms[key]) ? globalPerms[key] : [];
|
|
395
|
+
const repoArr = Array.isArray(repoPerms[key]) ? repoPerms[key] : [];
|
|
396
|
+
if (globalArr.length > 0 || repoArr.length > 0) {
|
|
397
|
+
mergedPerms[key] = [.../* @__PURE__ */ new Set([...globalArr, ...repoArr])];
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
merged.permissions = mergedPerms;
|
|
401
|
+
}
|
|
402
|
+
return merged;
|
|
403
|
+
}
|
|
404
|
+
function stripPermissionsAllow(settings) {
|
|
405
|
+
if (!settings.permissions || typeof settings.permissions !== "object") {
|
|
406
|
+
return settings;
|
|
407
|
+
}
|
|
408
|
+
const perms = { ...settings.permissions };
|
|
409
|
+
delete perms.allow;
|
|
410
|
+
if (Object.keys(perms).length === 0) {
|
|
411
|
+
const { permissions: _, ...rest } = settings;
|
|
412
|
+
return rest;
|
|
413
|
+
}
|
|
414
|
+
return { ...settings, permissions: perms };
|
|
415
|
+
}
|
|
416
|
+
var TEMPLATE_MANAGED_KEYS, TEMPLATE_MANAGED_PERMISSION_KEYS, ARRAY_PERMISSION_KEYS;
|
|
388
417
|
var init_settings_merge = __esm({
|
|
389
418
|
"src/lib/settings-merge.ts"() {
|
|
390
419
|
"use strict";
|
|
@@ -398,6 +427,7 @@ var init_settings_merge = __esm({
|
|
|
398
427
|
"ask",
|
|
399
428
|
"additionalDirectories"
|
|
400
429
|
];
|
|
430
|
+
ARRAY_PERMISSION_KEYS = ["deny", "ask"];
|
|
401
431
|
}
|
|
402
432
|
});
|
|
403
433
|
|
|
@@ -460,6 +490,25 @@ function mergeDiscoveredHooks(existing, discovered, hooksRelPath = ".claude/hook
|
|
|
460
490
|
}
|
|
461
491
|
return merged;
|
|
462
492
|
}
|
|
493
|
+
function stripDiscoveredHooks(config2, hooksRelPath = ".claude/hooks") {
|
|
494
|
+
const prefix = `bash ${hooksRelPath}/`;
|
|
495
|
+
const stripped = {};
|
|
496
|
+
for (const [event, matchers] of Object.entries(config2)) {
|
|
497
|
+
const filteredMatchers = [];
|
|
498
|
+
for (const matcher of matchers) {
|
|
499
|
+
const filteredHooks = matcher.hooks.filter(
|
|
500
|
+
(h) => !(h.command && h.command.startsWith(prefix) && h.command.endsWith(".sh"))
|
|
501
|
+
);
|
|
502
|
+
if (filteredHooks.length > 0) {
|
|
503
|
+
filteredMatchers.push({ matcher: matcher.matcher, hooks: filteredHooks });
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (filteredMatchers.length > 0) {
|
|
507
|
+
stripped[event] = filteredMatchers;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return stripped;
|
|
511
|
+
}
|
|
463
512
|
var init_hook_registry = __esm({
|
|
464
513
|
"src/lib/hook-registry.ts"() {
|
|
465
514
|
"use strict";
|
|
@@ -687,9 +736,15 @@ async function executeSyncToLocal(options) {
|
|
|
687
736
|
}
|
|
688
737
|
byType[typeName] = result;
|
|
689
738
|
}
|
|
739
|
+
const globalSettingsFiles = syncData.global_settings ?? [];
|
|
740
|
+
let globalSettings = {};
|
|
741
|
+
for (const gf of globalSettingsFiles) {
|
|
742
|
+
const parsed = JSON.parse(substituteVariables(gf.content, repoData));
|
|
743
|
+
globalSettings = { ...globalSettings, ...parsed };
|
|
744
|
+
}
|
|
690
745
|
const specialTypes = {
|
|
691
746
|
claude_md: () => join4(projectPath, "CLAUDE.md"),
|
|
692
|
-
settings: () => join4(projectPath, ".claude", "settings.
|
|
747
|
+
settings: () => join4(projectPath, ".claude", "settings.json")
|
|
693
748
|
};
|
|
694
749
|
for (const [typeName, getPath] of Object.entries(specialTypes)) {
|
|
695
750
|
const remoteFiles = syncData[typeName] ?? [];
|
|
@@ -703,25 +758,28 @@ async function executeSyncToLocal(options) {
|
|
|
703
758
|
} catch {
|
|
704
759
|
}
|
|
705
760
|
if (typeName === "settings") {
|
|
706
|
-
const
|
|
761
|
+
const repoSettings = JSON.parse(remoteContent);
|
|
762
|
+
const combinedTemplate = mergeGlobalAndRepoSettings(globalSettings, repoSettings);
|
|
707
763
|
const hooksDir = join4(projectPath, ".claude", "hooks");
|
|
708
764
|
const discovered = await discoverHooks(hooksDir);
|
|
709
765
|
if (localContent === void 0) {
|
|
766
|
+
let finalSettings = stripPermissionsAllow(combinedTemplate);
|
|
710
767
|
if (discovered.size > 0) {
|
|
711
|
-
|
|
712
|
-
|
|
768
|
+
finalSettings.hooks = mergeDiscoveredHooks(
|
|
769
|
+
finalSettings.hooks ?? {},
|
|
713
770
|
discovered
|
|
714
771
|
);
|
|
715
772
|
}
|
|
716
773
|
if (!dryRun) {
|
|
717
774
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
718
|
-
await writeFile2(targetPath, JSON.stringify(
|
|
775
|
+
await writeFile2(targetPath, JSON.stringify(finalSettings, null, 2) + "\n", "utf-8");
|
|
719
776
|
}
|
|
720
777
|
result.created.push(remote.name);
|
|
721
778
|
totals.created++;
|
|
722
779
|
} else {
|
|
723
780
|
const localSettings = JSON.parse(localContent);
|
|
724
|
-
|
|
781
|
+
let merged = mergeSettings(combinedTemplate, localSettings);
|
|
782
|
+
merged = stripPermissionsAllow(merged);
|
|
725
783
|
if (discovered.size > 0) {
|
|
726
784
|
merged.hooks = mergeDiscoveredHooks(
|
|
727
785
|
merged.hooks ?? {},
|
|
@@ -1107,7 +1165,7 @@ import { join as join6, extname } from "node:path";
|
|
|
1107
1165
|
function compositeKey(type, name, category) {
|
|
1108
1166
|
return category ? `${type}:${category}/${name}` : `${type}:${name}`;
|
|
1109
1167
|
}
|
|
1110
|
-
async function scanLocalFiles(claudeDir) {
|
|
1168
|
+
async function scanLocalFiles(claudeDir, projectPath) {
|
|
1111
1169
|
const result = /* @__PURE__ */ new Map();
|
|
1112
1170
|
await scanCommands(join6(claudeDir, "commands", "cbp"), result);
|
|
1113
1171
|
await scanSubfolderType(join6(claudeDir, "agents"), "agent", "AGENT.md", result);
|
|
@@ -1115,6 +1173,7 @@ async function scanLocalFiles(claudeDir) {
|
|
|
1115
1173
|
await scanFlatType(join6(claudeDir, "rules"), "rule", ".md", result);
|
|
1116
1174
|
await scanFlatType(join6(claudeDir, "hooks"), "hook", ".sh", result);
|
|
1117
1175
|
await scanTemplates(join6(claudeDir, "templates"), result);
|
|
1176
|
+
await scanSettings(claudeDir, projectPath, result);
|
|
1118
1177
|
return result;
|
|
1119
1178
|
}
|
|
1120
1179
|
async function scanCommands(dir, result) {
|
|
@@ -1190,9 +1249,43 @@ async function scanTemplates(dir, result) {
|
|
|
1190
1249
|
}
|
|
1191
1250
|
}
|
|
1192
1251
|
}
|
|
1252
|
+
async function scanSettings(claudeDir, projectPath, result) {
|
|
1253
|
+
const settingsPath = join6(claudeDir, "settings.json");
|
|
1254
|
+
let raw;
|
|
1255
|
+
try {
|
|
1256
|
+
raw = await readFile6(settingsPath, "utf-8");
|
|
1257
|
+
} catch {
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
let parsed;
|
|
1261
|
+
try {
|
|
1262
|
+
parsed = JSON.parse(raw);
|
|
1263
|
+
} catch {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
parsed = stripPermissionsAllow(parsed);
|
|
1267
|
+
if (parsed.hooks && typeof parsed.hooks === "object") {
|
|
1268
|
+
const hooksDir = projectPath ? join6(projectPath, ".claude", "hooks") : join6(claudeDir, "hooks");
|
|
1269
|
+
const discovered = await discoverHooks(hooksDir);
|
|
1270
|
+
if (discovered.size > 0) {
|
|
1271
|
+
parsed.hooks = stripDiscoveredHooks(
|
|
1272
|
+
parsed.hooks,
|
|
1273
|
+
".claude/hooks"
|
|
1274
|
+
);
|
|
1275
|
+
if (Object.keys(parsed.hooks).length === 0) {
|
|
1276
|
+
delete parsed.hooks;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
const content = JSON.stringify(parsed, null, 2) + "\n";
|
|
1281
|
+
const key = compositeKey("settings", "settings", null);
|
|
1282
|
+
result.set(key, { type: "settings", name: "settings", category: null, content });
|
|
1283
|
+
}
|
|
1193
1284
|
var init_fileMapper = __esm({
|
|
1194
1285
|
"src/cli/fileMapper.ts"() {
|
|
1195
1286
|
"use strict";
|
|
1287
|
+
init_settings_merge();
|
|
1288
|
+
init_hook_registry();
|
|
1196
1289
|
}
|
|
1197
1290
|
});
|
|
1198
1291
|
|
|
@@ -1275,6 +1368,7 @@ async function runPush() {
|
|
|
1275
1368
|
const flags = parseFlags(3);
|
|
1276
1369
|
const dryRun = hasFlag("dry-run", 3);
|
|
1277
1370
|
const force = hasFlag("force", 3);
|
|
1371
|
+
const isGlobal = hasFlag("global", 3);
|
|
1278
1372
|
validateApiKey();
|
|
1279
1373
|
const config2 = await resolveConfig(flags);
|
|
1280
1374
|
const { repoId, projectPath } = config2;
|
|
@@ -1293,7 +1387,7 @@ async function runPush() {
|
|
|
1293
1387
|
return;
|
|
1294
1388
|
}
|
|
1295
1389
|
console.log(" Scanning local files...");
|
|
1296
|
-
const localFiles = await scanLocalFiles(claudeDir);
|
|
1390
|
+
const localFiles = await scanLocalFiles(claudeDir, projectPath);
|
|
1297
1391
|
console.log(` Found ${localFiles.size} local files.`);
|
|
1298
1392
|
console.log(" Fetching remote state...");
|
|
1299
1393
|
const [syncRes, repoRes] = await Promise.all([
|
|
@@ -1382,7 +1476,8 @@ async function runPush() {
|
|
|
1382
1476
|
type: f.type,
|
|
1383
1477
|
name: f.name,
|
|
1384
1478
|
category: f.category,
|
|
1385
|
-
content: f.content
|
|
1479
|
+
content: f.content,
|
|
1480
|
+
...f.type === "settings" ? { scope: isGlobal ? "global" : "repo" } : {}
|
|
1386
1481
|
})),
|
|
1387
1482
|
delete_keys: toDelete
|
|
1388
1483
|
});
|
|
@@ -1416,7 +1511,8 @@ function flattenSyncData(data) {
|
|
|
1416
1511
|
skills: "skill",
|
|
1417
1512
|
rules: "rule",
|
|
1418
1513
|
hooks: "hook",
|
|
1419
|
-
templates: "template"
|
|
1514
|
+
templates: "template",
|
|
1515
|
+
settings: "settings"
|
|
1420
1516
|
};
|
|
1421
1517
|
for (const [syncKey, typeName] of Object.entries(typeMap)) {
|
|
1422
1518
|
const files = data[syncKey] ?? [];
|
|
@@ -1596,7 +1692,7 @@ async function runInit() {
|
|
|
1596
1692
|
allFiles.push({ type: "claude_md", name: file.name, content: file.content });
|
|
1597
1693
|
}
|
|
1598
1694
|
for (const file of settingsFiles) {
|
|
1599
|
-
allFiles.push({ type: "settings", name: file.name, content: file.content });
|
|
1695
|
+
allFiles.push({ type: "settings", name: file.name, content: file.content, scope: file.scope ?? "repo" });
|
|
1600
1696
|
}
|
|
1601
1697
|
if (allFiles.length > 0) {
|
|
1602
1698
|
await apiPost("/sync/files", {
|
|
@@ -23380,21 +23476,6 @@ function registerReadTools(server) {
|
|
|
23380
23476
|
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
23381
23477
|
}
|
|
23382
23478
|
});
|
|
23383
|
-
server.registerTool("get_handoff", {
|
|
23384
|
-
description: "Get handoff state for a repo (status, summary, resume command/context).",
|
|
23385
|
-
inputSchema: {
|
|
23386
|
-
repo_id: external_exports.string().uuid().describe("The repo UUID")
|
|
23387
|
-
}
|
|
23388
|
-
}, async ({ repo_id }) => {
|
|
23389
|
-
try {
|
|
23390
|
-
const res = await apiGet(`/repos/${repo_id}`);
|
|
23391
|
-
const { id, name, handoff_status, handoff_summary, handoff_resume_command, handoff_resume_context, handoff_updated_at } = res.data;
|
|
23392
|
-
const data = { id, name, handoff_status, handoff_summary, handoff_resume_command, handoff_resume_context, handoff_updated_at };
|
|
23393
|
-
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
23394
|
-
} catch (err) {
|
|
23395
|
-
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
23396
|
-
}
|
|
23397
|
-
});
|
|
23398
23479
|
server.registerTool("get_sync_status", {
|
|
23399
23480
|
description: "Get cross-repo sync status. Shows which repos need a claude files sync based on latest updates.",
|
|
23400
23481
|
inputSchema: {}
|
|
@@ -23406,6 +23487,22 @@ function registerReadTools(server) {
|
|
|
23406
23487
|
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
23407
23488
|
}
|
|
23408
23489
|
});
|
|
23490
|
+
server.registerTool("get_next_action", {
|
|
23491
|
+
description: "Compute the next action for a repo based on current workflow state. Returns command, instructions, state, and context.",
|
|
23492
|
+
inputSchema: {
|
|
23493
|
+
repo_id: external_exports.string().uuid().describe("The repo UUID"),
|
|
23494
|
+
worktree_id: external_exports.string().uuid().optional().describe("Optional worktree UUID to filter by assignment")
|
|
23495
|
+
}
|
|
23496
|
+
}, async ({ repo_id, worktree_id }) => {
|
|
23497
|
+
try {
|
|
23498
|
+
const params = {};
|
|
23499
|
+
if (worktree_id) params.worktree_id = worktree_id;
|
|
23500
|
+
const res = await apiGet(`/repos/${repo_id}/next-action`, params);
|
|
23501
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
23502
|
+
} catch (err) {
|
|
23503
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
23504
|
+
}
|
|
23505
|
+
});
|
|
23409
23506
|
server.registerTool("get_worktrees", {
|
|
23410
23507
|
description: "List worktrees for a repo. Optionally filter by status.",
|
|
23411
23508
|
inputSchema: {
|
|
@@ -23429,63 +23526,232 @@ var init_read = __esm({
|
|
|
23429
23526
|
}
|
|
23430
23527
|
});
|
|
23431
23528
|
|
|
23432
|
-
// src/lib/
|
|
23529
|
+
// src/lib/git-pr.ts
|
|
23433
23530
|
import { exec as execCb } from "node:child_process";
|
|
23434
23531
|
import { promisify } from "node:util";
|
|
23435
|
-
async function
|
|
23436
|
-
|
|
23532
|
+
async function createPR(options) {
|
|
23533
|
+
const { repoPath, head, base, title, body } = options;
|
|
23437
23534
|
try {
|
|
23438
|
-
|
|
23439
|
-
|
|
23440
|
-
|
|
23535
|
+
try {
|
|
23536
|
+
const { stdout: existing } = await exec(
|
|
23537
|
+
`gh pr list --head "${head}" --base "${base}" --json number,url --jq '.[0]'`,
|
|
23538
|
+
{ cwd: repoPath }
|
|
23539
|
+
);
|
|
23540
|
+
if (existing.trim()) {
|
|
23541
|
+
const pr = JSON.parse(existing.trim());
|
|
23542
|
+
return { pr_url: pr.url, pr_number: pr.number };
|
|
23543
|
+
}
|
|
23544
|
+
} catch {
|
|
23545
|
+
}
|
|
23546
|
+
const { stdout: stdout5 } = await exec(
|
|
23547
|
+
`gh pr create --head "${head}" --base "${base}" --title "${title.replace(/"/g, '\\"')}" --body "${body.replace(/"/g, '\\"')}"`,
|
|
23548
|
+
{ cwd: repoPath }
|
|
23549
|
+
);
|
|
23550
|
+
const prUrl = stdout5.trim();
|
|
23551
|
+
const prNumber = parseInt(prUrl.split("/").pop() ?? "0", 10);
|
|
23552
|
+
return { pr_url: prUrl, pr_number: prNumber || null };
|
|
23553
|
+
} catch (err) {
|
|
23554
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
23555
|
+
return { pr_url: null, pr_number: null, error: errorMessage };
|
|
23556
|
+
}
|
|
23557
|
+
}
|
|
23558
|
+
async function getPRStatus(repoPath, prNumber) {
|
|
23559
|
+
try {
|
|
23560
|
+
const { stdout: stdout5 } = await exec(
|
|
23561
|
+
`gh pr view ${prNumber} --json state,mergeable,title,url,number`,
|
|
23562
|
+
{ cwd: repoPath }
|
|
23563
|
+
);
|
|
23564
|
+
const pr = JSON.parse(stdout5.trim());
|
|
23441
23565
|
return {
|
|
23442
|
-
|
|
23443
|
-
|
|
23444
|
-
|
|
23566
|
+
state: pr.state,
|
|
23567
|
+
mergeable: pr.mergeable,
|
|
23568
|
+
title: pr.title,
|
|
23569
|
+
url: pr.url,
|
|
23570
|
+
number: pr.number
|
|
23445
23571
|
};
|
|
23446
|
-
}
|
|
23447
|
-
|
|
23572
|
+
} catch (err) {
|
|
23573
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
23448
23574
|
return {
|
|
23449
|
-
|
|
23450
|
-
|
|
23575
|
+
state: "UNKNOWN",
|
|
23576
|
+
mergeable: "UNKNOWN",
|
|
23577
|
+
title: "",
|
|
23578
|
+
url: "",
|
|
23579
|
+
number: prNumber,
|
|
23580
|
+
error: errorMessage
|
|
23451
23581
|
};
|
|
23452
23582
|
}
|
|
23453
|
-
|
|
23583
|
+
}
|
|
23584
|
+
var exec;
|
|
23585
|
+
var init_git_pr = __esm({
|
|
23586
|
+
"src/lib/git-pr.ts"() {
|
|
23587
|
+
"use strict";
|
|
23588
|
+
exec = promisify(execCb);
|
|
23589
|
+
}
|
|
23590
|
+
});
|
|
23591
|
+
|
|
23592
|
+
// src/lib/promotion.ts
|
|
23593
|
+
async function promoteCheckpoint(checkpointId) {
|
|
23594
|
+
try {
|
|
23595
|
+
const checkpointRes = await apiGet(`/checkpoints/${checkpointId}`);
|
|
23596
|
+
const checkpoint = checkpointRes.data;
|
|
23597
|
+
if (!checkpoint.branch_name) {
|
|
23598
|
+
return {
|
|
23599
|
+
promoted: false,
|
|
23600
|
+
pr_url: null,
|
|
23601
|
+
pr_number: null,
|
|
23602
|
+
checklist_id: null,
|
|
23603
|
+
message: "Checkpoint has no branch_name set",
|
|
23604
|
+
error: "No branch_name on checkpoint"
|
|
23605
|
+
};
|
|
23606
|
+
}
|
|
23607
|
+
const repoRes = await apiGet(`/repos/${checkpoint.repo_id}`);
|
|
23608
|
+
const repo = repoRes.data;
|
|
23609
|
+
if (!repo.path) {
|
|
23610
|
+
return {
|
|
23611
|
+
promoted: false,
|
|
23612
|
+
pr_url: null,
|
|
23613
|
+
pr_number: null,
|
|
23614
|
+
checklist_id: null,
|
|
23615
|
+
message: "Repo path not configured",
|
|
23616
|
+
error: "Repo path not configured"
|
|
23617
|
+
};
|
|
23618
|
+
}
|
|
23619
|
+
const baseBranch = repo.git_branch ?? "development";
|
|
23620
|
+
const chkNumber = checkpoint.number.toString().padStart(3, "0");
|
|
23621
|
+
const checklist = await createChecklistFromTemplates(
|
|
23622
|
+
checkpoint.repo_id,
|
|
23623
|
+
checkpointId,
|
|
23624
|
+
"feat_to_development",
|
|
23625
|
+
`CHK-${chkNumber}: ${checkpoint.title ?? "Untitled"} \u2192 ${baseBranch}`
|
|
23626
|
+
);
|
|
23627
|
+
const prResult = await createPR({
|
|
23628
|
+
repoPath: repo.path,
|
|
23629
|
+
head: checkpoint.branch_name,
|
|
23630
|
+
base: baseBranch,
|
|
23631
|
+
title: `CHK-${chkNumber}: ${checkpoint.title ?? "Checkpoint completion"}`,
|
|
23632
|
+
body: `## Checkpoint CHK-${chkNumber}
|
|
23633
|
+
|
|
23634
|
+
**Goal**: ${checkpoint.goal ?? "N/A"}
|
|
23635
|
+
|
|
23636
|
+
Automatically created by CodeByPlan promotion engine.`
|
|
23637
|
+
});
|
|
23638
|
+
if (checklist && prResult.pr_url) {
|
|
23639
|
+
await apiPut(`/merge-checklists/${checklist.id}`, {
|
|
23640
|
+
pr_url: prResult.pr_url,
|
|
23641
|
+
pr_number: prResult.pr_number,
|
|
23642
|
+
status: "in_progress"
|
|
23643
|
+
});
|
|
23644
|
+
}
|
|
23645
|
+
return {
|
|
23646
|
+
promoted: true,
|
|
23647
|
+
pr_url: prResult.pr_url,
|
|
23648
|
+
pr_number: prResult.pr_number,
|
|
23649
|
+
checklist_id: checklist?.id ?? null,
|
|
23650
|
+
message: prResult.error ? `PR creation issue: ${prResult.error}` : `PR created: ${prResult.pr_url}`
|
|
23651
|
+
};
|
|
23652
|
+
} catch (err) {
|
|
23653
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
23454
23654
|
return {
|
|
23455
|
-
|
|
23456
|
-
|
|
23457
|
-
|
|
23655
|
+
promoted: false,
|
|
23656
|
+
pr_url: null,
|
|
23657
|
+
pr_number: null,
|
|
23658
|
+
checklist_id: null,
|
|
23659
|
+
message: "Promotion failed",
|
|
23660
|
+
error: errorMessage
|
|
23458
23661
|
};
|
|
23459
23662
|
}
|
|
23460
|
-
|
|
23663
|
+
}
|
|
23664
|
+
async function promoteToMain(repoId) {
|
|
23461
23665
|
try {
|
|
23462
|
-
|
|
23463
|
-
|
|
23464
|
-
|
|
23465
|
-
|
|
23666
|
+
const repoRes = await apiGet(`/repos/${repoId}`);
|
|
23667
|
+
const repo = repoRes.data;
|
|
23668
|
+
if (!repo.path) {
|
|
23669
|
+
return {
|
|
23670
|
+
promoted: false,
|
|
23671
|
+
pr_url: null,
|
|
23672
|
+
pr_number: null,
|
|
23673
|
+
checklist_id: null,
|
|
23674
|
+
message: "Repo path not configured",
|
|
23675
|
+
error: "Repo path not configured"
|
|
23676
|
+
};
|
|
23677
|
+
}
|
|
23678
|
+
const sourceBranch = repo.git_branch ?? "development";
|
|
23679
|
+
const prResult = await createPR({
|
|
23680
|
+
repoPath: repo.path,
|
|
23681
|
+
head: sourceBranch,
|
|
23682
|
+
base: "main",
|
|
23683
|
+
title: `Promote ${sourceBranch} \u2192 main`,
|
|
23684
|
+
body: `Promotion from ${sourceBranch} to main.
|
|
23685
|
+
|
|
23686
|
+
Automatically created by CodeByPlan promotion engine.`
|
|
23687
|
+
});
|
|
23466
23688
|
return {
|
|
23467
|
-
|
|
23468
|
-
|
|
23689
|
+
promoted: true,
|
|
23690
|
+
pr_url: prResult.pr_url,
|
|
23691
|
+
pr_number: prResult.pr_number,
|
|
23692
|
+
checklist_id: null,
|
|
23693
|
+
message: prResult.error ? `PR creation issue: ${prResult.error}` : `PR created: ${prResult.pr_url}`
|
|
23469
23694
|
};
|
|
23470
23695
|
} catch (err) {
|
|
23471
|
-
try {
|
|
23472
|
-
await exec(`git checkout ${sourceBranch}`, { cwd: repo.path });
|
|
23473
|
-
} catch {
|
|
23474
|
-
}
|
|
23475
23696
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
23476
23697
|
return {
|
|
23477
|
-
|
|
23478
|
-
|
|
23698
|
+
promoted: false,
|
|
23699
|
+
pr_url: null,
|
|
23700
|
+
pr_number: null,
|
|
23701
|
+
checklist_id: null,
|
|
23702
|
+
message: "Promotion to main failed",
|
|
23479
23703
|
error: errorMessage
|
|
23480
23704
|
};
|
|
23481
23705
|
}
|
|
23482
23706
|
}
|
|
23483
|
-
|
|
23484
|
-
|
|
23485
|
-
|
|
23707
|
+
async function createChecklistFromTemplates(repoId, checkpointId, branchLevel, title) {
|
|
23708
|
+
try {
|
|
23709
|
+
const templatesRes = await apiGet(
|
|
23710
|
+
`/repos/${repoId}/checklist-templates`,
|
|
23711
|
+
{ branch_level: branchLevel }
|
|
23712
|
+
);
|
|
23713
|
+
const templates = templatesRes.data ?? [];
|
|
23714
|
+
const items = templates.map((t) => ({
|
|
23715
|
+
title: t.title,
|
|
23716
|
+
description: t.description,
|
|
23717
|
+
is_required: t.is_required,
|
|
23718
|
+
checked: false,
|
|
23719
|
+
checked_at: null
|
|
23720
|
+
}));
|
|
23721
|
+
const checklistRes = await apiPost(
|
|
23722
|
+
"/merge-checklists",
|
|
23723
|
+
{
|
|
23724
|
+
checkpoint_id: checkpointId,
|
|
23725
|
+
branch_level: branchLevel,
|
|
23726
|
+
title,
|
|
23727
|
+
status: "pending",
|
|
23728
|
+
items
|
|
23729
|
+
}
|
|
23730
|
+
);
|
|
23731
|
+
return checklistRes.data;
|
|
23732
|
+
} catch {
|
|
23733
|
+
try {
|
|
23734
|
+
const checklistRes = await apiPost(
|
|
23735
|
+
"/merge-checklists",
|
|
23736
|
+
{
|
|
23737
|
+
checkpoint_id: checkpointId,
|
|
23738
|
+
branch_level: branchLevel,
|
|
23739
|
+
title,
|
|
23740
|
+
status: "pending",
|
|
23741
|
+
items: []
|
|
23742
|
+
}
|
|
23743
|
+
);
|
|
23744
|
+
return checklistRes.data;
|
|
23745
|
+
} catch {
|
|
23746
|
+
return null;
|
|
23747
|
+
}
|
|
23748
|
+
}
|
|
23749
|
+
}
|
|
23750
|
+
var init_promotion = __esm({
|
|
23751
|
+
"src/lib/promotion.ts"() {
|
|
23486
23752
|
"use strict";
|
|
23487
23753
|
init_api();
|
|
23488
|
-
|
|
23754
|
+
init_git_pr();
|
|
23489
23755
|
}
|
|
23490
23756
|
});
|
|
23491
23757
|
|
|
@@ -23514,27 +23780,33 @@ function registerWriteTools(server) {
|
|
|
23514
23780
|
description: "Create a new checkpoint for a repo. Optionally connect it to a launch via launch_id.",
|
|
23515
23781
|
inputSchema: {
|
|
23516
23782
|
repo_id: external_exports.string().uuid().describe("The repo UUID"),
|
|
23517
|
-
title: external_exports.string().describe("Checkpoint title"),
|
|
23783
|
+
title: external_exports.string().optional().describe("Checkpoint title (optional \u2014 Claude can generate if missing)"),
|
|
23518
23784
|
number: external_exports.number().int().describe("Checkpoint number (e.g. 1 for CHK-001)"),
|
|
23519
|
-
goal: external_exports.string().optional().describe("Checkpoint goal description"),
|
|
23785
|
+
goal: external_exports.string().optional().describe("Checkpoint goal description (max 300 chars, brief overview)"),
|
|
23520
23786
|
deadline: external_exports.string().optional().describe("Deadline date (ISO format)"),
|
|
23521
23787
|
status: external_exports.string().optional().describe("Initial status (default: pending). Use 'draft' for checkpoints not ready for development."),
|
|
23522
23788
|
launch_id: external_exports.string().uuid().optional().describe("Optional launch UUID to connect this checkpoint to"),
|
|
23789
|
+
ideas: external_exports.array(external_exports.object({
|
|
23790
|
+
description: external_exports.string().describe("Idea description"),
|
|
23791
|
+
requirements: external_exports.array(external_exports.string()).optional().describe("List of requirements for this idea"),
|
|
23792
|
+
images: external_exports.array(external_exports.string()).optional().describe("Image URLs for this idea")
|
|
23793
|
+
})).optional().describe("Ideas array \u2014 each idea has description, requirements[], images[]"),
|
|
23523
23794
|
context: external_exports.any().optional().describe("Context JSONB (decisions, discoveries, dependencies, constraints, qa_answers)"),
|
|
23524
23795
|
research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)"),
|
|
23525
23796
|
qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
|
|
23526
23797
|
}
|
|
23527
|
-
}, async ({ repo_id, title, number: number3, goal, deadline, status, launch_id, context, research, qa }) => {
|
|
23798
|
+
}, async ({ repo_id, title, number: number3, goal, deadline, status, launch_id, ideas, context, research, qa }) => {
|
|
23528
23799
|
try {
|
|
23529
23800
|
const body = {
|
|
23530
23801
|
repo_id,
|
|
23531
|
-
title,
|
|
23802
|
+
title: title ?? null,
|
|
23532
23803
|
number: number3,
|
|
23533
23804
|
goal: goal ?? null,
|
|
23534
23805
|
deadline: deadline ?? null,
|
|
23535
23806
|
status: status ?? "pending",
|
|
23536
23807
|
launch_id: launch_id ?? null
|
|
23537
23808
|
};
|
|
23809
|
+
if (ideas !== void 0) body.ideas = ideas;
|
|
23538
23810
|
if (context !== void 0) body.context = context;
|
|
23539
23811
|
if (research !== void 0) body.research = research;
|
|
23540
23812
|
if (qa !== void 0) body.qa = qa;
|
|
@@ -23548,19 +23820,25 @@ function registerWriteTools(server) {
|
|
|
23548
23820
|
description: "Update an existing checkpoint. Can connect or disconnect a launch via launch_id.",
|
|
23549
23821
|
inputSchema: {
|
|
23550
23822
|
checkpoint_id: external_exports.string().uuid().describe("The checkpoint UUID"),
|
|
23551
|
-
title: external_exports.string().optional().describe("New title"),
|
|
23552
|
-
goal: external_exports.string().optional().describe("New goal"),
|
|
23823
|
+
title: external_exports.string().nullable().optional().describe("New title (or null to clear)"),
|
|
23824
|
+
goal: external_exports.string().optional().describe("New goal (max 300 chars, brief overview)"),
|
|
23553
23825
|
status: external_exports.string().optional().describe("New status (draft, pending, active, completed)"),
|
|
23554
23826
|
deadline: external_exports.string().optional().describe("New deadline (ISO format)"),
|
|
23555
23827
|
completed_at: external_exports.string().optional().describe("Completion timestamp (ISO format)"),
|
|
23556
23828
|
launch_id: external_exports.string().uuid().nullable().optional().describe("Launch UUID to connect (or null to disconnect)"),
|
|
23557
23829
|
worktree_id: external_exports.string().uuid().nullable().optional().describe("Worktree UUID to assign (or null to unassign)"),
|
|
23558
23830
|
assigned_to: external_exports.string().nullable().optional().describe("Who/what claimed this checkpoint"),
|
|
23831
|
+
branch_name: external_exports.string().nullable().optional().describe("Git branch name for this checkpoint (e.g. feat/CHK-061-git-overhaul)"),
|
|
23832
|
+
ideas: external_exports.array(external_exports.object({
|
|
23833
|
+
description: external_exports.string().describe("Idea description"),
|
|
23834
|
+
requirements: external_exports.array(external_exports.string()).optional().describe("List of requirements for this idea"),
|
|
23835
|
+
images: external_exports.array(external_exports.string()).optional().describe("Image URLs for this idea")
|
|
23836
|
+
})).optional().describe("Ideas array \u2014 each idea has description, requirements[], images[]"),
|
|
23559
23837
|
context: external_exports.any().optional().describe("Context JSONB (decisions, discoveries, dependencies, constraints, qa_answers)"),
|
|
23560
23838
|
research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)"),
|
|
23561
23839
|
qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
|
|
23562
23840
|
}
|
|
23563
|
-
}, async ({ checkpoint_id, title, goal, status, deadline, completed_at, launch_id, worktree_id, assigned_to, context, research, qa }) => {
|
|
23841
|
+
}, async ({ checkpoint_id, title, goal, status, deadline, completed_at, launch_id, worktree_id, assigned_to, branch_name, ideas, context, research, qa }) => {
|
|
23564
23842
|
const update = {};
|
|
23565
23843
|
if (title !== void 0) update.title = title;
|
|
23566
23844
|
if (goal !== void 0) update.goal = goal;
|
|
@@ -23570,6 +23848,8 @@ function registerWriteTools(server) {
|
|
|
23570
23848
|
if (launch_id !== void 0) update.launch_id = launch_id;
|
|
23571
23849
|
if (worktree_id !== void 0) update.worktree_id = worktree_id;
|
|
23572
23850
|
if (assigned_to !== void 0) update.assigned_to = assigned_to;
|
|
23851
|
+
if (branch_name !== void 0) update.branch_name = branch_name;
|
|
23852
|
+
if (ideas !== void 0) update.ideas = ideas;
|
|
23573
23853
|
if (context !== void 0) update.context = context;
|
|
23574
23854
|
if (research !== void 0) update.research = research;
|
|
23575
23855
|
if (qa !== void 0) update.qa = qa;
|
|
@@ -23584,7 +23864,7 @@ function registerWriteTools(server) {
|
|
|
23584
23864
|
}
|
|
23585
23865
|
});
|
|
23586
23866
|
server.registerTool("complete_checkpoint", {
|
|
23587
|
-
description: "Mark a checkpoint as completed. Sets status to 'completed', completed_at to now, and triggers
|
|
23867
|
+
description: "Mark a checkpoint as completed. Sets status to 'completed', completed_at to now, and triggers promotion (creates PR from feat branch to development).",
|
|
23588
23868
|
inputSchema: {
|
|
23589
23869
|
checkpoint_id: external_exports.string().uuid().describe("The checkpoint UUID")
|
|
23590
23870
|
}
|
|
@@ -23595,8 +23875,13 @@ function registerWriteTools(server) {
|
|
|
23595
23875
|
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
23596
23876
|
});
|
|
23597
23877
|
const checkpoint = res.data;
|
|
23598
|
-
const
|
|
23599
|
-
|
|
23878
|
+
const featToDevResult = await promoteCheckpoint(checkpoint_id);
|
|
23879
|
+
let devToMainResult = null;
|
|
23880
|
+
const repoRes = await apiGet(`/repos/${checkpoint.repo_id}`);
|
|
23881
|
+
if (repoRes.data.auto_push_enabled) {
|
|
23882
|
+
devToMainResult = await promoteToMain(checkpoint.repo_id);
|
|
23883
|
+
}
|
|
23884
|
+
return { content: [{ type: "text", text: JSON.stringify({ checkpoint, promotion: { feat_to_development: featToDevResult, development_to_main: devToMainResult } }, null, 2) }] };
|
|
23600
23885
|
} catch (err) {
|
|
23601
23886
|
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
23602
23887
|
}
|
|
@@ -23958,31 +24243,6 @@ function registerWriteTools(server) {
|
|
|
23958
24243
|
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
23959
24244
|
}
|
|
23960
24245
|
});
|
|
23961
|
-
server.registerTool("update_handoff", {
|
|
23962
|
-
description: "Update handoff state for a repo (status, summary, resume command/context). Automatically sets handoff_updated_at.",
|
|
23963
|
-
inputSchema: {
|
|
23964
|
-
repo_id: external_exports.string().uuid().describe("The repo UUID"),
|
|
23965
|
-
status: external_exports.string().optional().describe("Handoff status"),
|
|
23966
|
-
summary: external_exports.string().optional().describe("Handoff summary"),
|
|
23967
|
-
resume_command: external_exports.string().optional().describe("Resume command"),
|
|
23968
|
-
resume_context: external_exports.string().optional().describe("Resume context")
|
|
23969
|
-
}
|
|
23970
|
-
}, async ({ repo_id, status, summary, resume_command, resume_context }) => {
|
|
23971
|
-
const body = {};
|
|
23972
|
-
if (status !== void 0) body.status = status;
|
|
23973
|
-
if (summary !== void 0) body.summary = summary;
|
|
23974
|
-
if (resume_command !== void 0) body.resume_command = resume_command;
|
|
23975
|
-
if (resume_context !== void 0) body.resume_context = resume_context;
|
|
23976
|
-
if (Object.keys(body).length === 0) {
|
|
23977
|
-
return { content: [{ type: "text", text: "Error: No fields to update" }], isError: true };
|
|
23978
|
-
}
|
|
23979
|
-
try {
|
|
23980
|
-
const res = await apiPatch(`/repos/${repo_id}/handoff`, body);
|
|
23981
|
-
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
23982
|
-
} catch (err) {
|
|
23983
|
-
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
23984
|
-
}
|
|
23985
|
-
});
|
|
23986
24246
|
server.registerTool("create_worktree", {
|
|
23987
24247
|
description: "Create a new worktree for a repo.",
|
|
23988
24248
|
inputSchema: {
|
|
@@ -24017,13 +24277,74 @@ function registerWriteTools(server) {
|
|
|
24017
24277
|
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
24018
24278
|
}
|
|
24019
24279
|
});
|
|
24280
|
+
server.registerTool("create_pr", {
|
|
24281
|
+
description: "Create a GitHub PR for a repo. Uses gh CLI.",
|
|
24282
|
+
inputSchema: {
|
|
24283
|
+
repo_id: external_exports.string().uuid().describe("The repo UUID"),
|
|
24284
|
+
head: external_exports.string().describe("Source branch name"),
|
|
24285
|
+
base: external_exports.string().describe("Target branch name"),
|
|
24286
|
+
title: external_exports.string().describe("PR title"),
|
|
24287
|
+
body: external_exports.string().optional().describe("PR description body")
|
|
24288
|
+
}
|
|
24289
|
+
}, async ({ repo_id, head, base, title, body }) => {
|
|
24290
|
+
try {
|
|
24291
|
+
const repoRes = await apiGet(`/repos/${repo_id}`);
|
|
24292
|
+
const repo = repoRes.data;
|
|
24293
|
+
if (!repo.path) {
|
|
24294
|
+
return { content: [{ type: "text", text: "Error: Repo path is not configured." }], isError: true };
|
|
24295
|
+
}
|
|
24296
|
+
const result = await createPR({
|
|
24297
|
+
repoPath: repo.path,
|
|
24298
|
+
head,
|
|
24299
|
+
base,
|
|
24300
|
+
title,
|
|
24301
|
+
body: body ?? ""
|
|
24302
|
+
});
|
|
24303
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
24304
|
+
} catch (err) {
|
|
24305
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
24306
|
+
}
|
|
24307
|
+
});
|
|
24308
|
+
server.registerTool("get_pr_status", {
|
|
24309
|
+
description: "Get the status of a GitHub PR by number.",
|
|
24310
|
+
inputSchema: {
|
|
24311
|
+
repo_id: external_exports.string().uuid().describe("The repo UUID"),
|
|
24312
|
+
pr_number: external_exports.number().int().describe("The PR number")
|
|
24313
|
+
}
|
|
24314
|
+
}, async ({ repo_id, pr_number }) => {
|
|
24315
|
+
try {
|
|
24316
|
+
const repoRes = await apiGet(`/repos/${repo_id}`);
|
|
24317
|
+
const repo = repoRes.data;
|
|
24318
|
+
if (!repo.path) {
|
|
24319
|
+
return { content: [{ type: "text", text: "Error: Repo path is not configured." }], isError: true };
|
|
24320
|
+
}
|
|
24321
|
+
const status = await getPRStatus(repo.path, pr_number);
|
|
24322
|
+
return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] };
|
|
24323
|
+
} catch (err) {
|
|
24324
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
24325
|
+
}
|
|
24326
|
+
});
|
|
24327
|
+
server.registerTool("promote_checkpoint", {
|
|
24328
|
+
description: "Trigger full promotion flow for a checkpoint. Creates merge checklist from templates and GitHub PR (feat branch \u2192 development).",
|
|
24329
|
+
inputSchema: {
|
|
24330
|
+
checkpoint_id: external_exports.string().uuid().describe("The checkpoint UUID")
|
|
24331
|
+
}
|
|
24332
|
+
}, async ({ checkpoint_id }) => {
|
|
24333
|
+
try {
|
|
24334
|
+
const result = await promoteCheckpoint(checkpoint_id);
|
|
24335
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
24336
|
+
} catch (err) {
|
|
24337
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
24338
|
+
}
|
|
24339
|
+
});
|
|
24020
24340
|
}
|
|
24021
24341
|
var init_write = __esm({
|
|
24022
24342
|
"src/tools/write.ts"() {
|
|
24023
24343
|
"use strict";
|
|
24024
24344
|
init_zod();
|
|
24025
24345
|
init_api();
|
|
24026
|
-
|
|
24346
|
+
init_promotion();
|
|
24347
|
+
init_git_pr();
|
|
24027
24348
|
init_sync_engine();
|
|
24028
24349
|
}
|
|
24029
24350
|
});
|
|
@@ -24033,30 +24354,45 @@ import { exec as execCb2 } from "node:child_process";
|
|
|
24033
24354
|
import { promisify as promisify2 } from "node:util";
|
|
24034
24355
|
function registerFileGenTools(server) {
|
|
24035
24356
|
server.registerTool("git_commit", {
|
|
24036
|
-
description: "Stage files and create a git commit in a repo. If files are specified, stages only those files. Otherwise stages all changes.",
|
|
24357
|
+
description: "Stage files and create a git commit in a repo. If files are specified, stages only those files. Otherwise stages all changes. Optionally verifies the current branch matches branch_name.",
|
|
24037
24358
|
inputSchema: {
|
|
24038
24359
|
repo_id: external_exports.string().uuid().describe("The repo UUID"),
|
|
24039
24360
|
message: external_exports.string().describe("Commit message"),
|
|
24040
|
-
files: external_exports.array(external_exports.string()).optional().describe("Specific file paths to stage (relative to repo root). If omitted, stages all changes.")
|
|
24361
|
+
files: external_exports.array(external_exports.string()).optional().describe("Specific file paths to stage (relative to repo root). If omitted, stages all changes."),
|
|
24362
|
+
branch_name: external_exports.string().optional().describe("Expected branch name. If provided, verifies the repo is on this branch before committing.")
|
|
24041
24363
|
}
|
|
24042
|
-
}, async ({ repo_id, message, files }) => {
|
|
24364
|
+
}, async ({ repo_id, message, files, branch_name }) => {
|
|
24043
24365
|
try {
|
|
24044
24366
|
const repoRes = await apiGet(`/repos/${repo_id}`);
|
|
24045
24367
|
const repo = repoRes.data;
|
|
24046
24368
|
if (!repo.path) {
|
|
24047
24369
|
return { content: [{ type: "text", text: "Error: Repo path is not configured." }], isError: true };
|
|
24048
24370
|
}
|
|
24371
|
+
if (branch_name) {
|
|
24372
|
+
const { stdout: currentBranch } = await exec2("git rev-parse --abbrev-ref HEAD", { cwd: repo.path });
|
|
24373
|
+
if (currentBranch.trim() !== branch_name) {
|
|
24374
|
+
return {
|
|
24375
|
+
content: [{
|
|
24376
|
+
type: "text",
|
|
24377
|
+
text: `Error: Expected branch "${branch_name}" but currently on "${currentBranch.trim()}".`
|
|
24378
|
+
}],
|
|
24379
|
+
isError: true
|
|
24380
|
+
};
|
|
24381
|
+
}
|
|
24382
|
+
}
|
|
24049
24383
|
const addCmd = files && files.length > 0 ? `git add ${files.map((f) => `"${f}"`).join(" ")}` : "git add .";
|
|
24050
24384
|
await exec2(addCmd, { cwd: repo.path });
|
|
24051
24385
|
const { stdout: stdout5, stderr } = await exec2(
|
|
24052
24386
|
`git commit -m "${message.replace(/"/g, '\\"')}"`,
|
|
24053
24387
|
{ cwd: repo.path }
|
|
24054
24388
|
);
|
|
24389
|
+
const { stdout: branch } = await exec2("git rev-parse --abbrev-ref HEAD", { cwd: repo.path });
|
|
24055
24390
|
return {
|
|
24056
24391
|
content: [{
|
|
24057
24392
|
type: "text",
|
|
24058
24393
|
text: JSON.stringify({
|
|
24059
24394
|
status: "committed",
|
|
24395
|
+
branch: branch.trim(),
|
|
24060
24396
|
output: stdout5.trim(),
|
|
24061
24397
|
warnings: stderr.trim() || void 0
|
|
24062
24398
|
}, null, 2)
|