@codebyplan/cli 2.1.0 → 2.3.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.
Files changed (2) hide show
  1. package/dist/cli.js +332 -43
  2. package/package.json +1 -1
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.1.0";
40
+ VERSION = "2.3.0";
41
41
  PACKAGE_NAME = "@codebyplan/cli";
42
42
  }
43
43
  });
@@ -23526,63 +23526,269 @@ var init_read = __esm({
23526
23526
  }
23527
23527
  });
23528
23528
 
23529
- // src/lib/auto-push.ts
23529
+ // src/lib/git-pr.ts
23530
23530
  import { exec as execCb } from "node:child_process";
23531
23531
  import { promisify } from "node:util";
23532
- async function autoPushToMain(repoId) {
23533
- let repo;
23532
+ async function createPR(options) {
23533
+ const { repoPath, head, base, title, body } = options;
23534
23534
  try {
23535
- const res = await apiGet(`/repos/${repoId}`);
23536
- repo = res.data;
23537
- } catch {
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 mergePR(options) {
23559
+ const { repoPath, prNumber, mergeMethod } = options;
23560
+ try {
23561
+ const { stdout: stdout5 } = await exec(
23562
+ `gh pr merge ${prNumber} --${mergeMethod} --delete-branch`,
23563
+ { cwd: repoPath }
23564
+ );
23565
+ return { merged: true, message: stdout5.trim() || `PR #${prNumber} merged via ${mergeMethod}` };
23566
+ } catch (err) {
23567
+ const errorMessage = err instanceof Error ? err.message : String(err);
23568
+ return { merged: false, message: "Merge failed", error: errorMessage };
23569
+ }
23570
+ }
23571
+ async function getPRStatus(repoPath, prNumber) {
23572
+ try {
23573
+ const { stdout: stdout5 } = await exec(
23574
+ `gh pr view ${prNumber} --json state,mergeable,title,url,number`,
23575
+ { cwd: repoPath }
23576
+ );
23577
+ const pr = JSON.parse(stdout5.trim());
23538
23578
  return {
23539
- pushed: false,
23540
- message: "Repo not found",
23541
- error: "Repo not found"
23579
+ state: pr.state,
23580
+ mergeable: pr.mergeable,
23581
+ title: pr.title,
23582
+ url: pr.url,
23583
+ number: pr.number
23542
23584
  };
23543
- }
23544
- if (!repo.auto_push_enabled) {
23585
+ } catch (err) {
23586
+ const errorMessage = err instanceof Error ? err.message : String(err);
23545
23587
  return {
23546
- pushed: false,
23547
- message: "Auto-push is not enabled for this repo"
23588
+ state: "UNKNOWN",
23589
+ mergeable: "UNKNOWN",
23590
+ title: "",
23591
+ url: "",
23592
+ number: prNumber,
23593
+ error: errorMessage
23548
23594
  };
23549
23595
  }
23550
- if (!repo.path) {
23596
+ }
23597
+ var exec;
23598
+ var init_git_pr = __esm({
23599
+ "src/lib/git-pr.ts"() {
23600
+ "use strict";
23601
+ exec = promisify(execCb);
23602
+ }
23603
+ });
23604
+
23605
+ // src/lib/promotion.ts
23606
+ async function promoteCheckpoint(checkpointId) {
23607
+ try {
23608
+ const checkpointRes = await apiGet(`/checkpoints/${checkpointId}`);
23609
+ const checkpoint = checkpointRes.data;
23610
+ if (!checkpoint.branch_name) {
23611
+ return {
23612
+ promoted: false,
23613
+ pr_url: null,
23614
+ pr_number: null,
23615
+ checklist_id: null,
23616
+ message: "Checkpoint has no branch_name set",
23617
+ error: "No branch_name on checkpoint"
23618
+ };
23619
+ }
23620
+ const repoRes = await apiGet(`/repos/${checkpoint.repo_id}`);
23621
+ const repo = repoRes.data;
23622
+ if (!repo.path) {
23623
+ return {
23624
+ promoted: false,
23625
+ pr_url: null,
23626
+ pr_number: null,
23627
+ checklist_id: null,
23628
+ message: "Repo path not configured",
23629
+ error: "Repo path not configured"
23630
+ };
23631
+ }
23632
+ const baseBranch = repo.git_branch ?? "development";
23633
+ const chkNumber = checkpoint.number.toString().padStart(3, "0");
23634
+ const checklist = await createChecklistFromTemplates(
23635
+ checkpoint.repo_id,
23636
+ checkpointId,
23637
+ "feat_to_development",
23638
+ `CHK-${chkNumber}: ${checkpoint.title ?? "Untitled"} \u2192 ${baseBranch}`
23639
+ );
23640
+ const prResult = await createPR({
23641
+ repoPath: repo.path,
23642
+ head: checkpoint.branch_name,
23643
+ base: baseBranch,
23644
+ title: `CHK-${chkNumber}: ${checkpoint.title ?? "Checkpoint completion"}`,
23645
+ body: `## Checkpoint CHK-${chkNumber}
23646
+
23647
+ **Goal**: ${checkpoint.goal ?? "N/A"}
23648
+
23649
+ Automatically created by CodeByPlan promotion engine.`
23650
+ });
23651
+ if (checklist && prResult.pr_url) {
23652
+ await apiPut(`/merge-checklists/${checklist.id}`, {
23653
+ pr_url: prResult.pr_url,
23654
+ pr_number: prResult.pr_number,
23655
+ status: "in_progress"
23656
+ });
23657
+ }
23658
+ let mergeMessage = "";
23659
+ if (prResult.pr_number) {
23660
+ const mergeResult = await mergePR({
23661
+ repoPath: repo.path,
23662
+ prNumber: prResult.pr_number,
23663
+ mergeMethod: "merge"
23664
+ });
23665
+ mergeMessage = mergeResult.merged ? ` \u2192 Merged` : ` \u2192 Merge failed: ${mergeResult.error}`;
23666
+ if (mergeResult.merged && checklist) {
23667
+ await apiPut(`/merge-checklists/${checklist.id}`, {
23668
+ status: "completed",
23669
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
23670
+ });
23671
+ }
23672
+ }
23551
23673
  return {
23552
- pushed: false,
23553
- message: "Repo path is not configured",
23554
- error: "Repo path is not configured"
23674
+ promoted: true,
23675
+ pr_url: prResult.pr_url,
23676
+ pr_number: prResult.pr_number,
23677
+ checklist_id: checklist?.id ?? null,
23678
+ message: prResult.error ? `PR creation issue: ${prResult.error}` : `PR created: ${prResult.pr_url}${mergeMessage}`
23679
+ };
23680
+ } catch (err) {
23681
+ const errorMessage = err instanceof Error ? err.message : String(err);
23682
+ return {
23683
+ promoted: false,
23684
+ pr_url: null,
23685
+ pr_number: null,
23686
+ checklist_id: null,
23687
+ message: "Promotion failed",
23688
+ error: errorMessage
23555
23689
  };
23556
23690
  }
23557
- const sourceBranch = repo.git_branch ?? "development";
23691
+ }
23692
+ async function promoteToMain(repoId) {
23558
23693
  try {
23559
- await exec("git checkout main", { cwd: repo.path });
23560
- await exec(`git merge ${sourceBranch} --ff-only`, { cwd: repo.path });
23561
- await exec("git push origin main", { cwd: repo.path });
23562
- await exec(`git checkout ${sourceBranch}`, { cwd: repo.path });
23694
+ const repoRes = await apiGet(`/repos/${repoId}`);
23695
+ const repo = repoRes.data;
23696
+ if (!repo.path) {
23697
+ return {
23698
+ promoted: false,
23699
+ pr_url: null,
23700
+ pr_number: null,
23701
+ checklist_id: null,
23702
+ message: "Repo path not configured",
23703
+ error: "Repo path not configured"
23704
+ };
23705
+ }
23706
+ const sourceBranch = repo.git_branch ?? "development";
23707
+ const prResult = await createPR({
23708
+ repoPath: repo.path,
23709
+ head: sourceBranch,
23710
+ base: "main",
23711
+ title: `Promote ${sourceBranch} \u2192 main`,
23712
+ body: `Promotion from ${sourceBranch} to main.
23713
+
23714
+ Automatically created by CodeByPlan promotion engine.`
23715
+ });
23716
+ let mergeMessage = "";
23717
+ if (prResult.pr_number) {
23718
+ const mergeResult = await mergePR({
23719
+ repoPath: repo.path,
23720
+ prNumber: prResult.pr_number,
23721
+ mergeMethod: "merge"
23722
+ });
23723
+ mergeMessage = mergeResult.merged ? ` \u2192 Merged` : ` \u2192 Merge failed: ${mergeResult.error}`;
23724
+ }
23563
23725
  return {
23564
- pushed: true,
23565
- message: `Merged ${sourceBranch} -> main and pushed to origin`
23726
+ promoted: true,
23727
+ pr_url: prResult.pr_url,
23728
+ pr_number: prResult.pr_number,
23729
+ checklist_id: null,
23730
+ message: prResult.error ? `PR creation issue: ${prResult.error}` : `PR created: ${prResult.pr_url}${mergeMessage}`
23566
23731
  };
23567
23732
  } catch (err) {
23568
- try {
23569
- await exec(`git checkout ${sourceBranch}`, { cwd: repo.path });
23570
- } catch {
23571
- }
23572
23733
  const errorMessage = err instanceof Error ? err.message : String(err);
23573
23734
  return {
23574
- pushed: false,
23575
- message: "Auto-push failed",
23735
+ promoted: false,
23736
+ pr_url: null,
23737
+ pr_number: null,
23738
+ checklist_id: null,
23739
+ message: "Promotion to main failed",
23576
23740
  error: errorMessage
23577
23741
  };
23578
23742
  }
23579
23743
  }
23580
- var exec;
23581
- var init_auto_push = __esm({
23582
- "src/lib/auto-push.ts"() {
23744
+ async function createChecklistFromTemplates(repoId, checkpointId, branchLevel, title) {
23745
+ try {
23746
+ const templatesRes = await apiGet(
23747
+ `/repos/${repoId}/checklist-templates`,
23748
+ { branch_level: branchLevel }
23749
+ );
23750
+ const templates = templatesRes.data ?? [];
23751
+ const items = templates.map((t) => ({
23752
+ title: t.title,
23753
+ description: t.description,
23754
+ is_required: t.is_required,
23755
+ checked: false,
23756
+ checked_at: null
23757
+ }));
23758
+ const checklistRes = await apiPost(
23759
+ "/merge-checklists",
23760
+ {
23761
+ checkpoint_id: checkpointId,
23762
+ branch_level: branchLevel,
23763
+ title,
23764
+ status: "pending",
23765
+ items
23766
+ }
23767
+ );
23768
+ return checklistRes.data;
23769
+ } catch {
23770
+ try {
23771
+ const checklistRes = await apiPost(
23772
+ "/merge-checklists",
23773
+ {
23774
+ checkpoint_id: checkpointId,
23775
+ branch_level: branchLevel,
23776
+ title,
23777
+ status: "pending",
23778
+ items: []
23779
+ }
23780
+ );
23781
+ return checklistRes.data;
23782
+ } catch {
23783
+ return null;
23784
+ }
23785
+ }
23786
+ }
23787
+ var init_promotion = __esm({
23788
+ "src/lib/promotion.ts"() {
23583
23789
  "use strict";
23584
23790
  init_api();
23585
- exec = promisify(execCb);
23791
+ init_git_pr();
23586
23792
  }
23587
23793
  });
23588
23794
 
@@ -23659,6 +23865,7 @@ function registerWriteTools(server) {
23659
23865
  launch_id: external_exports.string().uuid().nullable().optional().describe("Launch UUID to connect (or null to disconnect)"),
23660
23866
  worktree_id: external_exports.string().uuid().nullable().optional().describe("Worktree UUID to assign (or null to unassign)"),
23661
23867
  assigned_to: external_exports.string().nullable().optional().describe("Who/what claimed this checkpoint"),
23868
+ branch_name: external_exports.string().nullable().optional().describe("Git branch name for this checkpoint (e.g. feat/CHK-061-git-overhaul)"),
23662
23869
  ideas: external_exports.array(external_exports.object({
23663
23870
  description: external_exports.string().describe("Idea description"),
23664
23871
  requirements: external_exports.array(external_exports.string()).optional().describe("List of requirements for this idea"),
@@ -23668,7 +23875,7 @@ function registerWriteTools(server) {
23668
23875
  research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)"),
23669
23876
  qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
23670
23877
  }
23671
- }, async ({ checkpoint_id, title, goal, status, deadline, completed_at, launch_id, worktree_id, assigned_to, ideas, context, research, qa }) => {
23878
+ }, async ({ checkpoint_id, title, goal, status, deadline, completed_at, launch_id, worktree_id, assigned_to, branch_name, ideas, context, research, qa }) => {
23672
23879
  const update = {};
23673
23880
  if (title !== void 0) update.title = title;
23674
23881
  if (goal !== void 0) update.goal = goal;
@@ -23678,6 +23885,7 @@ function registerWriteTools(server) {
23678
23885
  if (launch_id !== void 0) update.launch_id = launch_id;
23679
23886
  if (worktree_id !== void 0) update.worktree_id = worktree_id;
23680
23887
  if (assigned_to !== void 0) update.assigned_to = assigned_to;
23888
+ if (branch_name !== void 0) update.branch_name = branch_name;
23681
23889
  if (ideas !== void 0) update.ideas = ideas;
23682
23890
  if (context !== void 0) update.context = context;
23683
23891
  if (research !== void 0) update.research = research;
@@ -23693,7 +23901,7 @@ function registerWriteTools(server) {
23693
23901
  }
23694
23902
  });
23695
23903
  server.registerTool("complete_checkpoint", {
23696
- description: "Mark a checkpoint as completed. Sets status to 'completed', completed_at to now, and triggers auto-push to main if enabled for the repo.",
23904
+ description: "Mark a checkpoint as completed. Sets status to 'completed', completed_at to now, and triggers promotion (creates PR from feat branch to development).",
23697
23905
  inputSchema: {
23698
23906
  checkpoint_id: external_exports.string().uuid().describe("The checkpoint UUID")
23699
23907
  }
@@ -23704,8 +23912,13 @@ function registerWriteTools(server) {
23704
23912
  completed_at: (/* @__PURE__ */ new Date()).toISOString()
23705
23913
  });
23706
23914
  const checkpoint = res.data;
23707
- const pushResult = await autoPushToMain(checkpoint.repo_id);
23708
- return { content: [{ type: "text", text: JSON.stringify({ checkpoint, autoPush: pushResult }, null, 2) }] };
23915
+ const featToDevResult = await promoteCheckpoint(checkpoint_id);
23916
+ let devToMainResult = null;
23917
+ const repoRes = await apiGet(`/repos/${checkpoint.repo_id}`);
23918
+ if (repoRes.data.auto_push_enabled) {
23919
+ devToMainResult = await promoteToMain(checkpoint.repo_id);
23920
+ }
23921
+ return { content: [{ type: "text", text: JSON.stringify({ checkpoint, promotion: { feat_to_development: featToDevResult, development_to_main: devToMainResult } }, null, 2) }] };
23709
23922
  } catch (err) {
23710
23923
  return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23711
23924
  }
@@ -24101,13 +24314,74 @@ function registerWriteTools(server) {
24101
24314
  return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24102
24315
  }
24103
24316
  });
24317
+ server.registerTool("create_pr", {
24318
+ description: "Create a GitHub PR for a repo. Uses gh CLI.",
24319
+ inputSchema: {
24320
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
24321
+ head: external_exports.string().describe("Source branch name"),
24322
+ base: external_exports.string().describe("Target branch name"),
24323
+ title: external_exports.string().describe("PR title"),
24324
+ body: external_exports.string().optional().describe("PR description body")
24325
+ }
24326
+ }, async ({ repo_id, head, base, title, body }) => {
24327
+ try {
24328
+ const repoRes = await apiGet(`/repos/${repo_id}`);
24329
+ const repo = repoRes.data;
24330
+ if (!repo.path) {
24331
+ return { content: [{ type: "text", text: "Error: Repo path is not configured." }], isError: true };
24332
+ }
24333
+ const result = await createPR({
24334
+ repoPath: repo.path,
24335
+ head,
24336
+ base,
24337
+ title,
24338
+ body: body ?? ""
24339
+ });
24340
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
24341
+ } catch (err) {
24342
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24343
+ }
24344
+ });
24345
+ server.registerTool("get_pr_status", {
24346
+ description: "Get the status of a GitHub PR by number.",
24347
+ inputSchema: {
24348
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
24349
+ pr_number: external_exports.number().int().describe("The PR number")
24350
+ }
24351
+ }, async ({ repo_id, pr_number }) => {
24352
+ try {
24353
+ const repoRes = await apiGet(`/repos/${repo_id}`);
24354
+ const repo = repoRes.data;
24355
+ if (!repo.path) {
24356
+ return { content: [{ type: "text", text: "Error: Repo path is not configured." }], isError: true };
24357
+ }
24358
+ const status = await getPRStatus(repo.path, pr_number);
24359
+ return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] };
24360
+ } catch (err) {
24361
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24362
+ }
24363
+ });
24364
+ server.registerTool("promote_checkpoint", {
24365
+ description: "Trigger full promotion flow for a checkpoint. Creates merge checklist from templates and GitHub PR (feat branch \u2192 development).",
24366
+ inputSchema: {
24367
+ checkpoint_id: external_exports.string().uuid().describe("The checkpoint UUID")
24368
+ }
24369
+ }, async ({ checkpoint_id }) => {
24370
+ try {
24371
+ const result = await promoteCheckpoint(checkpoint_id);
24372
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
24373
+ } catch (err) {
24374
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24375
+ }
24376
+ });
24104
24377
  }
24105
24378
  var init_write = __esm({
24106
24379
  "src/tools/write.ts"() {
24107
24380
  "use strict";
24108
24381
  init_zod();
24109
24382
  init_api();
24110
- init_auto_push();
24383
+ init_promotion();
24384
+ init_git_pr();
24111
24385
  init_sync_engine();
24112
24386
  }
24113
24387
  });
@@ -24117,30 +24391,45 @@ import { exec as execCb2 } from "node:child_process";
24117
24391
  import { promisify as promisify2 } from "node:util";
24118
24392
  function registerFileGenTools(server) {
24119
24393
  server.registerTool("git_commit", {
24120
- description: "Stage files and create a git commit in a repo. If files are specified, stages only those files. Otherwise stages all changes.",
24394
+ 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.",
24121
24395
  inputSchema: {
24122
24396
  repo_id: external_exports.string().uuid().describe("The repo UUID"),
24123
24397
  message: external_exports.string().describe("Commit message"),
24124
- files: external_exports.array(external_exports.string()).optional().describe("Specific file paths to stage (relative to repo root). If omitted, stages all changes.")
24398
+ files: external_exports.array(external_exports.string()).optional().describe("Specific file paths to stage (relative to repo root). If omitted, stages all changes."),
24399
+ branch_name: external_exports.string().optional().describe("Expected branch name. If provided, verifies the repo is on this branch before committing.")
24125
24400
  }
24126
- }, async ({ repo_id, message, files }) => {
24401
+ }, async ({ repo_id, message, files, branch_name }) => {
24127
24402
  try {
24128
24403
  const repoRes = await apiGet(`/repos/${repo_id}`);
24129
24404
  const repo = repoRes.data;
24130
24405
  if (!repo.path) {
24131
24406
  return { content: [{ type: "text", text: "Error: Repo path is not configured." }], isError: true };
24132
24407
  }
24408
+ if (branch_name) {
24409
+ const { stdout: currentBranch } = await exec2("git rev-parse --abbrev-ref HEAD", { cwd: repo.path });
24410
+ if (currentBranch.trim() !== branch_name) {
24411
+ return {
24412
+ content: [{
24413
+ type: "text",
24414
+ text: `Error: Expected branch "${branch_name}" but currently on "${currentBranch.trim()}".`
24415
+ }],
24416
+ isError: true
24417
+ };
24418
+ }
24419
+ }
24133
24420
  const addCmd = files && files.length > 0 ? `git add ${files.map((f) => `"${f}"`).join(" ")}` : "git add .";
24134
24421
  await exec2(addCmd, { cwd: repo.path });
24135
24422
  const { stdout: stdout5, stderr } = await exec2(
24136
24423
  `git commit -m "${message.replace(/"/g, '\\"')}"`,
24137
24424
  { cwd: repo.path }
24138
24425
  );
24426
+ const { stdout: branch } = await exec2("git rev-parse --abbrev-ref HEAD", { cwd: repo.path });
24139
24427
  return {
24140
24428
  content: [{
24141
24429
  type: "text",
24142
24430
  text: JSON.stringify({
24143
24431
  status: "committed",
24432
+ branch: branch.trim(),
24144
24433
  output: stdout5.trim(),
24145
24434
  warnings: stderr.trim() || void 0
24146
24435
  }, null, 2)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codebyplan/cli",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "MCP server for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {