@codebyplan/cli 2.1.0 → 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.
Files changed (2) hide show
  1. package/dist/cli.js +295 -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.2.0";
41
41
  PACKAGE_NAME = "@codebyplan/cli";
42
42
  }
43
43
  });
@@ -23526,63 +23526,232 @@ 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 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());
23538
23565
  return {
23539
- pushed: false,
23540
- message: "Repo not found",
23541
- error: "Repo not found"
23566
+ state: pr.state,
23567
+ mergeable: pr.mergeable,
23568
+ title: pr.title,
23569
+ url: pr.url,
23570
+ number: pr.number
23542
23571
  };
23543
- }
23544
- if (!repo.auto_push_enabled) {
23572
+ } catch (err) {
23573
+ const errorMessage = err instanceof Error ? err.message : String(err);
23545
23574
  return {
23546
- pushed: false,
23547
- message: "Auto-push is not enabled for this repo"
23575
+ state: "UNKNOWN",
23576
+ mergeable: "UNKNOWN",
23577
+ title: "",
23578
+ url: "",
23579
+ number: prNumber,
23580
+ error: errorMessage
23548
23581
  };
23549
23582
  }
23550
- if (!repo.path) {
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
+ }
23551
23645
  return {
23552
- pushed: false,
23553
- message: "Repo path is not configured",
23554
- error: "Repo path is not configured"
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);
23654
+ return {
23655
+ promoted: false,
23656
+ pr_url: null,
23657
+ pr_number: null,
23658
+ checklist_id: null,
23659
+ message: "Promotion failed",
23660
+ error: errorMessage
23555
23661
  };
23556
23662
  }
23557
- const sourceBranch = repo.git_branch ?? "development";
23663
+ }
23664
+ async function promoteToMain(repoId) {
23558
23665
  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 });
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
+ });
23563
23688
  return {
23564
- pushed: true,
23565
- message: `Merged ${sourceBranch} -> main and pushed to origin`
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}`
23566
23694
  };
23567
23695
  } catch (err) {
23568
- try {
23569
- await exec(`git checkout ${sourceBranch}`, { cwd: repo.path });
23570
- } catch {
23571
- }
23572
23696
  const errorMessage = err instanceof Error ? err.message : String(err);
23573
23697
  return {
23574
- pushed: false,
23575
- message: "Auto-push failed",
23698
+ promoted: false,
23699
+ pr_url: null,
23700
+ pr_number: null,
23701
+ checklist_id: null,
23702
+ message: "Promotion to main failed",
23576
23703
  error: errorMessage
23577
23704
  };
23578
23705
  }
23579
23706
  }
23580
- var exec;
23581
- var init_auto_push = __esm({
23582
- "src/lib/auto-push.ts"() {
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"() {
23583
23752
  "use strict";
23584
23753
  init_api();
23585
- exec = promisify(execCb);
23754
+ init_git_pr();
23586
23755
  }
23587
23756
  });
23588
23757
 
@@ -23659,6 +23828,7 @@ function registerWriteTools(server) {
23659
23828
  launch_id: external_exports.string().uuid().nullable().optional().describe("Launch UUID to connect (or null to disconnect)"),
23660
23829
  worktree_id: external_exports.string().uuid().nullable().optional().describe("Worktree UUID to assign (or null to unassign)"),
23661
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)"),
23662
23832
  ideas: external_exports.array(external_exports.object({
23663
23833
  description: external_exports.string().describe("Idea description"),
23664
23834
  requirements: external_exports.array(external_exports.string()).optional().describe("List of requirements for this idea"),
@@ -23668,7 +23838,7 @@ function registerWriteTools(server) {
23668
23838
  research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)"),
23669
23839
  qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
23670
23840
  }
23671
- }, async ({ checkpoint_id, title, goal, status, deadline, completed_at, launch_id, worktree_id, assigned_to, ideas, 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 }) => {
23672
23842
  const update = {};
23673
23843
  if (title !== void 0) update.title = title;
23674
23844
  if (goal !== void 0) update.goal = goal;
@@ -23678,6 +23848,7 @@ function registerWriteTools(server) {
23678
23848
  if (launch_id !== void 0) update.launch_id = launch_id;
23679
23849
  if (worktree_id !== void 0) update.worktree_id = worktree_id;
23680
23850
  if (assigned_to !== void 0) update.assigned_to = assigned_to;
23851
+ if (branch_name !== void 0) update.branch_name = branch_name;
23681
23852
  if (ideas !== void 0) update.ideas = ideas;
23682
23853
  if (context !== void 0) update.context = context;
23683
23854
  if (research !== void 0) update.research = research;
@@ -23693,7 +23864,7 @@ function registerWriteTools(server) {
23693
23864
  }
23694
23865
  });
23695
23866
  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.",
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).",
23697
23868
  inputSchema: {
23698
23869
  checkpoint_id: external_exports.string().uuid().describe("The checkpoint UUID")
23699
23870
  }
@@ -23704,8 +23875,13 @@ function registerWriteTools(server) {
23704
23875
  completed_at: (/* @__PURE__ */ new Date()).toISOString()
23705
23876
  });
23706
23877
  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) }] };
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) }] };
23709
23885
  } catch (err) {
23710
23886
  return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23711
23887
  }
@@ -24101,13 +24277,74 @@ function registerWriteTools(server) {
24101
24277
  return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24102
24278
  }
24103
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
+ });
24104
24340
  }
24105
24341
  var init_write = __esm({
24106
24342
  "src/tools/write.ts"() {
24107
24343
  "use strict";
24108
24344
  init_zod();
24109
24345
  init_api();
24110
- init_auto_push();
24346
+ init_promotion();
24347
+ init_git_pr();
24111
24348
  init_sync_engine();
24112
24349
  }
24113
24350
  });
@@ -24117,30 +24354,45 @@ import { exec as execCb2 } from "node:child_process";
24117
24354
  import { promisify as promisify2 } from "node:util";
24118
24355
  function registerFileGenTools(server) {
24119
24356
  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.",
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.",
24121
24358
  inputSchema: {
24122
24359
  repo_id: external_exports.string().uuid().describe("The repo UUID"),
24123
24360
  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.")
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.")
24125
24363
  }
24126
- }, async ({ repo_id, message, files }) => {
24364
+ }, async ({ repo_id, message, files, branch_name }) => {
24127
24365
  try {
24128
24366
  const repoRes = await apiGet(`/repos/${repo_id}`);
24129
24367
  const repo = repoRes.data;
24130
24368
  if (!repo.path) {
24131
24369
  return { content: [{ type: "text", text: "Error: Repo path is not configured." }], isError: true };
24132
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
+ }
24133
24383
  const addCmd = files && files.length > 0 ? `git add ${files.map((f) => `"${f}"`).join(" ")}` : "git add .";
24134
24384
  await exec2(addCmd, { cwd: repo.path });
24135
24385
  const { stdout: stdout5, stderr } = await exec2(
24136
24386
  `git commit -m "${message.replace(/"/g, '\\"')}"`,
24137
24387
  { cwd: repo.path }
24138
24388
  );
24389
+ const { stdout: branch } = await exec2("git rev-parse --abbrev-ref HEAD", { cwd: repo.path });
24139
24390
  return {
24140
24391
  content: [{
24141
24392
  type: "text",
24142
24393
  text: JSON.stringify({
24143
24394
  status: "committed",
24395
+ branch: branch.trim(),
24144
24396
  output: stdout5.trim(),
24145
24397
  warnings: stderr.trim() || void 0
24146
24398
  }, 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.2.0",
4
4
  "description": "MCP server for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {