contribute-now 0.7.5 → 0.8.0-dev.1cf48ff

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 (3) hide show
  1. package/README.md +55 -8
  2. package/dist/cli.js +1853 -809
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -5644,6 +5644,242 @@ var init_dist = __esm(() => {
5644
5644
  init_types2();
5645
5645
  });
5646
5646
 
5647
+ // src/utils/gh.ts
5648
+ var exports_gh = {};
5649
+ __export(exports_gh, {
5650
+ isRepoFork: () => isRepoFork,
5651
+ getRepoLabels: () => getRepoLabels,
5652
+ getPRForBranch: () => getPRForBranch,
5653
+ getPRContent: () => getPRContent,
5654
+ getMergedPRForBranch: () => getMergedPRForBranch,
5655
+ getIssueContent: () => getIssueContent,
5656
+ getCurrentRepoInfo: () => getCurrentRepoInfo,
5657
+ createPRFill: () => createPRFill,
5658
+ createPR: () => createPR,
5659
+ checkRepoPermissions: () => checkRepoPermissions,
5660
+ checkGhInstalled: () => checkGhInstalled,
5661
+ checkGhAuth: () => checkGhAuth,
5662
+ addLabelsToPR: () => addLabelsToPR,
5663
+ addLabelsToIssue: () => addLabelsToIssue
5664
+ });
5665
+ import { execFile as execFileCb2 } from "child_process";
5666
+ function run2(args) {
5667
+ return new Promise((resolve5) => {
5668
+ execFileCb2("gh", args, (error2, stdout2, stderr) => {
5669
+ resolve5({
5670
+ exitCode: error2 ? error2.code === "ENOENT" ? 127 : error2.status ?? 1 : 0,
5671
+ stdout: stdout2 ?? "",
5672
+ stderr: stderr ?? ""
5673
+ });
5674
+ });
5675
+ });
5676
+ }
5677
+ async function checkGhInstalled() {
5678
+ try {
5679
+ const { exitCode } = await run2(["--version"]);
5680
+ return exitCode === 0;
5681
+ } catch {
5682
+ return false;
5683
+ }
5684
+ }
5685
+ async function checkGhAuth() {
5686
+ try {
5687
+ const { exitCode } = await run2(["auth", "status"]);
5688
+ return exitCode === 0;
5689
+ } catch {
5690
+ return false;
5691
+ }
5692
+ }
5693
+ async function checkRepoPermissions(owner, repo) {
5694
+ if (!SAFE_SLUG.test(owner) || !SAFE_SLUG.test(repo))
5695
+ return null;
5696
+ const { exitCode, stdout: stdout2 } = await run2(["api", `repos/${owner}/${repo}`, "--jq", ".permissions"]);
5697
+ if (exitCode !== 0)
5698
+ return null;
5699
+ try {
5700
+ return JSON.parse(stdout2.trim());
5701
+ } catch {
5702
+ return null;
5703
+ }
5704
+ }
5705
+ async function isRepoFork() {
5706
+ const { exitCode, stdout: stdout2 } = await run2(["repo", "view", "--json", "isFork", "-q", ".isFork"]);
5707
+ if (exitCode !== 0)
5708
+ return null;
5709
+ const val = stdout2.trim();
5710
+ if (val === "true")
5711
+ return true;
5712
+ if (val === "false")
5713
+ return false;
5714
+ return null;
5715
+ }
5716
+ async function getCurrentRepoInfo() {
5717
+ const { exitCode, stdout: stdout2 } = await run2([
5718
+ "repo",
5719
+ "view",
5720
+ "--json",
5721
+ "nameWithOwner",
5722
+ "-q",
5723
+ ".nameWithOwner"
5724
+ ]);
5725
+ if (exitCode !== 0)
5726
+ return null;
5727
+ const nameWithOwner = stdout2.trim();
5728
+ if (!nameWithOwner)
5729
+ return null;
5730
+ const [owner, repo] = nameWithOwner.split("/");
5731
+ if (!owner || !repo)
5732
+ return null;
5733
+ return { owner, repo };
5734
+ }
5735
+ async function createPR(options) {
5736
+ const args = [
5737
+ "pr",
5738
+ "create",
5739
+ "--base",
5740
+ options.base,
5741
+ "--title",
5742
+ options.title,
5743
+ "--body",
5744
+ options.body
5745
+ ];
5746
+ if (options.draft)
5747
+ args.push("--draft");
5748
+ return run2(args);
5749
+ }
5750
+ async function createPRFill(base, draft) {
5751
+ const args = ["pr", "create", "--base", base, "--fill"];
5752
+ if (draft)
5753
+ args.push("--draft");
5754
+ return run2(args);
5755
+ }
5756
+ async function getPRForBranch(headBranch) {
5757
+ const { exitCode, stdout: stdout2 } = await run2([
5758
+ "pr",
5759
+ "list",
5760
+ "--head",
5761
+ headBranch,
5762
+ "--state",
5763
+ "open",
5764
+ "--json",
5765
+ "number,url,title,state",
5766
+ "--limit",
5767
+ "1"
5768
+ ]);
5769
+ if (exitCode !== 0)
5770
+ return null;
5771
+ try {
5772
+ const prs = JSON.parse(stdout2.trim());
5773
+ return prs.length > 0 ? prs[0] : null;
5774
+ } catch {
5775
+ return null;
5776
+ }
5777
+ }
5778
+ async function getMergedPRForBranch(headBranch) {
5779
+ const { exitCode, stdout: stdout2 } = await run2([
5780
+ "pr",
5781
+ "list",
5782
+ "--head",
5783
+ headBranch,
5784
+ "--state",
5785
+ "merged",
5786
+ "--json",
5787
+ "number,url,title,state",
5788
+ "--limit",
5789
+ "1"
5790
+ ]);
5791
+ if (exitCode !== 0)
5792
+ return null;
5793
+ try {
5794
+ const prs = JSON.parse(stdout2.trim());
5795
+ return prs.length > 0 ? prs[0] : null;
5796
+ } catch {
5797
+ return null;
5798
+ }
5799
+ }
5800
+ async function getRepoLabels() {
5801
+ const PAGE_SIZE = 100;
5802
+ const allLabels = [];
5803
+ let page = 1;
5804
+ while (true) {
5805
+ const { exitCode, stdout: stdout2 } = await run2([
5806
+ "label",
5807
+ "list",
5808
+ "--json",
5809
+ "name,description,color",
5810
+ "--limit",
5811
+ String(PAGE_SIZE),
5812
+ "--page",
5813
+ String(page)
5814
+ ]);
5815
+ if (exitCode !== 0)
5816
+ break;
5817
+ let batch = [];
5818
+ try {
5819
+ batch = JSON.parse(stdout2.trim());
5820
+ } catch {
5821
+ break;
5822
+ }
5823
+ if (!Array.isArray(batch) || batch.length === 0)
5824
+ break;
5825
+ for (const item of batch) {
5826
+ if (typeof item.name === "string" && item.name.trim().length > 0) {
5827
+ allLabels.push({
5828
+ name: item.name.trim(),
5829
+ description: typeof item.description === "string" ? item.description.trim() : "",
5830
+ color: typeof item.color === "string" ? item.color.trim().replace(/^#/, "") : ""
5831
+ });
5832
+ }
5833
+ }
5834
+ if (batch.length < PAGE_SIZE)
5835
+ break;
5836
+ page++;
5837
+ }
5838
+ return allLabels;
5839
+ }
5840
+ async function getIssueContent(issueNumber) {
5841
+ const { exitCode, stdout: stdout2 } = await run2([
5842
+ "issue",
5843
+ "view",
5844
+ String(issueNumber),
5845
+ "--json",
5846
+ "title,body"
5847
+ ]);
5848
+ if (exitCode !== 0)
5849
+ return null;
5850
+ try {
5851
+ const parsed = JSON.parse(stdout2.trim());
5852
+ const title = typeof parsed.title === "string" ? parsed.title : "";
5853
+ const body = typeof parsed.body === "string" ? parsed.body : "";
5854
+ return { title, body };
5855
+ } catch {
5856
+ return null;
5857
+ }
5858
+ }
5859
+ async function getPRContent(prNumber) {
5860
+ const { exitCode, stdout: stdout2 } = await run2(["pr", "view", String(prNumber), "--json", "title,body"]);
5861
+ if (exitCode !== 0)
5862
+ return null;
5863
+ try {
5864
+ const parsed = JSON.parse(stdout2.trim());
5865
+ const title = typeof parsed.title === "string" ? parsed.title : "";
5866
+ const body = typeof parsed.body === "string" ? parsed.body : "";
5867
+ return { title, body };
5868
+ } catch {
5869
+ return null;
5870
+ }
5871
+ }
5872
+ async function addLabelsToIssue(issueNumber, labels) {
5873
+ return run2(["issue", "edit", String(issueNumber), "--add-label", labels.join(",")]);
5874
+ }
5875
+ async function addLabelsToPR(prNumber, labels) {
5876
+ return run2(["pr", "edit", String(prNumber), "--add-label", labels.join(",")]);
5877
+ }
5878
+ var SAFE_SLUG;
5879
+ var init_gh = __esm(() => {
5880
+ SAFE_SLUG = /^[\w.-]+$/;
5881
+ });
5882
+
5647
5883
  // node_modules/consola/dist/core.mjs
5648
5884
  var LogLevels = {
5649
5885
  silent: Number.NEGATIVE_INFINITY,
@@ -7206,7 +7442,7 @@ function configExists(cwd = process.cwd()) {
7206
7442
  var VALID_WORKFLOWS = ["clean-flow", "github-flow", "git-flow"];
7207
7443
  var VALID_ROLES = ["maintainer", "contributor"];
7208
7444
  var VALID_CONVENTIONS = ["conventional", "clean-commit", "none"];
7209
- var VALID_AI_PROVIDERS = ["copilot", "ollama-cloud"];
7445
+ var VALID_AI_PROVIDERS = ["copilot", "ollama-cloud", "openrouter"];
7210
7446
  function isAIEnabled(config, cliNoAI = false) {
7211
7447
  return config.aiEnabled !== false && !cliNoAI;
7212
7448
  }
@@ -9115,6 +9351,24 @@ var COMMAND_GUIDES = {
9115
9351
  description: "check config, remotes, and workflow resolution together"
9116
9352
  }
9117
9353
  ]
9354
+ },
9355
+ label: {
9356
+ summary: "Apply existing labels or get ranked suggestions for issues and pull requests.",
9357
+ examples: [
9358
+ { command: "cn label --help", description: "learn label add and suggest usage" },
9359
+ {
9360
+ command: "cn label add --issue 42 bug,enhancement",
9361
+ description: "apply labels to an issue"
9362
+ },
9363
+ {
9364
+ command: 'cn label add --pr 7 "good first issue"',
9365
+ description: "apply a label with spaces to a PR"
9366
+ },
9367
+ {
9368
+ command: "cn label suggest --issue 42",
9369
+ description: "get ranked label suggestions from content"
9370
+ }
9371
+ ]
9118
9372
  }
9119
9373
  };
9120
9374
  var LOADING_TIPS = [
@@ -10624,6 +10878,7 @@ import { join as join5, resolve as resolve4 } from "path";
10624
10878
  var CONTRIBUTE_NOW_SECRETS_DIRNAME = ".contribute-now";
10625
10879
  var CONTRIBUTE_NOW_SECRETS_STORE_DIRNAME = "secrets";
10626
10880
  var OLLAMA_CLOUD_API_KEY = "ollama.cloud.apiKey";
10881
+ var OPENROUTER_API_KEY = "openrouter.apiKey";
10627
10882
  function getSecretsStorePath(baseDir = homedir()) {
10628
10883
  return resolve4(baseDir, CONTRIBUTE_NOW_SECRETS_DIRNAME, CONTRIBUTE_NOW_SECRETS_STORE_DIRNAME);
10629
10884
  }
@@ -10689,6 +10944,36 @@ async function deleteOllamaCloudApiKey(baseDir = homedir()) {
10689
10944
  writeSecretsStore(nextStore, baseDir);
10690
10945
  return true;
10691
10946
  }
10947
+ async function hasOpenRouterApiKey(baseDir = homedir()) {
10948
+ return typeof readSecretsStore(baseDir)?.[OPENROUTER_API_KEY] === "string";
10949
+ }
10950
+ async function getOpenRouterApiKey(baseDir = homedir()) {
10951
+ return readSecretsStore(baseDir)?.[OPENROUTER_API_KEY] ?? null;
10952
+ }
10953
+ async function setOpenRouterApiKey(value, baseDir = homedir()) {
10954
+ const existingStore = readSecretsStore(baseDir) ?? {};
10955
+ writeSecretsStore({
10956
+ ...existingStore,
10957
+ [OPENROUTER_API_KEY]: value
10958
+ }, baseDir);
10959
+ }
10960
+ async function deleteOpenRouterApiKey(baseDir = homedir()) {
10961
+ const existingStore = readSecretsStore(baseDir);
10962
+ if (!existingStore || !(OPENROUTER_API_KEY in existingStore)) {
10963
+ return false;
10964
+ }
10965
+ const nextStore = { ...existingStore };
10966
+ delete nextStore[OPENROUTER_API_KEY];
10967
+ if (Object.keys(nextStore).length === 0) {
10968
+ try {
10969
+ rmSync(getSecretsFilePath(baseDir), { force: true });
10970
+ rmSync(getSecretsStorePath(baseDir), { recursive: true, force: true });
10971
+ } catch {}
10972
+ return true;
10973
+ }
10974
+ writeSecretsStore(nextStore, baseDir);
10975
+ return true;
10976
+ }
10692
10977
 
10693
10978
  // src/utils/copilot.ts
10694
10979
  var CONVENTIONAL_COMMIT_SYSTEM_PROMPT = `Git commit message generator. Format: <type>[!][(<scope>)]: <description>
@@ -10756,6 +11041,8 @@ Rules: title concise present tense, describes the PR theme not individual commit
10756
11041
  var CONFLICT_RESOLUTION_SYSTEM_PROMPT = `Git merge conflict advisor. Explain each side, suggest resolution strategy. Never auto-resolve \u2014 guidance only. Be concise and actionable.`;
10757
11042
  var DEFAULT_OLLAMA_CLOUD_MODEL = "gpt-oss:120b";
10758
11043
  var DEFAULT_OLLAMA_CLOUD_HOST = "https://ollama.com/v1";
11044
+ var DEFAULT_OPENROUTER_MODEL = "openai/gpt-4o-mini";
11045
+ var DEFAULT_OPENROUTER_HOST = "https://openrouter.ai/api/v1";
10759
11046
  function prioritizeOllamaCloudModels(models, preferredModel = DEFAULT_OLLAMA_CLOUD_MODEL) {
10760
11047
  const uniqueModels = [...new Set(models.map((model) => model.trim()).filter(Boolean))];
10761
11048
  const sortedModels = [...uniqueModels].sort((left, right) => left.localeCompare(right));
@@ -10763,7 +11050,9 @@ function prioritizeOllamaCloudModels(models, preferredModel = DEFAULT_OLLAMA_CLO
10763
11050
  }
10764
11051
  function extractOllamaCloudModelIds(payload) {
10765
11052
  const records = typeof payload === "object" && payload !== null ? Array.isArray(payload.data) ? payload.data : Array.isArray(payload.models) ? payload.models : [] : [];
10766
- return [...new Set(records.map(getOllamaCloudModelId).filter(Boolean))].sort((left, right) => left.localeCompare(right));
11053
+ return [
11054
+ ...new Set(records.map(getOllamaCloudModelId).filter((id) => id !== null))
11055
+ ].sort((left, right) => left.localeCompare(right));
10767
11056
  }
10768
11057
  function getOllamaCloudModelId(record) {
10769
11058
  if (typeof record !== "object" || record === null) {
@@ -10792,6 +11081,40 @@ function normalizeOllamaCloudHost(host) {
10792
11081
  const trimmed = (host?.trim() || DEFAULT_OLLAMA_CLOUD_HOST).replace(/\/+$/, "");
10793
11082
  return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
10794
11083
  }
11084
+ function extractOpenRouterModelIds(payload) {
11085
+ const records = typeof payload === "object" && payload !== null ? Array.isArray(payload.data) ? payload.data : [] : [];
11086
+ return [
11087
+ ...new Set(records.map(getOpenRouterModelId).filter((id) => id !== null))
11088
+ ].sort((left, right) => left.localeCompare(right));
11089
+ }
11090
+ function getOpenRouterModelId(record) {
11091
+ if (typeof record !== "object" || record === null) {
11092
+ return null;
11093
+ }
11094
+ const candidate = typeof record.id === "string" ? record.id : null;
11095
+ const normalized = candidate?.trim();
11096
+ return normalized ? normalized : null;
11097
+ }
11098
+ async function fetchOpenRouterModels(apiKey) {
11099
+ const response = await fetch(`${DEFAULT_OPENROUTER_HOST}/models`, {
11100
+ headers: {
11101
+ Accept: "application/json",
11102
+ Authorization: `Bearer ${apiKey}`
11103
+ }
11104
+ });
11105
+ if (!response.ok) {
11106
+ if (response.status === 401 || response.status === 403) {
11107
+ throw new Error("OpenRouter authentication failed");
11108
+ }
11109
+ throw new Error(`OpenRouter model lookup failed (${response.status} ${response.statusText})`);
11110
+ }
11111
+ return extractOpenRouterModelIds(await response.json());
11112
+ }
11113
+ function prioritizeOpenRouterModels(models, preferredModel = DEFAULT_OPENROUTER_MODEL) {
11114
+ const uniqueModels = [...new Set(models.map((model) => model.trim()).filter(Boolean))];
11115
+ const sortedModels = [...uniqueModels].sort((left, right) => left.localeCompare(right));
11116
+ return sortedModels.includes(preferredModel) ? [preferredModel, ...sortedModels.filter((model) => model !== preferredModel)] : sortedModels;
11117
+ }
10795
11118
  function resolveAIConfig(config) {
10796
11119
  const resolvedConfig = config ?? readConfig();
10797
11120
  const provider = resolvedConfig?.aiProvider ?? "copilot";
@@ -10803,6 +11126,14 @@ function resolveAIConfig(config) {
10803
11126
  host: DEFAULT_OLLAMA_CLOUD_HOST
10804
11127
  };
10805
11128
  }
11129
+ if (provider === "openrouter") {
11130
+ return {
11131
+ provider,
11132
+ providerLabel: "OpenRouter",
11133
+ model: resolvedConfig?.aiModel?.trim() || DEFAULT_OPENROUTER_MODEL,
11134
+ host: DEFAULT_OPENROUTER_HOST
11135
+ };
11136
+ }
10806
11137
  return {
10807
11138
  provider: "copilot",
10808
11139
  providerLabel: "GitHub Copilot"
@@ -10996,6 +11327,28 @@ async function checkCopilotAvailable2() {
10996
11327
  return `Could not reach Ollama Cloud API: ${msg}`;
10997
11328
  }
10998
11329
  }
11330
+ if (aiConfig.provider === "openrouter") {
11331
+ if (!await hasOpenRouterApiKey()) {
11332
+ return "OpenRouter API key not found. Run `cn setup` to save it.";
11333
+ }
11334
+ try {
11335
+ const apiKey = await getOpenRouterApiKey();
11336
+ if (!apiKey) {
11337
+ return "OpenRouter API key not found. Run `cn setup` to save it.";
11338
+ }
11339
+ await fetchOpenRouterModels(apiKey);
11340
+ return null;
11341
+ } catch (err) {
11342
+ const msg = err instanceof Error ? err.message : String(err);
11343
+ if (msg === "OpenRouter authentication failed") {
11344
+ return "OpenRouter authentication failed. Update your saved API key with `cn setup`.";
11345
+ }
11346
+ if (msg.startsWith("OpenRouter model lookup failed")) {
11347
+ return msg.replace("model lookup", "health check");
11348
+ }
11349
+ return `Could not reach OpenRouter API: ${msg}`;
11350
+ }
11351
+ }
10999
11352
  try {
11000
11353
  const client = await getManagedClient();
11001
11354
  try {
@@ -11097,21 +11450,64 @@ async function callOllamaCloud(systemMessage, userMessage, model, timeoutMs = CO
11097
11450
  clearTimeout(timer);
11098
11451
  }
11099
11452
  }
11100
- async function callAI(systemMessage, userMessage, model, timeoutMs = COPILOT_TIMEOUT_MS) {
11453
+ async function callOpenRouter(systemMessage, userMessage, model, timeoutMs = COPILOT_TIMEOUT_MS) {
11101
11454
  const aiConfig = resolveAIConfig();
11102
- if (aiConfig.provider === "ollama-cloud") {
11103
- return callOllamaCloud(systemMessage, userMessage, model, timeoutMs);
11455
+ const apiKey = await getOpenRouterApiKey();
11456
+ if (!apiKey) {
11457
+ throw new Error("OpenRouter API key is not configured");
11104
11458
  }
11105
- return callCopilot(systemMessage, userMessage, model, timeoutMs);
11106
- }
11107
- function getCommitSystemPrompt(convention) {
11108
- if (convention === "conventional")
11109
- return CONVENTIONAL_COMMIT_SYSTEM_PROMPT;
11110
- return CLEAN_COMMIT_SYSTEM_PROMPT;
11111
- }
11112
- function extractJson(raw) {
11113
- let text = raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
11114
- if (text.startsWith("[") || text.startsWith("{"))
11459
+ const controller = new AbortController;
11460
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
11461
+ try {
11462
+ const response = await fetch(`${DEFAULT_OPENROUTER_HOST}/chat/completions`, {
11463
+ method: "POST",
11464
+ headers: {
11465
+ Authorization: `Bearer ${apiKey}`,
11466
+ "Content-Type": "application/json",
11467
+ "HTTP-Referer": "https://github.com/warengonzaga/contribute-now",
11468
+ "X-Title": "contribute-now"
11469
+ },
11470
+ body: JSON.stringify({
11471
+ model: model?.trim() || aiConfig.model || DEFAULT_OPENROUTER_MODEL,
11472
+ messages: [
11473
+ { role: "system", content: systemMessage },
11474
+ { role: "user", content: userMessage }
11475
+ ],
11476
+ stream: false
11477
+ }),
11478
+ signal: controller.signal
11479
+ });
11480
+ if (!response.ok) {
11481
+ const body = await response.text();
11482
+ if (response.status === 401 || response.status === 403) {
11483
+ throw new Error("OpenRouter authentication failed");
11484
+ }
11485
+ throw new Error(`OpenRouter request failed (${response.status} ${response.statusText}): ${body.slice(0, 200)}`);
11486
+ }
11487
+ const data = await response.json();
11488
+ return data.choices?.[0]?.message?.content?.trim() || null;
11489
+ } finally {
11490
+ clearTimeout(timer);
11491
+ }
11492
+ }
11493
+ async function callAI(systemMessage, userMessage, model, timeoutMs = COPILOT_TIMEOUT_MS) {
11494
+ const aiConfig = resolveAIConfig();
11495
+ if (aiConfig.provider === "ollama-cloud") {
11496
+ return callOllamaCloud(systemMessage, userMessage, model, timeoutMs);
11497
+ }
11498
+ if (aiConfig.provider === "openrouter") {
11499
+ return callOpenRouter(systemMessage, userMessage, model, timeoutMs);
11500
+ }
11501
+ return callCopilot(systemMessage, userMessage, model, timeoutMs);
11502
+ }
11503
+ function getCommitSystemPrompt(convention) {
11504
+ if (convention === "conventional")
11505
+ return CONVENTIONAL_COMMIT_SYSTEM_PROMPT;
11506
+ return CLEAN_COMMIT_SYSTEM_PROMPT;
11507
+ }
11508
+ function extractJson(raw) {
11509
+ let text = raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
11510
+ if (text.startsWith("[") || text.startsWith("{"))
11115
11511
  return text;
11116
11512
  const arrayStart = text.indexOf("[");
11117
11513
  const objStart = text.indexOf("{");
@@ -11651,145 +12047,8 @@ async function promptForBranchName(options) {
11651
12047
  }
11652
12048
  }
11653
12049
 
11654
- // src/utils/gh.ts
11655
- import { execFile as execFileCb2 } from "child_process";
11656
- function run2(args) {
11657
- return new Promise((resolve5) => {
11658
- execFileCb2("gh", args, (error2, stdout2, stderr) => {
11659
- resolve5({
11660
- exitCode: error2 ? error2.code === "ENOENT" ? 127 : error2.status ?? 1 : 0,
11661
- stdout: stdout2 ?? "",
11662
- stderr: stderr ?? ""
11663
- });
11664
- });
11665
- });
11666
- }
11667
- async function checkGhInstalled() {
11668
- try {
11669
- const { exitCode } = await run2(["--version"]);
11670
- return exitCode === 0;
11671
- } catch {
11672
- return false;
11673
- }
11674
- }
11675
- async function checkGhAuth() {
11676
- try {
11677
- const { exitCode } = await run2(["auth", "status"]);
11678
- return exitCode === 0;
11679
- } catch {
11680
- return false;
11681
- }
11682
- }
11683
- var SAFE_SLUG = /^[\w.-]+$/;
11684
- async function checkRepoPermissions(owner, repo) {
11685
- if (!SAFE_SLUG.test(owner) || !SAFE_SLUG.test(repo))
11686
- return null;
11687
- const { exitCode, stdout: stdout2 } = await run2(["api", `repos/${owner}/${repo}`, "--jq", ".permissions"]);
11688
- if (exitCode !== 0)
11689
- return null;
11690
- try {
11691
- return JSON.parse(stdout2.trim());
11692
- } catch {
11693
- return null;
11694
- }
11695
- }
11696
- async function isRepoFork() {
11697
- const { exitCode, stdout: stdout2 } = await run2(["repo", "view", "--json", "isFork", "-q", ".isFork"]);
11698
- if (exitCode !== 0)
11699
- return null;
11700
- const val = stdout2.trim();
11701
- if (val === "true")
11702
- return true;
11703
- if (val === "false")
11704
- return false;
11705
- return null;
11706
- }
11707
- async function getCurrentRepoInfo() {
11708
- const { exitCode, stdout: stdout2 } = await run2([
11709
- "repo",
11710
- "view",
11711
- "--json",
11712
- "nameWithOwner",
11713
- "-q",
11714
- ".nameWithOwner"
11715
- ]);
11716
- if (exitCode !== 0)
11717
- return null;
11718
- const nameWithOwner = stdout2.trim();
11719
- if (!nameWithOwner)
11720
- return null;
11721
- const [owner, repo] = nameWithOwner.split("/");
11722
- if (!owner || !repo)
11723
- return null;
11724
- return { owner, repo };
11725
- }
11726
- async function createPR(options) {
11727
- const args = [
11728
- "pr",
11729
- "create",
11730
- "--base",
11731
- options.base,
11732
- "--title",
11733
- options.title,
11734
- "--body",
11735
- options.body
11736
- ];
11737
- if (options.draft)
11738
- args.push("--draft");
11739
- return run2(args);
11740
- }
11741
- async function createPRFill(base, draft) {
11742
- const args = ["pr", "create", "--base", base, "--fill"];
11743
- if (draft)
11744
- args.push("--draft");
11745
- return run2(args);
11746
- }
11747
- async function getPRForBranch(headBranch) {
11748
- const { exitCode, stdout: stdout2 } = await run2([
11749
- "pr",
11750
- "list",
11751
- "--head",
11752
- headBranch,
11753
- "--state",
11754
- "open",
11755
- "--json",
11756
- "number,url,title,state",
11757
- "--limit",
11758
- "1"
11759
- ]);
11760
- if (exitCode !== 0)
11761
- return null;
11762
- try {
11763
- const prs = JSON.parse(stdout2.trim());
11764
- return prs.length > 0 ? prs[0] : null;
11765
- } catch {
11766
- return null;
11767
- }
11768
- }
11769
- async function getMergedPRForBranch(headBranch) {
11770
- const { exitCode, stdout: stdout2 } = await run2([
11771
- "pr",
11772
- "list",
11773
- "--head",
11774
- headBranch,
11775
- "--state",
11776
- "merged",
11777
- "--json",
11778
- "number,url,title,state",
11779
- "--limit",
11780
- "1"
11781
- ]);
11782
- if (exitCode !== 0)
11783
- return null;
11784
- try {
11785
- const prs = JSON.parse(stdout2.trim());
11786
- return prs.length > 0 ? prs[0] : null;
11787
- } catch {
11788
- return null;
11789
- }
11790
- }
11791
-
11792
12050
  // src/commands/clean.ts
12051
+ init_gh();
11793
12052
  async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
11794
12053
  if (!config)
11795
12054
  return "skipped";
@@ -11992,185 +12251,77 @@ ${import_picocolors8.default.bold("Stale branches (remote deleted, likely squash
11992
12251
  }
11993
12252
  });
11994
12253
 
11995
- // src/commands/discard.ts
12254
+ // src/commands/commit.ts
11996
12255
  var import_picocolors9 = __toESM(require_picocolors(), 1);
11997
- var discard_default = defineCommand({
12256
+
12257
+ // src/utils/convention.ts
12258
+ var CLEAN_COMMIT_PATTERN = /^(\uD83D\uDCE6|\uD83D\uDD27|\uD83D\uDDD1\uFE0F?|\uD83D\uDD12|\u2699\uFE0F?|\u2615|\uD83E\uDDEA|\uD83D\uDCD6|\uD83D\uDE80) (new|update|remove|security|setup|chore|test|docs|release)(!?)( \([a-zA-Z0-9][a-zA-Z0-9-]*\))?: .{1,72}$/u;
12259
+ var CONVENTIONAL_COMMIT_PATTERN = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(!?)(\([a-zA-Z0-9][a-zA-Z0-9._-]*\))?: .{1,72}$/;
12260
+ var CONVENTION_LABELS = {
12261
+ conventional: "Conventional Commits",
12262
+ "clean-commit": "Clean Commit (by WGTech Labs)",
12263
+ none: "No convention"
12264
+ };
12265
+ var CONVENTION_DESCRIPTIONS = {
12266
+ conventional: "Conventional Commits \u2014 feat: | fix: | docs: | chore: etc. (conventionalcommits.org)",
12267
+ "clean-commit": "Clean Commit \u2014 \uD83D\uDCE6 new: | \uD83D\uDD27 update: | \uD83D\uDDD1\uFE0F remove: etc. (by WGTech Labs)",
12268
+ none: "No commit convention enforcement"
12269
+ };
12270
+ var CONVENTION_FORMAT_HINTS = {
12271
+ conventional: [
12272
+ "Format: <type>[!][(<scope>)]: <description>",
12273
+ "Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert",
12274
+ "Examples: feat: add login page | fix(auth): resolve token expiry | docs: update README",
12275
+ "Do not use backticks or markdown formatting in the message."
12276
+ ],
12277
+ "clean-commit": [
12278
+ "Format: <emoji> <type>[!][(<scope>)]: <description>",
12279
+ "Types: \uD83D\uDCE6 new | \uD83D\uDD27 update | \uD83D\uDDD1\uFE0F remove | \uD83D\uDD12 security | \u2699\uFE0F setup | \u2615 chore | \uD83E\uDDEA test | \uD83D\uDCD6 docs | \uD83D\uDE80 release",
12280
+ "Examples: \uD83D\uDCE6 new: user auth | \uD83D\uDD27 update (api): improve errors | \u2699\uFE0F setup (ci): add workflow",
12281
+ "Do not use backticks or markdown formatting in the message."
12282
+ ]
12283
+ };
12284
+ function hasUnsupportedCommitMessageChars(message) {
12285
+ return message.includes("`");
12286
+ }
12287
+ function validateCommitMessage(message, convention) {
12288
+ if (convention === "none")
12289
+ return true;
12290
+ if (hasUnsupportedCommitMessageChars(message))
12291
+ return false;
12292
+ if (convention === "clean-commit")
12293
+ return CLEAN_COMMIT_PATTERN.test(message);
12294
+ if (convention === "conventional")
12295
+ return CONVENTIONAL_COMMIT_PATTERN.test(message);
12296
+ return true;
12297
+ }
12298
+ function getValidationError(convention) {
12299
+ if (convention === "none")
12300
+ return [];
12301
+ return [
12302
+ `Commit message does not follow ${CONVENTION_LABELS[convention]} format.`,
12303
+ "Do not use backticks or markdown formatting in commit messages.",
12304
+ ...CONVENTION_FORMAT_HINTS[convention]
12305
+ ];
12306
+ }
12307
+
12308
+ // src/commands/commit.ts
12309
+ function isEmptyGroupCommitResult(detail) {
12310
+ return /no changes added to commit|nothing to commit/i.test(detail);
12311
+ }
12312
+ var commit_default = defineCommand({
11998
12313
  meta: {
11999
- name: "discard",
12000
- description: "Discard the current feature branch and return to the base branch"
12314
+ name: "commit",
12315
+ description: "Stage changes and create a commit message (AI-powered)"
12001
12316
  },
12002
12317
  args: {
12003
- force: {
12318
+ model: {
12319
+ type: "string",
12320
+ description: "AI model to use for commit message generation"
12321
+ },
12322
+ "no-ai": {
12004
12323
  type: "boolean",
12005
- alias: "f",
12006
- description: "Skip confirmation and discard immediately",
12007
- default: false
12008
- }
12009
- },
12010
- async run({ args }) {
12011
- if (!await isGitRepo()) {
12012
- error("Not inside a git repository.");
12013
- process.exit(1);
12014
- }
12015
- await assertCleanGitState("discarding a branch");
12016
- const config = readConfig();
12017
- if (!config) {
12018
- error("No repo config found. Run `cn setup` first.");
12019
- process.exit(1);
12020
- }
12021
- const currentBranch = await getCurrentBranch();
12022
- const baseBranch = getBaseBranch(config);
12023
- await projectHeading("discard", "\uD83D\uDDD1\uFE0F");
12024
- if (isBranchProtected(currentBranch, config)) {
12025
- error(`${import_picocolors9.default.bold(currentBranch)} is a protected branch and cannot be discarded.`);
12026
- info(`Switch to a feature branch first, then run ${import_picocolors9.default.bold("cn discard")}.`);
12027
- process.exit(1);
12028
- }
12029
- if (currentBranch === baseBranch) {
12030
- info(`You are already on ${import_picocolors9.default.bold(baseBranch)}.`);
12031
- process.exit(0);
12032
- }
12033
- const { origin } = config;
12034
- const localWork = await hasLocalWork(origin, currentBranch);
12035
- const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
12036
- if (hasWork) {
12037
- if (localWork.uncommitted) {
12038
- warn("You have uncommitted changes in your working tree.");
12039
- }
12040
- if (localWork.unpushedCommits > 0) {
12041
- warn(`You have ${import_picocolors9.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on this branch.`);
12042
- }
12043
- warn("Discarding this branch will permanently lose that work.");
12044
- const SAVE_FIRST = "Save my changes first (cn save), then discard";
12045
- const DISCARD_ANYWAY = "Discard anyway \u2014 I do not need this work";
12046
- const CANCEL = "Keep the branch, take me back";
12047
- const action = await selectPrompt("This branch has unsaved work. What would you like to do?", [SAVE_FIRST, DISCARD_ANYWAY, CANCEL]);
12048
- if (action === CANCEL) {
12049
- info("Discard cancelled. Your branch is untouched.");
12050
- process.exit(0);
12051
- }
12052
- if (action === SAVE_FIRST) {
12053
- if (!localWork.uncommitted) {
12054
- info("No uncommitted changes to stash \u2014 unpushed commits will still be lost.");
12055
- const confirm = await confirmPrompt("Continue discarding the branch?");
12056
- if (!confirm) {
12057
- info("Discard cancelled.");
12058
- process.exit(0);
12059
- }
12060
- } else {
12061
- const stashResult = await stashChanges(`work-in-progress on ${currentBranch}`);
12062
- if (stashResult.exitCode !== 0) {
12063
- error(`Failed to save changes: ${stashResult.stderr}`);
12064
- process.exit(1);
12065
- }
12066
- success(`Changes saved. Use ${import_picocolors9.default.bold("cn save --restore")} to bring them back.`);
12067
- }
12068
- }
12069
- } else if (!args.force) {
12070
- const confirmed = await confirmPrompt(`Discard ${import_picocolors9.default.bold(currentBranch)} and return to ${import_picocolors9.default.bold(baseBranch)}?`);
12071
- if (!confirmed) {
12072
- info("Discard cancelled.");
12073
- process.exit(0);
12074
- }
12075
- }
12076
- const upstreamRef = await getUpstreamRef();
12077
- let deleteRemote = false;
12078
- if (upstreamRef) {
12079
- deleteRemote = await confirmPrompt(`Also delete the remote branch ${import_picocolors9.default.bold(upstreamRef)}?`);
12080
- }
12081
- const checkoutResult = await checkoutBranch(baseBranch);
12082
- if (checkoutResult.exitCode !== 0) {
12083
- error(`Failed to switch to ${import_picocolors9.default.bold(baseBranch)}: ${checkoutResult.stderr}`);
12084
- process.exit(1);
12085
- }
12086
- const deleteResult = await forceDeleteBranch(currentBranch);
12087
- if (deleteResult.exitCode !== 0) {
12088
- error(`Failed to delete branch ${import_picocolors9.default.bold(currentBranch)}: ${deleteResult.stderr}`);
12089
- process.exit(1);
12090
- }
12091
- success(`Discarded ${import_picocolors9.default.bold(currentBranch)} and switched back to ${import_picocolors9.default.bold(baseBranch)}`);
12092
- if (deleteRemote) {
12093
- const remoteDeleteResult = await deleteRemoteBranch(origin, currentBranch);
12094
- if (remoteDeleteResult.exitCode !== 0) {
12095
- warn(`Could not delete remote branch: ${remoteDeleteResult.stderr.trim()}`);
12096
- } else {
12097
- success(`Deleted remote branch ${import_picocolors9.default.bold(`${origin}/${currentBranch}`)}`);
12098
- }
12099
- }
12100
- }
12101
- });
12102
-
12103
- // src/commands/commit.ts
12104
- var import_picocolors10 = __toESM(require_picocolors(), 1);
12105
-
12106
- // src/utils/convention.ts
12107
- var CLEAN_COMMIT_PATTERN = /^(\uD83D\uDCE6|\uD83D\uDD27|\uD83D\uDDD1\uFE0F?|\uD83D\uDD12|\u2699\uFE0F?|\u2615|\uD83E\uDDEA|\uD83D\uDCD6|\uD83D\uDE80) (new|update|remove|security|setup|chore|test|docs|release)(!?)( \([a-zA-Z0-9][a-zA-Z0-9-]*\))?: .{1,72}$/u;
12108
- var CONVENTIONAL_COMMIT_PATTERN = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(!?)(\([a-zA-Z0-9][a-zA-Z0-9._-]*\))?: .{1,72}$/;
12109
- var CONVENTION_LABELS = {
12110
- conventional: "Conventional Commits",
12111
- "clean-commit": "Clean Commit (by WGTech Labs)",
12112
- none: "No convention"
12113
- };
12114
- var CONVENTION_DESCRIPTIONS = {
12115
- conventional: "Conventional Commits \u2014 feat: | fix: | docs: | chore: etc. (conventionalcommits.org)",
12116
- "clean-commit": "Clean Commit \u2014 \uD83D\uDCE6 new: | \uD83D\uDD27 update: | \uD83D\uDDD1\uFE0F remove: etc. (by WGTech Labs)",
12117
- none: "No commit convention enforcement"
12118
- };
12119
- var CONVENTION_FORMAT_HINTS = {
12120
- conventional: [
12121
- "Format: <type>[!][(<scope>)]: <description>",
12122
- "Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert",
12123
- "Examples: feat: add login page | fix(auth): resolve token expiry | docs: update README",
12124
- "Do not use backticks or markdown formatting in the message."
12125
- ],
12126
- "clean-commit": [
12127
- "Format: <emoji> <type>[!][(<scope>)]: <description>",
12128
- "Types: \uD83D\uDCE6 new | \uD83D\uDD27 update | \uD83D\uDDD1\uFE0F remove | \uD83D\uDD12 security | \u2699\uFE0F setup | \u2615 chore | \uD83E\uDDEA test | \uD83D\uDCD6 docs | \uD83D\uDE80 release",
12129
- "Examples: \uD83D\uDCE6 new: user auth | \uD83D\uDD27 update (api): improve errors | \u2699\uFE0F setup (ci): add workflow",
12130
- "Do not use backticks or markdown formatting in the message."
12131
- ]
12132
- };
12133
- function hasUnsupportedCommitMessageChars(message) {
12134
- return message.includes("`");
12135
- }
12136
- function validateCommitMessage(message, convention) {
12137
- if (convention === "none")
12138
- return true;
12139
- if (hasUnsupportedCommitMessageChars(message))
12140
- return false;
12141
- if (convention === "clean-commit")
12142
- return CLEAN_COMMIT_PATTERN.test(message);
12143
- if (convention === "conventional")
12144
- return CONVENTIONAL_COMMIT_PATTERN.test(message);
12145
- return true;
12146
- }
12147
- function getValidationError(convention) {
12148
- if (convention === "none")
12149
- return [];
12150
- return [
12151
- `Commit message does not follow ${CONVENTION_LABELS[convention]} format.`,
12152
- "Do not use backticks or markdown formatting in commit messages.",
12153
- ...CONVENTION_FORMAT_HINTS[convention]
12154
- ];
12155
- }
12156
-
12157
- // src/commands/commit.ts
12158
- function isEmptyGroupCommitResult(detail) {
12159
- return /no changes added to commit|nothing to commit/i.test(detail);
12160
- }
12161
- var commit_default = defineCommand({
12162
- meta: {
12163
- name: "commit",
12164
- description: "Stage changes and create a commit message (AI-powered)"
12165
- },
12166
- args: {
12167
- model: {
12168
- type: "string",
12169
- description: "AI model to use for commit message generation"
12170
- },
12171
- "no-ai": {
12172
- type: "boolean",
12173
- description: "Skip AI and write commit message manually",
12324
+ description: "Skip AI and write commit message manually",
12174
12325
  default: false
12175
12326
  },
12176
12327
  group: {
@@ -12208,9 +12359,9 @@ var commit_default = defineCommand({
12208
12359
  process.exit(1);
12209
12360
  }
12210
12361
  console.log(`
12211
- ${import_picocolors10.default.bold("Changed files:")}`);
12362
+ ${import_picocolors9.default.bold("Changed files:")}`);
12212
12363
  for (const f3 of changedFiles) {
12213
- console.log(` ${import_picocolors10.default.dim("\u2022")} ${f3}`);
12364
+ console.log(` ${import_picocolors9.default.dim("\u2022")} ${f3}`);
12214
12365
  }
12215
12366
  const stageAction = await selectPrompt("No staged changes. How would you like to stage?", [
12216
12367
  "Stage all changes",
@@ -12252,8 +12403,8 @@ ${import_picocolors10.default.bold("Changed files:")}`);
12252
12403
  const dirs = new Set(stagedFiles.map((f3) => f3.split("/")[0]));
12253
12404
  if (dirs.size > 1) {
12254
12405
  console.log();
12255
- warn(`You're staging ${import_picocolors10.default.bold(String(stagedFiles.length))} files across ${import_picocolors10.default.bold(String(dirs.size))} directories in a single commit.`);
12256
- info(import_picocolors10.default.dim("Large commits mixing different topics make history harder to read and bisect. " + "For cleaner history, consider splitting into atomic commits."));
12406
+ warn(`You're staging ${import_picocolors9.default.bold(String(stagedFiles.length))} files across ${import_picocolors9.default.bold(String(dirs.size))} directories in a single commit.`);
12407
+ info(import_picocolors9.default.dim("Large commits mixing different topics make history harder to read and bisect. " + "For cleaner history, consider splitting into atomic commits."));
12257
12408
  const choice = await selectPrompt("How would you like to proceed?", [
12258
12409
  "Continue as single commit",
12259
12410
  "Switch to group mode (AI splits into atomic commits)",
@@ -12284,7 +12435,7 @@ ${import_picocolors10.default.bold("Changed files:")}`);
12284
12435
  if (commitMessage) {
12285
12436
  spinner.success("AI commit message generated.");
12286
12437
  console.log(`
12287
- ${import_picocolors10.default.dim("AI suggestion:")} ${import_picocolors10.default.bold(import_picocolors10.default.cyan(commitMessage))}`);
12438
+ ${import_picocolors9.default.dim("AI suggestion:")} ${import_picocolors9.default.bold(import_picocolors9.default.cyan(commitMessage))}`);
12288
12439
  } else {
12289
12440
  spinner.fail("AI did not return a commit message.");
12290
12441
  warn("Falling back to manual entry.");
@@ -12312,7 +12463,7 @@ ${import_picocolors10.default.bold("Changed files:")}`);
12312
12463
  if (regen) {
12313
12464
  spinner.success("Commit message regenerated.");
12314
12465
  console.log(`
12315
- ${import_picocolors10.default.dim("AI suggestion:")} ${import_picocolors10.default.bold(import_picocolors10.default.cyan(regen))}`);
12466
+ ${import_picocolors9.default.dim("AI suggestion:")} ${import_picocolors9.default.bold(import_picocolors9.default.cyan(regen))}`);
12316
12467
  const ok = await confirmPrompt("Use this message?");
12317
12468
  finalMessage = ok ? regen : await inputPrompt("Enter commit message manually");
12318
12469
  } else {
@@ -12327,7 +12478,7 @@ ${import_picocolors10.default.bold("Changed files:")}`);
12327
12478
  if (convention2 !== "none") {
12328
12479
  console.log();
12329
12480
  for (const hint of CONVENTION_FORMAT_HINTS[convention2]) {
12330
- console.log(import_picocolors10.default.dim(hint));
12481
+ console.log(import_picocolors9.default.dim(hint));
12331
12482
  }
12332
12483
  console.log();
12333
12484
  }
@@ -12351,7 +12502,7 @@ ${import_picocolors10.default.bold("Changed files:")}`);
12351
12502
  error(`Failed to commit: ${result.stderr}`);
12352
12503
  process.exit(1);
12353
12504
  }
12354
- success(`Committed: ${import_picocolors10.default.bold(finalMessage)}`);
12505
+ success(`Committed: ${import_picocolors9.default.bold(finalMessage)}`);
12355
12506
  }
12356
12507
  });
12357
12508
  async function runGroupCommit(model, config) {
@@ -12368,9 +12519,9 @@ async function runGroupCommit(model, config) {
12368
12519
  process.exit(1);
12369
12520
  }
12370
12521
  console.log(`
12371
- ${import_picocolors10.default.bold("Changed files:")}`);
12522
+ ${import_picocolors9.default.bold("Changed files:")}`);
12372
12523
  for (const f3 of changedFiles) {
12373
- console.log(` ${import_picocolors10.default.dim("\u2022")} ${f3}`);
12524
+ console.log(` ${import_picocolors9.default.dim("\u2022")} ${f3}`);
12374
12525
  }
12375
12526
  const spinner = createSpinner(changedFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD ? `Asking AI to group ${changedFiles.length} file(s) into logical commits (using optimized batching)...` : `Asking AI to group ${changedFiles.length} file(s) into logical commits...`, {
12376
12527
  tips: LOADING_TIPS
@@ -12416,13 +12567,13 @@ ${import_picocolors10.default.bold("Changed files:")}`);
12416
12567
  let commitAll = false;
12417
12568
  while (!proceedToCommit) {
12418
12569
  console.log(`
12419
- ${import_picocolors10.default.bold(`AI suggested ${validGroups.length} commit group(s):`)}
12570
+ ${import_picocolors9.default.bold(`AI suggested ${validGroups.length} commit group(s):`)}
12420
12571
  `);
12421
12572
  for (let i2 = 0;i2 < validGroups.length; i2++) {
12422
12573
  const g3 = validGroups[i2];
12423
- console.log(` ${import_picocolors10.default.cyan(`Group ${i2 + 1}:`)} ${import_picocolors10.default.bold(g3.message)}`);
12574
+ console.log(` ${import_picocolors9.default.cyan(`Group ${i2 + 1}:`)} ${import_picocolors9.default.bold(g3.message)}`);
12424
12575
  for (const f3 of g3.files) {
12425
- console.log(` ${import_picocolors10.default.dim("\u2022")} ${f3}`);
12576
+ console.log(` ${import_picocolors9.default.dim("\u2022")} ${f3}`);
12426
12577
  }
12427
12578
  console.log();
12428
12579
  }
@@ -12488,16 +12639,16 @@ ${import_picocolors10.default.bold(`AI suggested ${validGroups.length} commit gr
12488
12639
  continue;
12489
12640
  }
12490
12641
  committed++;
12491
- success(`Committed group ${i2 + 1}: ${import_picocolors10.default.bold(group.message)}`);
12642
+ success(`Committed group ${i2 + 1}: ${import_picocolors9.default.bold(group.message)}`);
12492
12643
  }
12493
12644
  } else {
12494
12645
  for (let i2 = 0;i2 < validGroups.length; i2++) {
12495
12646
  const group = validGroups[i2];
12496
- console.log(import_picocolors10.default.bold(`
12647
+ console.log(import_picocolors9.default.bold(`
12497
12648
  \u2500\u2500 Group ${i2 + 1}/${validGroups.length} \u2500\u2500`));
12498
- console.log(` ${import_picocolors10.default.cyan(group.message)}`);
12649
+ console.log(` ${import_picocolors9.default.cyan(group.message)}`);
12499
12650
  for (const f3 of group.files) {
12500
- console.log(` ${import_picocolors10.default.dim("\u2022")} ${f3}`);
12651
+ console.log(` ${import_picocolors9.default.dim("\u2022")} ${f3}`);
12501
12652
  }
12502
12653
  let message = group.message;
12503
12654
  let actionDone = false;
@@ -12521,7 +12672,7 @@ ${import_picocolors10.default.bold(`AI suggested ${validGroups.length} commit gr
12521
12672
  if (newMsg) {
12522
12673
  message = newMsg;
12523
12674
  group.message = newMsg;
12524
- regenSpinner.success(`New message: ${import_picocolors10.default.bold(message)}`);
12675
+ regenSpinner.success(`New message: ${import_picocolors9.default.bold(message)}`);
12525
12676
  } else {
12526
12677
  regenSpinner.fail("AI could not generate a new message. Keeping current one.");
12527
12678
  }
@@ -12584,7 +12735,7 @@ ${import_picocolors10.default.bold(`AI suggested ${validGroups.length} commit gr
12584
12735
  continue;
12585
12736
  }
12586
12737
  committed++;
12587
- success(`Committed group ${i2 + 1}: ${import_picocolors10.default.bold(message)}`);
12738
+ success(`Committed group ${i2 + 1}: ${import_picocolors9.default.bold(message)}`);
12588
12739
  actionDone = true;
12589
12740
  }
12590
12741
  }
@@ -12599,7 +12750,7 @@ ${import_picocolors10.default.bold(`AI suggested ${validGroups.length} commit gr
12599
12750
  }
12600
12751
 
12601
12752
  // src/commands/config.ts
12602
- var import_picocolors11 = __toESM(require_picocolors(), 1);
12753
+ var import_picocolors10 = __toESM(require_picocolors(), 1);
12603
12754
  var WORKFLOW_OPTIONS = [
12604
12755
  { value: "clean-flow", label: WORKFLOW_DESCRIPTIONS["clean-flow"] },
12605
12756
  { value: "github-flow", label: WORKFLOW_DESCRIPTIONS["github-flow"] },
@@ -12616,7 +12767,8 @@ var CONVENTION_OPTIONS = [
12616
12767
  ];
12617
12768
  var AI_PROVIDER_OPTIONS = [
12618
12769
  { value: "copilot", label: "GitHub Copilot" },
12619
- { value: "ollama-cloud", label: "Ollama Cloud" }
12770
+ { value: "ollama-cloud", label: "Ollama Cloud" },
12771
+ { value: "openrouter", label: "OpenRouter" }
12620
12772
  ];
12621
12773
  function parseBranchPrefixesInput(input, fallback) {
12622
12774
  const values = input.split(",").map((value) => value.trim()).filter(Boolean);
@@ -12650,6 +12802,10 @@ function finalizeEditedConfig(current, draft) {
12650
12802
  next.aiModel = (draft.aiModel?.trim() || DEFAULT_OLLAMA_CLOUD_MODEL).trim();
12651
12803
  return next;
12652
12804
  }
12805
+ if (next.aiProvider === "openrouter") {
12806
+ next.aiModel = (draft.aiModel?.trim() || DEFAULT_OPENROUTER_MODEL).trim();
12807
+ return next;
12808
+ }
12653
12809
  delete next.aiModel;
12654
12810
  return next;
12655
12811
  }
@@ -12657,6 +12813,8 @@ function buildConfigSnapshot(config, meta) {
12657
12813
  const aiConfig = resolveAIConfig(config);
12658
12814
  const aiEnabled = isAIEnabled(config);
12659
12815
  const usingOllamaCloud = aiEnabled && aiConfig.provider === "ollama-cloud";
12816
+ const usingOpenRouter = aiEnabled && aiConfig.provider === "openrouter";
12817
+ const needsSecretInfo = usingOllamaCloud || usingOpenRouter;
12660
12818
  return {
12661
12819
  source: meta.source,
12662
12820
  location: meta.location,
@@ -12677,7 +12835,8 @@ function buildConfigSnapshot(config, meta) {
12677
12835
  providerLabel: aiEnabled ? aiConfig.providerLabel : null,
12678
12836
  model: aiEnabled ? aiConfig.model ?? null : null,
12679
12837
  ollamaCloudApiKeyPresent: usingOllamaCloud ? meta.hasOllamaCloudApiKey : null,
12680
- secretsPath: usingOllamaCloud ? meta.secretsPath : null
12838
+ openrouterApiKeyPresent: usingOpenRouter ? meta.hasOpenRouterApiKey : null,
12839
+ secretsPath: needsSecretInfo ? meta.secretsPath : null
12681
12840
  }
12682
12841
  };
12683
12842
  }
@@ -12714,6 +12873,36 @@ async function promptForOllamaCloudModelSelection(apiKey, fallbackModel) {
12714
12873
  }
12715
12874
  return inputPrompt("Ollama Cloud model", fallbackModel);
12716
12875
  }
12876
+ async function promptForOpenRouterModelSelection(apiKey, fallbackModel) {
12877
+ if (apiKey) {
12878
+ try {
12879
+ info("Fetching available OpenRouter models...");
12880
+ const models = prioritizeOpenRouterModels(await fetchOpenRouterModels(apiKey));
12881
+ if (models.length > 0) {
12882
+ const manualChoice = "Enter model manually";
12883
+ const choices = models.map((model) => ({
12884
+ value: model,
12885
+ label: model === DEFAULT_OPENROUTER_MODEL ? `${model} (default)` : model
12886
+ }));
12887
+ const selected = await selectPrompt("OpenRouter model", [
12888
+ ...choices.map((choice) => choice.label),
12889
+ manualChoice
12890
+ ]);
12891
+ if (selected !== manualChoice) {
12892
+ return choices.find((choice) => choice.label === selected)?.value ?? fallbackModel;
12893
+ }
12894
+ } else {
12895
+ warn("OpenRouter returned no available models. Enter the model name manually.");
12896
+ }
12897
+ } catch (err) {
12898
+ const message = err instanceof Error ? err.message : String(err);
12899
+ warn(`Could not fetch OpenRouter models: ${message}`);
12900
+ }
12901
+ } else {
12902
+ warn("No OpenRouter API key is available yet, so the model list cannot be fetched.");
12903
+ }
12904
+ return inputPrompt("OpenRouter model", fallbackModel);
12905
+ }
12717
12906
  async function selectCurrentValue(message, options, current) {
12718
12907
  const choices = options.map((option) => ({
12719
12908
  value: option.value,
@@ -12728,7 +12917,7 @@ async function selectBooleanValue(message, current, trueLabel, falseLabel) {
12728
12917
  { value: "false", label: falseLabel }
12729
12918
  ], current ? "true" : "false").then((value) => value === "true");
12730
12919
  }
12731
- async function promptForConfigEdits(current, hasExistingOllamaApiKey) {
12920
+ async function promptForConfigEdits(current, hasExistingOllamaApiKey, hasExistingOpenRouterApiKey) {
12732
12921
  const workflow = await selectCurrentValue("Workflow mode", WORKFLOW_OPTIONS, current.workflow);
12733
12922
  const role = await selectCurrentValue("Your role in this clone", ROLE_OPTIONS, current.role);
12734
12923
  const mainBranch = await inputPrompt("Main branch name", current.mainBranch);
@@ -12749,6 +12938,8 @@ async function promptForConfigEdits(current, hasExistingOllamaApiKey) {
12749
12938
  let aiModel;
12750
12939
  let ollamaApiKeyAction = "keep";
12751
12940
  let ollamaApiKey;
12941
+ let openrouterApiKeyAction = "keep";
12942
+ let openrouterApiKey;
12752
12943
  if (aiEnabled) {
12753
12944
  const currentProvider = current.aiProvider ?? "copilot";
12754
12945
  aiProvider = await selectCurrentValue("AI provider", AI_PROVIDER_OPTIONS, currentProvider);
@@ -12780,16 +12971,72 @@ async function promptForConfigEdits(current, hasExistingOllamaApiKey) {
12780
12971
  }
12781
12972
  const modelLookupApiKey = ollamaApiKeyAction === "set" ? ollamaApiKey ?? null : ollamaApiKeyAction === "keep" ? await getOllamaCloudApiKey() : null;
12782
12973
  aiModel = await promptForOllamaCloudModelSelection(modelLookupApiKey, current.aiProvider === "ollama-cloud" ? current.aiModel ?? DEFAULT_OLLAMA_CLOUD_MODEL : DEFAULT_OLLAMA_CLOUD_MODEL);
12783
- } else if (hasExistingOllamaApiKey) {
12784
- const shouldDeleteStoredKey = await confirmPrompt("Delete the stored Ollama Cloud API key from the local secrets store?");
12974
+ if (hasExistingOpenRouterApiKey) {
12975
+ const shouldDeleteOpenRouterKey = await confirmPrompt("Delete the stored OpenRouter API key from the local secrets store?");
12976
+ if (shouldDeleteOpenRouterKey) {
12977
+ openrouterApiKeyAction = "delete";
12978
+ }
12979
+ }
12980
+ } else if (aiProvider === "openrouter") {
12981
+ if (hasExistingOpenRouterApiKey) {
12982
+ const apiKeyChoice = await selectPrompt("OpenRouter API key", [
12983
+ "Keep existing stored key",
12984
+ "Replace stored key",
12985
+ "Delete stored key"
12986
+ ]);
12987
+ if (apiKeyChoice === "Replace stored key") {
12988
+ openrouterApiKey = (await passwordPrompt("Enter the new OpenRouter API key")).trim();
12989
+ if (!openrouterApiKey) {
12990
+ throw new Error("OpenRouter API key cannot be empty when replacing the stored key.");
12991
+ }
12992
+ openrouterApiKeyAction = "set";
12993
+ } else if (apiKeyChoice === "Delete stored key") {
12994
+ openrouterApiKeyAction = "delete";
12995
+ }
12996
+ } else {
12997
+ const addApiKey = await confirmPrompt("No OpenRouter API key is stored. Add one now?");
12998
+ if (addApiKey) {
12999
+ openrouterApiKey = (await passwordPrompt("Enter your OpenRouter API key")).trim();
13000
+ if (!openrouterApiKey) {
13001
+ throw new Error("OpenRouter API key cannot be empty when enabling OpenRouter.");
13002
+ }
13003
+ openrouterApiKeyAction = "set";
13004
+ }
13005
+ }
13006
+ const modelLookupApiKey = openrouterApiKeyAction === "set" ? openrouterApiKey ?? null : openrouterApiKeyAction === "keep" ? await getOpenRouterApiKey() : null;
13007
+ aiModel = await promptForOpenRouterModelSelection(modelLookupApiKey, current.aiProvider === "openrouter" ? current.aiModel ?? DEFAULT_OPENROUTER_MODEL : DEFAULT_OPENROUTER_MODEL);
13008
+ if (hasExistingOllamaApiKey) {
13009
+ const shouldDeleteStoredKey = await confirmPrompt("Delete the stored Ollama Cloud API key from the local secrets store?");
13010
+ if (shouldDeleteStoredKey) {
13011
+ ollamaApiKeyAction = "delete";
13012
+ }
13013
+ }
13014
+ } else {
13015
+ if (hasExistingOllamaApiKey) {
13016
+ const shouldDeleteStoredKey = await confirmPrompt("Delete the stored Ollama Cloud API key from the local secrets store?");
13017
+ if (shouldDeleteStoredKey) {
13018
+ ollamaApiKeyAction = "delete";
13019
+ }
13020
+ }
13021
+ if (hasExistingOpenRouterApiKey) {
13022
+ const shouldDeleteOpenRouterKey = await confirmPrompt("Delete the stored OpenRouter API key from the local secrets store?");
13023
+ if (shouldDeleteOpenRouterKey) {
13024
+ openrouterApiKeyAction = "delete";
13025
+ }
13026
+ }
13027
+ }
13028
+ } else {
13029
+ if (hasExistingOllamaApiKey) {
13030
+ const shouldDeleteStoredKey = await confirmPrompt("AI is disabled. Delete the stored Ollama Cloud API key from the local secrets store?");
12785
13031
  if (shouldDeleteStoredKey) {
12786
13032
  ollamaApiKeyAction = "delete";
12787
13033
  }
12788
13034
  }
12789
- } else if (hasExistingOllamaApiKey) {
12790
- const shouldDeleteStoredKey = await confirmPrompt("AI is disabled. Delete the stored Ollama Cloud API key from the local secrets store?");
12791
- if (shouldDeleteStoredKey) {
12792
- ollamaApiKeyAction = "delete";
13035
+ if (hasExistingOpenRouterApiKey) {
13036
+ const shouldDeleteOpenRouterKey = await confirmPrompt("AI is disabled. Delete the stored OpenRouter API key from the local secrets store?");
13037
+ if (shouldDeleteOpenRouterKey) {
13038
+ openrouterApiKeyAction = "delete";
13039
+ }
12793
13040
  }
12794
13041
  }
12795
13042
  return {
@@ -12808,17 +13055,17 @@ async function promptForConfigEdits(current, hasExistingOllamaApiKey) {
12808
13055
  showTips
12809
13056
  }),
12810
13057
  ollamaApiKeyAction,
12811
- ollamaApiKey
13058
+ ollamaApiKey,
13059
+ openrouterApiKeyAction,
13060
+ openrouterApiKey
12812
13061
  };
12813
13062
  }
12814
- async function applyOllamaApiKeyEdit(result) {
13063
+ async function applyApiKeyEdits(result) {
12815
13064
  if (result.ollamaApiKeyAction === "set" && result.ollamaApiKey) {
12816
13065
  await setOllamaCloudApiKey(result.ollamaApiKey);
12817
13066
  success("Stored Ollama Cloud API key in the local secrets store.");
12818
- info(`Secrets path: ${import_picocolors11.default.bold(getSecretsStorePath())}`);
12819
- return;
12820
- }
12821
- if (result.ollamaApiKeyAction === "delete") {
13067
+ info(`Secrets path: ${import_picocolors10.default.bold(getSecretsStorePath())}`);
13068
+ } else if (result.ollamaApiKeyAction === "delete") {
12822
13069
  const deleted = await deleteOllamaCloudApiKey();
12823
13070
  if (deleted) {
12824
13071
  success("Deleted stored Ollama Cloud API key.");
@@ -12826,31 +13073,49 @@ async function applyOllamaApiKeyEdit(result) {
12826
13073
  info("No stored Ollama Cloud API key was found to delete.");
12827
13074
  }
12828
13075
  }
13076
+ if (result.openrouterApiKeyAction === "set" && result.openrouterApiKey) {
13077
+ await setOpenRouterApiKey(result.openrouterApiKey);
13078
+ success("Stored OpenRouter API key in the local secrets store.");
13079
+ info(`Secrets path: ${import_picocolors10.default.bold(getSecretsStorePath())}`);
13080
+ } else if (result.openrouterApiKeyAction === "delete") {
13081
+ const deleted = await deleteOpenRouterApiKey();
13082
+ if (deleted) {
13083
+ success("Deleted stored OpenRouter API key.");
13084
+ } else {
13085
+ info("No stored OpenRouter API key was found to delete.");
13086
+ }
13087
+ }
12829
13088
  }
12830
13089
  function printConfigSummary(snapshot) {
12831
- info(`Config source: ${import_picocolors11.default.bold(snapshot.source)}`);
12832
- info(`Config path: ${import_picocolors11.default.bold(snapshot.location)}`);
12833
- info(`Workflow: ${import_picocolors11.default.bold(snapshot.workflowLabel)}`);
12834
- info(`Convention: ${import_picocolors11.default.bold(snapshot.commitConventionLabel)}`);
12835
- info(`Role: ${import_picocolors11.default.bold(snapshot.role)}`);
13090
+ info(`Config source: ${import_picocolors10.default.bold(snapshot.source)}`);
13091
+ info(`Config path: ${import_picocolors10.default.bold(snapshot.location)}`);
13092
+ info(`Workflow: ${import_picocolors10.default.bold(snapshot.workflowLabel)}`);
13093
+ info(`Convention: ${import_picocolors10.default.bold(snapshot.commitConventionLabel)}`);
13094
+ info(`Role: ${import_picocolors10.default.bold(snapshot.role)}`);
12836
13095
  if (snapshot.devBranch) {
12837
- info(`Main: ${import_picocolors11.default.bold(snapshot.mainBranch)} | Dev: ${import_picocolors11.default.bold(snapshot.devBranch)}`);
13096
+ info(`Main: ${import_picocolors10.default.bold(snapshot.mainBranch)} | Dev: ${import_picocolors10.default.bold(snapshot.devBranch)}`);
12838
13097
  } else {
12839
- info(`Main: ${import_picocolors11.default.bold(snapshot.mainBranch)}`);
13098
+ info(`Main: ${import_picocolors10.default.bold(snapshot.mainBranch)}`);
12840
13099
  }
12841
- info(`Origin: ${import_picocolors11.default.bold(snapshot.origin)} | Upstream: ${import_picocolors11.default.bold(snapshot.upstream)}`);
12842
- info(`Branch prefixes: ${import_picocolors11.default.bold(snapshot.branchPrefixes.join(", "))}`);
12843
- info(`Guides: ${import_picocolors11.default.bold(snapshot.showTips ? "shown" : "hidden")}`);
12844
- info(`AI: ${import_picocolors11.default.bold(snapshot.ai.enabled ? "enabled" : "disabled")}`);
13100
+ info(`Origin: ${import_picocolors10.default.bold(snapshot.origin)} | Upstream: ${import_picocolors10.default.bold(snapshot.upstream)}`);
13101
+ info(`Branch prefixes: ${import_picocolors10.default.bold(snapshot.branchPrefixes.join(", "))}`);
13102
+ info(`Guides: ${import_picocolors10.default.bold(snapshot.showTips ? "shown" : "hidden")}`);
13103
+ info(`AI: ${import_picocolors10.default.bold(snapshot.ai.enabled ? "enabled" : "disabled")}`);
12845
13104
  if (snapshot.ai.enabled && snapshot.ai.providerLabel) {
12846
- info(`AI provider: ${import_picocolors11.default.bold(snapshot.ai.providerLabel)}`);
13105
+ info(`AI provider: ${import_picocolors10.default.bold(snapshot.ai.providerLabel)}`);
12847
13106
  if (snapshot.ai.model) {
12848
- info(`AI model: ${import_picocolors11.default.bold(snapshot.ai.model)}`);
13107
+ info(`AI model: ${import_picocolors10.default.bold(snapshot.ai.model)}`);
12849
13108
  }
12850
13109
  if (snapshot.ai.provider === "ollama-cloud") {
12851
- info(`Ollama Cloud API key: ${import_picocolors11.default.bold(snapshot.ai.ollamaCloudApiKeyPresent ? "stored" : "missing")}`);
13110
+ info(`Ollama Cloud API key: ${import_picocolors10.default.bold(snapshot.ai.ollamaCloudApiKeyPresent ? "stored" : "missing")}`);
13111
+ if (snapshot.ai.secretsPath) {
13112
+ info(`Secrets path: ${import_picocolors10.default.bold(snapshot.ai.secretsPath)}`);
13113
+ }
13114
+ }
13115
+ if (snapshot.ai.provider === "openrouter") {
13116
+ info(`OpenRouter API key: ${import_picocolors10.default.bold(snapshot.ai.openrouterApiKeyPresent ? "stored" : "missing")}`);
12852
13117
  if (snapshot.ai.secretsPath) {
12853
- info(`Secrets path: ${import_picocolors11.default.bold(snapshot.ai.secretsPath)}`);
13118
+ info(`Secrets path: ${import_picocolors10.default.bold(snapshot.ai.secretsPath)}`);
12854
13119
  }
12855
13120
  }
12856
13121
  }
@@ -12898,14 +13163,15 @@ var config_default = defineCommand({
12898
13163
  }
12899
13164
  if (args.edit) {
12900
13165
  try {
12901
- const editResult = await promptForConfigEdits(config, await hasOllamaCloudApiKey());
13166
+ const editResult = await promptForConfigEdits(config, await hasOllamaCloudApiKey(), await hasOpenRouterApiKey());
12902
13167
  writeConfig(editResult.config);
12903
- await applyOllamaApiKeyEdit(editResult);
13168
+ await applyApiKeyEdits(editResult);
12904
13169
  success("Updated repo config.");
12905
13170
  printConfigSummary(buildConfigSnapshot(editResult.config, {
12906
13171
  source,
12907
13172
  location: getConfigLocationLabel(),
12908
13173
  hasOllamaCloudApiKey: await hasOllamaCloudApiKey(),
13174
+ hasOpenRouterApiKey: await hasOpenRouterApiKey(),
12909
13175
  secretsPath: getSecretsStorePath()
12910
13176
  }));
12911
13177
  return;
@@ -12918,6 +13184,7 @@ var config_default = defineCommand({
12918
13184
  source,
12919
13185
  location: getConfigLocationLabel(),
12920
13186
  hasOllamaCloudApiKey: await hasOllamaCloudApiKey(),
13187
+ hasOpenRouterApiKey: await hasOpenRouterApiKey(),
12921
13188
  secretsPath: getSecretsStorePath()
12922
13189
  });
12923
13190
  if (args.json) {
@@ -12926,40 +13193,148 @@ var config_default = defineCommand({
12926
13193
  }
12927
13194
  printConfigSummary(snapshot);
12928
13195
  console.log();
12929
- console.log(` ${import_picocolors11.default.dim("Run `cn config --edit` to update these settings.")}`);
13196
+ console.log(` ${import_picocolors10.default.dim("Run `cn config --edit` to update these settings.")}`);
12930
13197
  console.log();
12931
13198
  }
12932
13199
  });
12933
13200
 
12934
- // src/commands/doctor.ts
12935
- import { execFile as execFileCb3 } from "child_process";
12936
- var import_picocolors12 = __toESM(require_picocolors(), 1);
12937
- // package.json
12938
- var package_default = {
12939
- name: "contribute-now",
12940
- version: "0.7.5",
12941
- description: "Developer CLI that automates git workflows \u2014 branching, syncing, committing, and PRs \u2014 with multi-workflow and commit convention support.",
12942
- type: "module",
12943
- bin: {
12944
- contrib: "dist/cli.js",
12945
- contribute: "dist/cli.js",
12946
- cn: "dist/cli.js"
13201
+ // src/commands/discard.ts
13202
+ var import_picocolors11 = __toESM(require_picocolors(), 1);
13203
+ var discard_default = defineCommand({
13204
+ meta: {
13205
+ name: "discard",
13206
+ description: "Discard the current feature branch and return to the base branch"
12947
13207
  },
12948
- files: [
12949
- "dist"
12950
- ],
12951
- scripts: {
12952
- build: "bun build src/cli.ts --outfile dist/cli.js --target bun && bun run scripts/add-shebang.mjs",
12953
- cli: "bun run src/cli.ts --",
12954
- dev: "bun src/cli.ts",
12955
- test: "bun test",
12956
- lint: "biome check .",
12957
- "lint:fix": "biome check --write .",
12958
- format: "biome format --write .",
12959
- "landing:install": "bun install --cwd landing",
12960
- "landing:dev": "bun run --cwd landing dev",
12961
- "landing:build": "bun run --cwd landing build",
12962
- "landing:preview": "bun run --cwd landing preview"
13208
+ args: {
13209
+ force: {
13210
+ type: "boolean",
13211
+ alias: "f",
13212
+ description: "Skip confirmation and discard immediately",
13213
+ default: false
13214
+ }
13215
+ },
13216
+ async run({ args }) {
13217
+ if (!await isGitRepo()) {
13218
+ error("Not inside a git repository.");
13219
+ process.exit(1);
13220
+ }
13221
+ await assertCleanGitState("discarding a branch");
13222
+ const config = readConfig();
13223
+ if (!config) {
13224
+ error("No repo config found. Run `cn setup` first.");
13225
+ process.exit(1);
13226
+ }
13227
+ const currentBranch = await getCurrentBranch();
13228
+ const baseBranch = getBaseBranch(config);
13229
+ await projectHeading("discard", "\uD83D\uDDD1\uFE0F");
13230
+ if (isBranchProtected(currentBranch, config)) {
13231
+ error(`${import_picocolors11.default.bold(currentBranch)} is a protected branch and cannot be discarded.`);
13232
+ info(`Switch to a feature branch first, then run ${import_picocolors11.default.bold("cn discard")}.`);
13233
+ process.exit(1);
13234
+ }
13235
+ if (currentBranch === baseBranch) {
13236
+ info(`You are already on ${import_picocolors11.default.bold(baseBranch)}.`);
13237
+ process.exit(0);
13238
+ }
13239
+ const { origin } = config;
13240
+ const localWork = await hasLocalWork(origin, currentBranch);
13241
+ const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
13242
+ if (hasWork) {
13243
+ if (localWork.uncommitted) {
13244
+ warn("You have uncommitted changes in your working tree.");
13245
+ }
13246
+ if (localWork.unpushedCommits > 0) {
13247
+ warn(`You have ${import_picocolors11.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on this branch.`);
13248
+ }
13249
+ warn("Discarding this branch will permanently lose that work.");
13250
+ const SAVE_FIRST = "Save my changes first (cn save), then discard";
13251
+ const DISCARD_ANYWAY = "Discard anyway \u2014 I do not need this work";
13252
+ const CANCEL = "Keep the branch, take me back";
13253
+ const action = await selectPrompt("This branch has unsaved work. What would you like to do?", [SAVE_FIRST, DISCARD_ANYWAY, CANCEL]);
13254
+ if (action === CANCEL) {
13255
+ info("Discard cancelled. Your branch is untouched.");
13256
+ process.exit(0);
13257
+ }
13258
+ if (action === SAVE_FIRST) {
13259
+ if (!localWork.uncommitted) {
13260
+ info("No uncommitted changes to stash \u2014 unpushed commits will still be lost.");
13261
+ const confirm = await confirmPrompt("Continue discarding the branch?");
13262
+ if (!confirm) {
13263
+ info("Discard cancelled.");
13264
+ process.exit(0);
13265
+ }
13266
+ } else {
13267
+ const stashResult = await stashChanges(`work-in-progress on ${currentBranch}`);
13268
+ if (stashResult.exitCode !== 0) {
13269
+ error(`Failed to save changes: ${stashResult.stderr}`);
13270
+ process.exit(1);
13271
+ }
13272
+ success(`Changes saved. Use ${import_picocolors11.default.bold("cn save --restore")} to bring them back.`);
13273
+ }
13274
+ }
13275
+ } else if (!args.force) {
13276
+ const confirmed = await confirmPrompt(`Discard ${import_picocolors11.default.bold(currentBranch)} and return to ${import_picocolors11.default.bold(baseBranch)}?`);
13277
+ if (!confirmed) {
13278
+ info("Discard cancelled.");
13279
+ process.exit(0);
13280
+ }
13281
+ }
13282
+ const upstreamRef = await getUpstreamRef();
13283
+ let deleteRemote = false;
13284
+ if (upstreamRef) {
13285
+ deleteRemote = await confirmPrompt(`Also delete the remote branch ${import_picocolors11.default.bold(upstreamRef)}?`);
13286
+ }
13287
+ const checkoutResult = await checkoutBranch(baseBranch);
13288
+ if (checkoutResult.exitCode !== 0) {
13289
+ error(`Failed to switch to ${import_picocolors11.default.bold(baseBranch)}: ${checkoutResult.stderr}`);
13290
+ process.exit(1);
13291
+ }
13292
+ const deleteResult = await forceDeleteBranch(currentBranch);
13293
+ if (deleteResult.exitCode !== 0) {
13294
+ error(`Failed to delete branch ${import_picocolors11.default.bold(currentBranch)}: ${deleteResult.stderr}`);
13295
+ process.exit(1);
13296
+ }
13297
+ success(`Discarded ${import_picocolors11.default.bold(currentBranch)} and switched back to ${import_picocolors11.default.bold(baseBranch)}`);
13298
+ if (deleteRemote) {
13299
+ const remoteDeleteResult = await deleteRemoteBranch(origin, currentBranch);
13300
+ if (remoteDeleteResult.exitCode !== 0) {
13301
+ warn(`Could not delete remote branch: ${remoteDeleteResult.stderr.trim()}`);
13302
+ } else {
13303
+ success(`Deleted remote branch ${import_picocolors11.default.bold(`${origin}/${currentBranch}`)}`);
13304
+ }
13305
+ }
13306
+ }
13307
+ });
13308
+
13309
+ // src/commands/doctor.ts
13310
+ import { execFile as execFileCb3 } from "child_process";
13311
+ var import_picocolors12 = __toESM(require_picocolors(), 1);
13312
+ // package.json
13313
+ var package_default = {
13314
+ name: "contribute-now",
13315
+ version: "0.8.0-dev.1cf48ff",
13316
+ description: "Developer CLI that automates git workflows \u2014 branching, syncing, committing, and PRs \u2014 with multi-workflow and commit convention support.",
13317
+ type: "module",
13318
+ bin: {
13319
+ contrib: "dist/cli.js",
13320
+ contribute: "dist/cli.js",
13321
+ cn: "dist/cli.js"
13322
+ },
13323
+ files: [
13324
+ "dist"
13325
+ ],
13326
+ scripts: {
13327
+ build: "bun build src/cli.ts --outfile dist/cli.js --target bun && bun run scripts/add-shebang.mjs",
13328
+ cli: "bun run src/cli.ts --",
13329
+ dev: "bun src/cli.ts",
13330
+ test: "bun test",
13331
+ lint: "biome check .",
13332
+ "lint:fix": "biome check --write .",
13333
+ format: "biome format --write .",
13334
+ "landing:install": "bun install --cwd landing",
13335
+ "landing:dev": "bun run --cwd landing dev",
13336
+ "landing:build": "bun run --cwd landing build",
13337
+ "landing:preview": "bun run --cwd landing preview"
12963
13338
  },
12964
13339
  engines: {
12965
13340
  bun: ">=1.0"
@@ -12995,6 +13370,9 @@ var package_default = {
12995
13370
  }
12996
13371
  };
12997
13372
 
13373
+ // src/commands/doctor.ts
13374
+ init_gh();
13375
+
12998
13376
  // src/utils/remote.ts
12999
13377
  function parseRepoFromUrl(url) {
13000
13378
  const httpsMatch = url.match(/https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
@@ -13190,6 +13568,15 @@ async function configSection() {
13190
13568
  detail: hasSecretsStore() ? "stored in the local secrets store" : "run `cn setup` to save it"
13191
13569
  });
13192
13570
  }
13571
+ if (aiConfig.provider === "openrouter") {
13572
+ const hasApiKey = await hasOpenRouterApiKey();
13573
+ checks.push({
13574
+ label: hasApiKey ? "OpenRouter API key present" : "OpenRouter API key missing",
13575
+ ok: true,
13576
+ warning: !hasApiKey,
13577
+ detail: hasSecretsStore() ? "stored in the local secrets store" : "run `cn setup` to save it"
13578
+ });
13579
+ }
13193
13580
  }
13194
13581
  if (hasDevBranch(config.workflow)) {
13195
13582
  checks.push({
@@ -13325,178 +13712,780 @@ function envSection() {
13325
13712
  checks.push({ label: `${name} = ${display}`, ok: true });
13326
13713
  }
13327
13714
  }
13328
- if (checks.length === 0) {
13329
- checks.push({ label: "No relevant environment variables set", ok: true });
13715
+ if (checks.length === 0) {
13716
+ checks.push({ label: "No relevant environment variables set", ok: true });
13717
+ }
13718
+ return { title: "Environment", checks };
13719
+ }
13720
+ var doctor_default = defineCommand({
13721
+ meta: {
13722
+ name: "doctor",
13723
+ description: "Diagnose the contribute-now CLI environment and configuration"
13724
+ },
13725
+ args: {
13726
+ json: {
13727
+ type: "boolean",
13728
+ description: "Output report as JSON",
13729
+ default: false
13730
+ }
13731
+ },
13732
+ async run({ args }) {
13733
+ const isJson = args.json;
13734
+ const [tool, deps, config, git, fork, workflow] = await Promise.all([
13735
+ toolSection(),
13736
+ depsSection(),
13737
+ configSection(),
13738
+ gitSection(),
13739
+ forkSection(),
13740
+ workflowSection()
13741
+ ]);
13742
+ const env2 = envSection();
13743
+ const report = {
13744
+ sections: [tool, deps, config, git, fork, workflow, env2]
13745
+ };
13746
+ if (isJson) {
13747
+ console.log(toJson(report));
13748
+ return;
13749
+ }
13750
+ await projectHeading("doctor", "\uD83E\uDE7A");
13751
+ printReport(report);
13752
+ const total = report.sections.flatMap((s2) => s2.checks);
13753
+ const failures = total.filter((c3) => !c3.ok);
13754
+ const warnings = total.filter((c3) => c3.ok && c3.warning);
13755
+ if (failures.length === 0 && warnings.length === 0) {
13756
+ console.log(` ${import_picocolors12.default.green("All checks passed!")} No issues detected.
13757
+ `);
13758
+ } else {
13759
+ if (failures.length > 0) {
13760
+ console.log(` ${import_picocolors12.default.red(`${failures.length} issue${failures.length !== 1 ? "s" : ""} found.`)}`);
13761
+ }
13762
+ if (warnings.length > 0) {
13763
+ console.log(` ${import_picocolors12.default.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}.`)}`);
13764
+ }
13765
+ console.log();
13766
+ }
13767
+ }
13768
+ });
13769
+
13770
+ // src/commands/hook.ts
13771
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
13772
+ import { join as join6 } from "path";
13773
+ var import_picocolors13 = __toESM(require_picocolors(), 1);
13774
+ var HOOK_MARKER = "# managed by contribute-now";
13775
+ function getHooksDir(cwd = process.cwd()) {
13776
+ return join6(cwd, ".git", "hooks");
13777
+ }
13778
+ function getHookPath(cwd = process.cwd()) {
13779
+ return join6(getHooksDir(cwd), "commit-msg");
13780
+ }
13781
+ function generateHookScript() {
13782
+ return `#!/bin/sh
13783
+ ${HOOK_MARKER}
13784
+ # Validates commit messages against your configured convention.
13785
+ # Install: cn hook install
13786
+ # Uninstall: cn hook uninstall
13787
+
13788
+ commit_msg_file="$1"
13789
+ commit_msg=$(head -1 "$commit_msg_file")
13790
+
13791
+ # Skip merge commits and fixup/squash commits
13792
+ case "$commit_msg" in
13793
+ Merge\\ *|fixup!*|squash!*|amend!*) exit 0 ;;
13794
+ esac
13795
+
13796
+ # Detect available package runner
13797
+ if command -v cn >/dev/null 2>&1; then
13798
+ cn validate --file "$commit_msg_file"
13799
+ elif command -v bunx >/dev/null 2>&1; then
13800
+ bunx cn validate --file "$commit_msg_file"
13801
+ else
13802
+ echo "Warning: Neither cn nor bunx is available. Skipping commit message validation."
13803
+ exit 0
13804
+ fi
13805
+ `;
13806
+ }
13807
+ var hook_default = defineCommand({
13808
+ meta: {
13809
+ name: "hook",
13810
+ description: "Install or uninstall the commit-msg git hook"
13811
+ },
13812
+ args: {
13813
+ action: {
13814
+ type: "positional",
13815
+ description: "Action to perform: install or uninstall",
13816
+ required: true
13817
+ }
13818
+ },
13819
+ async run({ args }) {
13820
+ if (!await isGitRepo()) {
13821
+ error("Not inside a git repository.");
13822
+ process.exit(1);
13823
+ }
13824
+ const action = args.action;
13825
+ if (action !== "install" && action !== "uninstall") {
13826
+ error(`Unknown action "${action}". Use "install" or "uninstall".`);
13827
+ process.exit(1);
13828
+ }
13829
+ if (action === "install") {
13830
+ await installHook();
13831
+ } else {
13832
+ await uninstallHook();
13833
+ }
13834
+ }
13835
+ });
13836
+ async function installHook() {
13837
+ await projectHeading("hook install", "\uD83E\uDE9D");
13838
+ const config = readConfig();
13839
+ if (!config) {
13840
+ error("No repo config found. Run `cn setup` first.");
13841
+ process.exit(1);
13842
+ }
13843
+ if (config.commitConvention === "none") {
13844
+ warn('Commit convention is set to "none". No hook to install.');
13845
+ info("Change your convention with `cn setup` first.", "");
13846
+ process.exit(0);
13847
+ }
13848
+ const hookPath = getHookPath();
13849
+ const hooksDir = getHooksDir();
13850
+ if (existsSync6(hookPath)) {
13851
+ const existing = readFileSync5(hookPath, "utf-8");
13852
+ if (!existing.includes(HOOK_MARKER)) {
13853
+ error("A commit-msg hook already exists and was not installed by contribute-now.");
13854
+ warn(`Path: ${hookPath}`);
13855
+ warn("Remove it manually or back it up before installing.");
13856
+ process.exit(1);
13857
+ }
13858
+ info("Updating existing contribute-now hook...");
13859
+ }
13860
+ if (!existsSync6(hooksDir)) {
13861
+ mkdirSync5(hooksDir, { recursive: true });
13862
+ }
13863
+ writeFileSync5(hookPath, generateHookScript(), { mode: 493 });
13864
+ success(`commit-msg hook installed.`);
13865
+ info(`Convention: ${import_picocolors13.default.bold(CONVENTION_LABELS[config.commitConvention])}`, "");
13866
+ info(`Path: ${import_picocolors13.default.dim(hookPath)}`, "");
13867
+ warn("Note: hooks can be bypassed with `git commit --no-verify`.");
13868
+ }
13869
+ async function uninstallHook() {
13870
+ await projectHeading("hook uninstall", "\uD83E\uDE9D");
13871
+ const hookPath = getHookPath();
13872
+ if (!existsSync6(hookPath)) {
13873
+ info("No commit-msg hook found. Nothing to uninstall.");
13874
+ return;
13875
+ }
13876
+ const content = readFileSync5(hookPath, "utf-8");
13877
+ if (!content.includes(HOOK_MARKER)) {
13878
+ error("The commit-msg hook was not installed by contribute-now. Leaving it untouched.");
13879
+ process.exit(1);
13880
+ }
13881
+ rmSync2(hookPath);
13882
+ success("commit-msg hook removed.");
13883
+ }
13884
+
13885
+ // src/commands/label.ts
13886
+ init_gh();
13887
+ var import_picocolors14 = __toESM(require_picocolors(), 1);
13888
+
13889
+ // src/utils/label.ts
13890
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync6, statSync as statSync4, writeFileSync as writeFileSync6 } from "fs";
13891
+ import { dirname as dirname5, join as join7, resolve as resolve5 } from "path";
13892
+
13893
+ // src/data/clean-labels.ts
13894
+ var CLEAN_LABELS = [
13895
+ {
13896
+ name: "bug",
13897
+ description: "[Type] Something isn't working [issues, PRs]",
13898
+ color: "d73a4a"
13899
+ },
13900
+ {
13901
+ name: "enhancement",
13902
+ description: "[Type] New feature or improvement to existing functionality [issues, PRs]",
13903
+ color: "1a7f37"
13904
+ },
13905
+ {
13906
+ name: "documentation",
13907
+ description: "[Type] Improvements or additions to docs, README, or guides [issues, PRs]",
13908
+ color: "0075ca"
13909
+ },
13910
+ {
13911
+ name: "refactor",
13912
+ description: "[Type] Code improvement without changing functionality [PRs]",
13913
+ color: "8957e5"
13914
+ },
13915
+ {
13916
+ name: "performance",
13917
+ description: "[Type] Optimization, speed, or resource usage improvements [issues, PRs]",
13918
+ color: "e3795c"
13919
+ },
13920
+ {
13921
+ name: "security",
13922
+ description: "[Type] Security vulnerability or hardening [issues, PRs]",
13923
+ color: "d4a72c"
13924
+ },
13925
+ {
13926
+ name: "blocked",
13927
+ description: "[Status] Waiting on another issue, decision, or external factor [issues]",
13928
+ color: "cf222e"
13929
+ },
13930
+ {
13931
+ name: "needs triage",
13932
+ description: "[Status] New issue \u2014 needs review and categorization [issues]",
13933
+ color: "e16f24"
13934
+ },
13935
+ {
13936
+ name: "awaiting response",
13937
+ description: "[Status] Waiting for more information from the reporter [issues]",
13938
+ color: "1a7ec7"
13939
+ },
13940
+ {
13941
+ name: "ready",
13942
+ description: "[Status] Triaged and ready to be picked up [issues]",
13943
+ color: "2da44e"
13944
+ },
13945
+ {
13946
+ name: "good first issue",
13947
+ description: "[Community] Good for newcomers \u2014 well-scoped and documented [issues]",
13948
+ color: "7057ff"
13949
+ },
13950
+ {
13951
+ name: "help wanted",
13952
+ description: "[Community] Open for community contribution [issues]",
13953
+ color: "0e8a16"
13954
+ },
13955
+ {
13956
+ name: "maintainer only",
13957
+ description: "[Community] Reserved for maintainers \u2014 not open for external contribution [issues, PRs]",
13958
+ color: "b60205"
13959
+ },
13960
+ {
13961
+ name: "duplicate",
13962
+ description: "[Resolution] This issue or pull request already exists [issues, PRs]",
13963
+ color: "cfd3d7"
13964
+ },
13965
+ {
13966
+ name: "invalid",
13967
+ description: "[Resolution] This doesn't seem right [issues, PRs]",
13968
+ color: "cfd3d7"
13969
+ },
13970
+ {
13971
+ name: "wontfix",
13972
+ description: "[Resolution] This will not be worked on [issues]",
13973
+ color: "cfd3d7"
13974
+ },
13975
+ {
13976
+ name: "core",
13977
+ description: "[Area] Core logic, business rules, and primary functionality [issues, PRs]",
13978
+ color: "0052cc"
13979
+ },
13980
+ {
13981
+ name: "interface",
13982
+ description: "[Area] User-facing layer \u2014 UI, CLI, API endpoints, or SDK surface [issues, PRs]",
13983
+ color: "5319e7"
13984
+ },
13985
+ {
13986
+ name: "data",
13987
+ description: "[Area] Database, storage, caching, or data models [issues, PRs]",
13988
+ color: "006b75"
13989
+ },
13990
+ {
13991
+ name: "infra",
13992
+ description: "[Area] Build system, CI/CD, deployment, config, and DevOps [issues, PRs]",
13993
+ color: "e16f24"
13994
+ },
13995
+ {
13996
+ name: "testing",
13997
+ description: "[Area] Unit tests, integration tests, E2E, and test tooling [issues, PRs]",
13998
+ color: "1a7f37"
13999
+ }
14000
+ ];
14001
+
14002
+ // src/utils/label.ts
14003
+ var LABEL_CACHE_DIRNAME = "contribute-now";
14004
+ var LABEL_CACHE_FILENAME = "labels.json";
14005
+ function findRepoRoot3(cwd = process.cwd()) {
14006
+ let current = resolve5(cwd);
14007
+ while (true) {
14008
+ if (existsSync7(join7(current, ".git"))) {
14009
+ return current;
14010
+ }
14011
+ const parent = dirname5(current);
14012
+ if (parent === current) {
14013
+ return null;
14014
+ }
14015
+ current = parent;
14016
+ }
14017
+ }
14018
+ function resolveGitDir3(cwd = process.cwd()) {
14019
+ const repoRoot = findRepoRoot3(cwd);
14020
+ if (!repoRoot) {
14021
+ return null;
14022
+ }
14023
+ const dotGitPath = join7(repoRoot, ".git");
14024
+ try {
14025
+ const stat = statSync4(dotGitPath);
14026
+ if (stat.isDirectory()) {
14027
+ return dotGitPath;
14028
+ }
14029
+ if (!stat.isFile()) {
14030
+ return null;
14031
+ }
14032
+ const content = readFileSync6(dotGitPath, "utf-8").trim();
14033
+ const match = /^gitdir:\s*(.+)$/i.exec(content);
14034
+ if (!match) {
14035
+ return null;
14036
+ }
14037
+ return resolve5(repoRoot, match[1].trim());
14038
+ } catch {
14039
+ return null;
14040
+ }
14041
+ }
14042
+ function getLabelCachePath(cwd = process.cwd()) {
14043
+ const gitDir = resolveGitDir3(cwd);
14044
+ if (!gitDir) {
14045
+ return null;
14046
+ }
14047
+ return join7(gitDir, LABEL_CACHE_DIRNAME, LABEL_CACHE_FILENAME);
14048
+ }
14049
+ function readLabelCache(cwd = process.cwd()) {
14050
+ const cachePath = getLabelCachePath(cwd);
14051
+ if (!cachePath || !existsSync7(cachePath)) {
14052
+ return null;
14053
+ }
14054
+ try {
14055
+ const raw = JSON.parse(readFileSync6(cachePath, "utf-8"));
14056
+ if (!Array.isArray(raw.labels) || typeof raw.fetchedAt !== "string" || raw.source !== "clean-labels" && raw.source !== "repo") {
14057
+ return null;
14058
+ }
14059
+ const validatedLabels = [];
14060
+ for (const entry of raw.labels) {
14061
+ if (typeof entry !== "object" || entry === null || typeof entry.name !== "string" || !entry.name) {
14062
+ continue;
14063
+ }
14064
+ const e3 = entry;
14065
+ validatedLabels.push({
14066
+ name: e3.name.trim(),
14067
+ description: typeof e3.description === "string" ? e3.description.trim() : "",
14068
+ color: typeof e3.color === "string" ? e3.color.trim() : ""
14069
+ });
14070
+ }
14071
+ if (validatedLabels.length === 0) {
14072
+ return null;
14073
+ }
14074
+ return {
14075
+ labels: validatedLabels,
14076
+ source: raw.source,
14077
+ fetchedAt: raw.fetchedAt
14078
+ };
14079
+ } catch {
14080
+ return null;
14081
+ }
14082
+ }
14083
+ function writeLabelCache(cache, cwd = process.cwd()) {
14084
+ const cachePath = getLabelCachePath(cwd);
14085
+ if (!cachePath) {
14086
+ return;
14087
+ }
14088
+ mkdirSync6(dirname5(cachePath), { recursive: true });
14089
+ writeFileSync6(cachePath, `${JSON.stringify(cache, null, 2)}
14090
+ `, "utf-8");
14091
+ }
14092
+ function normalizeLabelName(name) {
14093
+ return name.toLowerCase().trim();
14094
+ }
14095
+ function isCleanLabelsMatch(repoLabels) {
14096
+ const cleanNames = new Set(CLEAN_LABELS.map((l2) => normalizeLabelName(l2.name)));
14097
+ const repoNames = new Set(repoLabels.map((l2) => normalizeLabelName(l2.name)));
14098
+ if (cleanNames.size !== repoNames.size) {
14099
+ return false;
14100
+ }
14101
+ for (const name of cleanNames) {
14102
+ if (!repoNames.has(name)) {
14103
+ return false;
14104
+ }
14105
+ }
14106
+ return true;
14107
+ }
14108
+ function buildEffectiveLabelSource(repoLabels) {
14109
+ if (isCleanLabelsMatch(repoLabels)) {
14110
+ return {
14111
+ labels: CLEAN_LABELS.map((cl) => ({
14112
+ name: cl.name,
14113
+ description: cl.description,
14114
+ color: cl.color
14115
+ })),
14116
+ source: "clean-labels"
14117
+ };
14118
+ }
14119
+ return { labels: repoLabels, source: "repo" };
14120
+ }
14121
+ async function syncLabelCache(cwd = process.cwd()) {
14122
+ const { getRepoLabels: getRepoLabels2 } = await Promise.resolve().then(() => (init_gh(), exports_gh));
14123
+ const repoLabels = await getRepoLabels2();
14124
+ if (repoLabels.length === 0) {
14125
+ return null;
14126
+ }
14127
+ const { labels, source } = buildEffectiveLabelSource(repoLabels);
14128
+ const cache = {
14129
+ labels,
14130
+ source,
14131
+ fetchedAt: new Date().toISOString()
14132
+ };
14133
+ writeLabelCache(cache, cwd);
14134
+ return cache;
14135
+ }
14136
+ async function getActiveLabels(cwd = process.cwd(), force = false) {
14137
+ if (!force) {
14138
+ const cached = readLabelCache(cwd);
14139
+ if (cached) {
14140
+ return cached;
14141
+ }
14142
+ }
14143
+ return syncLabelCache(cwd);
14144
+ }
14145
+ function parseLabelsCsv(csv) {
14146
+ return csv.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
14147
+ }
14148
+ function validateLabels(requested, available) {
14149
+ const availableNormalized = new Map;
14150
+ for (const label of available) {
14151
+ availableNormalized.set(normalizeLabelName(label.name), label.name);
14152
+ }
14153
+ const valid = [];
14154
+ const invalid = [];
14155
+ for (const req of requested) {
14156
+ const normalized = normalizeLabelName(req);
14157
+ const canonical = availableNormalized.get(normalized);
14158
+ if (canonical !== undefined) {
14159
+ valid.push(canonical);
14160
+ } else {
14161
+ invalid.push(req);
14162
+ }
14163
+ }
14164
+ return { valid, invalid };
14165
+ }
14166
+ function findCloseMatches(input, available, maxResults = 3) {
14167
+ const needle = normalizeLabelName(input);
14168
+ const scored = available.map((label) => {
14169
+ const haystack = normalizeLabelName(label.name);
14170
+ let score = 0;
14171
+ if (haystack === needle) {
14172
+ score = 100;
14173
+ } else if (haystack.startsWith(needle) || needle.startsWith(haystack)) {
14174
+ score = 60;
14175
+ } else if (haystack.includes(needle) || needle.includes(haystack)) {
14176
+ score = 40;
14177
+ } else {
14178
+ const needleBigrams = toBigrams(needle);
14179
+ const haystackBigrams = toBigrams(haystack);
14180
+ const overlap = [...needleBigrams].filter((b2) => haystackBigrams.has(b2)).length;
14181
+ const union = new Set([...needleBigrams, ...haystackBigrams]).size;
14182
+ score = union > 0 ? Math.round(overlap / union * 30) : 0;
14183
+ }
14184
+ return { name: label.name, score };
14185
+ });
14186
+ return scored.filter((item) => item.score > 0).sort((a2, b2) => b2.score - a2.score).slice(0, maxResults).map((item) => item.name);
14187
+ }
14188
+ function scoreLabelsForContent(content, labels) {
14189
+ const contentTokens = tokenize(content.toLowerCase());
14190
+ const scored = labels.map((label) => {
14191
+ const nameTokens = tokenize(label.name.toLowerCase());
14192
+ const descTokens = [...tokenize(stripDescriptionMeta(label.description).toLowerCase())].filter((t2) => t2.length > 3 && !STOP_WORDS.has(t2));
14193
+ let score = 0;
14194
+ for (const token of nameTokens) {
14195
+ if (contentTokens.has(token))
14196
+ score += 3;
14197
+ }
14198
+ const normalizedName = normalizeLabelName(label.name);
14199
+ if (content.toLowerCase().includes(normalizedName)) {
14200
+ score += 5;
14201
+ }
14202
+ for (const token of descTokens) {
14203
+ if (contentTokens.has(token))
14204
+ score += 1;
14205
+ }
14206
+ return { label, score };
14207
+ });
14208
+ return scored.filter((item) => item.score > 0).sort((a2, b2) => b2.score - a2.score);
14209
+ }
14210
+ var STOP_WORDS = new Set([
14211
+ "the",
14212
+ "and",
14213
+ "for",
14214
+ "with",
14215
+ "this",
14216
+ "that",
14217
+ "from",
14218
+ "into",
14219
+ "over",
14220
+ "under",
14221
+ "have",
14222
+ "will",
14223
+ "been",
14224
+ "more",
14225
+ "than",
14226
+ "also",
14227
+ "when",
14228
+ "what",
14229
+ "which",
14230
+ "some",
14231
+ "such",
14232
+ "its",
14233
+ "not",
14234
+ "only",
14235
+ "any",
14236
+ "each",
14237
+ "both"
14238
+ ]);
14239
+ function stripDescriptionMeta(description) {
14240
+ return description.replace(/^\[[\w\s]+\]\s*/u, "").replace(/\s*\[[\w,\s]+\]$/u, "").trim();
14241
+ }
14242
+ function tokenize(text) {
14243
+ return new Set(text.split(/[\s\-_/,.:;!?()[\]{}"']+/).filter((t2) => t2.length > 0));
14244
+ }
14245
+ function toBigrams(text) {
14246
+ const bigrams = new Set;
14247
+ for (let i2 = 0;i2 < text.length - 1; i2++) {
14248
+ bigrams.add(text.slice(i2, i2 + 2));
14249
+ }
14250
+ return bigrams;
14251
+ }
14252
+
14253
+ // src/commands/label.ts
14254
+ async function requireGitRepository() {
14255
+ if (!await isGitRepo()) {
14256
+ error("Not inside a git repository.");
14257
+ process.exit(1);
14258
+ }
14259
+ }
14260
+ async function requireGhCli() {
14261
+ if (!await checkGhInstalled()) {
14262
+ error("GitHub CLI (gh) is required. Install it at https://cli.github.com");
14263
+ process.exit(1);
14264
+ }
14265
+ if (!await checkGhAuth()) {
14266
+ error("Not authenticated with GitHub CLI. Run `gh auth login` first.");
14267
+ process.exit(1);
13330
14268
  }
13331
- return { title: "Environment", checks };
13332
14269
  }
13333
- var doctor_default = defineCommand({
14270
+ function extractLabelsCsv(rawArgs) {
14271
+ const knownFlagsWithValues = new Set(["--issue", "--pr", "-i", "-p"]);
14272
+ const parts = [];
14273
+ let skipNext = false;
14274
+ for (const arg of rawArgs) {
14275
+ if (skipNext) {
14276
+ skipNext = false;
14277
+ continue;
14278
+ }
14279
+ if (knownFlagsWithValues.has(arg)) {
14280
+ skipNext = true;
14281
+ continue;
14282
+ }
14283
+ if (arg.startsWith("-")) {
14284
+ continue;
14285
+ }
14286
+ parts.push(arg);
14287
+ }
14288
+ return parts.join(" ");
14289
+ }
14290
+ function formatSourceNote(source) {
14291
+ return source === "clean-labels" ? "(source: Clean Labels dataset)" : "(source: repo labels)";
14292
+ }
14293
+ var addCommand = defineCommand({
13334
14294
  meta: {
13335
- name: "doctor",
13336
- description: "Diagnose the contribute-now CLI environment and configuration"
14295
+ name: "add",
14296
+ description: "Apply existing labels to an issue or pull request"
13337
14297
  },
13338
14298
  args: {
13339
- json: {
13340
- type: "boolean",
13341
- description: "Output report as JSON",
13342
- default: false
14299
+ issue: {
14300
+ type: "string",
14301
+ alias: "i",
14302
+ description: "Issue number to label"
14303
+ },
14304
+ pr: {
14305
+ type: "string",
14306
+ alias: "p",
14307
+ description: "Pull request number to label"
14308
+ },
14309
+ labels: {
14310
+ type: "positional",
14311
+ description: "Comma-separated label names (spaces are part of label names)",
14312
+ required: false
13343
14313
  }
13344
14314
  },
13345
- async run({ args }) {
13346
- const isJson = args.json;
13347
- const [tool, deps, config, git, fork, workflow] = await Promise.all([
13348
- toolSection(),
13349
- depsSection(),
13350
- configSection(),
13351
- gitSection(),
13352
- forkSection(),
13353
- workflowSection()
13354
- ]);
13355
- const env2 = envSection();
13356
- const report = {
13357
- sections: [tool, deps, config, git, fork, workflow, env2]
13358
- };
13359
- if (isJson) {
13360
- console.log(toJson(report));
13361
- return;
14315
+ async run({ args, rawArgs }) {
14316
+ await requireGitRepository();
14317
+ await requireGhCli();
14318
+ await projectHeading("label add", "\uD83C\uDFF7\uFE0F");
14319
+ const hasIssue = Boolean(args.issue);
14320
+ const hasPr = Boolean(args.pr);
14321
+ if (!hasIssue && !hasPr) {
14322
+ error("Provide a target: --issue <number> or --pr <number>");
14323
+ process.exit(1);
13362
14324
  }
13363
- await projectHeading("doctor", "\uD83E\uDE7A");
13364
- printReport(report);
13365
- const total = report.sections.flatMap((s2) => s2.checks);
13366
- const failures = total.filter((c3) => !c3.ok);
13367
- const warnings = total.filter((c3) => c3.ok && c3.warning);
13368
- if (failures.length === 0 && warnings.length === 0) {
13369
- console.log(` ${import_picocolors12.default.green("All checks passed!")} No issues detected.
13370
- `);
13371
- } else {
13372
- if (failures.length > 0) {
13373
- console.log(` ${import_picocolors12.default.red(`${failures.length} issue${failures.length !== 1 ? "s" : ""} found.`)}`);
14325
+ if (hasIssue && hasPr) {
14326
+ error("Use either --issue or --pr, not both.");
14327
+ process.exit(1);
14328
+ }
14329
+ const targetNumber = Number(hasIssue ? args.issue : args.pr);
14330
+ if (!Number.isInteger(targetNumber) || targetNumber <= 0) {
14331
+ error(`Invalid ${hasIssue ? "issue" : "PR"} number: ${String(hasIssue ? args.issue : args.pr)}`);
14332
+ process.exit(1);
14333
+ }
14334
+ const labelsCsv = extractLabelsCsv(rawArgs);
14335
+ if (!labelsCsv) {
14336
+ error("No labels provided. Pass a comma-separated list after the target flag.");
14337
+ info("Example: cn label add --issue 42 bug,enhancement", "");
14338
+ process.exit(1);
14339
+ }
14340
+ const requested = parseLabelsCsv(labelsCsv);
14341
+ if (requested.length === 0) {
14342
+ error("No valid label names found in input.");
14343
+ process.exit(1);
14344
+ }
14345
+ let cache = await getActiveLabels();
14346
+ if (!cache) {
14347
+ error("Could not load repository labels. Make sure you are authenticated with `gh auth login`.");
14348
+ process.exit(1);
14349
+ }
14350
+ let { valid, invalid } = validateLabels(requested, cache.labels);
14351
+ if (invalid.length > 0) {
14352
+ warn(`Unknown label(s) detected \u2014 resyncing label cache\u2026`);
14353
+ const freshCache = await syncLabelCache();
14354
+ if (freshCache) {
14355
+ cache = freshCache;
14356
+ const revalidated = validateLabels(requested, cache.labels);
14357
+ valid = revalidated.valid;
14358
+ invalid = revalidated.invalid;
13374
14359
  }
13375
- if (warnings.length > 0) {
13376
- console.log(` ${import_picocolors12.default.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}.`)}`);
14360
+ }
14361
+ if (invalid.length > 0) {
14362
+ error(`Unknown label(s): ${invalid.map((l2) => import_picocolors14.default.bold(l2)).join(", ")}`);
14363
+ console.log();
14364
+ for (const label of invalid) {
14365
+ const suggestions = findCloseMatches(label, cache.labels);
14366
+ if (suggestions.length > 0) {
14367
+ info(` Did you mean for "${label}": ${suggestions.map((s2) => import_picocolors14.default.cyan(s2)).join(", ")}`, "");
14368
+ }
13377
14369
  }
13378
14370
  console.log();
14371
+ info(`Available labels: ${cache.labels.map((l2) => import_picocolors14.default.dim(l2.name)).join(", ")}`, "");
14372
+ process.exit(1);
14373
+ }
14374
+ const targetLabel = hasIssue ? `issue #${targetNumber}` : `PR #${targetNumber}`;
14375
+ info(`Applying ${valid.length} label(s) to ${import_picocolors14.default.bold(targetLabel)}\u2026`, "\uD83C\uDFF7\uFE0F");
14376
+ const result = hasIssue ? await addLabelsToIssue(targetNumber, valid) : await addLabelsToPR(targetNumber, valid);
14377
+ if (result.exitCode !== 0) {
14378
+ const stderr = result.stderr.trim();
14379
+ const isDrift = /not found|does not exist/i.test(stderr) || /not found|does not exist/i.test(result.stdout);
14380
+ if (isDrift) {
14381
+ warn("Label not found on remote \u2014 resyncing and retrying\u2026");
14382
+ const freshCache = await syncLabelCache();
14383
+ if (freshCache) {
14384
+ const revalidated = validateLabels(requested, freshCache.labels);
14385
+ if (revalidated.invalid.length === 0) {
14386
+ const retry = hasIssue ? await addLabelsToIssue(targetNumber, revalidated.valid) : await addLabelsToPR(targetNumber, revalidated.valid);
14387
+ if (retry.exitCode === 0) {
14388
+ success(`Applied to ${import_picocolors14.default.bold(targetLabel)}: ${revalidated.valid.map((l2) => import_picocolors14.default.cyan(l2)).join(", ")}`);
14389
+ return;
14390
+ }
14391
+ error(`Retry failed: ${retry.stderr.trim() || retry.stdout.trim()}`);
14392
+ process.exit(1);
14393
+ }
14394
+ }
14395
+ }
14396
+ error(`Failed to apply labels: ${stderr || result.stdout.trim()}`);
14397
+ info("Run `cn label add --help` for usage guidance.", "");
14398
+ process.exit(1);
13379
14399
  }
14400
+ success(`Applied to ${import_picocolors14.default.bold(targetLabel)}: ${valid.map((l2) => import_picocolors14.default.cyan(l2)).join(", ")}`);
14401
+ const sourceNote = formatSourceNote(cache.source);
14402
+ info(sourceNote, "");
13380
14403
  }
13381
14404
  });
13382
-
13383
- // src/commands/hook.ts
13384
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
13385
- import { join as join6 } from "path";
13386
- var import_picocolors13 = __toESM(require_picocolors(), 1);
13387
- var HOOK_MARKER = "# managed by contribute-now";
13388
- function getHooksDir(cwd = process.cwd()) {
13389
- return join6(cwd, ".git", "hooks");
13390
- }
13391
- function getHookPath(cwd = process.cwd()) {
13392
- return join6(getHooksDir(cwd), "commit-msg");
13393
- }
13394
- function generateHookScript() {
13395
- return `#!/bin/sh
13396
- ${HOOK_MARKER}
13397
- # Validates commit messages against your configured convention.
13398
- # Install: cn hook install
13399
- # Uninstall: cn hook uninstall
13400
-
13401
- commit_msg_file="$1"
13402
- commit_msg=$(head -1 "$commit_msg_file")
13403
-
13404
- # Skip merge commits and fixup/squash commits
13405
- case "$commit_msg" in
13406
- Merge\\ *|fixup!*|squash!*|amend!*) exit 0 ;;
13407
- esac
13408
-
13409
- # Detect available package runner
13410
- if command -v cn >/dev/null 2>&1; then
13411
- cn validate --file "$commit_msg_file"
13412
- elif command -v bunx >/dev/null 2>&1; then
13413
- bunx cn validate --file "$commit_msg_file"
13414
- else
13415
- echo "Warning: Neither cn nor bunx is available. Skipping commit message validation."
13416
- exit 0
13417
- fi
13418
- `;
13419
- }
13420
- var hook_default = defineCommand({
14405
+ var suggestCommand = defineCommand({
13421
14406
  meta: {
13422
- name: "hook",
13423
- description: "Install or uninstall the commit-msg git hook"
14407
+ name: "suggest",
14408
+ description: "Suggest labels for an issue or pull request based on its content"
13424
14409
  },
13425
14410
  args: {
13426
- action: {
13427
- type: "positional",
13428
- description: "Action to perform: install or uninstall",
13429
- required: true
14411
+ issue: {
14412
+ type: "string",
14413
+ alias: "i",
14414
+ description: "Issue number to suggest labels for"
14415
+ },
14416
+ pr: {
14417
+ type: "string",
14418
+ alias: "p",
14419
+ description: "Pull request number to suggest labels for"
13430
14420
  }
13431
14421
  },
13432
14422
  async run({ args }) {
13433
- if (!await isGitRepo()) {
13434
- error("Not inside a git repository.");
14423
+ await requireGitRepository();
14424
+ await requireGhCli();
14425
+ await projectHeading("label suggest", "\uD83C\uDFF7\uFE0F");
14426
+ const hasIssue = Boolean(args.issue);
14427
+ const hasPr = Boolean(args.pr);
14428
+ if (!hasIssue && !hasPr) {
14429
+ error("Provide a target: --issue <number> or --pr <number>");
13435
14430
  process.exit(1);
13436
14431
  }
13437
- const action = args.action;
13438
- if (action !== "install" && action !== "uninstall") {
13439
- error(`Unknown action "${action}". Use "install" or "uninstall".`);
14432
+ if (hasIssue && hasPr) {
14433
+ error("Use either --issue or --pr, not both.");
13440
14434
  process.exit(1);
13441
14435
  }
13442
- if (action === "install") {
13443
- await installHook();
13444
- } else {
13445
- await uninstallHook();
14436
+ const targetNumber = Number(hasIssue ? args.issue : args.pr);
14437
+ if (!Number.isInteger(targetNumber) || targetNumber <= 0) {
14438
+ error(`Invalid ${hasIssue ? "issue" : "PR"} number: ${String(hasIssue ? args.issue : args.pr)}`);
14439
+ process.exit(1);
13446
14440
  }
13447
- }
13448
- });
13449
- async function installHook() {
13450
- await projectHeading("hook install", "\uD83E\uDE9D");
13451
- const config = readConfig();
13452
- if (!config) {
13453
- error("No repo config found. Run `cn setup` first.");
13454
- process.exit(1);
13455
- }
13456
- if (config.commitConvention === "none") {
13457
- warn('Commit convention is set to "none". No hook to install.');
13458
- info("Change your convention with `cn setup` first.", "");
13459
- process.exit(0);
13460
- }
13461
- const hookPath = getHookPath();
13462
- const hooksDir = getHooksDir();
13463
- if (existsSync6(hookPath)) {
13464
- const existing = readFileSync5(hookPath, "utf-8");
13465
- if (!existing.includes(HOOK_MARKER)) {
13466
- error("A commit-msg hook already exists and was not installed by contribute-now.");
13467
- warn(`Path: ${hookPath}`);
13468
- warn("Remove it manually or back it up before installing.");
14441
+ const targetLabel = hasIssue ? `issue #${targetNumber}` : `PR #${targetNumber}`;
14442
+ info(`Fetching ${import_picocolors14.default.bold(targetLabel)} content\u2026`, "");
14443
+ const content = hasIssue ? await getIssueContent(targetNumber) : await getPRContent(targetNumber);
14444
+ if (!content) {
14445
+ error(`Could not fetch content for ${targetLabel}. Verify the number and your gh auth.`);
13469
14446
  process.exit(1);
13470
14447
  }
13471
- info("Updating existing contribute-now hook...");
13472
- }
13473
- if (!existsSync6(hooksDir)) {
13474
- mkdirSync5(hooksDir, { recursive: true });
13475
- }
13476
- writeFileSync5(hookPath, generateHookScript(), { mode: 493 });
13477
- success(`commit-msg hook installed.`);
13478
- info(`Convention: ${import_picocolors13.default.bold(CONVENTION_LABELS[config.commitConvention])}`, "");
13479
- info(`Path: ${import_picocolors13.default.dim(hookPath)}`, "");
13480
- warn("Note: hooks can be bypassed with `git commit --no-verify`.");
13481
- }
13482
- async function uninstallHook() {
13483
- await projectHeading("hook uninstall", "\uD83E\uDE9D");
13484
- const hookPath = getHookPath();
13485
- if (!existsSync6(hookPath)) {
13486
- info("No commit-msg hook found. Nothing to uninstall.");
13487
- return;
14448
+ const fullText = `${content.title}
14449
+
14450
+ ${content.body}`;
14451
+ const cache = await getActiveLabels();
14452
+ if (!cache) {
14453
+ error("Could not load repository labels. Run `cn label add --help` for setup guidance.");
14454
+ process.exit(1);
14455
+ }
14456
+ const ranked = scoreLabelsForContent(fullText, cache.labels);
14457
+ if (ranked.length === 0) {
14458
+ info(`No label suggestions found for ${import_picocolors14.default.bold(targetLabel)}.`);
14459
+ info(`Total labels available: ${cache.labels.length}`, "");
14460
+ return;
14461
+ }
14462
+ const sourceNote = formatSourceNote(cache.source);
14463
+ console.log();
14464
+ console.log(` ${import_picocolors14.default.bold(`Suggested labels for ${import_picocolors14.default.cyan(targetLabel)}:`)} ${import_picocolors14.default.dim(sourceNote)}`);
14465
+ console.log();
14466
+ const topN = ranked.slice(0, 5);
14467
+ for (const { label, score } of topN) {
14468
+ const descPart = label.description ? import_picocolors14.default.dim(` \u2014 ${label.description}`) : "";
14469
+ const scorePart = import_picocolors14.default.dim(` [score: ${score}]`);
14470
+ console.log(` ${import_picocolors14.default.cyan("\u2022")} ${import_picocolors14.default.bold(label.name)}${descPart}${scorePart}`);
14471
+ }
14472
+ console.log();
14473
+ info(`Apply a label: cn label add --${hasIssue ? "issue" : "pr"} ${targetNumber} <label>`, "");
13488
14474
  }
13489
- const content = readFileSync5(hookPath, "utf-8");
13490
- if (!content.includes(HOOK_MARKER)) {
13491
- error("The commit-msg hook was not installed by contribute-now. Leaving it untouched.");
13492
- process.exit(1);
14475
+ });
14476
+ var label_default = defineCommand({
14477
+ meta: {
14478
+ name: "label",
14479
+ description: "Manage labels on issues and pull requests"
14480
+ },
14481
+ subCommands: {
14482
+ add: addCommand,
14483
+ suggest: suggestCommand
13493
14484
  }
13494
- rmSync2(hookPath);
13495
- success("commit-msg hook removed.");
13496
- }
14485
+ });
13497
14486
 
13498
14487
  // src/commands/log.ts
13499
- var import_picocolors14 = __toESM(require_picocolors(), 1);
14488
+ var import_picocolors15 = __toESM(require_picocolors(), 1);
13500
14489
  function getDefaultOverviewRemoteCommitCount(hasLocalUnpushedCommits) {
13501
14490
  return hasLocalUnpushedCommits ? 10 : 20;
13502
14491
  }
@@ -13594,9 +14583,9 @@ var log_default = defineCommand({
13594
14583
  } else if (mode === "local") {
13595
14584
  if (!compareRef) {
13596
14585
  console.log();
13597
- console.log(import_picocolors14.default.yellow(" \u26A0 Could not determine a comparison branch."));
13598
- console.log(import_picocolors14.default.dim(" No upstream tracking set and no remote base branch found."));
13599
- console.log(import_picocolors14.default.dim(` Use ${import_picocolors14.default.bold("cn log --full")} to see the full commit history instead.`));
14586
+ console.log(import_picocolors15.default.yellow(" \u26A0 Could not determine a comparison branch."));
14587
+ console.log(import_picocolors15.default.dim(" No upstream tracking set and no remote base branch found."));
14588
+ console.log(import_picocolors15.default.dim(` Use ${import_picocolors15.default.bold("cn log --full")} to see the full commit history instead.`));
13600
14589
  console.log();
13601
14590
  return;
13602
14591
  }
@@ -13615,8 +14604,8 @@ var log_default = defineCommand({
13615
14604
  const remoteBranch = compareRef ?? targetBranch;
13616
14605
  if (!remoteBranch) {
13617
14606
  console.log();
13618
- console.log(import_picocolors14.default.yellow(" \u26A0 Could not determine a remote branch to display."));
13619
- console.log(import_picocolors14.default.dim(" Set an upstream tracking branch or configure your base branch first."));
14607
+ console.log(import_picocolors15.default.yellow(" \u26A0 Could not determine a remote branch to display."));
14608
+ console.log(import_picocolors15.default.dim(" Set an upstream tracking branch or configure your base branch first."));
13620
14609
  console.log();
13621
14610
  return;
13622
14611
  }
@@ -13675,31 +14664,31 @@ async function getOverviewRemoteCommitCount(currentBranch, compareRef) {
13675
14664
  }
13676
14665
  function printModeHeader(mode, currentBranch, compareRef, usingFallback = false) {
13677
14666
  const branch = currentBranch ?? "HEAD";
13678
- const fallbackNote = usingFallback ? import_picocolors14.default.yellow(" (no upstream \u2014 comparing against base branch)") : "";
14667
+ const fallbackNote = usingFallback ? import_picocolors15.default.yellow(" (no upstream \u2014 comparing against base branch)") : "";
13679
14668
  switch (mode) {
13680
14669
  case "overview":
13681
- console.log(import_picocolors14.default.dim(` mode: ${import_picocolors14.default.bold("overview")} \u2014 local unpushed commits and remote branch history for ${import_picocolors14.default.bold(branch)}`) + fallbackNote);
14670
+ console.log(import_picocolors15.default.dim(` mode: ${import_picocolors15.default.bold("overview")} \u2014 local unpushed commits and remote branch history for ${import_picocolors15.default.bold(branch)}`) + fallbackNote);
13682
14671
  if (compareRef) {
13683
- console.log(import_picocolors14.default.dim(` remote source: ${import_picocolors14.default.bold(compareRef)}`));
14672
+ console.log(import_picocolors15.default.dim(` remote source: ${import_picocolors15.default.bold(compareRef)}`));
13684
14673
  }
13685
14674
  break;
13686
14675
  case "local":
13687
- console.log(import_picocolors14.default.dim(` mode: ${import_picocolors14.default.bold("local")} \u2014 unpushed commits on ${import_picocolors14.default.bold(branch)}`) + fallbackNote);
14676
+ console.log(import_picocolors15.default.dim(` mode: ${import_picocolors15.default.bold("local")} \u2014 unpushed commits on ${import_picocolors15.default.bold(branch)}`) + fallbackNote);
13688
14677
  if (compareRef) {
13689
- console.log(import_picocolors14.default.dim(` comparing: ${import_picocolors14.default.bold(compareRef)} \u279C ${import_picocolors14.default.bold("HEAD")}`));
14678
+ console.log(import_picocolors15.default.dim(` comparing: ${import_picocolors15.default.bold(compareRef)} \u279C ${import_picocolors15.default.bold("HEAD")}`));
13690
14679
  }
13691
14680
  break;
13692
14681
  case "remote":
13693
- console.log(import_picocolors14.default.dim(` mode: ${import_picocolors14.default.bold("remote")} \u2014 remote branch history relevant to ${import_picocolors14.default.bold(branch)}`) + fallbackNote);
14682
+ console.log(import_picocolors15.default.dim(` mode: ${import_picocolors15.default.bold("remote")} \u2014 remote branch history relevant to ${import_picocolors15.default.bold(branch)}`) + fallbackNote);
13694
14683
  if (compareRef) {
13695
- console.log(import_picocolors14.default.dim(` branch: ${import_picocolors14.default.bold(compareRef)}`));
14684
+ console.log(import_picocolors15.default.dim(` branch: ${import_picocolors15.default.bold(compareRef)}`));
13696
14685
  }
13697
14686
  break;
13698
14687
  case "full":
13699
- console.log(import_picocolors14.default.dim(` mode: ${import_picocolors14.default.bold("full")} \u2014 complete commit history for ${import_picocolors14.default.bold(branch)}`));
14688
+ console.log(import_picocolors15.default.dim(` mode: ${import_picocolors15.default.bold("full")} \u2014 complete commit history for ${import_picocolors15.default.bold(branch)}`));
13700
14689
  break;
13701
14690
  case "all":
13702
- console.log(import_picocolors14.default.dim(` mode: ${import_picocolors14.default.bold("all")} \u2014 commits across all branches`));
14691
+ console.log(import_picocolors15.default.dim(` mode: ${import_picocolors15.default.bold("all")} \u2014 commits across all branches`));
13703
14692
  break;
13704
14693
  }
13705
14694
  }
@@ -13723,7 +14712,7 @@ async function renderScopedLog(options) {
13723
14712
  }
13724
14713
  console.log();
13725
14714
  for (const entry of entries) {
13726
- const hashStr = import_picocolors14.default.yellow(entry.hash);
14715
+ const hashStr = import_picocolors15.default.yellow(entry.hash);
13727
14716
  const refsStr = entry.refs ? ` ${colorizeRefs(entry.refs, protectedBranches, currentBranch)}` : "";
13728
14717
  const subjectStr = colorizeSubject(entry.subject);
13729
14718
  console.log(` ${hashStr}${refsStr} ${subjectStr}`);
@@ -13742,10 +14731,10 @@ async function renderOverviewLog(options) {
13742
14731
  usingFallback
13743
14732
  } = options;
13744
14733
  console.log();
13745
- console.log(import_picocolors14.default.bold(import_picocolors14.default.cyan(" Local Unpushed Commits")));
14734
+ console.log(import_picocolors15.default.bold(import_picocolors15.default.cyan(" Local Unpushed Commits")));
13746
14735
  if (!compareRef) {
13747
- console.log(import_picocolors14.default.dim(" No comparison branch detected for local commit status."));
13748
- console.log(import_picocolors14.default.dim(" Set an upstream tracking branch or run cn log --full to inspect the current branch history."));
14736
+ console.log(import_picocolors15.default.dim(" No comparison branch detected for local commit status."));
14737
+ console.log(import_picocolors15.default.dim(" Set an upstream tracking branch or run cn log --full to inspect the current branch history."));
13749
14738
  } else {
13750
14739
  await renderScopedLog({
13751
14740
  mode: "local",
@@ -13757,11 +14746,11 @@ async function renderOverviewLog(options) {
13757
14746
  });
13758
14747
  }
13759
14748
  console.log();
13760
- console.log(import_picocolors14.default.bold(import_picocolors14.default.cyan(" Remote Branch History")));
14749
+ console.log(import_picocolors15.default.bold(import_picocolors15.default.cyan(" Remote Branch History")));
13761
14750
  if (!compareRef) {
13762
- console.log(import_picocolors14.default.dim(" No remote branch detected."));
14751
+ console.log(import_picocolors15.default.dim(" No remote branch detected."));
13763
14752
  if (usingFallback) {
13764
- console.log(import_picocolors14.default.dim(" Configure your base branch or upstream tracking to enable the split view."));
14753
+ console.log(import_picocolors15.default.dim(" Configure your base branch or upstream tracking to enable the split view."));
13765
14754
  }
13766
14755
  return;
13767
14756
  }
@@ -13774,15 +14763,15 @@ async function renderOverviewLog(options) {
13774
14763
  currentBranch
13775
14764
  });
13776
14765
  if (!hasRemoteHistory) {
13777
- console.log(import_picocolors14.default.dim(" No remote history found for the selected branch."));
14766
+ console.log(import_picocolors15.default.dim(" No remote history found for the selected branch."));
13778
14767
  }
13779
14768
  }
13780
14769
  function printEmptyState(mode) {
13781
14770
  console.log();
13782
14771
  if (mode === "local") {
13783
- console.log(import_picocolors14.default.dim(" No local unpushed commits \u2014 you're up to date with remote!"));
14772
+ console.log(import_picocolors15.default.dim(" No local unpushed commits \u2014 you're up to date with remote!"));
13784
14773
  } else {
13785
- console.log(import_picocolors14.default.dim(" No remote-only commits \u2014 your local branch is up to date!"));
14774
+ console.log(import_picocolors15.default.dim(" No remote-only commits \u2014 your local branch is up to date!"));
13786
14775
  }
13787
14776
  console.log();
13788
14777
  }
@@ -13791,7 +14780,7 @@ async function renderFullLog(options) {
13791
14780
  if (showGraph) {
13792
14781
  const lines = await getLogGraph({ count, all, branch: targetBranch });
13793
14782
  if (lines.length === 0) {
13794
- console.log(import_picocolors14.default.dim(" No commits found."));
14783
+ console.log(import_picocolors15.default.dim(" No commits found."));
13795
14784
  console.log();
13796
14785
  return false;
13797
14786
  }
@@ -13802,13 +14791,13 @@ async function renderFullLog(options) {
13802
14791
  } else {
13803
14792
  const entries = await getLogEntries({ count, all, branch: targetBranch });
13804
14793
  if (entries.length === 0) {
13805
- console.log(import_picocolors14.default.dim(" No commits found."));
14794
+ console.log(import_picocolors15.default.dim(" No commits found."));
13806
14795
  console.log();
13807
14796
  return false;
13808
14797
  }
13809
14798
  console.log();
13810
14799
  for (const entry of entries) {
13811
- const hashStr = import_picocolors14.default.yellow(entry.hash);
14800
+ const hashStr = import_picocolors15.default.yellow(entry.hash);
13812
14801
  const refsStr = entry.refs ? ` ${colorizeRefs(entry.refs, protectedBranches, currentBranch)}` : "";
13813
14802
  const subjectStr = colorizeSubject(entry.subject);
13814
14803
  console.log(` ${hashStr}${refsStr} ${subjectStr}`);
@@ -13820,33 +14809,33 @@ function printFooter(mode, count, overviewRemoteCount, targetBranch) {
13820
14809
  console.log();
13821
14810
  switch (mode) {
13822
14811
  case "overview":
13823
- console.log(import_picocolors14.default.dim(` Showing up to ${count} local commits and ${overviewRemoteCount} remote commits`));
14812
+ console.log(import_picocolors15.default.dim(` Showing up to ${count} local commits and ${overviewRemoteCount} remote commits`));
13824
14813
  break;
13825
14814
  case "local":
13826
- console.log(import_picocolors14.default.dim(` Showing up to ${count} unpushed commits`));
14815
+ console.log(import_picocolors15.default.dim(` Showing up to ${count} unpushed commits`));
13827
14816
  break;
13828
14817
  case "remote":
13829
- console.log(import_picocolors14.default.dim(` Showing ${count} most recent commits from the remote branch`));
14818
+ console.log(import_picocolors15.default.dim(` Showing ${count} most recent commits from the remote branch`));
13830
14819
  break;
13831
14820
  case "full":
13832
- console.log(import_picocolors14.default.dim(` Showing ${count} most recent commits${targetBranch ? ` (${targetBranch})` : ""}`));
14821
+ console.log(import_picocolors15.default.dim(` Showing ${count} most recent commits${targetBranch ? ` (${targetBranch})` : ""}`));
13833
14822
  break;
13834
14823
  case "all":
13835
- console.log(import_picocolors14.default.dim(` Showing ${count} most recent commits (all branches)`));
14824
+ console.log(import_picocolors15.default.dim(` Showing ${count} most recent commits (all branches)`));
13836
14825
  break;
13837
14826
  }
13838
14827
  }
13839
14828
  function colorizeGraphLine(line, protectedBranches, currentBranch) {
13840
14829
  const match = line.match(/^([|/\\*\s_.-]*)([a-f0-9]{7,12})(\s+\(([^)]+)\))?\s*(.*)/);
13841
14830
  if (!match) {
13842
- return import_picocolors14.default.cyan(line);
14831
+ return import_picocolors15.default.cyan(line);
13843
14832
  }
13844
14833
  const [, graphPart = "", hash, , refs, subject = ""] = match;
13845
14834
  const parts = [];
13846
14835
  if (graphPart) {
13847
14836
  parts.push(colorizeGraphChars(graphPart));
13848
14837
  }
13849
- parts.push(import_picocolors14.default.yellow(hash));
14838
+ parts.push(import_picocolors15.default.yellow(hash));
13850
14839
  if (refs) {
13851
14840
  parts.push(` (${colorizeRefs(refs, protectedBranches, currentBranch)})`);
13852
14841
  }
@@ -13857,15 +14846,15 @@ function colorizeGraphChars(graphPart) {
13857
14846
  return graphPart.split("").map((ch) => {
13858
14847
  switch (ch) {
13859
14848
  case "*":
13860
- return import_picocolors14.default.green(ch);
14849
+ return import_picocolors15.default.green(ch);
13861
14850
  case "|":
13862
- return import_picocolors14.default.cyan(ch);
14851
+ return import_picocolors15.default.cyan(ch);
13863
14852
  case "/":
13864
14853
  case "\\":
13865
- return import_picocolors14.default.cyan(ch);
14854
+ return import_picocolors15.default.cyan(ch);
13866
14855
  case "-":
13867
14856
  case "_":
13868
- return import_picocolors14.default.cyan(ch);
14857
+ return import_picocolors15.default.cyan(ch);
13869
14858
  default:
13870
14859
  return ch;
13871
14860
  }
@@ -13877,50 +14866,50 @@ function colorizeRefs(refs, protectedBranches, currentBranch) {
13877
14866
  if (trimmed.startsWith("HEAD ->") || trimmed === "HEAD") {
13878
14867
  const branchName = trimmed.replace("HEAD -> ", "");
13879
14868
  if (trimmed === "HEAD") {
13880
- return import_picocolors14.default.bold(import_picocolors14.default.cyan("HEAD"));
14869
+ return import_picocolors15.default.bold(import_picocolors15.default.cyan("HEAD"));
13881
14870
  }
13882
- return `${import_picocolors14.default.bold(import_picocolors14.default.cyan("HEAD"))} ${import_picocolors14.default.dim("->")} ${colorizeRefName(branchName, protectedBranches, currentBranch)}`;
14871
+ return `${import_picocolors15.default.bold(import_picocolors15.default.cyan("HEAD"))} ${import_picocolors15.default.dim("->")} ${colorizeRefName(branchName, protectedBranches, currentBranch)}`;
13883
14872
  }
13884
14873
  if (trimmed.startsWith("tag:")) {
13885
- return import_picocolors14.default.bold(import_picocolors14.default.magenta(trimmed));
14874
+ return import_picocolors15.default.bold(import_picocolors15.default.magenta(trimmed));
13886
14875
  }
13887
14876
  return colorizeRefName(trimmed, protectedBranches, currentBranch);
13888
- }).join(import_picocolors14.default.dim(", "));
14877
+ }).join(import_picocolors15.default.dim(", "));
13889
14878
  }
13890
14879
  function colorizeRefName(name, protectedBranches, currentBranch) {
13891
14880
  const isRemote = name.includes("/");
13892
14881
  const localName = isRemote ? name.split("/").slice(1).join("/") : name;
13893
14882
  if (protectedBranches.includes(localName)) {
13894
- return isRemote ? import_picocolors14.default.bold(import_picocolors14.default.red(name)) : import_picocolors14.default.bold(import_picocolors14.default.red(name));
14883
+ return isRemote ? import_picocolors15.default.bold(import_picocolors15.default.red(name)) : import_picocolors15.default.bold(import_picocolors15.default.red(name));
13895
14884
  }
13896
14885
  if (localName === currentBranch) {
13897
- return import_picocolors14.default.bold(import_picocolors14.default.green(name));
14886
+ return import_picocolors15.default.bold(import_picocolors15.default.green(name));
13898
14887
  }
13899
14888
  if (isRemote) {
13900
- return import_picocolors14.default.blue(name);
14889
+ return import_picocolors15.default.blue(name);
13901
14890
  }
13902
- return import_picocolors14.default.green(name);
14891
+ return import_picocolors15.default.green(name);
13903
14892
  }
13904
14893
  function colorizeSubject(subject) {
13905
14894
  const emojiMatch = subject.match(/^((?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F)+\s*)/u);
13906
14895
  if (emojiMatch) {
13907
14896
  const emoji = emojiMatch[1];
13908
14897
  const rest = subject.slice(emoji.length);
13909
- return `${emoji}${import_picocolors14.default.white(rest)}`;
14898
+ return `${emoji}${import_picocolors15.default.white(rest)}`;
13910
14899
  }
13911
14900
  if (subject.startsWith("Merge ")) {
13912
- return import_picocolors14.default.dim(subject);
14901
+ return import_picocolors15.default.dim(subject);
13913
14902
  }
13914
- return import_picocolors14.default.white(subject);
14903
+ return import_picocolors15.default.white(subject);
13915
14904
  }
13916
14905
 
13917
14906
  // src/commands/save.ts
13918
14907
  import { execFile as execFileCb4 } from "child_process";
13919
- var import_picocolors15 = __toESM(require_picocolors(), 1);
14908
+ var import_picocolors16 = __toESM(require_picocolors(), 1);
13920
14909
  function gitRun(args) {
13921
- return new Promise((resolve5) => {
14910
+ return new Promise((resolve6) => {
13922
14911
  execFileCb4("git", args, (err, stdout2, stderr) => {
13923
- resolve5({
14912
+ resolve6({
13924
14913
  exitCode: err ? err.code === "ENOENT" ? 127 : err.status ?? 1 : 0,
13925
14914
  stdout: stdout2 ?? "",
13926
14915
  stderr: stderr ?? ""
@@ -13990,8 +14979,8 @@ async function handleSave(message) {
13990
14979
  info("No uncommitted changes to save.");
13991
14980
  return;
13992
14981
  }
13993
- success(`Saved: ${import_picocolors15.default.dim(label)}`);
13994
- info(`Use ${import_picocolors15.default.bold("cn save --restore")} to bring them back.`, "");
14982
+ success(`Saved: ${import_picocolors16.default.dim(label)}`);
14983
+ info(`Use ${import_picocolors16.default.bold("cn save --restore")} to bring them back.`, "");
13995
14984
  }
13996
14985
  async function handleRestore() {
13997
14986
  await projectHeading("save --restore", "\uD83D\uDCBE");
@@ -14007,7 +14996,7 @@ async function handleRestore() {
14007
14996
  warn("You may have conflicts. Resolve them and run `git stash drop` when done.");
14008
14997
  process.exit(1);
14009
14998
  }
14010
- success(`Restored: ${import_picocolors15.default.dim(stashes[0].message)}`);
14999
+ success(`Restored: ${import_picocolors16.default.dim(stashes[0].message)}`);
14011
15000
  return;
14012
15001
  }
14013
15002
  const choices = stashes.map((s2) => `${s2.index} ${s2.message}`);
@@ -14020,7 +15009,7 @@ async function handleRestore() {
14020
15009
  process.exit(1);
14021
15010
  }
14022
15011
  const match = stashes.find((s2) => String(s2.index) === idx);
14023
- success(`Restored: ${import_picocolors15.default.dim(match?.message ?? "saved changes")}`);
15012
+ success(`Restored: ${import_picocolors16.default.dim(match?.message ?? "saved changes")}`);
14024
15013
  }
14025
15014
  async function handleList() {
14026
15015
  await projectHeading("save --list", "\uD83D\uDCBE");
@@ -14031,13 +15020,13 @@ async function handleList() {
14031
15020
  }
14032
15021
  console.log();
14033
15022
  for (const s2 of stashes) {
14034
- const idx = import_picocolors15.default.dim(`[${s2.index}]`);
15023
+ const idx = import_picocolors16.default.dim(`[${s2.index}]`);
14035
15024
  const msg = s2.message;
14036
15025
  console.log(` ${idx} ${msg}`);
14037
15026
  }
14038
15027
  console.log();
14039
- info(`Use ${import_picocolors15.default.bold("cn save --restore")} to bring changes back.`, "");
14040
- info(`Use ${import_picocolors15.default.bold("cn save --drop")} to discard saved changes.`, "");
15028
+ info(`Use ${import_picocolors16.default.bold("cn save --restore")} to bring changes back.`, "");
15029
+ info(`Use ${import_picocolors16.default.bold("cn save --drop")} to discard saved changes.`, "");
14041
15030
  }
14042
15031
  async function handleDrop() {
14043
15032
  await projectHeading("save --drop", "\uD83D\uDCBE");
@@ -14055,7 +15044,7 @@ async function handleDrop() {
14055
15044
  process.exit(1);
14056
15045
  }
14057
15046
  const match = stashes.find((s2) => String(s2.index) === idx);
14058
- success(`Dropped: ${import_picocolors15.default.dim(match?.message ?? "saved changes")}`);
15047
+ success(`Dropped: ${import_picocolors16.default.dim(match?.message ?? "saved changes")}`);
14059
15048
  }
14060
15049
  async function getStashList() {
14061
15050
  const result = await gitRun(["stash", "list"]);
@@ -14072,7 +15061,8 @@ async function getStashList() {
14072
15061
  }
14073
15062
 
14074
15063
  // src/commands/setup.ts
14075
- var import_picocolors16 = __toESM(require_picocolors(), 1);
15064
+ var import_picocolors17 = __toESM(require_picocolors(), 1);
15065
+ init_gh();
14076
15066
  async function shouldContinueSetupWithExistingConfig(options) {
14077
15067
  const { existingConfig, hasConfigFile, confirm, onInfo, onWarn, onSuccess, summary } = options;
14078
15068
  if (existingConfig) {
@@ -14121,6 +15111,32 @@ async function promptForOllamaCloudModel(apiKey, host = DEFAULT_OLLAMA_CLOUD_HOS
14121
15111
  }
14122
15112
  return inputPrompt(`Ollama Cloud model (default: ${DEFAULT_OLLAMA_CLOUD_MODEL} \u2014 press Enter to keep)`, DEFAULT_OLLAMA_CLOUD_MODEL);
14123
15113
  }
15114
+ async function promptForOpenRouterModel(apiKey) {
15115
+ try {
15116
+ info("Fetching available OpenRouter models...");
15117
+ const models = prioritizeOpenRouterModels(await fetchOpenRouterModels(apiKey));
15118
+ if (models.length > 0) {
15119
+ const manualChoice = "Enter model manually";
15120
+ const choices = models.map((model) => ({
15121
+ value: model,
15122
+ label: model === DEFAULT_OPENROUTER_MODEL ? `${model} (default)` : model
15123
+ }));
15124
+ const selected = await selectPrompt("Which OpenRouter model should this clone use?", [
15125
+ ...choices.map((choice) => choice.label),
15126
+ manualChoice
15127
+ ]);
15128
+ if (selected !== manualChoice) {
15129
+ return choices.find((choice) => choice.label === selected)?.value ?? DEFAULT_OPENROUTER_MODEL;
15130
+ }
15131
+ } else {
15132
+ warn("OpenRouter returned no available models. Enter the model name manually.");
15133
+ }
15134
+ } catch (err) {
15135
+ const message = err instanceof Error ? err.message : String(err);
15136
+ warn(`Could not fetch OpenRouter models: ${message}`);
15137
+ }
15138
+ return inputPrompt(`OpenRouter model (default: ${DEFAULT_OPENROUTER_MODEL} \u2014 press Enter to keep)`, DEFAULT_OPENROUTER_MODEL);
15139
+ }
14124
15140
  var setup_default = defineCommand({
14125
15141
  meta: {
14126
15142
  name: "setup",
@@ -14155,7 +15171,7 @@ var setup_default = defineCommand({
14155
15171
  workflow = "github-flow";
14156
15172
  else if (workflowChoice.startsWith("Git Flow"))
14157
15173
  workflow = "git-flow";
14158
- info(`Workflow: ${import_picocolors16.default.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
15174
+ info(`Workflow: ${import_picocolors17.default.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
14159
15175
  const conventionChoice = await selectPrompt("Which commit convention should this project use?", [
14160
15176
  `${CONVENTION_DESCRIPTIONS["clean-commit"]} (recommended)`,
14161
15177
  CONVENTION_DESCRIPTIONS.conventional,
@@ -14172,9 +15188,16 @@ var setup_default = defineCommand({
14172
15188
  if (enableAI) {
14173
15189
  const providerChoice = await selectPrompt("Which AI provider should this clone use?", [
14174
15190
  "GitHub Copilot \u2014 use your existing GitHub/Copilot auth",
14175
- "Ollama Cloud \u2014 use an API key stored in the local secrets store"
15191
+ "Ollama Cloud \u2014 use an API key stored in the local secrets store",
15192
+ "OpenRouter \u2014 use an API key stored in the local secrets store"
14176
15193
  ]);
14177
- aiProvider = providerChoice.startsWith("Ollama Cloud") ? "ollama-cloud" : "copilot";
15194
+ if (providerChoice.startsWith("Ollama Cloud")) {
15195
+ aiProvider = "ollama-cloud";
15196
+ } else if (providerChoice.startsWith("OpenRouter")) {
15197
+ aiProvider = "openrouter";
15198
+ } else {
15199
+ aiProvider = "copilot";
15200
+ }
14178
15201
  if (aiProvider === "ollama-cloud") {
14179
15202
  const apiKey = (await passwordPrompt("Enter your Ollama Cloud API key")).trim();
14180
15203
  if (!apiKey) {
@@ -14185,12 +15208,28 @@ var setup_default = defineCommand({
14185
15208
  try {
14186
15209
  await setOllamaCloudApiKey(apiKey);
14187
15210
  success("Stored Ollama Cloud API key in the local secrets store.");
14188
- info(`Secrets path: ${import_picocolors16.default.bold(getSecretsStorePath())}`);
15211
+ info(`Secrets path: ${import_picocolors17.default.bold(getSecretsStorePath())}`);
14189
15212
  } catch (err) {
14190
15213
  const message = err instanceof Error ? err.message : String(err);
14191
15214
  error(`Failed to store Ollama Cloud API key: ${message}`);
14192
15215
  process.exit(1);
14193
15216
  }
15217
+ } else if (aiProvider === "openrouter") {
15218
+ const apiKey = (await passwordPrompt("Enter your OpenRouter API key")).trim();
15219
+ if (!apiKey) {
15220
+ error("OpenRouter API key is required when OpenRouter is selected.");
15221
+ process.exit(1);
15222
+ }
15223
+ aiModel = await promptForOpenRouterModel(apiKey);
15224
+ try {
15225
+ await setOpenRouterApiKey(apiKey);
15226
+ success("Stored OpenRouter API key in the local secrets store.");
15227
+ info(`Secrets path: ${import_picocolors17.default.bold(getSecretsStorePath())}`);
15228
+ } catch (err) {
15229
+ const message = err instanceof Error ? err.message : String(err);
15230
+ error(`Failed to store OpenRouter API key: ${message}`);
15231
+ process.exit(1);
15232
+ }
14194
15233
  }
14195
15234
  }
14196
15235
  const showTips = await confirmPrompt("Show beginner quick guides and loading tips in command output?");
@@ -14247,15 +15286,15 @@ var setup_default = defineCommand({
14247
15286
  detectedRole = roleChoice;
14248
15287
  detectionSource = "user selection";
14249
15288
  } else {
14250
- info(`Detected role: ${import_picocolors16.default.bold(detectedRole)} (via ${detectionSource})`);
14251
- const confirmed = await confirmPrompt(`Role detected as ${import_picocolors16.default.bold(detectedRole)}. Is this correct?`);
15289
+ info(`Detected role: ${import_picocolors17.default.bold(detectedRole)} (via ${detectionSource})`);
15290
+ const confirmed = await confirmPrompt(`Role detected as ${import_picocolors17.default.bold(detectedRole)}. Is this correct?`);
14252
15291
  if (!confirmed) {
14253
15292
  const roleChoice = await selectPrompt("Select your role:", ["maintainer", "contributor"]);
14254
15293
  detectedRole = roleChoice;
14255
15294
  }
14256
15295
  }
14257
15296
  const defaultConfig = getDefaultConfig();
14258
- info(import_picocolors16.default.dim("Tip: press Enter to keep the default branch name shown in each prompt."));
15297
+ info(import_picocolors17.default.dim("Tip: press Enter to keep the default branch name shown in each prompt."));
14259
15298
  const mainBranchDefault = defaultConfig.mainBranch;
14260
15299
  const mainBranch = await inputPrompt(`Main branch name (default: ${mainBranchDefault} \u2014 press Enter to keep)`, mainBranchDefault);
14261
15300
  let devBranch;
@@ -14281,7 +15320,7 @@ var setup_default = defineCommand({
14281
15320
  error("Setup cannot continue without the upstream remote for contributors.");
14282
15321
  process.exit(1);
14283
15322
  }
14284
- success(`Added remote ${import_picocolors16.default.bold(upstreamRemote)} \u2192 ${upstreamUrl}`);
15323
+ success(`Added remote ${import_picocolors17.default.bold(upstreamRemote)} \u2192 ${upstreamUrl}`);
14285
15324
  } else {
14286
15325
  error("An upstream remote URL is required for contributors.");
14287
15326
  info("Add it manually: git remote add upstream <url>", "");
@@ -14304,67 +15343,67 @@ var setup_default = defineCommand({
14304
15343
  showTips
14305
15344
  };
14306
15345
  writeConfig(config);
14307
- success(`Config written to ${import_picocolors16.default.bold(getConfigLocationLabel())}`);
15346
+ success(`Config written to ${import_picocolors17.default.bold(getConfigLocationLabel())}`);
14308
15347
  info("This setup is stored locally for this clone and does not modify tracked files.", "");
14309
15348
  const syncRemote = config.role === "contributor" ? config.upstream : config.origin;
14310
- info(`Fetching ${import_picocolors16.default.bold(syncRemote)} to verify branch configuration...`, "");
15349
+ info(`Fetching ${import_picocolors17.default.bold(syncRemote)} to verify branch configuration...`, "");
14311
15350
  await fetchRemote(syncRemote);
14312
15351
  const mainRef = `${syncRemote}/${config.mainBranch}`;
14313
15352
  if (!await refExists(mainRef)) {
14314
- warn(`Main branch ref ${import_picocolors16.default.bold(mainRef)} not found on remote.`);
15353
+ warn(`Main branch ref ${import_picocolors17.default.bold(mainRef)} not found on remote.`);
14315
15354
  warn("Config was saved \u2014 verify the branch name and re-run setup if needed.");
14316
15355
  }
14317
15356
  if (config.devBranch) {
14318
15357
  const devRef = `${syncRemote}/${config.devBranch}`;
14319
15358
  if (!await refExists(devRef)) {
14320
- warn(`Dev branch ref ${import_picocolors16.default.bold(devRef)} not found on remote.`);
15359
+ warn(`Dev branch ref ${import_picocolors17.default.bold(devRef)} not found on remote.`);
14321
15360
  warn("Config was saved \u2014 verify the branch name and re-run setup if needed.");
14322
15361
  }
14323
15362
  }
14324
15363
  console.log();
14325
15364
  const resolvedAIConfig = resolveAIConfig(config);
14326
- info(`Workflow: ${import_picocolors16.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
14327
- info(`Convention: ${import_picocolors16.default.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
14328
- info(`AI: ${import_picocolors16.default.bold(isAIEnabled(config) ? "enabled" : "disabled")}`);
15365
+ info(`Workflow: ${import_picocolors17.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
15366
+ info(`Convention: ${import_picocolors17.default.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
15367
+ info(`AI: ${import_picocolors17.default.bold(isAIEnabled(config) ? "enabled" : "disabled")}`);
14329
15368
  if (isAIEnabled(config)) {
14330
- info(`AI provider: ${import_picocolors16.default.bold(resolvedAIConfig.providerLabel)}`);
15369
+ info(`AI provider: ${import_picocolors17.default.bold(resolvedAIConfig.providerLabel)}`);
14331
15370
  if (resolvedAIConfig.model) {
14332
- info(`AI model: ${import_picocolors16.default.bold(resolvedAIConfig.model)}`);
15371
+ info(`AI model: ${import_picocolors17.default.bold(resolvedAIConfig.model)}`);
14333
15372
  }
14334
15373
  }
14335
- info(`Guides: ${import_picocolors16.default.bold(shouldShowTips(config) ? "shown" : "hidden")}`);
14336
- info(`Role: ${import_picocolors16.default.bold(config.role)}`);
15374
+ info(`Guides: ${import_picocolors17.default.bold(shouldShowTips(config) ? "shown" : "hidden")}`);
15375
+ info(`Role: ${import_picocolors17.default.bold(config.role)}`);
14337
15376
  if (config.devBranch) {
14338
- info(`Main: ${import_picocolors16.default.bold(config.mainBranch)} | Dev: ${import_picocolors16.default.bold(config.devBranch)}`);
15377
+ info(`Main: ${import_picocolors17.default.bold(config.mainBranch)} | Dev: ${import_picocolors17.default.bold(config.devBranch)}`);
14339
15378
  } else {
14340
- info(`Main: ${import_picocolors16.default.bold(config.mainBranch)}`);
15379
+ info(`Main: ${import_picocolors17.default.bold(config.mainBranch)}`);
14341
15380
  }
14342
- info(`Origin: ${import_picocolors16.default.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${import_picocolors16.default.bold(config.upstream)}` : ""}`);
15381
+ info(`Origin: ${import_picocolors17.default.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${import_picocolors17.default.bold(config.upstream)}` : ""}`);
14343
15382
  }
14344
15383
  });
14345
15384
  function logConfigSummary(config) {
14346
15385
  const aiConfig = resolveAIConfig(config);
14347
- info(`Workflow: ${import_picocolors16.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
14348
- info(`Convention: ${import_picocolors16.default.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
14349
- info(`AI: ${import_picocolors16.default.bold(isAIEnabled(config) ? "enabled" : "disabled")}`);
15386
+ info(`Workflow: ${import_picocolors17.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
15387
+ info(`Convention: ${import_picocolors17.default.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
15388
+ info(`AI: ${import_picocolors17.default.bold(isAIEnabled(config) ? "enabled" : "disabled")}`);
14350
15389
  if (isAIEnabled(config)) {
14351
- info(`AI provider: ${import_picocolors16.default.bold(aiConfig.providerLabel)}`);
15390
+ info(`AI provider: ${import_picocolors17.default.bold(aiConfig.providerLabel)}`);
14352
15391
  if (aiConfig.model) {
14353
- info(`AI model: ${import_picocolors16.default.bold(aiConfig.model)}`);
15392
+ info(`AI model: ${import_picocolors17.default.bold(aiConfig.model)}`);
14354
15393
  }
14355
15394
  }
14356
- info(`Guides: ${import_picocolors16.default.bold(shouldShowTips(config) ? "shown" : "hidden")}`);
14357
- info(`Role: ${import_picocolors16.default.bold(config.role)}`);
15395
+ info(`Guides: ${import_picocolors17.default.bold(shouldShowTips(config) ? "shown" : "hidden")}`);
15396
+ info(`Role: ${import_picocolors17.default.bold(config.role)}`);
14358
15397
  if (config.devBranch) {
14359
- info(`Main: ${import_picocolors16.default.bold(config.mainBranch)} | Dev: ${import_picocolors16.default.bold(config.devBranch)}`);
15398
+ info(`Main: ${import_picocolors17.default.bold(config.mainBranch)} | Dev: ${import_picocolors17.default.bold(config.devBranch)}`);
14360
15399
  } else {
14361
- info(`Main: ${import_picocolors16.default.bold(config.mainBranch)}`);
15400
+ info(`Main: ${import_picocolors17.default.bold(config.mainBranch)}`);
14362
15401
  }
14363
- info(`Origin: ${import_picocolors16.default.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${import_picocolors16.default.bold(config.upstream)}` : ""}`);
15402
+ info(`Origin: ${import_picocolors17.default.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${import_picocolors17.default.bold(config.upstream)}` : ""}`);
14364
15403
  }
14365
15404
 
14366
15405
  // src/commands/start.ts
14367
- var import_picocolors17 = __toESM(require_picocolors(), 1);
15406
+ var import_picocolors18 = __toESM(require_picocolors(), 1);
14368
15407
  var start_default = defineCommand({
14369
15408
  meta: {
14370
15409
  name: "start",
@@ -14416,16 +15455,16 @@ var start_default = defineCommand({
14416
15455
  warn("Start cancelled.");
14417
15456
  process.exit(0);
14418
15457
  }
14419
- info(`Creating branch: ${import_picocolors17.default.bold(branchName)}`);
15458
+ info(`Creating branch: ${import_picocolors18.default.bold(branchName)}`);
14420
15459
  await fetchRemote(syncSource.remote);
14421
15460
  if (!await refExists(syncSource.ref)) {
14422
- warn(`Remote ref ${import_picocolors17.default.bold(syncSource.ref)} not found. Creating branch from local ${import_picocolors17.default.bold(baseBranch)}.`);
15461
+ warn(`Remote ref ${import_picocolors18.default.bold(syncSource.ref)} not found. Creating branch from local ${import_picocolors18.default.bold(baseBranch)}.`);
14423
15462
  }
14424
15463
  const currentBranch = await getCurrentBranch();
14425
15464
  if (currentBranch === baseBranch && await refExists(syncSource.ref)) {
14426
15465
  const ahead = await countCommitsAhead(baseBranch, syncSource.ref);
14427
15466
  if (ahead > 0) {
14428
- warn(`You are on ${import_picocolors17.default.bold(baseBranch)} with ${import_picocolors17.default.bold(String(ahead))} local commit${ahead > 1 ? "s" : ""} not in ${import_picocolors17.default.bold(syncSource.ref)}.`);
15467
+ warn(`You are on ${import_picocolors18.default.bold(baseBranch)} with ${import_picocolors18.default.bold(String(ahead))} local commit${ahead > 1 ? "s" : ""} not in ${import_picocolors18.default.bold(syncSource.ref)}.`);
14429
15468
  info(" Syncing will discard those commits. Consider backing them up first (e.g. create a branch).");
14430
15469
  const proceed = await confirmPrompt("Discard local commits and sync to remote?");
14431
15470
  if (!proceed) {
@@ -14442,10 +15481,10 @@ var start_default = defineCommand({
14442
15481
  error(`Failed to create branch: ${result2.stderr}`);
14443
15482
  process.exit(1);
14444
15483
  }
14445
- success(`Created ${import_picocolors17.default.bold(branchName)} from ${import_picocolors17.default.bold(syncSource.ref)}`);
15484
+ success(`Created ${import_picocolors18.default.bold(branchName)} from ${import_picocolors18.default.bold(syncSource.ref)}`);
14446
15485
  return;
14447
15486
  }
14448
- error(`Failed to update ${import_picocolors17.default.bold(baseBranch)}: ${updateResult.stderr}`);
15487
+ error(`Failed to update ${import_picocolors18.default.bold(baseBranch)}: ${updateResult.stderr}`);
14449
15488
  info("Make sure your base branch exists locally or the remote ref is available.", "");
14450
15489
  process.exit(1);
14451
15490
  }
@@ -14454,12 +15493,13 @@ var start_default = defineCommand({
14454
15493
  error(`Failed to create branch: ${result.stderr}`);
14455
15494
  process.exit(1);
14456
15495
  }
14457
- success(`Created ${import_picocolors17.default.bold(branchName)} from latest ${import_picocolors17.default.bold(baseBranch)}`);
15496
+ success(`Created ${import_picocolors18.default.bold(branchName)} from latest ${import_picocolors18.default.bold(baseBranch)}`);
14458
15497
  }
14459
15498
  });
14460
15499
 
14461
15500
  // src/commands/status.ts
14462
- var import_picocolors18 = __toESM(require_picocolors(), 1);
15501
+ var import_picocolors19 = __toESM(require_picocolors(), 1);
15502
+ init_gh();
14463
15503
  var status_default = defineCommand({
14464
15504
  meta: {
14465
15505
  name: "status",
@@ -14476,8 +15516,8 @@ var status_default = defineCommand({
14476
15516
  process.exit(1);
14477
15517
  }
14478
15518
  await projectHeading("status", "\uD83D\uDCCA");
14479
- console.log(` ${import_picocolors18.default.dim("Workflow:")} ${import_picocolors18.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
14480
- console.log(` ${import_picocolors18.default.dim("Role:")} ${import_picocolors18.default.bold(config.role)}`);
15519
+ console.log(` ${import_picocolors19.default.dim("Workflow:")} ${import_picocolors19.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
15520
+ console.log(` ${import_picocolors19.default.dim("Role:")} ${import_picocolors19.default.bold(config.role)}`);
14481
15521
  console.log();
14482
15522
  await fetchAll();
14483
15523
  const currentBranch = await getCurrentBranch();
@@ -14486,7 +15526,7 @@ var status_default = defineCommand({
14486
15526
  const isContributor = config.role === "contributor";
14487
15527
  const [dirty, fileStatus] = await Promise.all([hasUncommittedChanges(), getFileStatus()]);
14488
15528
  if (dirty) {
14489
- console.log(` ${import_picocolors18.default.yellow("\u26A0")} ${import_picocolors18.default.yellow("Uncommitted changes in working tree")}`);
15529
+ console.log(` ${import_picocolors19.default.yellow("\u26A0")} ${import_picocolors19.default.yellow("Uncommitted changes in working tree")}`);
14490
15530
  console.log();
14491
15531
  }
14492
15532
  const mainRemote = `${origin}/${mainBranch}`;
@@ -14505,16 +15545,16 @@ var status_default = defineCommand({
14505
15545
  if (isFeatureBranch) {
14506
15546
  const branchDiv = await getDivergence(currentBranch, baseBranch);
14507
15547
  const branchLine = formatStatus(currentBranch, baseBranch, branchDiv.ahead, branchDiv.behind);
14508
- console.log(branchLine + import_picocolors18.default.dim(` (current ${import_picocolors18.default.green("*")})`));
15548
+ console.log(branchLine + import_picocolors19.default.dim(` (current ${import_picocolors19.default.green("*")})`));
14509
15549
  branchStatus = await detectBranchStatus(currentBranch, baseBranch);
14510
15550
  if (branchStatus.merged) {
14511
- console.log(` ${import_picocolors18.default.green("\u2713")} ${import_picocolors18.default.green("Branch merged")} \u2014 ${import_picocolors18.default.dim(branchStatus.mergedReason ?? "all commits reachable from base")}`);
15551
+ console.log(` ${import_picocolors19.default.green("\u2713")} ${import_picocolors19.default.green("Branch merged")} \u2014 ${import_picocolors19.default.dim(branchStatus.mergedReason ?? "all commits reachable from base")}`);
14512
15552
  }
14513
15553
  if (branchStatus.stale) {
14514
- console.log(` ${import_picocolors18.default.yellow("\u23F3")} ${import_picocolors18.default.yellow("Branch is stale")} \u2014 ${import_picocolors18.default.dim(`last commit ${branchStatus.staleDaysAgo} days ago`)}`);
15554
+ console.log(` ${import_picocolors19.default.yellow("\u23F3")} ${import_picocolors19.default.yellow("Branch is stale")} \u2014 ${import_picocolors19.default.dim(`last commit ${branchStatus.staleDaysAgo} days ago`)}`);
14515
15555
  }
14516
15556
  } else if (currentBranch) {
14517
- console.log(import_picocolors18.default.dim(` (on ${import_picocolors18.default.bold(currentBranch)} branch)`));
15557
+ console.log(import_picocolors19.default.dim(` (on ${import_picocolors19.default.bold(currentBranch)} branch)`));
14518
15558
  }
14519
15559
  let branchesAligned = true;
14520
15560
  {
@@ -14551,20 +15591,20 @@ var status_default = defineCommand({
14551
15591
  }
14552
15592
  branchesAligned = groups.size === 1;
14553
15593
  console.log();
14554
- console.log(` ${import_picocolors18.default.bold("\uD83D\uDD17 Branch Alignment")}`);
15594
+ console.log(` ${import_picocolors19.default.bold("\uD83D\uDD17 Branch Alignment")}`);
14555
15595
  for (const [hash, names] of groups) {
14556
15596
  const short = hash.slice(0, 7);
14557
- const nameStr = names.map((n2) => import_picocolors18.default.bold(n2)).join(import_picocolors18.default.dim(" \xB7 "));
14558
- console.log(` ${import_picocolors18.default.yellow(short)} ${import_picocolors18.default.dim("\u2500\u2500")} ${nameStr}`);
15597
+ const nameStr = names.map((n2) => import_picocolors19.default.bold(n2)).join(import_picocolors19.default.dim(" \xB7 "));
15598
+ console.log(` ${import_picocolors19.default.yellow(short)} ${import_picocolors19.default.dim("\u2500\u2500")} ${nameStr}`);
14559
15599
  const subject = await getCommitSubject(hash);
14560
15600
  if (subject) {
14561
- console.log(` ${import_picocolors18.default.dim(subject)}`);
15601
+ console.log(` ${import_picocolors19.default.dim(subject)}`);
14562
15602
  }
14563
15603
  }
14564
15604
  if (branchesAligned) {
14565
- console.log(` ${import_picocolors18.default.green("\u2713")} ${import_picocolors18.default.green("All branches aligned")} ${import_picocolors18.default.dim("\u2014 ready to start")}`);
15605
+ console.log(` ${import_picocolors19.default.green("\u2713")} ${import_picocolors19.default.green("All branches aligned")} ${import_picocolors19.default.dim("\u2014 ready to start")}`);
14566
15606
  } else {
14567
- console.log(` ${import_picocolors18.default.yellow("\u26A0")} ${import_picocolors18.default.yellow("Branches are not fully aligned")}`);
15607
+ console.log(` ${import_picocolors19.default.yellow("\u26A0")} ${import_picocolors19.default.yellow("Branches are not fully aligned")}`);
14568
15608
  }
14569
15609
  }
14570
15610
  }
@@ -14572,41 +15612,41 @@ var status_default = defineCommand({
14572
15612
  if (hasFiles) {
14573
15613
  console.log();
14574
15614
  if (fileStatus.staged.length > 0) {
14575
- console.log(` ${import_picocolors18.default.green("Staged for commit:")}`);
15615
+ console.log(` ${import_picocolors19.default.green("Staged for commit:")}`);
14576
15616
  for (const { file, status } of fileStatus.staged) {
14577
- console.log(` ${import_picocolors18.default.green("+")} ${import_picocolors18.default.dim(`${status}:`)} ${file}`);
15617
+ console.log(` ${import_picocolors19.default.green("+")} ${import_picocolors19.default.dim(`${status}:`)} ${file}`);
14578
15618
  }
14579
15619
  }
14580
15620
  if (fileStatus.modified.length > 0) {
14581
- console.log(` ${import_picocolors18.default.yellow("Unstaged changes:")}`);
15621
+ console.log(` ${import_picocolors19.default.yellow("Unstaged changes:")}`);
14582
15622
  for (const { file, status } of fileStatus.modified) {
14583
- console.log(` ${import_picocolors18.default.yellow("~")} ${import_picocolors18.default.dim(`${status}:`)} ${file}`);
15623
+ console.log(` ${import_picocolors19.default.yellow("~")} ${import_picocolors19.default.dim(`${status}:`)} ${file}`);
14584
15624
  }
14585
15625
  }
14586
15626
  if (fileStatus.untracked.length > 0) {
14587
- console.log(` ${import_picocolors18.default.red("Untracked files:")}`);
15627
+ console.log(` ${import_picocolors19.default.red("Untracked files:")}`);
14588
15628
  for (const file of fileStatus.untracked) {
14589
- console.log(` ${import_picocolors18.default.red("?")} ${file}`);
15629
+ console.log(` ${import_picocolors19.default.red("?")} ${file}`);
14590
15630
  }
14591
15631
  }
14592
15632
  } else if (!dirty) {
14593
- console.log(` ${import_picocolors18.default.green("\u2713")} ${import_picocolors18.default.dim("Working tree clean")}`);
15633
+ console.log(` ${import_picocolors19.default.green("\u2713")} ${import_picocolors19.default.dim("Working tree clean")}`);
14594
15634
  }
14595
15635
  console.log();
14596
15636
  }
14597
15637
  });
14598
15638
  function formatStatus(branch, base, ahead, behind) {
14599
- const label = import_picocolors18.default.bold(branch.padEnd(20));
15639
+ const label = import_picocolors19.default.bold(branch.padEnd(20));
14600
15640
  if (ahead === 0 && behind === 0) {
14601
- return ` ${import_picocolors18.default.green("\u2713")} ${label} ${import_picocolors18.default.dim(`in sync with ${base}`)}`;
15641
+ return ` ${import_picocolors19.default.green("\u2713")} ${label} ${import_picocolors19.default.dim(`in sync with ${base}`)}`;
14602
15642
  }
14603
15643
  if (ahead > 0 && behind === 0) {
14604
- return ` ${import_picocolors18.default.yellow("\u2191")} ${label} ${import_picocolors18.default.yellow(`${ahead} commit${ahead !== 1 ? "s" : ""} ahead of ${base}`)}`;
15644
+ return ` ${import_picocolors19.default.yellow("\u2191")} ${label} ${import_picocolors19.default.yellow(`${ahead} commit${ahead !== 1 ? "s" : ""} ahead of ${base}`)}`;
14605
15645
  }
14606
15646
  if (behind > 0 && ahead === 0) {
14607
- return ` ${import_picocolors18.default.red("\u2193")} ${label} ${import_picocolors18.default.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
15647
+ return ` ${import_picocolors19.default.red("\u2193")} ${label} ${import_picocolors19.default.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
14608
15648
  }
14609
- return ` ${import_picocolors18.default.red("\u26A1")} ${label} ${import_picocolors18.default.yellow(`${ahead} ahead`)}${import_picocolors18.default.dim(", ")}${import_picocolors18.default.red(`${behind} behind`)} ${import_picocolors18.default.dim(base)}`;
15649
+ return ` ${import_picocolors19.default.red("\u26A1")} ${label} ${import_picocolors19.default.yellow(`${ahead} ahead`)}${import_picocolors19.default.dim(", ")}${import_picocolors19.default.red(`${behind} behind`)} ${import_picocolors19.default.dim(base)}`;
14610
15650
  }
14611
15651
  var STALE_THRESHOLD_DAYS = 14;
14612
15652
  async function detectBranchStatus(branch, baseBranch) {
@@ -14657,15 +15697,16 @@ async function detectBranchStatus(branch, baseBranch) {
14657
15697
  }
14658
15698
 
14659
15699
  // src/commands/submit.ts
14660
- var import_picocolors19 = __toESM(require_picocolors(), 1);
15700
+ var import_picocolors20 = __toESM(require_picocolors(), 1);
15701
+ init_gh();
14661
15702
  async function performSquashMerge(origin, baseBranch, featureBranch, options) {
14662
- info(`Checking out ${import_picocolors19.default.bold(baseBranch)}...`);
15703
+ info(`Checking out ${import_picocolors20.default.bold(baseBranch)}...`);
14663
15704
  const coResult = await checkoutBranch(baseBranch);
14664
15705
  if (coResult.exitCode !== 0) {
14665
15706
  error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
14666
15707
  process.exit(1);
14667
15708
  }
14668
- info(`Squash merging ${import_picocolors19.default.bold(featureBranch)} into ${import_picocolors19.default.bold(baseBranch)}...`);
15709
+ info(`Squash merging ${import_picocolors20.default.bold(featureBranch)} into ${import_picocolors20.default.bold(baseBranch)}...`);
14669
15710
  const mergeResult = await mergeSquash(featureBranch);
14670
15711
  if (mergeResult.exitCode !== 0) {
14671
15712
  error(`Squash merge failed: ${mergeResult.stderr}`);
@@ -14685,7 +15726,7 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
14685
15726
  message = aiMsg;
14686
15727
  spinner.success("AI commit message generated.");
14687
15728
  console.log(`
14688
- ${import_picocolors19.default.dim("AI suggestion:")} ${import_picocolors19.default.bold(import_picocolors19.default.cyan(message))}`);
15729
+ ${import_picocolors20.default.dim("AI suggestion:")} ${import_picocolors20.default.bold(import_picocolors20.default.cyan(message))}`);
14689
15730
  break;
14690
15731
  }
14691
15732
  spinner.fail("AI did not return a commit message.");
@@ -14725,7 +15766,7 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
14725
15766
  message = regen;
14726
15767
  spinner.success("Commit message regenerated.");
14727
15768
  console.log(`
14728
- ${import_picocolors19.default.dim("AI suggestion:")} ${import_picocolors19.default.bold(import_picocolors19.default.cyan(regen))}`);
15769
+ ${import_picocolors20.default.dim("AI suggestion:")} ${import_picocolors20.default.bold(import_picocolors20.default.cyan(regen))}`);
14729
15770
  } else {
14730
15771
  spinner.fail("Regeneration failed.");
14731
15772
  }
@@ -14741,13 +15782,13 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
14741
15782
  error(`Commit failed: ${commitResult.stderr}`);
14742
15783
  process.exit(1);
14743
15784
  }
14744
- info(`Pushing ${import_picocolors19.default.bold(baseBranch)} to ${origin}...`);
15785
+ info(`Pushing ${import_picocolors20.default.bold(baseBranch)} to ${origin}...`);
14745
15786
  const pushResult = await pushBranch(origin, baseBranch);
14746
15787
  if (pushResult.exitCode !== 0) {
14747
15788
  error(`Failed to push ${baseBranch}: ${pushResult.stderr}`);
14748
15789
  process.exit(1);
14749
15790
  }
14750
- info(`Deleting local branch ${import_picocolors19.default.bold(featureBranch)}...`);
15791
+ info(`Deleting local branch ${import_picocolors20.default.bold(featureBranch)}...`);
14751
15792
  const delLocal = await forceDeleteBranch(featureBranch);
14752
15793
  if (delLocal.exitCode !== 0) {
14753
15794
  warn(`Could not delete local branch: ${delLocal.stderr.trim()}`);
@@ -14755,14 +15796,14 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
14755
15796
  const remoteBranchRef = `${origin}/${featureBranch}`;
14756
15797
  const remoteExists = await branchExists(remoteBranchRef);
14757
15798
  if (remoteExists) {
14758
- info(`Deleting remote branch ${import_picocolors19.default.bold(featureBranch)}...`);
15799
+ info(`Deleting remote branch ${import_picocolors20.default.bold(featureBranch)}...`);
14759
15800
  const delRemote = await deleteRemoteBranch(origin, featureBranch);
14760
15801
  if (delRemote.exitCode !== 0) {
14761
15802
  warn(`Could not delete remote branch: ${delRemote.stderr.trim()}`);
14762
15803
  }
14763
15804
  }
14764
- success(`Squash merged ${import_picocolors19.default.bold(featureBranch)} into ${import_picocolors19.default.bold(baseBranch)} and pushed.`);
14765
- info(`Run ${import_picocolors19.default.bold("cn start")} to begin a new feature.`, "");
15805
+ success(`Squash merged ${import_picocolors20.default.bold(featureBranch)} into ${import_picocolors20.default.bold(baseBranch)} and pushed.`);
15806
+ info(`Run ${import_picocolors20.default.bold("cn start")} to begin a new feature.`, "");
14766
15807
  }
14767
15808
  var submit_default = defineCommand({
14768
15809
  meta: {
@@ -14819,7 +15860,7 @@ var submit_default = defineCommand({
14819
15860
  }
14820
15861
  if (protectedBranches.includes(currentBranch)) {
14821
15862
  await projectHeading("submit", "\uD83D\uDE80");
14822
- warn(`You're on ${import_picocolors19.default.bold(currentBranch)}, which is a protected branch. PRs should come from feature branches.`);
15863
+ warn(`You're on ${import_picocolors20.default.bold(currentBranch)}, which is a protected branch. PRs should come from feature branches.`);
14823
15864
  await fetchAll();
14824
15865
  const remoteRef = `${origin}/${currentBranch}`;
14825
15866
  const localWork = await hasLocalWork(origin, currentBranch);
@@ -14828,11 +15869,11 @@ var submit_default = defineCommand({
14828
15869
  const hasAnything = hasCommits || dirty;
14829
15870
  if (!hasAnything) {
14830
15871
  error("No local changes or commits to move. Switch to a feature branch first.");
14831
- info(` Run ${import_picocolors19.default.bold("cn start")} to create a new feature branch.`, "");
15872
+ info(` Run ${import_picocolors20.default.bold("cn start")} to create a new feature branch.`, "");
14832
15873
  process.exit(1);
14833
15874
  }
14834
15875
  if (hasCommits) {
14835
- info(`Found ${import_picocolors19.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${import_picocolors19.default.bold(currentBranch)}.`);
15876
+ info(`Found ${import_picocolors20.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${import_picocolors20.default.bold(currentBranch)}.`);
14836
15877
  }
14837
15878
  if (dirty) {
14838
15879
  info("You also have uncommitted changes in the working tree.");
@@ -14862,12 +15903,12 @@ var submit_default = defineCommand({
14862
15903
  error(`Failed to create branch: ${branchResult.stderr}`);
14863
15904
  process.exit(1);
14864
15905
  }
14865
- success(`Created ${import_picocolors19.default.bold(newBranchName)} with your changes.`);
15906
+ success(`Created ${import_picocolors20.default.bold(newBranchName)} with your changes.`);
14866
15907
  await updateLocalBranch(currentBranch, remoteRef);
14867
- info(`Reset ${import_picocolors19.default.bold(currentBranch)} back to ${import_picocolors19.default.bold(remoteRef)} \u2014 no damage done.`, "");
15908
+ info(`Reset ${import_picocolors20.default.bold(currentBranch)} back to ${import_picocolors20.default.bold(remoteRef)} \u2014 no damage done.`, "");
14868
15909
  console.log();
14869
- success(`You're now on ${import_picocolors19.default.bold(newBranchName)} with all your work intact.`);
14870
- info(`Run ${import_picocolors19.default.bold("cn submit")} again to push and create your PR.`, "");
15910
+ success(`You're now on ${import_picocolors20.default.bold(newBranchName)} with all your work intact.`);
15911
+ info(`Run ${import_picocolors20.default.bold("cn submit")} again to push and create your PR.`, "");
14871
15912
  return;
14872
15913
  }
14873
15914
  await projectHeading("submit", "\uD83D\uDE80");
@@ -14876,7 +15917,7 @@ var submit_default = defineCommand({
14876
15917
  if (ghInstalled && ghAuthed) {
14877
15918
  const mergedPR = await getMergedPRForBranch(currentBranch);
14878
15919
  if (mergedPR) {
14879
- warn(`PR #${mergedPR.number} (${import_picocolors19.default.bold(mergedPR.title)}) was already merged.`);
15920
+ warn(`PR #${mergedPR.number} (${import_picocolors20.default.bold(mergedPR.title)}) was already merged.`);
14880
15921
  const localWork = await hasLocalWork(origin, currentBranch);
14881
15922
  const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
14882
15923
  if (hasWork) {
@@ -14884,7 +15925,7 @@ var submit_default = defineCommand({
14884
15925
  warn("You have uncommitted changes in your working tree.");
14885
15926
  }
14886
15927
  if (localWork.unpushedCommits > 0) {
14887
- warn(`You have ${import_picocolors19.default.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not in the merged PR.`);
15928
+ warn(`You have ${import_picocolors20.default.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not in the merged PR.`);
14888
15929
  }
14889
15930
  const SAVE_NEW_BRANCH = "Save changes to a new branch";
14890
15931
  const DISCARD = "Discard all changes and clean up";
@@ -14911,10 +15952,10 @@ var submit_default = defineCommand({
14911
15952
  error(`Failed to rename branch: ${renameResult.stderr}`);
14912
15953
  process.exit(1);
14913
15954
  }
14914
- success(`Renamed ${import_picocolors19.default.bold(currentBranch)} \u2192 ${import_picocolors19.default.bold(newBranchName)}`);
15955
+ success(`Renamed ${import_picocolors20.default.bold(currentBranch)} \u2192 ${import_picocolors20.default.bold(newBranchName)}`);
14915
15956
  await unsetUpstream();
14916
15957
  const syncSource2 = getSyncSource(config);
14917
- info(`Syncing ${import_picocolors19.default.bold(newBranchName)} with latest ${import_picocolors19.default.bold(baseBranch)}...`);
15958
+ info(`Syncing ${import_picocolors20.default.bold(newBranchName)} with latest ${import_picocolors20.default.bold(baseBranch)}...`);
14918
15959
  await fetchRemote(syncSource2.remote);
14919
15960
  let rebaseResult;
14920
15961
  if (staleUpstreamHash) {
@@ -14925,17 +15966,17 @@ var submit_default = defineCommand({
14925
15966
  }
14926
15967
  if (rebaseResult.exitCode !== 0) {
14927
15968
  warn("Rebase encountered conflicts. Resolve them manually, then run:");
14928
- info(` ${import_picocolors19.default.bold("git rebase --continue")}`, "");
15969
+ info(` ${import_picocolors20.default.bold("git rebase --continue")}`, "");
14929
15970
  } else {
14930
- success(`Rebased ${import_picocolors19.default.bold(newBranchName)} onto ${import_picocolors19.default.bold(syncSource2.ref)}.`);
15971
+ success(`Rebased ${import_picocolors20.default.bold(newBranchName)} onto ${import_picocolors20.default.bold(syncSource2.ref)}.`);
14931
15972
  }
14932
- info(`All your changes are preserved. Run ${import_picocolors19.default.bold("cn submit")} when ready to create a new PR.`, "");
15973
+ info(`All your changes are preserved. Run ${import_picocolors20.default.bold("cn submit")} when ready to create a new PR.`, "");
14933
15974
  return;
14934
15975
  }
14935
15976
  warn("Discarding local changes...");
14936
15977
  }
14937
15978
  const syncSource = getSyncSource(config);
14938
- info(`Switching to ${import_picocolors19.default.bold(baseBranch)} and syncing...`);
15979
+ info(`Switching to ${import_picocolors20.default.bold(baseBranch)} and syncing...`);
14939
15980
  await fetchRemote(syncSource.remote);
14940
15981
  await resetHard("HEAD");
14941
15982
  const coResult = await checkoutBranch(baseBranch);
@@ -14944,23 +15985,23 @@ var submit_default = defineCommand({
14944
15985
  process.exit(1);
14945
15986
  }
14946
15987
  await updateLocalBranch(baseBranch, syncSource.ref);
14947
- success(`Synced ${import_picocolors19.default.bold(baseBranch)} with ${import_picocolors19.default.bold(syncSource.ref)}.`);
14948
- info(`Deleting stale branch ${import_picocolors19.default.bold(currentBranch)}...`);
15988
+ success(`Synced ${import_picocolors20.default.bold(baseBranch)} with ${import_picocolors20.default.bold(syncSource.ref)}.`);
15989
+ info(`Deleting stale branch ${import_picocolors20.default.bold(currentBranch)}...`);
14949
15990
  const delResult = await forceDeleteBranch(currentBranch);
14950
15991
  if (delResult.exitCode === 0) {
14951
- success(`Deleted ${import_picocolors19.default.bold(currentBranch)}.`);
15992
+ success(`Deleted ${import_picocolors20.default.bold(currentBranch)}.`);
14952
15993
  } else {
14953
15994
  warn(`Could not delete branch: ${delResult.stderr.trim()}`);
14954
15995
  }
14955
15996
  console.log();
14956
- info(`You're now on ${import_picocolors19.default.bold(baseBranch)}. Run ${import_picocolors19.default.bold("cn start")} to begin a new feature.`);
15997
+ info(`You're now on ${import_picocolors20.default.bold(baseBranch)}. Run ${import_picocolors20.default.bold("cn start")} to begin a new feature.`);
14957
15998
  return;
14958
15999
  }
14959
16000
  }
14960
16001
  if (ghInstalled && ghAuthed) {
14961
16002
  const existingPR = await getPRForBranch(currentBranch);
14962
16003
  if (existingPR) {
14963
- info(`Pushing ${import_picocolors19.default.bold(currentBranch)} to ${origin}...`);
16004
+ info(`Pushing ${import_picocolors20.default.bold(currentBranch)} to ${origin}...`);
14964
16005
  const pushResult2 = await pushSetUpstream(origin, currentBranch);
14965
16006
  if (pushResult2.exitCode !== 0) {
14966
16007
  error(`Failed to push: ${pushResult2.stderr}`);
@@ -14971,8 +16012,8 @@ var submit_default = defineCommand({
14971
16012
  }
14972
16013
  process.exit(1);
14973
16014
  }
14974
- success(`Pushed changes to existing PR #${existingPR.number}: ${import_picocolors19.default.bold(existingPR.title)}`);
14975
- console.log(` ${import_picocolors19.default.cyan(existingPR.url)}`);
16015
+ success(`Pushed changes to existing PR #${existingPR.number}: ${import_picocolors20.default.bold(existingPR.title)}`);
16016
+ console.log(` ${import_picocolors20.default.cyan(existingPR.url)}`);
14976
16017
  return;
14977
16018
  }
14978
16019
  }
@@ -14994,10 +16035,10 @@ var submit_default = defineCommand({
14994
16035
  prBody = result.body;
14995
16036
  spinner.success("PR description generated.");
14996
16037
  console.log(`
14997
- ${import_picocolors19.default.dim("AI title:")} ${import_picocolors19.default.bold(import_picocolors19.default.cyan(prTitle))}`);
16038
+ ${import_picocolors20.default.dim("AI title:")} ${import_picocolors20.default.bold(import_picocolors20.default.cyan(prTitle))}`);
14998
16039
  console.log(`
14999
- ${import_picocolors19.default.dim("AI body preview:")}`);
15000
- console.log(import_picocolors19.default.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
16040
+ ${import_picocolors20.default.dim("AI body preview:")}`);
16041
+ console.log(import_picocolors20.default.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
15001
16042
  } else {
15002
16043
  spinner.fail("AI did not return a PR description.");
15003
16044
  }
@@ -15108,7 +16149,7 @@ ${import_picocolors19.default.dim("AI body preview:")}`);
15108
16149
  warn("Submit cancelled.");
15109
16150
  return;
15110
16151
  }
15111
- info(`Pushing ${import_picocolors19.default.bold(currentBranch)} to ${origin}...`);
16152
+ info(`Pushing ${import_picocolors20.default.bold(currentBranch)} to ${origin}...`);
15112
16153
  const pushResult = await pushSetUpstream(origin, currentBranch);
15113
16154
  if (pushResult.exitCode !== 0) {
15114
16155
  error(`Failed to push: ${pushResult.stderr}`);
@@ -15127,7 +16168,7 @@ ${import_picocolors19.default.dim("AI body preview:")}`);
15127
16168
  const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/compare/${baseBranch}...${currentBranch}?expand=1`;
15128
16169
  console.log();
15129
16170
  info("Create your PR manually:", "");
15130
- console.log(` ${import_picocolors19.default.cyan(prUrl)}`);
16171
+ console.log(` ${import_picocolors20.default.cyan(prUrl)}`);
15131
16172
  } else {
15132
16173
  info("gh CLI not available. Create your PR manually on GitHub.", "");
15133
16174
  }
@@ -15161,7 +16202,7 @@ ${import_picocolors19.default.dim("AI body preview:")}`);
15161
16202
  });
15162
16203
 
15163
16204
  // src/commands/switch.ts
15164
- var import_picocolors20 = __toESM(require_picocolors(), 1);
16205
+ var import_picocolors21 = __toESM(require_picocolors(), 1);
15165
16206
  var switch_default = defineCommand({
15166
16207
  meta: {
15167
16208
  name: "switch",
@@ -15193,11 +16234,11 @@ var switch_default = defineCommand({
15193
16234
  const choices = localBranches.filter((b2) => b2.name !== currentBranch).map((b2) => {
15194
16235
  const labels = [];
15195
16236
  if (protectedBranches.includes(b2.name))
15196
- labels.push(import_picocolors20.default.red("protected"));
16237
+ labels.push(import_picocolors21.default.red("protected"));
15197
16238
  if (b2.upstream)
15198
- labels.push(import_picocolors20.default.dim(`\u2192 ${b2.upstream}`));
16239
+ labels.push(import_picocolors21.default.dim(`\u2192 ${b2.upstream}`));
15199
16240
  if (b2.gone)
15200
- labels.push(import_picocolors20.default.red("remote gone"));
16241
+ labels.push(import_picocolors21.default.red("remote gone"));
15201
16242
  const suffix = labels.length > 0 ? ` ${labels.join(" \xB7 ")}` : "";
15202
16243
  return `${b2.name}${suffix}`;
15203
16244
  });
@@ -15209,7 +16250,7 @@ var switch_default = defineCommand({
15209
16250
  targetBranch = selected.split(/\s{2,}/)[0].trim();
15210
16251
  }
15211
16252
  if (targetBranch === currentBranch) {
15212
- info(`Already on ${import_picocolors20.default.bold(targetBranch)}.`);
16253
+ info(`Already on ${import_picocolors21.default.bold(targetBranch)}.`);
15213
16254
  return;
15214
16255
  }
15215
16256
  if (await hasUncommittedChanges()) {
@@ -15228,7 +16269,7 @@ var switch_default = defineCommand({
15228
16269
  const stashMsg = `contrib-save: auto-save from ${currentBranch}`;
15229
16270
  try {
15230
16271
  await exec("git", ["stash", "push", "-m", stashMsg]);
15231
- info(`Saved changes: ${import_picocolors20.default.dim(stashMsg)}`);
16272
+ info(`Saved changes: ${import_picocolors21.default.dim(stashMsg)}`);
15232
16273
  } catch {
15233
16274
  error("Failed to save changes. Please commit or save manually.");
15234
16275
  process.exit(1);
@@ -15244,9 +16285,9 @@ var switch_default = defineCommand({
15244
16285
  }
15245
16286
  process.exit(1);
15246
16287
  }
15247
- success(`Switched to ${import_picocolors20.default.bold(targetBranch)}`);
15248
- info(`Your changes from ${import_picocolors20.default.bold(currentBranch ?? "previous branch")} are saved.`, "");
15249
- info(`Use ${import_picocolors20.default.bold("cn save --restore")} to bring them back.`, "");
16288
+ success(`Switched to ${import_picocolors21.default.bold(targetBranch)}`);
16289
+ info(`Your changes from ${import_picocolors21.default.bold(currentBranch ?? "previous branch")} are saved.`, "");
16290
+ info(`Use ${import_picocolors21.default.bold("cn save --restore")} to bring them back.`, "");
15250
16291
  return;
15251
16292
  }
15252
16293
  const result = await checkoutBranch(targetBranch);
@@ -15254,12 +16295,12 @@ var switch_default = defineCommand({
15254
16295
  error(`Failed to switch to ${targetBranch}: ${result.stderr}`);
15255
16296
  process.exit(1);
15256
16297
  }
15257
- success(`Switched to ${import_picocolors20.default.bold(targetBranch)}`);
16298
+ success(`Switched to ${import_picocolors21.default.bold(targetBranch)}`);
15258
16299
  }
15259
16300
  });
15260
16301
 
15261
16302
  // src/commands/sync.ts
15262
- var import_picocolors21 = __toESM(require_picocolors(), 1);
16303
+ var import_picocolors22 = __toESM(require_picocolors(), 1);
15263
16304
  var sync_default = defineCommand({
15264
16305
  meta: {
15265
16306
  name: "sync",
@@ -15311,24 +16352,24 @@ var sync_default = defineCommand({
15311
16352
  await fetchRemote(origin);
15312
16353
  }
15313
16354
  if (!await refExists(syncSource.ref)) {
15314
- error(`Remote ref ${import_picocolors21.default.bold(syncSource.ref)} does not exist.`);
16355
+ error(`Remote ref ${import_picocolors22.default.bold(syncSource.ref)} does not exist.`);
15315
16356
  info("This can happen if the branch was renamed or deleted on the remote.", "");
15316
- info(`Check your config: the base branch may need updating via ${import_picocolors21.default.bold("cn setup")}.`, "");
16357
+ info(`Check your config: the base branch may need updating via ${import_picocolors22.default.bold("cn setup")}.`, "");
15317
16358
  process.exit(1);
15318
16359
  }
15319
16360
  let allowMergeCommit = false;
15320
16361
  const div = await getDivergence(baseBranch, syncSource.ref);
15321
16362
  if (div.ahead > 0 || div.behind > 0) {
15322
- info(`${import_picocolors21.default.bold(baseBranch)} is ${import_picocolors21.default.yellow(`${div.ahead} ahead`)} and ${import_picocolors21.default.red(`${div.behind} behind`)} ${syncSource.ref}`);
16363
+ info(`${import_picocolors22.default.bold(baseBranch)} is ${import_picocolors22.default.yellow(`${div.ahead} ahead`)} and ${import_picocolors22.default.red(`${div.behind} behind`)} ${syncSource.ref}`);
15323
16364
  } else {
15324
- info(`${import_picocolors21.default.bold(baseBranch)} is already in sync with ${syncSource.ref}`);
16365
+ info(`${import_picocolors22.default.bold(baseBranch)} is already in sync with ${syncSource.ref}`);
15325
16366
  }
15326
16367
  if (div.ahead > 0) {
15327
16368
  const currentBranch = await getCurrentBranch();
15328
16369
  const protectedBranches = getProtectedBranches(config);
15329
16370
  const isOnProtected = currentBranch && protectedBranches.includes(currentBranch);
15330
16371
  if (isOnProtected) {
15331
- warn(`You have ${import_picocolors21.default.bold(String(div.ahead))} local commit${div.ahead !== 1 ? "s" : ""} on ${import_picocolors21.default.bold(baseBranch)} that aren't on the remote.`);
16372
+ warn(`You have ${import_picocolors22.default.bold(String(div.ahead))} local commit${div.ahead !== 1 ? "s" : ""} on ${import_picocolors22.default.bold(baseBranch)} that aren't on the remote.`);
15332
16373
  info("Pulling now could create a merge commit, which breaks clean history.");
15333
16374
  console.log();
15334
16375
  const MOVE_BRANCH = "Move my commits to a new feature branch, then sync";
@@ -15358,7 +16399,7 @@ var sync_default = defineCommand({
15358
16399
  error(`Failed to create branch: ${branchResult.stderr}`);
15359
16400
  process.exit(1);
15360
16401
  }
15361
- success(`Created ${import_picocolors21.default.bold(newBranchName)} with your commits.`);
16402
+ success(`Created ${import_picocolors22.default.bold(newBranchName)} with your commits.`);
15362
16403
  const coResult2 = await checkoutBranch(baseBranch);
15363
16404
  if (coResult2.exitCode !== 0) {
15364
16405
  error(`Failed to checkout ${baseBranch}: ${coResult2.stderr}`);
@@ -15366,11 +16407,11 @@ var sync_default = defineCommand({
15366
16407
  }
15367
16408
  const remoteRef = syncSource.ref;
15368
16409
  await updateLocalBranch(baseBranch, remoteRef);
15369
- success(`Reset ${import_picocolors21.default.bold(baseBranch)} to ${import_picocolors21.default.bold(remoteRef)}.`);
15370
- success(`${import_picocolors21.default.bold(baseBranch)} is now in sync with ${syncSource.ref}`);
16410
+ success(`Reset ${import_picocolors22.default.bold(baseBranch)} to ${import_picocolors22.default.bold(remoteRef)}.`);
16411
+ success(`${import_picocolors22.default.bold(baseBranch)} is now in sync with ${syncSource.ref}`);
15371
16412
  console.log();
15372
- info(`Your commits are safe on ${import_picocolors21.default.bold(newBranchName)}.`, "");
15373
- info(`Run ${import_picocolors21.default.bold(`git checkout ${newBranchName}`)} then ${import_picocolors21.default.bold("cn update")} to rebase onto the synced ${import_picocolors21.default.bold(baseBranch)}.`, "");
16413
+ info(`Your commits are safe on ${import_picocolors22.default.bold(newBranchName)}.`, "");
16414
+ info(`Run ${import_picocolors22.default.bold(`git checkout ${newBranchName}`)} then ${import_picocolors22.default.bold("cn update")} to rebase onto the synced ${import_picocolors22.default.bold(baseBranch)}.`, "");
15374
16415
  return;
15375
16416
  }
15376
16417
  allowMergeCommit = true;
@@ -15378,7 +16419,7 @@ var sync_default = defineCommand({
15378
16419
  }
15379
16420
  }
15380
16421
  if (!args.yes) {
15381
- const ok = await confirmPrompt(`This will pull ${import_picocolors21.default.bold(syncSource.ref)} into local ${import_picocolors21.default.bold(baseBranch)}.`);
16422
+ const ok = await confirmPrompt(`This will pull ${import_picocolors22.default.bold(syncSource.ref)} into local ${import_picocolors22.default.bold(baseBranch)}.`);
15382
16423
  if (!ok)
15383
16424
  process.exit(0);
15384
16425
  }
@@ -15392,8 +16433,8 @@ var sync_default = defineCommand({
15392
16433
  if (allowMergeCommit) {
15393
16434
  error(`Pull failed: ${pullResult.stderr.trim()}`);
15394
16435
  } else {
15395
- error(`Fast-forward pull failed. Your local ${import_picocolors21.default.bold(baseBranch)} may have diverged.`);
15396
- info(`Use ${import_picocolors21.default.bold("cn sync")} again and choose "Move my commits to a new feature branch" to fix this.`, "");
16436
+ error(`Fast-forward pull failed. Your local ${import_picocolors22.default.bold(baseBranch)} may have diverged.`);
16437
+ info(`Use ${import_picocolors22.default.bold("cn sync")} again and choose "Move my commits to a new feature branch" to fix this.`, "");
15397
16438
  }
15398
16439
  process.exit(1);
15399
16440
  }
@@ -15401,7 +16442,7 @@ var sync_default = defineCommand({
15401
16442
  if (hasDevBranch(workflow) && role === "maintainer") {
15402
16443
  const mainDiv = await getDivergence(config.mainBranch, `${origin}/${config.mainBranch}`);
15403
16444
  if (mainDiv.behind > 0) {
15404
- info(`Also syncing ${import_picocolors21.default.bold(config.mainBranch)}...`);
16445
+ info(`Also syncing ${import_picocolors22.default.bold(config.mainBranch)}...`);
15405
16446
  const mainCoResult = await checkoutBranch(config.mainBranch);
15406
16447
  if (mainCoResult.exitCode === 0) {
15407
16448
  const mainPullResult = await pullFastForwardOnly(origin, config.mainBranch);
@@ -15442,20 +16483,20 @@ var sync_default = defineCommand({
15442
16483
  }
15443
16484
  }
15444
16485
  console.log();
15445
- console.log(` ${import_picocolors21.default.bold("\uD83D\uDD17 Branch Alignment")}`);
16486
+ console.log(` ${import_picocolors22.default.bold("\uD83D\uDD17 Branch Alignment")}`);
15446
16487
  for (const [hash, names] of groups) {
15447
16488
  const short = hash.slice(0, 7);
15448
- const nameStr = names.map((n2) => import_picocolors21.default.bold(n2)).join(import_picocolors21.default.dim(" \xB7 "));
15449
- console.log(` ${import_picocolors21.default.yellow(short)} ${import_picocolors21.default.dim("\u2500\u2500")} ${nameStr}`);
16489
+ const nameStr = names.map((n2) => import_picocolors22.default.bold(n2)).join(import_picocolors22.default.dim(" \xB7 "));
16490
+ console.log(` ${import_picocolors22.default.yellow(short)} ${import_picocolors22.default.dim("\u2500\u2500")} ${nameStr}`);
15450
16491
  const subject = await getCommitSubject(hash);
15451
16492
  if (subject) {
15452
- console.log(` ${import_picocolors21.default.dim(subject)}`);
16493
+ console.log(` ${import_picocolors22.default.dim(subject)}`);
15453
16494
  }
15454
16495
  }
15455
16496
  if (groups.size === 1) {
15456
- console.log(` ${import_picocolors21.default.green("\u2713")} ${import_picocolors21.default.green("All branches aligned")} ${import_picocolors21.default.dim("\u2014 ready to start")}`);
16497
+ console.log(` ${import_picocolors22.default.green("\u2713")} ${import_picocolors22.default.green("All branches aligned")} ${import_picocolors22.default.dim("\u2014 ready to start")}`);
15457
16498
  } else {
15458
- console.log(` ${import_picocolors21.default.yellow("\u26A0")} ${import_picocolors21.default.yellow("Branches are not fully aligned")}`);
16499
+ console.log(` ${import_picocolors22.default.yellow("\u26A0")} ${import_picocolors22.default.yellow("Branches are not fully aligned")}`);
15459
16500
  }
15460
16501
  }
15461
16502
  }
@@ -15463,8 +16504,9 @@ var sync_default = defineCommand({
15463
16504
  });
15464
16505
 
15465
16506
  // src/commands/update.ts
15466
- import { readFileSync as readFileSync6 } from "fs";
15467
- var import_picocolors22 = __toESM(require_picocolors(), 1);
16507
+ import { readFileSync as readFileSync7 } from "fs";
16508
+ var import_picocolors23 = __toESM(require_picocolors(), 1);
16509
+ init_gh();
15468
16510
  function hasStaleBranchWorkToPreserve(uniqueCommitsAheadOfBase, hasUncommittedChanges2) {
15469
16511
  return hasUncommittedChanges2 || uniqueCommitsAheadOfBase > 0;
15470
16512
  }
@@ -15505,7 +16547,7 @@ var update_default = defineCommand({
15505
16547
  }
15506
16548
  if (protectedBranches.includes(currentBranch)) {
15507
16549
  await projectHeading("update", "\uD83D\uDD03");
15508
- warn(`You're on ${import_picocolors22.default.bold(currentBranch)}, which is a protected branch. Updates (rebase) apply to feature branches.`);
16550
+ warn(`You're on ${import_picocolors23.default.bold(currentBranch)}, which is a protected branch. Updates (rebase) apply to feature branches.`);
15509
16551
  await fetchAll();
15510
16552
  const { origin } = config;
15511
16553
  const remoteRef = `${origin}/${currentBranch}`;
@@ -15514,12 +16556,12 @@ var update_default = defineCommand({
15514
16556
  const hasCommits = localWork.unpushedCommits > 0;
15515
16557
  const hasAnything = hasCommits || dirty;
15516
16558
  if (!hasAnything) {
15517
- info(`No local changes found on ${import_picocolors22.default.bold(currentBranch)}.`);
15518
- info(`Use ${import_picocolors22.default.bold("cn sync")} to sync protected branches, or ${import_picocolors22.default.bold("cn start")} to create a feature branch.`);
16559
+ info(`No local changes found on ${import_picocolors23.default.bold(currentBranch)}.`);
16560
+ info(`Use ${import_picocolors23.default.bold("cn sync")} to sync protected branches, or ${import_picocolors23.default.bold("cn start")} to create a feature branch.`);
15519
16561
  process.exit(1);
15520
16562
  }
15521
16563
  if (hasCommits) {
15522
- info(`Found ${import_picocolors22.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${import_picocolors22.default.bold(currentBranch)}.`);
16564
+ info(`Found ${import_picocolors23.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${import_picocolors23.default.bold(currentBranch)}.`);
15523
16565
  }
15524
16566
  if (dirty) {
15525
16567
  info("You also have uncommitted changes in the working tree.");
@@ -15549,12 +16591,12 @@ var update_default = defineCommand({
15549
16591
  error(`Failed to create branch: ${branchResult.stderr}`);
15550
16592
  process.exit(1);
15551
16593
  }
15552
- success(`Created ${import_picocolors22.default.bold(newBranchName)} with your changes.`);
16594
+ success(`Created ${import_picocolors23.default.bold(newBranchName)} with your changes.`);
15553
16595
  await updateLocalBranch(currentBranch, remoteRef);
15554
- info(`Reset ${import_picocolors22.default.bold(currentBranch)} back to ${import_picocolors22.default.bold(remoteRef)} \u2014 no damage done.`, "");
16596
+ info(`Reset ${import_picocolors23.default.bold(currentBranch)} back to ${import_picocolors23.default.bold(remoteRef)} \u2014 no damage done.`, "");
15555
16597
  console.log();
15556
- success(`You're now on ${import_picocolors22.default.bold(newBranchName)} with all your work intact.`);
15557
- info(`Run ${import_picocolors22.default.bold("cn update")} again to rebase onto latest ${import_picocolors22.default.bold(baseBranch)}.`, "");
16598
+ success(`You're now on ${import_picocolors23.default.bold(newBranchName)} with all your work intact.`);
16599
+ info(`Run ${import_picocolors23.default.bold("cn update")} again to rebase onto latest ${import_picocolors23.default.bold(baseBranch)}.`, "");
15558
16600
  return;
15559
16601
  }
15560
16602
  if (await hasUncommittedChanges()) {
@@ -15564,8 +16606,8 @@ var update_default = defineCommand({
15564
16606
  await projectHeading("update", "\uD83D\uDD03");
15565
16607
  const mergedPR = await getMergedPRForBranch(currentBranch);
15566
16608
  if (mergedPR) {
15567
- warn(`PR #${mergedPR.number} (${import_picocolors22.default.bold(mergedPR.title)}) has already been merged.`);
15568
- info(`Link: ${import_picocolors22.default.underline(mergedPR.url)}`, "");
16609
+ warn(`PR #${mergedPR.number} (${import_picocolors23.default.bold(mergedPR.title)}) has already been merged.`);
16610
+ info(`Link: ${import_picocolors23.default.underline(mergedPR.url)}`, "");
15569
16611
  const uniqueCommitsAheadOfBase = await countCommitsAhead(currentBranch, syncSource.ref);
15570
16612
  const dirty = await hasUncommittedChanges();
15571
16613
  const hasWork = hasStaleBranchWorkToPreserve(uniqueCommitsAheadOfBase, dirty);
@@ -15574,12 +16616,12 @@ var update_default = defineCommand({
15574
16616
  info("You have uncommitted local changes.");
15575
16617
  }
15576
16618
  if (uniqueCommitsAheadOfBase > 0) {
15577
- info(`You have ${uniqueCommitsAheadOfBase} local commit(s) not in ${import_picocolors22.default.bold(syncSource.ref)}.`);
16619
+ info(`You have ${uniqueCommitsAheadOfBase} local commit(s) not in ${import_picocolors23.default.bold(syncSource.ref)}.`);
15578
16620
  }
15579
16621
  const SAVE_NEW_BRANCH = "Save changes to a new branch";
15580
16622
  const DISCARD = "Discard all changes and clean up";
15581
16623
  const CANCEL = "Cancel";
15582
- const action = await selectPrompt(`${import_picocolors22.default.bold(currentBranch)} is stale but has local work. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
16624
+ const action = await selectPrompt(`${import_picocolors23.default.bold(currentBranch)} is stale but has local work. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
15583
16625
  if (action === CANCEL) {
15584
16626
  info("No changes made. You are still on your current branch.");
15585
16627
  return;
@@ -15601,7 +16643,7 @@ var update_default = defineCommand({
15601
16643
  error(`Failed to rename branch: ${renameResult.stderr}`);
15602
16644
  process.exit(1);
15603
16645
  }
15604
- success(`Renamed ${import_picocolors22.default.bold(currentBranch)} \u2192 ${import_picocolors22.default.bold(newBranchName)}`);
16646
+ success(`Renamed ${import_picocolors23.default.bold(currentBranch)} \u2192 ${import_picocolors23.default.bold(newBranchName)}`);
15605
16647
  await unsetUpstream();
15606
16648
  await fetchRemote(syncSource.remote);
15607
16649
  let rebaseResult2;
@@ -15613,11 +16655,11 @@ var update_default = defineCommand({
15613
16655
  }
15614
16656
  if (rebaseResult2.exitCode !== 0) {
15615
16657
  warn("Rebase encountered conflicts. Resolve them manually, then run:");
15616
- info(` ${import_picocolors22.default.bold("git rebase --continue")}`, "");
16658
+ info(` ${import_picocolors23.default.bold("git rebase --continue")}`, "");
15617
16659
  } else {
15618
- success(`Rebased ${import_picocolors22.default.bold(newBranchName)} onto ${import_picocolors22.default.bold(syncSource.ref)}.`);
16660
+ success(`Rebased ${import_picocolors23.default.bold(newBranchName)} onto ${import_picocolors23.default.bold(syncSource.ref)}.`);
15619
16661
  }
15620
- info(`All your changes are preserved. Run ${import_picocolors22.default.bold("cn submit")} when ready to create a new PR.`, "");
16662
+ info(`All your changes are preserved. Run ${import_picocolors23.default.bold("cn submit")} when ready to create a new PR.`, "");
15621
16663
  return;
15622
16664
  }
15623
16665
  warn("Discarding local changes...");
@@ -15636,24 +16678,24 @@ var update_default = defineCommand({
15636
16678
  process.exit(1);
15637
16679
  }
15638
16680
  await updateLocalBranch(baseBranch, syncSource.ref);
15639
- success(`Synced ${import_picocolors22.default.bold(baseBranch)} with ${import_picocolors22.default.bold(syncSource.ref)}.`);
15640
- info(`Deleting stale branch ${import_picocolors22.default.bold(currentBranch)}...`);
16681
+ success(`Synced ${import_picocolors23.default.bold(baseBranch)} with ${import_picocolors23.default.bold(syncSource.ref)}.`);
16682
+ info(`Deleting stale branch ${import_picocolors23.default.bold(currentBranch)}...`);
15641
16683
  await forceDeleteBranch(currentBranch);
15642
- success(`Deleted ${import_picocolors22.default.bold(currentBranch)}.`);
15643
- info(`Run ${import_picocolors22.default.bold("cn start")} to begin a new feature branch.`, "");
16684
+ success(`Deleted ${import_picocolors23.default.bold(currentBranch)}.`);
16685
+ info(`Run ${import_picocolors23.default.bold("cn start")} to begin a new feature branch.`, "");
15644
16686
  return;
15645
16687
  }
15646
- info(`Updating ${import_picocolors22.default.bold(currentBranch)} with latest ${import_picocolors22.default.bold(baseBranch)}...`);
16688
+ info(`Updating ${import_picocolors23.default.bold(currentBranch)} with latest ${import_picocolors23.default.bold(baseBranch)}...`);
15647
16689
  await fetchRemote(syncSource.remote);
15648
16690
  if (!await refExists(syncSource.ref)) {
15649
- error(`Remote ref ${import_picocolors22.default.bold(syncSource.ref)} does not exist.`);
16691
+ error(`Remote ref ${import_picocolors23.default.bold(syncSource.ref)} does not exist.`);
15650
16692
  error("Run `git fetch --all` and verify your remote configuration.");
15651
16693
  process.exit(1);
15652
16694
  }
15653
16695
  await updateLocalBranch(baseBranch, syncSource.ref);
15654
16696
  const rebaseStrategy = await determineRebaseStrategy(currentBranch, syncSource.ref);
15655
16697
  if (rebaseStrategy.strategy === "onto" && rebaseStrategy.ontoOldBase) {
15656
- info(import_picocolors22.default.dim(`Using --onto rebase (branch was based on a different ref)`));
16698
+ info(import_picocolors23.default.dim(`Using --onto rebase (branch was based on a different ref)`));
15657
16699
  }
15658
16700
  const rebaseResult = rebaseStrategy.strategy === "onto" && rebaseStrategy.ontoOldBase ? await rebaseOnto(syncSource.ref, rebaseStrategy.ontoOldBase) : await rebase(syncSource.ref);
15659
16701
  if (rebaseResult.exitCode !== 0) {
@@ -15667,7 +16709,7 @@ var update_default = defineCommand({
15667
16709
  let conflictDiff = "";
15668
16710
  for (const file of conflictFiles.slice(0, 3)) {
15669
16711
  try {
15670
- const content = readFileSync6(file, "utf-8");
16712
+ const content = readFileSync7(file, "utf-8");
15671
16713
  if (content.includes("<<<<<<<")) {
15672
16714
  conflictDiff += `
15673
16715
  --- ${file} ---
@@ -15684,10 +16726,10 @@ ${content.slice(0, 2000)}
15684
16726
  if (suggestion) {
15685
16727
  spinner.success("AI conflict guidance ready.");
15686
16728
  console.log(`
15687
- ${import_picocolors22.default.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
15688
- console.log(import_picocolors22.default.dim("\u2500".repeat(60)));
16729
+ ${import_picocolors23.default.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
16730
+ console.log(import_picocolors23.default.dim("\u2500".repeat(60)));
15689
16731
  console.log(suggestion);
15690
- console.log(import_picocolors22.default.dim("\u2500".repeat(60)));
16732
+ console.log(import_picocolors23.default.dim("\u2500".repeat(60)));
15691
16733
  console.log();
15692
16734
  } else {
15693
16735
  spinner.fail("AI could not analyze the conflicts.");
@@ -15695,21 +16737,21 @@ ${import_picocolors22.default.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance
15695
16737
  }
15696
16738
  }
15697
16739
  }
15698
- console.log(import_picocolors22.default.bold("To resolve:"));
16740
+ console.log(import_picocolors23.default.bold("To resolve:"));
15699
16741
  console.log(` 1. Fix conflicts in the affected files`);
15700
- console.log(` 2. ${import_picocolors22.default.cyan("git add <resolved-files>")}`);
15701
- console.log(` 3. ${import_picocolors22.default.cyan("git rebase --continue")}`);
16742
+ console.log(` 2. ${import_picocolors23.default.cyan("git add <resolved-files>")}`);
16743
+ console.log(` 3. ${import_picocolors23.default.cyan("git rebase --continue")}`);
15702
16744
  console.log();
15703
- console.log(` Or abort: ${import_picocolors22.default.cyan("git rebase --abort")}`);
16745
+ console.log(` Or abort: ${import_picocolors23.default.cyan("git rebase --abort")}`);
15704
16746
  process.exit(1);
15705
16747
  }
15706
- success(`${import_picocolors22.default.bold(currentBranch)} has been rebased onto latest ${import_picocolors22.default.bold(baseBranch)}`);
16748
+ success(`${import_picocolors23.default.bold(currentBranch)} has been rebased onto latest ${import_picocolors23.default.bold(baseBranch)}`);
15707
16749
  }
15708
16750
  });
15709
16751
 
15710
16752
  // src/commands/validate.ts
15711
- import { readFileSync as readFileSync7 } from "fs";
15712
- var import_picocolors23 = __toESM(require_picocolors(), 1);
16753
+ import { readFileSync as readFileSync8 } from "fs";
16754
+ var import_picocolors24 = __toESM(require_picocolors(), 1);
15713
16755
  var validate_default = defineCommand({
15714
16756
  meta: {
15715
16757
  name: "validate",
@@ -15738,7 +16780,7 @@ var validate_default = defineCommand({
15738
16780
  info('Commit convention is set to "none". All messages are accepted.');
15739
16781
  process.exit(0);
15740
16782
  }
15741
- const message = args.file ? readFileSync7(args.file, "utf-8").split(/\r?\n/, 1)[0] ?? "" : args.message;
16783
+ const message = args.file ? readFileSync8(args.file, "utf-8").split(/\r?\n/, 1)[0] ?? "" : args.message;
15742
16784
  if (!message) {
15743
16785
  error("No commit message provided. Pass a message or use --file <path>.");
15744
16786
  process.exit(1);
@@ -15749,14 +16791,14 @@ var validate_default = defineCommand({
15749
16791
  }
15750
16792
  const errors = getValidationError(convention);
15751
16793
  for (const line of errors) {
15752
- console.error(import_picocolors23.default.red(` \u2717 ${line}`));
16794
+ console.error(import_picocolors24.default.red(` \u2717 ${line}`));
15753
16795
  }
15754
16796
  process.exit(1);
15755
16797
  }
15756
16798
  });
15757
16799
 
15758
16800
  // src/ui/banner.ts
15759
- var import_picocolors24 = __toESM(require_picocolors(), 1);
16801
+ var import_picocolors25 = __toESM(require_picocolors(), 1);
15760
16802
 
15761
16803
  // src/data/announcements.json
15762
16804
  var announcements_default = [
@@ -15817,9 +16859,9 @@ function getAuthor() {
15817
16859
  return typeof package_default.author === "string" ? package_default.author : "unknown";
15818
16860
  }
15819
16861
  function showBanner(variant = "small") {
15820
- console.log(import_picocolors24.default.cyan(`
16862
+ console.log(import_picocolors25.default.cyan(`
15821
16863
  ${LOGO}`));
15822
- console.log(` ${import_picocolors24.default.dim(`v${getVersion()}`)} ${import_picocolors24.default.dim("\u2014")} ${import_picocolors24.default.dim(`Built by ${getAuthor()}`)}`);
16864
+ console.log(` ${import_picocolors25.default.dim(`v${getVersion()}`)} ${import_picocolors25.default.dim("\u2014")} ${import_picocolors25.default.dim(`Built by ${getAuthor()}`)}`);
15823
16865
  const announcements = getActiveAnnouncements();
15824
16866
  if (announcements.length > 0) {
15825
16867
  console.log();
@@ -15828,27 +16870,27 @@ ${LOGO}`));
15828
16870
  if (variant === "big") {
15829
16871
  const panelLines = [
15830
16872
  {
15831
- label: import_picocolors24.default.bold(import_picocolors24.default.cyan("Getting Started")),
16873
+ label: import_picocolors25.default.bold(import_picocolors25.default.cyan("Getting Started")),
15832
16874
  rawLabel: "Getting Started",
15833
16875
  value: "",
15834
16876
  rawValue: ""
15835
16877
  },
15836
16878
  {
15837
- label: import_picocolors24.default.cyan("cn setup"),
16879
+ label: import_picocolors25.default.cyan("cn setup"),
15838
16880
  rawLabel: "cn setup",
15839
- value: import_picocolors24.default.dim("configure workflow, remotes, and defaults"),
16881
+ value: import_picocolors25.default.dim("configure workflow, remotes, and defaults"),
15840
16882
  rawValue: "configure workflow, remotes, and defaults"
15841
16883
  },
15842
16884
  {
15843
- label: import_picocolors24.default.cyan("cn doctor"),
16885
+ label: import_picocolors25.default.cyan("cn doctor"),
15844
16886
  rawLabel: "cn doctor",
15845
- value: import_picocolors24.default.dim("verify your environment before doing any work"),
16887
+ value: import_picocolors25.default.dim("verify your environment before doing any work"),
15846
16888
  rawValue: "verify your environment before doing any work"
15847
16889
  },
15848
16890
  {
15849
- label: import_picocolors24.default.cyan("cn start"),
16891
+ label: import_picocolors25.default.cyan("cn start"),
15850
16892
  rawLabel: "cn start",
15851
- value: import_picocolors24.default.dim("create a branch and begin the next task"),
16893
+ value: import_picocolors25.default.dim("create a branch and begin the next task"),
15852
16894
  rawValue: "create a branch and begin the next task"
15853
16895
  },
15854
16896
  {
@@ -15858,13 +16900,13 @@ ${LOGO}`));
15858
16900
  rawValue: ""
15859
16901
  },
15860
16902
  {
15861
- label: import_picocolors24.default.bold(import_picocolors24.default.cyan("Workflow")),
16903
+ label: import_picocolors25.default.bold(import_picocolors25.default.cyan("Workflow")),
15862
16904
  rawLabel: "Workflow",
15863
16905
  value: "",
15864
16906
  rawValue: ""
15865
16907
  },
15866
16908
  {
15867
- label: import_picocolors24.default.dim("cn setup \u2192 cn commit \u2192 cn update \u2192 cn submit"),
16909
+ label: import_picocolors25.default.dim("cn setup \u2192 cn commit \u2192 cn update \u2192 cn submit"),
15868
16910
  rawLabel: "cn setup \u2192 cn commit \u2192 cn update \u2192 cn submit",
15869
16911
  value: "",
15870
16912
  rawValue: ""
@@ -15889,22 +16931,22 @@ ${LOGO}`));
15889
16931
  return Math.max(max, lineLength);
15890
16932
  }, 0));
15891
16933
  console.log();
15892
- console.log(` ${import_picocolors24.default.dim(`\u250C${"\u2500".repeat(contentWidth + 2)}\u2510`)}`);
16934
+ console.log(` ${import_picocolors25.default.dim(`\u250C${"\u2500".repeat(contentWidth + 2)}\u2510`)}`);
15893
16935
  for (const line of rows) {
15894
16936
  if (!line.rawLabel && !line.rawValue) {
15895
- console.log(` ${import_picocolors24.default.dim("\u2502")} ${" ".repeat(contentWidth)} ${import_picocolors24.default.dim("\u2502")}`);
16937
+ console.log(` ${import_picocolors25.default.dim("\u2502")} ${" ".repeat(contentWidth)} ${import_picocolors25.default.dim("\u2502")}`);
15896
16938
  continue;
15897
16939
  }
15898
16940
  const left = line.rawValue ? `${line.label}${" ".repeat(Math.max(0, labelWidth - line.rawLabel.length + 2))}` : line.label;
15899
- const value = line.rawValue ? import_picocolors24.default.dim(line.rawValue) : "";
16941
+ const value = line.rawValue ? import_picocolors25.default.dim(line.rawValue) : "";
15900
16942
  const rawLength = line.rawValue ? labelWidth + 2 + line.rawValue.length : line.rawLabel.length;
15901
16943
  const trailing = " ".repeat(Math.max(0, contentWidth - rawLength));
15902
- console.log(` ${import_picocolors24.default.dim("\u2502")} ${left}${value}${trailing} ${import_picocolors24.default.dim("\u2502")}`);
16944
+ console.log(` ${import_picocolors25.default.dim("\u2502")} ${left}${value}${trailing} ${import_picocolors25.default.dim("\u2502")}`);
15903
16945
  }
15904
- console.log(` ${import_picocolors24.default.dim(`\u2514${"\u2500".repeat(contentWidth + 2)}\u2518`)}`);
16946
+ console.log(` ${import_picocolors25.default.dim(`\u2514${"\u2500".repeat(contentWidth + 2)}\u2518`)}`);
15905
16947
  console.log();
15906
- console.log(` ${import_picocolors24.default.dim("Star or contribute:")} ${import_picocolors24.default.dim(linkify("gh.waren.build/contribute-now", "https://gh.waren.build/contribute-now"))}`);
15907
- console.log(` ${import_picocolors24.default.dim("Sponsor:")} ${import_picocolors24.default.dim(linkify("warengonzaga.com/sponsor", "https://warengonzaga.com/sponsor"))}`);
16948
+ console.log(` ${import_picocolors25.default.dim("Star or contribute:")} ${import_picocolors25.default.dim(linkify("gh.waren.build/contribute-now", "https://gh.waren.build/contribute-now"))}`);
16949
+ console.log(` ${import_picocolors25.default.dim("Sponsor:")} ${import_picocolors25.default.dim(linkify("warengonzaga.com/sponsor", "https://warengonzaga.com/sponsor"))}`);
15908
16950
  }
15909
16951
  console.log();
15910
16952
  }
@@ -15936,7 +16978,7 @@ function renderAnnouncementBanner(announcement) {
15936
16978
  console.log(` ${tone.border(`\u250C${"\u2500".repeat(rawWidth + 2)}\u2510`)}`);
15937
16979
  for (const line of lines) {
15938
16980
  const trailing = " ".repeat(Math.max(0, rawWidth - line.length));
15939
- const content = line === title ? tone.title(line) : import_picocolors24.default.dim(line);
16981
+ const content = line === title ? tone.title(line) : import_picocolors25.default.dim(line);
15940
16982
  console.log(` ${tone.border("\u2502")} ${content}${trailing} ${tone.border("\u2502")}`);
15941
16983
  }
15942
16984
  console.log(` ${tone.border(`\u2514${"\u2500".repeat(rawWidth + 2)}\u2518`)}`);
@@ -15972,20 +17014,20 @@ function getAnnouncementTone(kind) {
15972
17014
  case "info":
15973
17015
  return {
15974
17016
  emoji: "\u2139",
15975
- border: import_picocolors24.default.blue,
15976
- title: (value) => import_picocolors24.default.bold(import_picocolors24.default.blue(value))
17017
+ border: import_picocolors25.default.blue,
17018
+ title: (value) => import_picocolors25.default.bold(import_picocolors25.default.blue(value))
15977
17019
  };
15978
17020
  case "warning":
15979
17021
  return {
15980
17022
  emoji: "\uD83D\uDEA8",
15981
- border: import_picocolors24.default.red,
15982
- title: (value) => import_picocolors24.default.bold(import_picocolors24.default.red(value))
17023
+ border: import_picocolors25.default.red,
17024
+ title: (value) => import_picocolors25.default.bold(import_picocolors25.default.red(value))
15983
17025
  };
15984
17026
  default:
15985
17027
  return {
15986
17028
  emoji: "\u26A0",
15987
- border: import_picocolors24.default.yellow,
15988
- title: (value) => import_picocolors24.default.bold(import_picocolors24.default.yellow(value))
17029
+ border: import_picocolors25.default.yellow,
17030
+ title: (value) => import_picocolors25.default.bold(import_picocolors25.default.yellow(value))
15989
17031
  };
15990
17032
  }
15991
17033
  }
@@ -16024,7 +17066,8 @@ if (!isVersion) {
16024
17066
  "branch",
16025
17067
  "hook",
16026
17068
  "validate",
16027
- "doctor"
17069
+ "doctor",
17070
+ "label"
16028
17071
  ];
16029
17072
  const isHelp = process.argv.includes("--help") || process.argv.includes("-h");
16030
17073
  const hasSubCommand = subCommands.some((cmd) => process.argv.includes(cmd));
@@ -16061,7 +17104,8 @@ var main = defineCommand({
16061
17104
  log: log_default,
16062
17105
  hook: hook_default,
16063
17106
  validate: validate_default,
16064
- doctor: doctor_default
17107
+ doctor: doctor_default,
17108
+ label: label_default
16065
17109
  },
16066
17110
  run({ args }) {
16067
17111
  if (args.version) {