contribute-now 0.8.0-dev.7db6dea → 0.8.0-dev.a835394

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 +51 -5
  2. package/dist/cli.js +2070 -792
  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,
@@ -7055,10 +7291,13 @@ import {
7055
7291
  statSync,
7056
7292
  writeFileSync
7057
7293
  } from "fs";
7294
+ import { homedir } from "os";
7058
7295
  import { dirname, join, resolve } from "path";
7059
7296
  var CONFIG_FILENAME = ".contributerc.json";
7060
7297
  var LOCAL_CONFIG_DIRNAME = "contribute-now";
7061
7298
  var LOCAL_CONFIG_FILENAME = "config.json";
7299
+ var GLOBAL_CONFIG_DIRNAME = ".contribute-now";
7300
+ var GLOBAL_CONFIG_FILENAME = "config.json";
7062
7301
  function findRepoRoot(cwd = process.cwd()) {
7063
7302
  let current = resolve(cwd);
7064
7303
  while (true) {
@@ -7148,13 +7387,44 @@ function parseConfigFile(path) {
7148
7387
  aiHost: _aiHost,
7149
7388
  ...config
7150
7389
  } = parsed;
7151
- return {
7390
+ const normalizedConfig = {
7152
7391
  ...config,
7153
- aiEnabled: parsed.aiEnabled !== false,
7154
7392
  aiProvider: parsed.aiProvider,
7155
7393
  aiModel: parsed.aiModel?.trim() || undefined,
7156
7394
  showTips: parsed.showTips !== false
7157
7395
  };
7396
+ if (typeof parsed.aiEnabled === "boolean") {
7397
+ normalizedConfig.aiEnabled = parsed.aiEnabled;
7398
+ }
7399
+ return normalizedConfig;
7400
+ } catch {
7401
+ return null;
7402
+ }
7403
+ }
7404
+ function parseGlobalConfigFile(path) {
7405
+ try {
7406
+ const raw = readFileSync(path, "utf-8");
7407
+ const parsed = JSON.parse(raw);
7408
+ if (typeof parsed !== "object" || parsed === null) {
7409
+ return null;
7410
+ }
7411
+ if (parsed.aiProvider !== undefined && (typeof parsed.aiProvider !== "string" || !VALID_AI_PROVIDERS.includes(parsed.aiProvider))) {
7412
+ console.error(`Invalid aiProvider "${String(parsed.aiProvider)}" in ${GLOBAL_CONFIG_FILENAME}. Valid: ${VALID_AI_PROVIDERS.join(", ")}`);
7413
+ return null;
7414
+ }
7415
+ if (parsed.aiModel !== undefined && (typeof parsed.aiModel !== "string" || !parsed.aiModel.trim())) {
7416
+ console.error(`Invalid ${GLOBAL_CONFIG_FILENAME}: aiModel must be a non-empty string.`);
7417
+ return null;
7418
+ }
7419
+ if (parsed.aiEnabled !== undefined && typeof parsed.aiEnabled !== "boolean") {
7420
+ console.error(`Invalid ${GLOBAL_CONFIG_FILENAME}: aiEnabled must be a boolean when set.`);
7421
+ return null;
7422
+ }
7423
+ return {
7424
+ aiEnabled: parsed.aiEnabled,
7425
+ aiProvider: parsed.aiProvider,
7426
+ aiModel: parsed.aiModel?.trim() || undefined
7427
+ };
7158
7428
  } catch {
7159
7429
  return null;
7160
7430
  }
@@ -7169,6 +7439,9 @@ function getConfigPath(cwd = process.cwd()) {
7169
7439
  function getLegacyConfigPath(cwd = process.cwd()) {
7170
7440
  return join(findRepoRoot(cwd) ?? cwd, CONFIG_FILENAME);
7171
7441
  }
7442
+ function getGlobalConfigPath(baseDir = homedir()) {
7443
+ return join(baseDir, GLOBAL_CONFIG_DIRNAME, GLOBAL_CONFIG_FILENAME);
7444
+ }
7172
7445
  function getLocalConfigPath(cwd = process.cwd()) {
7173
7446
  const gitDir = resolveGitDir(cwd);
7174
7447
  if (!gitDir) {
@@ -7203,12 +7476,25 @@ function getConfigLocationLabel(cwd = process.cwd()) {
7203
7476
  function configExists(cwd = process.cwd()) {
7204
7477
  return getConfigSource(cwd) !== null;
7205
7478
  }
7479
+ function globalConfigExists(baseDir = homedir()) {
7480
+ return existsSync(getGlobalConfigPath(baseDir));
7481
+ }
7206
7482
  var VALID_WORKFLOWS = ["clean-flow", "github-flow", "git-flow"];
7207
7483
  var VALID_ROLES = ["maintainer", "contributor"];
7208
7484
  var VALID_CONVENTIONS = ["conventional", "clean-commit", "none"];
7209
- var VALID_AI_PROVIDERS = ["copilot", "ollama-cloud"];
7210
- function isAIEnabled(config, cliNoAI = false) {
7211
- return config.aiEnabled !== false && !cliNoAI;
7485
+ var VALID_AI_PROVIDERS = ["copilot", "ollama-cloud", "openrouter"];
7486
+ function isAIEnabled(config, cliNoAI = false, globalConfig) {
7487
+ if (cliNoAI) {
7488
+ return false;
7489
+ }
7490
+ if (typeof config.aiEnabled === "boolean") {
7491
+ return config.aiEnabled;
7492
+ }
7493
+ const resolvedGlobalConfig = globalConfig === undefined ? readGlobalConfig() : globalConfig;
7494
+ if (typeof resolvedGlobalConfig?.aiEnabled === "boolean") {
7495
+ return resolvedGlobalConfig.aiEnabled;
7496
+ }
7497
+ return true;
7212
7498
  }
7213
7499
  function shouldShowTips(config) {
7214
7500
  return config?.showTips !== false;
@@ -7222,6 +7508,13 @@ function readConfig(cwd = process.cwd()) {
7222
7508
  return null;
7223
7509
  return parseConfigFile(path);
7224
7510
  }
7511
+ function readGlobalConfig(baseDir = homedir()) {
7512
+ const path = getGlobalConfigPath(baseDir);
7513
+ if (!existsSync(path)) {
7514
+ return null;
7515
+ }
7516
+ return parseGlobalConfigFile(path);
7517
+ }
7225
7518
  function writeConfig(config, cwd = process.cwd()) {
7226
7519
  const path = getConfigPath(cwd);
7227
7520
  const { aiHost: _aiHost, ...storedConfig } = config;
@@ -7229,6 +7522,15 @@ function writeConfig(config, cwd = process.cwd()) {
7229
7522
  writeFileSync(path, `${JSON.stringify(storedConfig, null, 2)}
7230
7523
  `, "utf-8");
7231
7524
  }
7525
+ function writeGlobalConfig(config, baseDir = homedir()) {
7526
+ const path = getGlobalConfigPath(baseDir);
7527
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
7528
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}
7529
+ `, {
7530
+ encoding: "utf-8",
7531
+ mode: 384
7532
+ });
7533
+ }
7232
7534
  function isGitignored(cwd = process.cwd()) {
7233
7535
  const gitignorePath = join(cwd, ".gitignore");
7234
7536
  if (!existsSync(gitignorePath))
@@ -9115,6 +9417,24 @@ var COMMAND_GUIDES = {
9115
9417
  description: "check config, remotes, and workflow resolution together"
9116
9418
  }
9117
9419
  ]
9420
+ },
9421
+ label: {
9422
+ summary: "Apply existing labels or get ranked suggestions for issues and pull requests.",
9423
+ examples: [
9424
+ { command: "cn label --help", description: "learn label add and suggest usage" },
9425
+ {
9426
+ command: "cn label add --issue 42 bug,enhancement",
9427
+ description: "apply labels to an issue"
9428
+ },
9429
+ {
9430
+ command: 'cn label add --pr 7 "good first issue"',
9431
+ description: "apply a label with spaces to a PR"
9432
+ },
9433
+ {
9434
+ command: "cn label suggest --issue 42",
9435
+ description: "get ranked label suggestions from content"
9436
+ }
9437
+ ]
9118
9438
  }
9119
9439
  };
9120
9440
  var LOADING_TIPS = [
@@ -10619,18 +10939,19 @@ init_dist();
10619
10939
 
10620
10940
  // src/utils/secrets.ts
10621
10941
  import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync4 } from "fs";
10622
- import { homedir } from "os";
10942
+ import { homedir as homedir2 } from "os";
10623
10943
  import { join as join5, resolve as resolve4 } from "path";
10624
10944
  var CONTRIBUTE_NOW_SECRETS_DIRNAME = ".contribute-now";
10625
10945
  var CONTRIBUTE_NOW_SECRETS_STORE_DIRNAME = "secrets";
10626
10946
  var OLLAMA_CLOUD_API_KEY = "ollama.cloud.apiKey";
10627
- function getSecretsStorePath(baseDir = homedir()) {
10947
+ var OPENROUTER_API_KEY = "openrouter.apiKey";
10948
+ function getSecretsStorePath(baseDir = homedir2()) {
10628
10949
  return resolve4(baseDir, CONTRIBUTE_NOW_SECRETS_DIRNAME, CONTRIBUTE_NOW_SECRETS_STORE_DIRNAME);
10629
10950
  }
10630
- function getSecretsFilePath(baseDir = homedir()) {
10951
+ function getSecretsFilePath(baseDir = homedir2()) {
10631
10952
  return join5(getSecretsStorePath(baseDir), "store.json");
10632
10953
  }
10633
- function readSecretsStore(baseDir = homedir()) {
10954
+ function readSecretsStore(baseDir = homedir2()) {
10634
10955
  const filePath = getSecretsFilePath(baseDir);
10635
10956
  if (!existsSync5(filePath)) {
10636
10957
  return null;
@@ -10642,7 +10963,7 @@ function readSecretsStore(baseDir = homedir()) {
10642
10963
  return null;
10643
10964
  }
10644
10965
  }
10645
- function writeSecretsStore(store, baseDir = homedir()) {
10966
+ function writeSecretsStore(store, baseDir = homedir2()) {
10646
10967
  const storePath = getSecretsStorePath(baseDir);
10647
10968
  const filePath = getSecretsFilePath(baseDir);
10648
10969
  mkdirSync4(storePath, { recursive: true, mode: 448 });
@@ -10656,23 +10977,23 @@ function writeSecretsStore(store, baseDir = homedir()) {
10656
10977
  chmodSync(filePath, 384);
10657
10978
  } catch {}
10658
10979
  }
10659
- function hasSecretsStore(baseDir = homedir()) {
10980
+ function hasSecretsStore(baseDir = homedir2()) {
10660
10981
  return existsSync5(getSecretsFilePath(baseDir));
10661
10982
  }
10662
- async function hasOllamaCloudApiKey(baseDir = homedir()) {
10983
+ async function hasOllamaCloudApiKey(baseDir = homedir2()) {
10663
10984
  return typeof readSecretsStore(baseDir)?.[OLLAMA_CLOUD_API_KEY] === "string";
10664
10985
  }
10665
- async function getOllamaCloudApiKey(baseDir = homedir()) {
10986
+ async function getOllamaCloudApiKey(baseDir = homedir2()) {
10666
10987
  return readSecretsStore(baseDir)?.[OLLAMA_CLOUD_API_KEY] ?? null;
10667
10988
  }
10668
- async function setOllamaCloudApiKey(value, baseDir = homedir()) {
10989
+ async function setOllamaCloudApiKey(value, baseDir = homedir2()) {
10669
10990
  const existingStore = readSecretsStore(baseDir) ?? {};
10670
10991
  writeSecretsStore({
10671
10992
  ...existingStore,
10672
10993
  [OLLAMA_CLOUD_API_KEY]: value
10673
10994
  }, baseDir);
10674
10995
  }
10675
- async function deleteOllamaCloudApiKey(baseDir = homedir()) {
10996
+ async function deleteOllamaCloudApiKey(baseDir = homedir2()) {
10676
10997
  const existingStore = readSecretsStore(baseDir);
10677
10998
  if (!existingStore || !(OLLAMA_CLOUD_API_KEY in existingStore)) {
10678
10999
  return false;
@@ -10689,6 +11010,36 @@ async function deleteOllamaCloudApiKey(baseDir = homedir()) {
10689
11010
  writeSecretsStore(nextStore, baseDir);
10690
11011
  return true;
10691
11012
  }
11013
+ async function hasOpenRouterApiKey(baseDir = homedir2()) {
11014
+ return typeof readSecretsStore(baseDir)?.[OPENROUTER_API_KEY] === "string";
11015
+ }
11016
+ async function getOpenRouterApiKey(baseDir = homedir2()) {
11017
+ return readSecretsStore(baseDir)?.[OPENROUTER_API_KEY] ?? null;
11018
+ }
11019
+ async function setOpenRouterApiKey(value, baseDir = homedir2()) {
11020
+ const existingStore = readSecretsStore(baseDir) ?? {};
11021
+ writeSecretsStore({
11022
+ ...existingStore,
11023
+ [OPENROUTER_API_KEY]: value
11024
+ }, baseDir);
11025
+ }
11026
+ async function deleteOpenRouterApiKey(baseDir = homedir2()) {
11027
+ const existingStore = readSecretsStore(baseDir);
11028
+ if (!existingStore || !(OPENROUTER_API_KEY in existingStore)) {
11029
+ return false;
11030
+ }
11031
+ const nextStore = { ...existingStore };
11032
+ delete nextStore[OPENROUTER_API_KEY];
11033
+ if (Object.keys(nextStore).length === 0) {
11034
+ try {
11035
+ rmSync(getSecretsFilePath(baseDir), { force: true });
11036
+ rmSync(getSecretsStorePath(baseDir), { recursive: true, force: true });
11037
+ } catch {}
11038
+ return true;
11039
+ }
11040
+ writeSecretsStore(nextStore, baseDir);
11041
+ return true;
11042
+ }
10692
11043
 
10693
11044
  // src/utils/copilot.ts
10694
11045
  var CONVENTIONAL_COMMIT_SYSTEM_PROMPT = `Git commit message generator. Format: <type>[!][(<scope>)]: <description>
@@ -10756,21 +11107,50 @@ Rules: title concise present tense, describes the PR theme not individual commit
10756
11107
  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
11108
  var DEFAULT_OLLAMA_CLOUD_MODEL = "gpt-oss:120b";
10758
11109
  var DEFAULT_OLLAMA_CLOUD_HOST = "https://ollama.com/v1";
10759
- function prioritizeOllamaCloudModels(models, preferredModel = DEFAULT_OLLAMA_CLOUD_MODEL) {
10760
- const uniqueModels = [...new Set(models.map((model) => model.trim()).filter(Boolean))];
10761
- const sortedModels = [...uniqueModels].sort((left, right) => left.localeCompare(right));
10762
- return sortedModels.includes(preferredModel) ? [preferredModel, ...sortedModels.filter((model) => model !== preferredModel)] : sortedModels;
10763
- }
10764
- function extractOllamaCloudModelIds(payload) {
10765
- 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));
10767
- }
10768
- function getOllamaCloudModelId(record) {
10769
- if (typeof record !== "object" || record === null) {
10770
- return null;
10771
- }
10772
- const candidate = typeof record.id === "string" ? record.id : typeof record.name === "string" ? record.name : null;
10773
- const normalized = candidate?.trim();
11110
+ var DEFAULT_OPENROUTER_MODEL = "openai/gpt-4o-mini";
11111
+ var DEFAULT_OPENROUTER_HOST = "https://openrouter.ai/api/v1";
11112
+ function resolveAIConfigFromSources(repoConfig, globalConfig) {
11113
+ const provider = repoConfig?.aiProvider ?? globalConfig?.aiProvider ?? "copilot";
11114
+ const useGlobalModel = !repoConfig?.aiModel && globalConfig?.aiProvider === provider;
11115
+ const globalModel = useGlobalModel ? globalConfig?.aiModel?.trim() : undefined;
11116
+ if (provider === "ollama-cloud") {
11117
+ return {
11118
+ provider,
11119
+ providerLabel: "Ollama Cloud",
11120
+ model: repoConfig?.aiModel?.trim() || globalModel || DEFAULT_OLLAMA_CLOUD_MODEL,
11121
+ host: DEFAULT_OLLAMA_CLOUD_HOST
11122
+ };
11123
+ }
11124
+ if (provider === "openrouter") {
11125
+ return {
11126
+ provider,
11127
+ providerLabel: "OpenRouter",
11128
+ model: repoConfig?.aiModel?.trim() || globalModel || DEFAULT_OPENROUTER_MODEL,
11129
+ host: DEFAULT_OPENROUTER_HOST
11130
+ };
11131
+ }
11132
+ return {
11133
+ provider: "copilot",
11134
+ providerLabel: "GitHub Copilot"
11135
+ };
11136
+ }
11137
+ function prioritizeOllamaCloudModels(models, preferredModel = DEFAULT_OLLAMA_CLOUD_MODEL) {
11138
+ const uniqueModels = [...new Set(models.map((model) => model.trim()).filter(Boolean))];
11139
+ const sortedModels = [...uniqueModels].sort((left, right) => left.localeCompare(right));
11140
+ return sortedModels.includes(preferredModel) ? [preferredModel, ...sortedModels.filter((model) => model !== preferredModel)] : sortedModels;
11141
+ }
11142
+ function extractOllamaCloudModelIds(payload) {
11143
+ const records = typeof payload === "object" && payload !== null ? Array.isArray(payload.data) ? payload.data : Array.isArray(payload.models) ? payload.models : [] : [];
11144
+ return [
11145
+ ...new Set(records.map(getOllamaCloudModelId).filter((id) => id !== null))
11146
+ ].sort((left, right) => left.localeCompare(right));
11147
+ }
11148
+ function getOllamaCloudModelId(record) {
11149
+ if (typeof record !== "object" || record === null) {
11150
+ return null;
11151
+ }
11152
+ const candidate = typeof record.id === "string" ? record.id : typeof record.name === "string" ? record.name : null;
11153
+ const normalized = candidate?.trim();
10774
11154
  return normalized ? normalized : null;
10775
11155
  }
10776
11156
  async function fetchOllamaCloudModels(apiKey, host) {
@@ -10792,21 +11172,44 @@ function normalizeOllamaCloudHost(host) {
10792
11172
  const trimmed = (host?.trim() || DEFAULT_OLLAMA_CLOUD_HOST).replace(/\/+$/, "");
10793
11173
  return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
10794
11174
  }
10795
- function resolveAIConfig(config) {
10796
- const resolvedConfig = config ?? readConfig();
10797
- const provider = resolvedConfig?.aiProvider ?? "copilot";
10798
- if (provider === "ollama-cloud") {
10799
- return {
10800
- provider,
10801
- providerLabel: "Ollama Cloud",
10802
- model: resolvedConfig?.aiModel?.trim() || DEFAULT_OLLAMA_CLOUD_MODEL,
10803
- host: DEFAULT_OLLAMA_CLOUD_HOST
10804
- };
11175
+ function extractOpenRouterModelIds(payload) {
11176
+ const records = typeof payload === "object" && payload !== null ? Array.isArray(payload.data) ? payload.data : [] : [];
11177
+ return [
11178
+ ...new Set(records.map(getOpenRouterModelId).filter((id) => id !== null))
11179
+ ].sort((left, right) => left.localeCompare(right));
11180
+ }
11181
+ function getOpenRouterModelId(record) {
11182
+ if (typeof record !== "object" || record === null) {
11183
+ return null;
10805
11184
  }
10806
- return {
10807
- provider: "copilot",
10808
- providerLabel: "GitHub Copilot"
10809
- };
11185
+ const candidate = typeof record.id === "string" ? record.id : null;
11186
+ const normalized = candidate?.trim();
11187
+ return normalized ? normalized : null;
11188
+ }
11189
+ async function fetchOpenRouterModels(apiKey) {
11190
+ const response = await fetch(`${DEFAULT_OPENROUTER_HOST}/models`, {
11191
+ headers: {
11192
+ Accept: "application/json",
11193
+ Authorization: `Bearer ${apiKey}`
11194
+ }
11195
+ });
11196
+ if (!response.ok) {
11197
+ if (response.status === 401 || response.status === 403) {
11198
+ throw new Error("OpenRouter authentication failed");
11199
+ }
11200
+ throw new Error(`OpenRouter model lookup failed (${response.status} ${response.statusText})`);
11201
+ }
11202
+ return extractOpenRouterModelIds(await response.json());
11203
+ }
11204
+ function prioritizeOpenRouterModels(models, preferredModel = DEFAULT_OPENROUTER_MODEL) {
11205
+ const uniqueModels = [...new Set(models.map((model) => model.trim()).filter(Boolean))];
11206
+ const sortedModels = [...uniqueModels].sort((left, right) => left.localeCompare(right));
11207
+ return sortedModels.includes(preferredModel) ? [preferredModel, ...sortedModels.filter((model) => model !== preferredModel)] : sortedModels;
11208
+ }
11209
+ function resolveAIConfig(config) {
11210
+ const repoConfig = config ?? readConfig();
11211
+ const globalConfig = readGlobalConfig();
11212
+ return resolveAIConfigFromSources(repoConfig, globalConfig);
10810
11213
  }
10811
11214
  function suppressSubprocessWarnings() {
10812
11215
  process.env.NODE_NO_WARNINGS = "1";
@@ -10996,6 +11399,28 @@ async function checkCopilotAvailable2() {
10996
11399
  return `Could not reach Ollama Cloud API: ${msg}`;
10997
11400
  }
10998
11401
  }
11402
+ if (aiConfig.provider === "openrouter") {
11403
+ if (!await hasOpenRouterApiKey()) {
11404
+ return "OpenRouter API key not found. Run `cn setup` to save it.";
11405
+ }
11406
+ try {
11407
+ const apiKey = await getOpenRouterApiKey();
11408
+ if (!apiKey) {
11409
+ return "OpenRouter API key not found. Run `cn setup` to save it.";
11410
+ }
11411
+ await fetchOpenRouterModels(apiKey);
11412
+ return null;
11413
+ } catch (err) {
11414
+ const msg = err instanceof Error ? err.message : String(err);
11415
+ if (msg === "OpenRouter authentication failed") {
11416
+ return "OpenRouter authentication failed. Update your saved API key with `cn setup`.";
11417
+ }
11418
+ if (msg.startsWith("OpenRouter model lookup failed")) {
11419
+ return msg.replace("model lookup", "health check");
11420
+ }
11421
+ return `Could not reach OpenRouter API: ${msg}`;
11422
+ }
11423
+ }
10999
11424
  try {
11000
11425
  const client = await getManagedClient();
11001
11426
  try {
@@ -11097,11 +11522,54 @@ async function callOllamaCloud(systemMessage, userMessage, model, timeoutMs = CO
11097
11522
  clearTimeout(timer);
11098
11523
  }
11099
11524
  }
11525
+ async function callOpenRouter(systemMessage, userMessage, model, timeoutMs = COPILOT_TIMEOUT_MS) {
11526
+ const aiConfig = resolveAIConfig();
11527
+ const apiKey = await getOpenRouterApiKey();
11528
+ if (!apiKey) {
11529
+ throw new Error("OpenRouter API key is not configured");
11530
+ }
11531
+ const controller = new AbortController;
11532
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
11533
+ try {
11534
+ const response = await fetch(`${DEFAULT_OPENROUTER_HOST}/chat/completions`, {
11535
+ method: "POST",
11536
+ headers: {
11537
+ Authorization: `Bearer ${apiKey}`,
11538
+ "Content-Type": "application/json",
11539
+ "HTTP-Referer": "https://github.com/warengonzaga/contribute-now",
11540
+ "X-Title": "contribute-now"
11541
+ },
11542
+ body: JSON.stringify({
11543
+ model: model?.trim() || aiConfig.model || DEFAULT_OPENROUTER_MODEL,
11544
+ messages: [
11545
+ { role: "system", content: systemMessage },
11546
+ { role: "user", content: userMessage }
11547
+ ],
11548
+ stream: false
11549
+ }),
11550
+ signal: controller.signal
11551
+ });
11552
+ if (!response.ok) {
11553
+ const body = await response.text();
11554
+ if (response.status === 401 || response.status === 403) {
11555
+ throw new Error("OpenRouter authentication failed");
11556
+ }
11557
+ throw new Error(`OpenRouter request failed (${response.status} ${response.statusText}): ${body.slice(0, 200)}`);
11558
+ }
11559
+ const data = await response.json();
11560
+ return data.choices?.[0]?.message?.content?.trim() || null;
11561
+ } finally {
11562
+ clearTimeout(timer);
11563
+ }
11564
+ }
11100
11565
  async function callAI(systemMessage, userMessage, model, timeoutMs = COPILOT_TIMEOUT_MS) {
11101
11566
  const aiConfig = resolveAIConfig();
11102
11567
  if (aiConfig.provider === "ollama-cloud") {
11103
11568
  return callOllamaCloud(systemMessage, userMessage, model, timeoutMs);
11104
11569
  }
11570
+ if (aiConfig.provider === "openrouter") {
11571
+ return callOpenRouter(systemMessage, userMessage, model, timeoutMs);
11572
+ }
11105
11573
  return callCopilot(systemMessage, userMessage, model, timeoutMs);
11106
11574
  }
11107
11575
  function getCommitSystemPrompt(convention) {
@@ -11651,145 +12119,8 @@ async function promptForBranchName(options) {
11651
12119
  }
11652
12120
  }
11653
12121
 
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
12122
  // src/commands/clean.ts
12123
+ init_gh();
11793
12124
  async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
11794
12125
  if (!config)
11795
12126
  return "skipped";
@@ -11992,116 +12323,8 @@ ${import_picocolors8.default.bold("Stale branches (remote deleted, likely squash
11992
12323
  }
11993
12324
  });
11994
12325
 
11995
- // src/commands/discard.ts
12326
+ // src/commands/commit.ts
11996
12327
  var import_picocolors9 = __toESM(require_picocolors(), 1);
11997
- var discard_default = defineCommand({
11998
- meta: {
11999
- name: "discard",
12000
- description: "Discard the current feature branch and return to the base branch"
12001
- },
12002
- args: {
12003
- force: {
12004
- 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
12328
 
12106
12329
  // src/utils/convention.ts
12107
12330
  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;
@@ -12208,9 +12431,9 @@ var commit_default = defineCommand({
12208
12431
  process.exit(1);
12209
12432
  }
12210
12433
  console.log(`
12211
- ${import_picocolors10.default.bold("Changed files:")}`);
12434
+ ${import_picocolors9.default.bold("Changed files:")}`);
12212
12435
  for (const f3 of changedFiles) {
12213
- console.log(` ${import_picocolors10.default.dim("\u2022")} ${f3}`);
12436
+ console.log(` ${import_picocolors9.default.dim("\u2022")} ${f3}`);
12214
12437
  }
12215
12438
  const stageAction = await selectPrompt("No staged changes. How would you like to stage?", [
12216
12439
  "Stage all changes",
@@ -12252,8 +12475,8 @@ ${import_picocolors10.default.bold("Changed files:")}`);
12252
12475
  const dirs = new Set(stagedFiles.map((f3) => f3.split("/")[0]));
12253
12476
  if (dirs.size > 1) {
12254
12477
  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."));
12478
+ 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.`);
12479
+ 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
12480
  const choice = await selectPrompt("How would you like to proceed?", [
12258
12481
  "Continue as single commit",
12259
12482
  "Switch to group mode (AI splits into atomic commits)",
@@ -12284,7 +12507,7 @@ ${import_picocolors10.default.bold("Changed files:")}`);
12284
12507
  if (commitMessage) {
12285
12508
  spinner.success("AI commit message generated.");
12286
12509
  console.log(`
12287
- ${import_picocolors10.default.dim("AI suggestion:")} ${import_picocolors10.default.bold(import_picocolors10.default.cyan(commitMessage))}`);
12510
+ ${import_picocolors9.default.dim("AI suggestion:")} ${import_picocolors9.default.bold(import_picocolors9.default.cyan(commitMessage))}`);
12288
12511
  } else {
12289
12512
  spinner.fail("AI did not return a commit message.");
12290
12513
  warn("Falling back to manual entry.");
@@ -12312,7 +12535,7 @@ ${import_picocolors10.default.bold("Changed files:")}`);
12312
12535
  if (regen) {
12313
12536
  spinner.success("Commit message regenerated.");
12314
12537
  console.log(`
12315
- ${import_picocolors10.default.dim("AI suggestion:")} ${import_picocolors10.default.bold(import_picocolors10.default.cyan(regen))}`);
12538
+ ${import_picocolors9.default.dim("AI suggestion:")} ${import_picocolors9.default.bold(import_picocolors9.default.cyan(regen))}`);
12316
12539
  const ok = await confirmPrompt("Use this message?");
12317
12540
  finalMessage = ok ? regen : await inputPrompt("Enter commit message manually");
12318
12541
  } else {
@@ -12327,7 +12550,7 @@ ${import_picocolors10.default.bold("Changed files:")}`);
12327
12550
  if (convention2 !== "none") {
12328
12551
  console.log();
12329
12552
  for (const hint of CONVENTION_FORMAT_HINTS[convention2]) {
12330
- console.log(import_picocolors10.default.dim(hint));
12553
+ console.log(import_picocolors9.default.dim(hint));
12331
12554
  }
12332
12555
  console.log();
12333
12556
  }
@@ -12351,7 +12574,7 @@ ${import_picocolors10.default.bold("Changed files:")}`);
12351
12574
  error(`Failed to commit: ${result.stderr}`);
12352
12575
  process.exit(1);
12353
12576
  }
12354
- success(`Committed: ${import_picocolors10.default.bold(finalMessage)}`);
12577
+ success(`Committed: ${import_picocolors9.default.bold(finalMessage)}`);
12355
12578
  }
12356
12579
  });
12357
12580
  async function runGroupCommit(model, config) {
@@ -12368,9 +12591,9 @@ async function runGroupCommit(model, config) {
12368
12591
  process.exit(1);
12369
12592
  }
12370
12593
  console.log(`
12371
- ${import_picocolors10.default.bold("Changed files:")}`);
12594
+ ${import_picocolors9.default.bold("Changed files:")}`);
12372
12595
  for (const f3 of changedFiles) {
12373
- console.log(` ${import_picocolors10.default.dim("\u2022")} ${f3}`);
12596
+ console.log(` ${import_picocolors9.default.dim("\u2022")} ${f3}`);
12374
12597
  }
12375
12598
  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
12599
  tips: LOADING_TIPS
@@ -12416,13 +12639,13 @@ ${import_picocolors10.default.bold("Changed files:")}`);
12416
12639
  let commitAll = false;
12417
12640
  while (!proceedToCommit) {
12418
12641
  console.log(`
12419
- ${import_picocolors10.default.bold(`AI suggested ${validGroups.length} commit group(s):`)}
12642
+ ${import_picocolors9.default.bold(`AI suggested ${validGroups.length} commit group(s):`)}
12420
12643
  `);
12421
12644
  for (let i2 = 0;i2 < validGroups.length; i2++) {
12422
12645
  const g3 = validGroups[i2];
12423
- console.log(` ${import_picocolors10.default.cyan(`Group ${i2 + 1}:`)} ${import_picocolors10.default.bold(g3.message)}`);
12646
+ console.log(` ${import_picocolors9.default.cyan(`Group ${i2 + 1}:`)} ${import_picocolors9.default.bold(g3.message)}`);
12424
12647
  for (const f3 of g3.files) {
12425
- console.log(` ${import_picocolors10.default.dim("\u2022")} ${f3}`);
12648
+ console.log(` ${import_picocolors9.default.dim("\u2022")} ${f3}`);
12426
12649
  }
12427
12650
  console.log();
12428
12651
  }
@@ -12488,16 +12711,16 @@ ${import_picocolors10.default.bold(`AI suggested ${validGroups.length} commit gr
12488
12711
  continue;
12489
12712
  }
12490
12713
  committed++;
12491
- success(`Committed group ${i2 + 1}: ${import_picocolors10.default.bold(group.message)}`);
12714
+ success(`Committed group ${i2 + 1}: ${import_picocolors9.default.bold(group.message)}`);
12492
12715
  }
12493
12716
  } else {
12494
12717
  for (let i2 = 0;i2 < validGroups.length; i2++) {
12495
12718
  const group = validGroups[i2];
12496
- console.log(import_picocolors10.default.bold(`
12719
+ console.log(import_picocolors9.default.bold(`
12497
12720
  \u2500\u2500 Group ${i2 + 1}/${validGroups.length} \u2500\u2500`));
12498
- console.log(` ${import_picocolors10.default.cyan(group.message)}`);
12721
+ console.log(` ${import_picocolors9.default.cyan(group.message)}`);
12499
12722
  for (const f3 of group.files) {
12500
- console.log(` ${import_picocolors10.default.dim("\u2022")} ${f3}`);
12723
+ console.log(` ${import_picocolors9.default.dim("\u2022")} ${f3}`);
12501
12724
  }
12502
12725
  let message = group.message;
12503
12726
  let actionDone = false;
@@ -12521,7 +12744,7 @@ ${import_picocolors10.default.bold(`AI suggested ${validGroups.length} commit gr
12521
12744
  if (newMsg) {
12522
12745
  message = newMsg;
12523
12746
  group.message = newMsg;
12524
- regenSpinner.success(`New message: ${import_picocolors10.default.bold(message)}`);
12747
+ regenSpinner.success(`New message: ${import_picocolors9.default.bold(message)}`);
12525
12748
  } else {
12526
12749
  regenSpinner.fail("AI could not generate a new message. Keeping current one.");
12527
12750
  }
@@ -12584,7 +12807,7 @@ ${import_picocolors10.default.bold(`AI suggested ${validGroups.length} commit gr
12584
12807
  continue;
12585
12808
  }
12586
12809
  committed++;
12587
- success(`Committed group ${i2 + 1}: ${import_picocolors10.default.bold(message)}`);
12810
+ success(`Committed group ${i2 + 1}: ${import_picocolors9.default.bold(message)}`);
12588
12811
  actionDone = true;
12589
12812
  }
12590
12813
  }
@@ -12599,7 +12822,7 @@ ${import_picocolors10.default.bold(`AI suggested ${validGroups.length} commit gr
12599
12822
  }
12600
12823
 
12601
12824
  // src/commands/config.ts
12602
- var import_picocolors11 = __toESM(require_picocolors(), 1);
12825
+ var import_picocolors10 = __toESM(require_picocolors(), 1);
12603
12826
  var WORKFLOW_OPTIONS = [
12604
12827
  { value: "clean-flow", label: WORKFLOW_DESCRIPTIONS["clean-flow"] },
12605
12828
  { value: "github-flow", label: WORKFLOW_DESCRIPTIONS["github-flow"] },
@@ -12616,7 +12839,8 @@ var CONVENTION_OPTIONS = [
12616
12839
  ];
12617
12840
  var AI_PROVIDER_OPTIONS = [
12618
12841
  { value: "copilot", label: "GitHub Copilot" },
12619
- { value: "ollama-cloud", label: "Ollama Cloud" }
12842
+ { value: "ollama-cloud", label: "Ollama Cloud" },
12843
+ { value: "openrouter", label: "OpenRouter" }
12620
12844
  ];
12621
12845
  function parseBranchPrefixesInput(input, fallback) {
12622
12846
  const values = input.split(",").map((value) => value.trim()).filter(Boolean);
@@ -12650,6 +12874,10 @@ function finalizeEditedConfig(current, draft) {
12650
12874
  next.aiModel = (draft.aiModel?.trim() || DEFAULT_OLLAMA_CLOUD_MODEL).trim();
12651
12875
  return next;
12652
12876
  }
12877
+ if (next.aiProvider === "openrouter") {
12878
+ next.aiModel = (draft.aiModel?.trim() || DEFAULT_OPENROUTER_MODEL).trim();
12879
+ return next;
12880
+ }
12653
12881
  delete next.aiModel;
12654
12882
  return next;
12655
12883
  }
@@ -12657,6 +12885,8 @@ function buildConfigSnapshot(config, meta) {
12657
12885
  const aiConfig = resolveAIConfig(config);
12658
12886
  const aiEnabled = isAIEnabled(config);
12659
12887
  const usingOllamaCloud = aiEnabled && aiConfig.provider === "ollama-cloud";
12888
+ const usingOpenRouter = aiEnabled && aiConfig.provider === "openrouter";
12889
+ const needsSecretInfo = usingOllamaCloud || usingOpenRouter;
12660
12890
  return {
12661
12891
  source: meta.source,
12662
12892
  location: meta.location,
@@ -12677,7 +12907,64 @@ function buildConfigSnapshot(config, meta) {
12677
12907
  providerLabel: aiEnabled ? aiConfig.providerLabel : null,
12678
12908
  model: aiEnabled ? aiConfig.model ?? null : null,
12679
12909
  ollamaCloudApiKeyPresent: usingOllamaCloud ? meta.hasOllamaCloudApiKey : null,
12680
- secretsPath: usingOllamaCloud ? meta.secretsPath : null
12910
+ openrouterApiKeyPresent: usingOpenRouter ? meta.hasOpenRouterApiKey : null,
12911
+ secretsPath: needsSecretInfo ? meta.secretsPath : null
12912
+ }
12913
+ };
12914
+ }
12915
+ function normalizeGlobalConfig(config) {
12916
+ const normalized = {
12917
+ aiEnabled: config.aiEnabled !== false
12918
+ };
12919
+ if (normalized.aiEnabled) {
12920
+ normalized.aiProvider = config.aiProvider ?? "copilot";
12921
+ if (normalized.aiProvider === "ollama-cloud") {
12922
+ normalized.aiModel = (config.aiModel?.trim() || DEFAULT_OLLAMA_CLOUD_MODEL).trim();
12923
+ } else if (normalized.aiProvider === "openrouter") {
12924
+ normalized.aiModel = (config.aiModel?.trim() || DEFAULT_OPENROUTER_MODEL).trim();
12925
+ }
12926
+ }
12927
+ return normalized;
12928
+ }
12929
+ function buildGlobalConfigSnapshot(config) {
12930
+ const normalized = normalizeGlobalConfig(config);
12931
+ const aiProvider = normalized.aiProvider ?? "copilot";
12932
+ const aiEnabled = normalized.aiEnabled !== false;
12933
+ const providerLabel = aiProvider === "ollama-cloud" ? "Ollama Cloud" : aiProvider === "openrouter" ? "OpenRouter" : "GitHub Copilot";
12934
+ return {
12935
+ location: getGlobalConfigPath(),
12936
+ exists: globalConfigExists(),
12937
+ ai: {
12938
+ enabled: aiEnabled,
12939
+ provider: aiProvider,
12940
+ providerLabel,
12941
+ model: aiEnabled ? normalized.aiModel ?? null : null
12942
+ }
12943
+ };
12944
+ }
12945
+ async function promptForGlobalConfigEdits(current) {
12946
+ const normalized = normalizeGlobalConfig(current);
12947
+ const aiEnabled = await selectBooleanValue("Global AI default", normalized.aiEnabled !== false, "Enabled", "Disabled");
12948
+ if (!aiEnabled) {
12949
+ return {
12950
+ config: {
12951
+ aiEnabled: false
12952
+ }
12953
+ };
12954
+ }
12955
+ const currentProvider = normalized.aiProvider ?? "copilot";
12956
+ const aiProvider = await selectCurrentValue("Global AI provider", AI_PROVIDER_OPTIONS, currentProvider);
12957
+ let aiModel;
12958
+ if (aiProvider === "ollama-cloud") {
12959
+ aiModel = await inputPrompt("Global Ollama Cloud model", normalized.aiModel ?? DEFAULT_OLLAMA_CLOUD_MODEL);
12960
+ } else if (aiProvider === "openrouter") {
12961
+ aiModel = await inputPrompt("Global OpenRouter model", normalized.aiModel ?? DEFAULT_OPENROUTER_MODEL);
12962
+ }
12963
+ return {
12964
+ config: {
12965
+ aiEnabled: true,
12966
+ aiProvider,
12967
+ aiModel: aiModel?.trim() || undefined
12681
12968
  }
12682
12969
  };
12683
12970
  }
@@ -12714,6 +13001,36 @@ async function promptForOllamaCloudModelSelection(apiKey, fallbackModel) {
12714
13001
  }
12715
13002
  return inputPrompt("Ollama Cloud model", fallbackModel);
12716
13003
  }
13004
+ async function promptForOpenRouterModelSelection(apiKey, fallbackModel) {
13005
+ if (apiKey) {
13006
+ try {
13007
+ info("Fetching available OpenRouter models...");
13008
+ const models = prioritizeOpenRouterModels(await fetchOpenRouterModels(apiKey));
13009
+ if (models.length > 0) {
13010
+ const manualChoice = "Enter model manually";
13011
+ const choices = models.map((model) => ({
13012
+ value: model,
13013
+ label: model === DEFAULT_OPENROUTER_MODEL ? `${model} (default)` : model
13014
+ }));
13015
+ const selected = await selectPrompt("OpenRouter model", [
13016
+ ...choices.map((choice) => choice.label),
13017
+ manualChoice
13018
+ ]);
13019
+ if (selected !== manualChoice) {
13020
+ return choices.find((choice) => choice.label === selected)?.value ?? fallbackModel;
13021
+ }
13022
+ } else {
13023
+ warn("OpenRouter returned no available models. Enter the model name manually.");
13024
+ }
13025
+ } catch (err) {
13026
+ const message = err instanceof Error ? err.message : String(err);
13027
+ warn(`Could not fetch OpenRouter models: ${message}`);
13028
+ }
13029
+ } else {
13030
+ warn("No OpenRouter API key is available yet, so the model list cannot be fetched.");
13031
+ }
13032
+ return inputPrompt("OpenRouter model", fallbackModel);
13033
+ }
12717
13034
  async function selectCurrentValue(message, options, current) {
12718
13035
  const choices = options.map((option) => ({
12719
13036
  value: option.value,
@@ -12728,7 +13045,7 @@ async function selectBooleanValue(message, current, trueLabel, falseLabel) {
12728
13045
  { value: "false", label: falseLabel }
12729
13046
  ], current ? "true" : "false").then((value) => value === "true");
12730
13047
  }
12731
- async function promptForConfigEdits(current, hasExistingOllamaApiKey) {
13048
+ async function promptForConfigEdits(current, hasExistingOllamaApiKey, hasExistingOpenRouterApiKey) {
12732
13049
  const workflow = await selectCurrentValue("Workflow mode", WORKFLOW_OPTIONS, current.workflow);
12733
13050
  const role = await selectCurrentValue("Your role in this clone", ROLE_OPTIONS, current.role);
12734
13051
  const mainBranch = await inputPrompt("Main branch name", current.mainBranch);
@@ -12749,6 +13066,8 @@ async function promptForConfigEdits(current, hasExistingOllamaApiKey) {
12749
13066
  let aiModel;
12750
13067
  let ollamaApiKeyAction = "keep";
12751
13068
  let ollamaApiKey;
13069
+ let openrouterApiKeyAction = "keep";
13070
+ let openrouterApiKey;
12752
13071
  if (aiEnabled) {
12753
13072
  const currentProvider = current.aiProvider ?? "copilot";
12754
13073
  aiProvider = await selectCurrentValue("AI provider", AI_PROVIDER_OPTIONS, currentProvider);
@@ -12780,16 +13099,72 @@ async function promptForConfigEdits(current, hasExistingOllamaApiKey) {
12780
13099
  }
12781
13100
  const modelLookupApiKey = ollamaApiKeyAction === "set" ? ollamaApiKey ?? null : ollamaApiKeyAction === "keep" ? await getOllamaCloudApiKey() : null;
12782
13101
  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?");
13102
+ if (hasExistingOpenRouterApiKey) {
13103
+ const shouldDeleteOpenRouterKey = await confirmPrompt("Delete the stored OpenRouter API key from the local secrets store?");
13104
+ if (shouldDeleteOpenRouterKey) {
13105
+ openrouterApiKeyAction = "delete";
13106
+ }
13107
+ }
13108
+ } else if (aiProvider === "openrouter") {
13109
+ if (hasExistingOpenRouterApiKey) {
13110
+ const apiKeyChoice = await selectPrompt("OpenRouter API key", [
13111
+ "Keep existing stored key",
13112
+ "Replace stored key",
13113
+ "Delete stored key"
13114
+ ]);
13115
+ if (apiKeyChoice === "Replace stored key") {
13116
+ openrouterApiKey = (await passwordPrompt("Enter the new OpenRouter API key")).trim();
13117
+ if (!openrouterApiKey) {
13118
+ throw new Error("OpenRouter API key cannot be empty when replacing the stored key.");
13119
+ }
13120
+ openrouterApiKeyAction = "set";
13121
+ } else if (apiKeyChoice === "Delete stored key") {
13122
+ openrouterApiKeyAction = "delete";
13123
+ }
13124
+ } else {
13125
+ const addApiKey = await confirmPrompt("No OpenRouter API key is stored. Add one now?");
13126
+ if (addApiKey) {
13127
+ openrouterApiKey = (await passwordPrompt("Enter your OpenRouter API key")).trim();
13128
+ if (!openrouterApiKey) {
13129
+ throw new Error("OpenRouter API key cannot be empty when enabling OpenRouter.");
13130
+ }
13131
+ openrouterApiKeyAction = "set";
13132
+ }
13133
+ }
13134
+ const modelLookupApiKey = openrouterApiKeyAction === "set" ? openrouterApiKey ?? null : openrouterApiKeyAction === "keep" ? await getOpenRouterApiKey() : null;
13135
+ aiModel = await promptForOpenRouterModelSelection(modelLookupApiKey, current.aiProvider === "openrouter" ? current.aiModel ?? DEFAULT_OPENROUTER_MODEL : DEFAULT_OPENROUTER_MODEL);
13136
+ if (hasExistingOllamaApiKey) {
13137
+ const shouldDeleteStoredKey = await confirmPrompt("Delete the stored Ollama Cloud API key from the local secrets store?");
13138
+ if (shouldDeleteStoredKey) {
13139
+ ollamaApiKeyAction = "delete";
13140
+ }
13141
+ }
13142
+ } else {
13143
+ if (hasExistingOllamaApiKey) {
13144
+ const shouldDeleteStoredKey = await confirmPrompt("Delete the stored Ollama Cloud API key from the local secrets store?");
13145
+ if (shouldDeleteStoredKey) {
13146
+ ollamaApiKeyAction = "delete";
13147
+ }
13148
+ }
13149
+ if (hasExistingOpenRouterApiKey) {
13150
+ const shouldDeleteOpenRouterKey = await confirmPrompt("Delete the stored OpenRouter API key from the local secrets store?");
13151
+ if (shouldDeleteOpenRouterKey) {
13152
+ openrouterApiKeyAction = "delete";
13153
+ }
13154
+ }
13155
+ }
13156
+ } else {
13157
+ if (hasExistingOllamaApiKey) {
13158
+ const shouldDeleteStoredKey = await confirmPrompt("AI is disabled. Delete the stored Ollama Cloud API key from the local secrets store?");
12785
13159
  if (shouldDeleteStoredKey) {
12786
13160
  ollamaApiKeyAction = "delete";
12787
13161
  }
12788
13162
  }
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";
13163
+ if (hasExistingOpenRouterApiKey) {
13164
+ const shouldDeleteOpenRouterKey = await confirmPrompt("AI is disabled. Delete the stored OpenRouter API key from the local secrets store?");
13165
+ if (shouldDeleteOpenRouterKey) {
13166
+ openrouterApiKeyAction = "delete";
13167
+ }
12793
13168
  }
12794
13169
  }
12795
13170
  return {
@@ -12808,17 +13183,17 @@ async function promptForConfigEdits(current, hasExistingOllamaApiKey) {
12808
13183
  showTips
12809
13184
  }),
12810
13185
  ollamaApiKeyAction,
12811
- ollamaApiKey
13186
+ ollamaApiKey,
13187
+ openrouterApiKeyAction,
13188
+ openrouterApiKey
12812
13189
  };
12813
13190
  }
12814
- async function applyOllamaApiKeyEdit(result) {
13191
+ async function applyApiKeyEdits(result) {
12815
13192
  if (result.ollamaApiKeyAction === "set" && result.ollamaApiKey) {
12816
13193
  await setOllamaCloudApiKey(result.ollamaApiKey);
12817
13194
  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") {
13195
+ info(`Secrets path: ${import_picocolors10.default.bold(getSecretsStorePath())}`);
13196
+ } else if (result.ollamaApiKeyAction === "delete") {
12822
13197
  const deleted = await deleteOllamaCloudApiKey();
12823
13198
  if (deleted) {
12824
13199
  success("Deleted stored Ollama Cloud API key.");
@@ -12826,33 +13201,60 @@ async function applyOllamaApiKeyEdit(result) {
12826
13201
  info("No stored Ollama Cloud API key was found to delete.");
12827
13202
  }
12828
13203
  }
13204
+ if (result.openrouterApiKeyAction === "set" && result.openrouterApiKey) {
13205
+ await setOpenRouterApiKey(result.openrouterApiKey);
13206
+ success("Stored OpenRouter API key in the local secrets store.");
13207
+ info(`Secrets path: ${import_picocolors10.default.bold(getSecretsStorePath())}`);
13208
+ } else if (result.openrouterApiKeyAction === "delete") {
13209
+ const deleted = await deleteOpenRouterApiKey();
13210
+ if (deleted) {
13211
+ success("Deleted stored OpenRouter API key.");
13212
+ } else {
13213
+ info("No stored OpenRouter API key was found to delete.");
13214
+ }
13215
+ }
12829
13216
  }
12830
13217
  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)}`);
13218
+ info(`Config source: ${import_picocolors10.default.bold(snapshot.source)}`);
13219
+ info(`Config path: ${import_picocolors10.default.bold(snapshot.location)}`);
13220
+ info(`Workflow: ${import_picocolors10.default.bold(snapshot.workflowLabel)}`);
13221
+ info(`Convention: ${import_picocolors10.default.bold(snapshot.commitConventionLabel)}`);
13222
+ info(`Role: ${import_picocolors10.default.bold(snapshot.role)}`);
12836
13223
  if (snapshot.devBranch) {
12837
- info(`Main: ${import_picocolors11.default.bold(snapshot.mainBranch)} | Dev: ${import_picocolors11.default.bold(snapshot.devBranch)}`);
13224
+ info(`Main: ${import_picocolors10.default.bold(snapshot.mainBranch)} | Dev: ${import_picocolors10.default.bold(snapshot.devBranch)}`);
12838
13225
  } else {
12839
- info(`Main: ${import_picocolors11.default.bold(snapshot.mainBranch)}`);
13226
+ info(`Main: ${import_picocolors10.default.bold(snapshot.mainBranch)}`);
12840
13227
  }
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")}`);
13228
+ info(`Origin: ${import_picocolors10.default.bold(snapshot.origin)} | Upstream: ${import_picocolors10.default.bold(snapshot.upstream)}`);
13229
+ info(`Branch prefixes: ${import_picocolors10.default.bold(snapshot.branchPrefixes.join(", "))}`);
13230
+ info(`Guides: ${import_picocolors10.default.bold(snapshot.showTips ? "shown" : "hidden")}`);
13231
+ info(`AI: ${import_picocolors10.default.bold(snapshot.ai.enabled ? "enabled" : "disabled")}`);
12845
13232
  if (snapshot.ai.enabled && snapshot.ai.providerLabel) {
12846
- info(`AI provider: ${import_picocolors11.default.bold(snapshot.ai.providerLabel)}`);
13233
+ info(`AI provider: ${import_picocolors10.default.bold(snapshot.ai.providerLabel)}`);
12847
13234
  if (snapshot.ai.model) {
12848
- info(`AI model: ${import_picocolors11.default.bold(snapshot.ai.model)}`);
13235
+ info(`AI model: ${import_picocolors10.default.bold(snapshot.ai.model)}`);
12849
13236
  }
12850
13237
  if (snapshot.ai.provider === "ollama-cloud") {
12851
- info(`Ollama Cloud API key: ${import_picocolors11.default.bold(snapshot.ai.ollamaCloudApiKeyPresent ? "stored" : "missing")}`);
13238
+ info(`Ollama Cloud API key: ${import_picocolors10.default.bold(snapshot.ai.ollamaCloudApiKeyPresent ? "stored" : "missing")}`);
12852
13239
  if (snapshot.ai.secretsPath) {
12853
- info(`Secrets path: ${import_picocolors11.default.bold(snapshot.ai.secretsPath)}`);
13240
+ info(`Secrets path: ${import_picocolors10.default.bold(snapshot.ai.secretsPath)}`);
12854
13241
  }
12855
13242
  }
13243
+ if (snapshot.ai.provider === "openrouter") {
13244
+ info(`OpenRouter API key: ${import_picocolors10.default.bold(snapshot.ai.openrouterApiKeyPresent ? "stored" : "missing")}`);
13245
+ if (snapshot.ai.secretsPath) {
13246
+ info(`Secrets path: ${import_picocolors10.default.bold(snapshot.ai.secretsPath)}`);
13247
+ }
13248
+ }
13249
+ }
13250
+ }
13251
+ function printGlobalConfigSummary(snapshot) {
13252
+ info(`Global config path: ${import_picocolors10.default.bold(snapshot.location)}`);
13253
+ info(`Global defaults file: ${import_picocolors10.default.bold(snapshot.exists ? "present" : "missing (using built-ins)")}`);
13254
+ info(`AI default: ${import_picocolors10.default.bold(snapshot.ai.enabled ? "enabled" : "disabled")}`);
13255
+ info(`AI provider: ${import_picocolors10.default.bold(snapshot.ai.providerLabel)}`);
13256
+ if (snapshot.ai.model) {
13257
+ info(`AI model: ${import_picocolors10.default.bold(snapshot.ai.model)}`);
12856
13258
  }
12857
13259
  }
12858
13260
  var config_default = defineCommand({
@@ -12861,6 +13263,11 @@ var config_default = defineCommand({
12861
13263
  description: "Inspect or edit the repo config without rerunning setup"
12862
13264
  },
12863
13265
  args: {
13266
+ global: {
13267
+ type: "boolean",
13268
+ description: "Read or edit global defaults instead of repo config",
13269
+ default: false
13270
+ },
12864
13271
  json: {
12865
13272
  type: "boolean",
12866
13273
  description: "Print the active repo config as JSON with metadata",
@@ -12873,15 +13280,44 @@ var config_default = defineCommand({
12873
13280
  }
12874
13281
  },
12875
13282
  async run({ args }) {
12876
- if (!await isGitRepo()) {
12877
- error("Not inside a git repository.");
12878
- process.exit(1);
12879
- }
12880
13283
  if (args.json && args.edit) {
12881
13284
  error("Use either --json or --edit, not both at the same time.");
12882
13285
  process.exit(1);
12883
13286
  }
12884
13287
  await projectHeading("config", "\u2699\uFE0F");
13288
+ if (args.global) {
13289
+ const currentGlobal = readGlobalConfig() ?? {};
13290
+ if (args.edit) {
13291
+ try {
13292
+ const editResult = await promptForGlobalConfigEdits(currentGlobal);
13293
+ writeGlobalConfig(normalizeGlobalConfig(editResult.config));
13294
+ success("Updated global defaults.");
13295
+ const snapshot3 = buildGlobalConfigSnapshot(readGlobalConfig() ?? {});
13296
+ printGlobalConfigSummary(snapshot3);
13297
+ if (args.json) {
13298
+ console.log(JSON.stringify(snapshot3, null, 2));
13299
+ }
13300
+ return;
13301
+ } catch (err) {
13302
+ error(err instanceof Error ? err.message : String(err));
13303
+ process.exit(1);
13304
+ }
13305
+ }
13306
+ const snapshot2 = buildGlobalConfigSnapshot(currentGlobal);
13307
+ if (args.json) {
13308
+ console.log(JSON.stringify(snapshot2, null, 2));
13309
+ return;
13310
+ }
13311
+ printGlobalConfigSummary(snapshot2);
13312
+ console.log();
13313
+ console.log(` ${import_picocolors10.default.dim("Run `cn config --global --edit` to update global defaults.")}`);
13314
+ console.log();
13315
+ return;
13316
+ }
13317
+ if (!await isGitRepo()) {
13318
+ error("Not inside a git repository.");
13319
+ process.exit(1);
13320
+ }
12885
13321
  if (!configExists()) {
12886
13322
  error("No repo config found. Run `cn setup` first.");
12887
13323
  process.exit(1);
@@ -12898,14 +13334,15 @@ var config_default = defineCommand({
12898
13334
  }
12899
13335
  if (args.edit) {
12900
13336
  try {
12901
- const editResult = await promptForConfigEdits(config, await hasOllamaCloudApiKey());
13337
+ const editResult = await promptForConfigEdits(config, await hasOllamaCloudApiKey(), await hasOpenRouterApiKey());
12902
13338
  writeConfig(editResult.config);
12903
- await applyOllamaApiKeyEdit(editResult);
13339
+ await applyApiKeyEdits(editResult);
12904
13340
  success("Updated repo config.");
12905
13341
  printConfigSummary(buildConfigSnapshot(editResult.config, {
12906
13342
  source,
12907
13343
  location: getConfigLocationLabel(),
12908
13344
  hasOllamaCloudApiKey: await hasOllamaCloudApiKey(),
13345
+ hasOpenRouterApiKey: await hasOpenRouterApiKey(),
12909
13346
  secretsPath: getSecretsStorePath()
12910
13347
  }));
12911
13348
  return;
@@ -12918,6 +13355,7 @@ var config_default = defineCommand({
12918
13355
  source,
12919
13356
  location: getConfigLocationLabel(),
12920
13357
  hasOllamaCloudApiKey: await hasOllamaCloudApiKey(),
13358
+ hasOpenRouterApiKey: await hasOpenRouterApiKey(),
12921
13359
  secretsPath: getSecretsStorePath()
12922
13360
  });
12923
13361
  if (args.json) {
@@ -12926,60 +13364,168 @@ var config_default = defineCommand({
12926
13364
  }
12927
13365
  printConfigSummary(snapshot);
12928
13366
  console.log();
12929
- console.log(` ${import_picocolors11.default.dim("Run `cn config --edit` to update these settings.")}`);
13367
+ console.log(` ${import_picocolors10.default.dim("Run `cn config --edit` to update these settings.")}`);
12930
13368
  console.log();
12931
13369
  }
12932
13370
  });
12933
13371
 
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.8.0-dev.7db6dea",
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"
12947
- },
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"
13372
+ // src/commands/discard.ts
13373
+ var import_picocolors11 = __toESM(require_picocolors(), 1);
13374
+ var discard_default = defineCommand({
13375
+ meta: {
13376
+ name: "discard",
13377
+ description: "Discard the current feature branch and return to the base branch"
12963
13378
  },
12964
- engines: {
12965
- bun: ">=1.0"
13379
+ args: {
13380
+ force: {
13381
+ type: "boolean",
13382
+ alias: "f",
13383
+ description: "Skip confirmation and discard immediately",
13384
+ default: false
13385
+ }
12966
13386
  },
12967
- keywords: [
12968
- "git",
12969
- "workflow",
12970
- "squash-merge",
12971
- "sync",
12972
- "cli",
12973
- "contribute",
12974
- "fork",
12975
- "dev-branch",
12976
- "clean-commit"
12977
- ],
12978
- author: "Waren Gonzaga",
12979
- license: "GPL-3.0",
12980
- repository: {
12981
- type: "git",
12982
- url: "git+https://github.com/warengonzaga/contribute-now.git"
13387
+ async run({ args }) {
13388
+ if (!await isGitRepo()) {
13389
+ error("Not inside a git repository.");
13390
+ process.exit(1);
13391
+ }
13392
+ await assertCleanGitState("discarding a branch");
13393
+ const config = readConfig();
13394
+ if (!config) {
13395
+ error("No repo config found. Run `cn setup` first.");
13396
+ process.exit(1);
13397
+ }
13398
+ const currentBranch = await getCurrentBranch();
13399
+ const baseBranch = getBaseBranch(config);
13400
+ await projectHeading("discard", "\uD83D\uDDD1\uFE0F");
13401
+ if (isBranchProtected(currentBranch, config)) {
13402
+ error(`${import_picocolors11.default.bold(currentBranch)} is a protected branch and cannot be discarded.`);
13403
+ info(`Switch to a feature branch first, then run ${import_picocolors11.default.bold("cn discard")}.`);
13404
+ process.exit(1);
13405
+ }
13406
+ if (currentBranch === baseBranch) {
13407
+ info(`You are already on ${import_picocolors11.default.bold(baseBranch)}.`);
13408
+ process.exit(0);
13409
+ }
13410
+ const { origin } = config;
13411
+ const localWork = await hasLocalWork(origin, currentBranch);
13412
+ const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
13413
+ if (hasWork) {
13414
+ if (localWork.uncommitted) {
13415
+ warn("You have uncommitted changes in your working tree.");
13416
+ }
13417
+ if (localWork.unpushedCommits > 0) {
13418
+ warn(`You have ${import_picocolors11.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on this branch.`);
13419
+ }
13420
+ warn("Discarding this branch will permanently lose that work.");
13421
+ const SAVE_FIRST = "Save my changes first (cn save), then discard";
13422
+ const DISCARD_ANYWAY = "Discard anyway \u2014 I do not need this work";
13423
+ const CANCEL = "Keep the branch, take me back";
13424
+ const action = await selectPrompt("This branch has unsaved work. What would you like to do?", [SAVE_FIRST, DISCARD_ANYWAY, CANCEL]);
13425
+ if (action === CANCEL) {
13426
+ info("Discard cancelled. Your branch is untouched.");
13427
+ process.exit(0);
13428
+ }
13429
+ if (action === SAVE_FIRST) {
13430
+ if (!localWork.uncommitted) {
13431
+ info("No uncommitted changes to stash \u2014 unpushed commits will still be lost.");
13432
+ const confirm = await confirmPrompt("Continue discarding the branch?");
13433
+ if (!confirm) {
13434
+ info("Discard cancelled.");
13435
+ process.exit(0);
13436
+ }
13437
+ } else {
13438
+ const stashResult = await stashChanges(`work-in-progress on ${currentBranch}`);
13439
+ if (stashResult.exitCode !== 0) {
13440
+ error(`Failed to save changes: ${stashResult.stderr}`);
13441
+ process.exit(1);
13442
+ }
13443
+ success(`Changes saved. Use ${import_picocolors11.default.bold("cn save --restore")} to bring them back.`);
13444
+ }
13445
+ }
13446
+ } else if (!args.force) {
13447
+ const confirmed = await confirmPrompt(`Discard ${import_picocolors11.default.bold(currentBranch)} and return to ${import_picocolors11.default.bold(baseBranch)}?`);
13448
+ if (!confirmed) {
13449
+ info("Discard cancelled.");
13450
+ process.exit(0);
13451
+ }
13452
+ }
13453
+ const upstreamRef = await getUpstreamRef();
13454
+ let deleteRemote = false;
13455
+ if (upstreamRef) {
13456
+ deleteRemote = await confirmPrompt(`Also delete the remote branch ${import_picocolors11.default.bold(upstreamRef)}?`);
13457
+ }
13458
+ const checkoutResult = await checkoutBranch(baseBranch);
13459
+ if (checkoutResult.exitCode !== 0) {
13460
+ error(`Failed to switch to ${import_picocolors11.default.bold(baseBranch)}: ${checkoutResult.stderr}`);
13461
+ process.exit(1);
13462
+ }
13463
+ const deleteResult = await forceDeleteBranch(currentBranch);
13464
+ if (deleteResult.exitCode !== 0) {
13465
+ error(`Failed to delete branch ${import_picocolors11.default.bold(currentBranch)}: ${deleteResult.stderr}`);
13466
+ process.exit(1);
13467
+ }
13468
+ success(`Discarded ${import_picocolors11.default.bold(currentBranch)} and switched back to ${import_picocolors11.default.bold(baseBranch)}`);
13469
+ if (deleteRemote) {
13470
+ const remoteDeleteResult = await deleteRemoteBranch(origin, currentBranch);
13471
+ if (remoteDeleteResult.exitCode !== 0) {
13472
+ warn(`Could not delete remote branch: ${remoteDeleteResult.stderr.trim()}`);
13473
+ } else {
13474
+ success(`Deleted remote branch ${import_picocolors11.default.bold(`${origin}/${currentBranch}`)}`);
13475
+ }
13476
+ }
13477
+ }
13478
+ });
13479
+
13480
+ // src/commands/doctor.ts
13481
+ import { execFile as execFileCb3 } from "child_process";
13482
+ var import_picocolors12 = __toESM(require_picocolors(), 1);
13483
+ // package.json
13484
+ var package_default = {
13485
+ name: "contribute-now",
13486
+ version: "0.8.0-dev.a835394",
13487
+ description: "Developer CLI that automates git workflows \u2014 branching, syncing, committing, and PRs \u2014 with multi-workflow and commit convention support.",
13488
+ type: "module",
13489
+ bin: {
13490
+ contrib: "dist/cli.js",
13491
+ contribute: "dist/cli.js",
13492
+ cn: "dist/cli.js"
13493
+ },
13494
+ files: [
13495
+ "dist"
13496
+ ],
13497
+ scripts: {
13498
+ build: "bun build src/cli.ts --outfile dist/cli.js --target bun && bun run scripts/add-shebang.mjs",
13499
+ cli: "bun run src/cli.ts --",
13500
+ dev: "bun src/cli.ts",
13501
+ test: "bun test",
13502
+ lint: "biome check .",
13503
+ "lint:fix": "biome check --write .",
13504
+ format: "biome format --write .",
13505
+ "landing:install": "bun install --cwd landing",
13506
+ "landing:dev": "bun run --cwd landing dev",
13507
+ "landing:build": "bun run --cwd landing build",
13508
+ "landing:preview": "bun run --cwd landing preview"
13509
+ },
13510
+ engines: {
13511
+ bun: ">=1.0"
13512
+ },
13513
+ keywords: [
13514
+ "git",
13515
+ "workflow",
13516
+ "squash-merge",
13517
+ "sync",
13518
+ "cli",
13519
+ "contribute",
13520
+ "fork",
13521
+ "dev-branch",
13522
+ "clean-commit"
13523
+ ],
13524
+ author: "Waren Gonzaga",
13525
+ license: "GPL-3.0",
13526
+ repository: {
13527
+ type: "git",
13528
+ url: "git+https://github.com/warengonzaga/contribute-now.git"
12983
13529
  },
12984
13530
  dependencies: {
12985
13531
  "@clack/prompts": "^1.0.1",
@@ -12995,6 +13541,9 @@ var package_default = {
12995
13541
  }
12996
13542
  };
12997
13543
 
13544
+ // src/commands/doctor.ts
13545
+ init_gh();
13546
+
12998
13547
  // src/utils/remote.ts
12999
13548
  function parseRepoFromUrl(url) {
13000
13549
  const httpsMatch = url.match(/https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
@@ -13190,6 +13739,15 @@ async function configSection() {
13190
13739
  detail: hasSecretsStore() ? "stored in the local secrets store" : "run `cn setup` to save it"
13191
13740
  });
13192
13741
  }
13742
+ if (aiConfig.provider === "openrouter") {
13743
+ const hasApiKey = await hasOpenRouterApiKey();
13744
+ checks.push({
13745
+ label: hasApiKey ? "OpenRouter API key present" : "OpenRouter API key missing",
13746
+ ok: true,
13747
+ warning: !hasApiKey,
13748
+ detail: hasSecretsStore() ? "stored in the local secrets store" : "run `cn setup` to save it"
13749
+ });
13750
+ }
13193
13751
  }
13194
13752
  if (hasDevBranch(config.workflow)) {
13195
13753
  checks.push({
@@ -13342,161 +13900,763 @@ var doctor_default = defineCommand({
13342
13900
  default: false
13343
13901
  }
13344
13902
  },
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;
13903
+ async run({ args }) {
13904
+ const isJson = args.json;
13905
+ const [tool, deps, config, git, fork, workflow] = await Promise.all([
13906
+ toolSection(),
13907
+ depsSection(),
13908
+ configSection(),
13909
+ gitSection(),
13910
+ forkSection(),
13911
+ workflowSection()
13912
+ ]);
13913
+ const env2 = envSection();
13914
+ const report = {
13915
+ sections: [tool, deps, config, git, fork, workflow, env2]
13916
+ };
13917
+ if (isJson) {
13918
+ console.log(toJson(report));
13919
+ return;
13920
+ }
13921
+ await projectHeading("doctor", "\uD83E\uDE7A");
13922
+ printReport(report);
13923
+ const total = report.sections.flatMap((s2) => s2.checks);
13924
+ const failures = total.filter((c3) => !c3.ok);
13925
+ const warnings = total.filter((c3) => c3.ok && c3.warning);
13926
+ if (failures.length === 0 && warnings.length === 0) {
13927
+ console.log(` ${import_picocolors12.default.green("All checks passed!")} No issues detected.
13928
+ `);
13929
+ } else {
13930
+ if (failures.length > 0) {
13931
+ console.log(` ${import_picocolors12.default.red(`${failures.length} issue${failures.length !== 1 ? "s" : ""} found.`)}`);
13932
+ }
13933
+ if (warnings.length > 0) {
13934
+ console.log(` ${import_picocolors12.default.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}.`)}`);
13935
+ }
13936
+ console.log();
13937
+ }
13938
+ }
13939
+ });
13940
+
13941
+ // src/commands/hook.ts
13942
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
13943
+ import { join as join6 } from "path";
13944
+ var import_picocolors13 = __toESM(require_picocolors(), 1);
13945
+ var HOOK_MARKER = "# managed by contribute-now";
13946
+ function getHooksDir(cwd = process.cwd()) {
13947
+ return join6(cwd, ".git", "hooks");
13948
+ }
13949
+ function getHookPath(cwd = process.cwd()) {
13950
+ return join6(getHooksDir(cwd), "commit-msg");
13951
+ }
13952
+ function generateHookScript() {
13953
+ return `#!/bin/sh
13954
+ ${HOOK_MARKER}
13955
+ # Validates commit messages against your configured convention.
13956
+ # Install: cn hook install
13957
+ # Uninstall: cn hook uninstall
13958
+
13959
+ commit_msg_file="$1"
13960
+ commit_msg=$(head -1 "$commit_msg_file")
13961
+
13962
+ # Skip merge commits and fixup/squash commits
13963
+ case "$commit_msg" in
13964
+ Merge\\ *|fixup!*|squash!*|amend!*) exit 0 ;;
13965
+ esac
13966
+
13967
+ # Detect available package runner
13968
+ if command -v cn >/dev/null 2>&1; then
13969
+ cn validate --file "$commit_msg_file"
13970
+ elif command -v bunx >/dev/null 2>&1; then
13971
+ bunx cn validate --file "$commit_msg_file"
13972
+ else
13973
+ echo "Warning: Neither cn nor bunx is available. Skipping commit message validation."
13974
+ exit 0
13975
+ fi
13976
+ `;
13977
+ }
13978
+ var hook_default = defineCommand({
13979
+ meta: {
13980
+ name: "hook",
13981
+ description: "Install or uninstall the commit-msg git hook"
13982
+ },
13983
+ args: {
13984
+ action: {
13985
+ type: "positional",
13986
+ description: "Action to perform: install or uninstall",
13987
+ required: true
13988
+ }
13989
+ },
13990
+ async run({ args }) {
13991
+ if (!await isGitRepo()) {
13992
+ error("Not inside a git repository.");
13993
+ process.exit(1);
13994
+ }
13995
+ const action = args.action;
13996
+ if (action !== "install" && action !== "uninstall") {
13997
+ error(`Unknown action "${action}". Use "install" or "uninstall".`);
13998
+ process.exit(1);
13999
+ }
14000
+ if (action === "install") {
14001
+ await installHook();
14002
+ } else {
14003
+ await uninstallHook();
14004
+ }
14005
+ }
14006
+ });
14007
+ async function installHook() {
14008
+ await projectHeading("hook install", "\uD83E\uDE9D");
14009
+ const config = readConfig();
14010
+ if (!config) {
14011
+ error("No repo config found. Run `cn setup` first.");
14012
+ process.exit(1);
14013
+ }
14014
+ if (config.commitConvention === "none") {
14015
+ warn('Commit convention is set to "none". No hook to install.');
14016
+ info("Change your convention with `cn setup` first.", "");
14017
+ process.exit(0);
14018
+ }
14019
+ const hookPath = getHookPath();
14020
+ const hooksDir = getHooksDir();
14021
+ if (existsSync6(hookPath)) {
14022
+ const existing = readFileSync5(hookPath, "utf-8");
14023
+ if (!existing.includes(HOOK_MARKER)) {
14024
+ error("A commit-msg hook already exists and was not installed by contribute-now.");
14025
+ warn(`Path: ${hookPath}`);
14026
+ warn("Remove it manually or back it up before installing.");
14027
+ process.exit(1);
14028
+ }
14029
+ info("Updating existing contribute-now hook...");
14030
+ }
14031
+ if (!existsSync6(hooksDir)) {
14032
+ mkdirSync5(hooksDir, { recursive: true });
14033
+ }
14034
+ writeFileSync5(hookPath, generateHookScript(), { mode: 493 });
14035
+ success(`commit-msg hook installed.`);
14036
+ info(`Convention: ${import_picocolors13.default.bold(CONVENTION_LABELS[config.commitConvention])}`, "");
14037
+ info(`Path: ${import_picocolors13.default.dim(hookPath)}`, "");
14038
+ warn("Note: hooks can be bypassed with `git commit --no-verify`.");
14039
+ }
14040
+ async function uninstallHook() {
14041
+ await projectHeading("hook uninstall", "\uD83E\uDE9D");
14042
+ const hookPath = getHookPath();
14043
+ if (!existsSync6(hookPath)) {
14044
+ info("No commit-msg hook found. Nothing to uninstall.");
14045
+ return;
14046
+ }
14047
+ const content = readFileSync5(hookPath, "utf-8");
14048
+ if (!content.includes(HOOK_MARKER)) {
14049
+ error("The commit-msg hook was not installed by contribute-now. Leaving it untouched.");
14050
+ process.exit(1);
14051
+ }
14052
+ rmSync2(hookPath);
14053
+ success("commit-msg hook removed.");
14054
+ }
14055
+
14056
+ // src/commands/label.ts
14057
+ init_gh();
14058
+ var import_picocolors14 = __toESM(require_picocolors(), 1);
14059
+
14060
+ // src/utils/label.ts
14061
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync6, statSync as statSync4, writeFileSync as writeFileSync6 } from "fs";
14062
+ import { dirname as dirname5, join as join7, resolve as resolve5 } from "path";
14063
+
14064
+ // src/data/clean-labels.ts
14065
+ var CLEAN_LABELS = [
14066
+ {
14067
+ name: "bug",
14068
+ description: "[Type] Something isn't working [issues, PRs]",
14069
+ color: "d73a4a"
14070
+ },
14071
+ {
14072
+ name: "enhancement",
14073
+ description: "[Type] New feature or improvement to existing functionality [issues, PRs]",
14074
+ color: "1a7f37"
14075
+ },
14076
+ {
14077
+ name: "documentation",
14078
+ description: "[Type] Improvements or additions to docs, README, or guides [issues, PRs]",
14079
+ color: "0075ca"
14080
+ },
14081
+ {
14082
+ name: "refactor",
14083
+ description: "[Type] Code improvement without changing functionality [PRs]",
14084
+ color: "8957e5"
14085
+ },
14086
+ {
14087
+ name: "performance",
14088
+ description: "[Type] Optimization, speed, or resource usage improvements [issues, PRs]",
14089
+ color: "e3795c"
14090
+ },
14091
+ {
14092
+ name: "security",
14093
+ description: "[Type] Security vulnerability or hardening [issues, PRs]",
14094
+ color: "d4a72c"
14095
+ },
14096
+ {
14097
+ name: "blocked",
14098
+ description: "[Status] Waiting on another issue, decision, or external factor [issues]",
14099
+ color: "cf222e"
14100
+ },
14101
+ {
14102
+ name: "needs triage",
14103
+ description: "[Status] New issue \u2014 needs review and categorization [issues]",
14104
+ color: "e16f24"
14105
+ },
14106
+ {
14107
+ name: "awaiting response",
14108
+ description: "[Status] Waiting for more information from the reporter [issues]",
14109
+ color: "1a7ec7"
14110
+ },
14111
+ {
14112
+ name: "ready",
14113
+ description: "[Status] Triaged and ready to be picked up [issues]",
14114
+ color: "2da44e"
14115
+ },
14116
+ {
14117
+ name: "good first issue",
14118
+ description: "[Community] Good for newcomers \u2014 well-scoped and documented [issues]",
14119
+ color: "7057ff"
14120
+ },
14121
+ {
14122
+ name: "help wanted",
14123
+ description: "[Community] Open for community contribution [issues]",
14124
+ color: "0e8a16"
14125
+ },
14126
+ {
14127
+ name: "maintainer only",
14128
+ description: "[Community] Reserved for maintainers \u2014 not open for external contribution [issues, PRs]",
14129
+ color: "b60205"
14130
+ },
14131
+ {
14132
+ name: "duplicate",
14133
+ description: "[Resolution] This issue or pull request already exists [issues, PRs]",
14134
+ color: "cfd3d7"
14135
+ },
14136
+ {
14137
+ name: "invalid",
14138
+ description: "[Resolution] This doesn't seem right [issues, PRs]",
14139
+ color: "cfd3d7"
14140
+ },
14141
+ {
14142
+ name: "wontfix",
14143
+ description: "[Resolution] This will not be worked on [issues]",
14144
+ color: "cfd3d7"
14145
+ },
14146
+ {
14147
+ name: "core",
14148
+ description: "[Area] Core logic, business rules, and primary functionality [issues, PRs]",
14149
+ color: "0052cc"
14150
+ },
14151
+ {
14152
+ name: "interface",
14153
+ description: "[Area] User-facing layer \u2014 UI, CLI, API endpoints, or SDK surface [issues, PRs]",
14154
+ color: "5319e7"
14155
+ },
14156
+ {
14157
+ name: "data",
14158
+ description: "[Area] Database, storage, caching, or data models [issues, PRs]",
14159
+ color: "006b75"
14160
+ },
14161
+ {
14162
+ name: "infra",
14163
+ description: "[Area] Build system, CI/CD, deployment, config, and DevOps [issues, PRs]",
14164
+ color: "e16f24"
14165
+ },
14166
+ {
14167
+ name: "testing",
14168
+ description: "[Area] Unit tests, integration tests, E2E, and test tooling [issues, PRs]",
14169
+ color: "1a7f37"
14170
+ }
14171
+ ];
14172
+
14173
+ // src/utils/label.ts
14174
+ var LABEL_CACHE_DIRNAME = "contribute-now";
14175
+ var LABEL_CACHE_FILENAME = "labels.json";
14176
+ function findRepoRoot3(cwd = process.cwd()) {
14177
+ let current = resolve5(cwd);
14178
+ while (true) {
14179
+ if (existsSync7(join7(current, ".git"))) {
14180
+ return current;
14181
+ }
14182
+ const parent = dirname5(current);
14183
+ if (parent === current) {
14184
+ return null;
14185
+ }
14186
+ current = parent;
14187
+ }
14188
+ }
14189
+ function resolveGitDir3(cwd = process.cwd()) {
14190
+ const repoRoot = findRepoRoot3(cwd);
14191
+ if (!repoRoot) {
14192
+ return null;
14193
+ }
14194
+ const dotGitPath = join7(repoRoot, ".git");
14195
+ try {
14196
+ const stat = statSync4(dotGitPath);
14197
+ if (stat.isDirectory()) {
14198
+ return dotGitPath;
14199
+ }
14200
+ if (!stat.isFile()) {
14201
+ return null;
14202
+ }
14203
+ const content = readFileSync6(dotGitPath, "utf-8").trim();
14204
+ const match = /^gitdir:\s*(.+)$/i.exec(content);
14205
+ if (!match) {
14206
+ return null;
14207
+ }
14208
+ return resolve5(repoRoot, match[1].trim());
14209
+ } catch {
14210
+ return null;
14211
+ }
14212
+ }
14213
+ function getLabelCachePath(cwd = process.cwd()) {
14214
+ const gitDir = resolveGitDir3(cwd);
14215
+ if (!gitDir) {
14216
+ return null;
14217
+ }
14218
+ return join7(gitDir, LABEL_CACHE_DIRNAME, LABEL_CACHE_FILENAME);
14219
+ }
14220
+ function readLabelCache(cwd = process.cwd()) {
14221
+ const cachePath = getLabelCachePath(cwd);
14222
+ if (!cachePath || !existsSync7(cachePath)) {
14223
+ return null;
14224
+ }
14225
+ try {
14226
+ const raw = JSON.parse(readFileSync6(cachePath, "utf-8"));
14227
+ if (!Array.isArray(raw.labels) || typeof raw.fetchedAt !== "string" || raw.source !== "clean-labels" && raw.source !== "repo") {
14228
+ return null;
14229
+ }
14230
+ const validatedLabels = [];
14231
+ for (const entry of raw.labels) {
14232
+ if (typeof entry !== "object" || entry === null || typeof entry.name !== "string" || !entry.name) {
14233
+ continue;
14234
+ }
14235
+ const e3 = entry;
14236
+ validatedLabels.push({
14237
+ name: e3.name.trim(),
14238
+ description: typeof e3.description === "string" ? e3.description.trim() : "",
14239
+ color: typeof e3.color === "string" ? e3.color.trim() : ""
14240
+ });
14241
+ }
14242
+ if (validatedLabels.length === 0) {
14243
+ return null;
14244
+ }
14245
+ return {
14246
+ labels: validatedLabels,
14247
+ source: raw.source,
14248
+ fetchedAt: raw.fetchedAt
14249
+ };
14250
+ } catch {
14251
+ return null;
14252
+ }
14253
+ }
14254
+ function writeLabelCache(cache, cwd = process.cwd()) {
14255
+ const cachePath = getLabelCachePath(cwd);
14256
+ if (!cachePath) {
14257
+ return;
14258
+ }
14259
+ mkdirSync6(dirname5(cachePath), { recursive: true });
14260
+ writeFileSync6(cachePath, `${JSON.stringify(cache, null, 2)}
14261
+ `, "utf-8");
14262
+ }
14263
+ function normalizeLabelName(name) {
14264
+ return name.toLowerCase().trim();
14265
+ }
14266
+ function isCleanLabelsMatch(repoLabels) {
14267
+ const cleanNames = new Set(CLEAN_LABELS.map((l2) => normalizeLabelName(l2.name)));
14268
+ const repoNames = new Set(repoLabels.map((l2) => normalizeLabelName(l2.name)));
14269
+ if (cleanNames.size !== repoNames.size) {
14270
+ return false;
14271
+ }
14272
+ for (const name of cleanNames) {
14273
+ if (!repoNames.has(name)) {
14274
+ return false;
14275
+ }
14276
+ }
14277
+ return true;
14278
+ }
14279
+ function buildEffectiveLabelSource(repoLabels) {
14280
+ if (isCleanLabelsMatch(repoLabels)) {
14281
+ return {
14282
+ labels: CLEAN_LABELS.map((cl) => ({
14283
+ name: cl.name,
14284
+ description: cl.description,
14285
+ color: cl.color
14286
+ })),
14287
+ source: "clean-labels"
14288
+ };
14289
+ }
14290
+ return { labels: repoLabels, source: "repo" };
14291
+ }
14292
+ async function syncLabelCache(cwd = process.cwd()) {
14293
+ const { getRepoLabels: getRepoLabels2 } = await Promise.resolve().then(() => (init_gh(), exports_gh));
14294
+ const repoLabels = await getRepoLabels2();
14295
+ if (repoLabels.length === 0) {
14296
+ return null;
14297
+ }
14298
+ const { labels, source } = buildEffectiveLabelSource(repoLabels);
14299
+ const cache = {
14300
+ labels,
14301
+ source,
14302
+ fetchedAt: new Date().toISOString()
14303
+ };
14304
+ writeLabelCache(cache, cwd);
14305
+ return cache;
14306
+ }
14307
+ async function getActiveLabels(cwd = process.cwd(), force = false) {
14308
+ if (!force) {
14309
+ const cached = readLabelCache(cwd);
14310
+ if (cached) {
14311
+ return cached;
14312
+ }
14313
+ }
14314
+ return syncLabelCache(cwd);
14315
+ }
14316
+ function parseLabelsCsv(csv) {
14317
+ return csv.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
14318
+ }
14319
+ function validateLabels(requested, available) {
14320
+ const availableNormalized = new Map;
14321
+ for (const label of available) {
14322
+ availableNormalized.set(normalizeLabelName(label.name), label.name);
14323
+ }
14324
+ const valid = [];
14325
+ const invalid = [];
14326
+ for (const req of requested) {
14327
+ const normalized = normalizeLabelName(req);
14328
+ const canonical = availableNormalized.get(normalized);
14329
+ if (canonical !== undefined) {
14330
+ valid.push(canonical);
14331
+ } else {
14332
+ invalid.push(req);
14333
+ }
14334
+ }
14335
+ return { valid, invalid };
14336
+ }
14337
+ function findCloseMatches(input, available, maxResults = 3) {
14338
+ const needle = normalizeLabelName(input);
14339
+ const scored = available.map((label) => {
14340
+ const haystack = normalizeLabelName(label.name);
14341
+ let score = 0;
14342
+ if (haystack === needle) {
14343
+ score = 100;
14344
+ } else if (haystack.startsWith(needle) || needle.startsWith(haystack)) {
14345
+ score = 60;
14346
+ } else if (haystack.includes(needle) || needle.includes(haystack)) {
14347
+ score = 40;
14348
+ } else {
14349
+ const needleBigrams = toBigrams(needle);
14350
+ const haystackBigrams = toBigrams(haystack);
14351
+ const overlap = [...needleBigrams].filter((b2) => haystackBigrams.has(b2)).length;
14352
+ const union = new Set([...needleBigrams, ...haystackBigrams]).size;
14353
+ score = union > 0 ? Math.round(overlap / union * 30) : 0;
14354
+ }
14355
+ return { name: label.name, score };
14356
+ });
14357
+ return scored.filter((item) => item.score > 0).sort((a2, b2) => b2.score - a2.score).slice(0, maxResults).map((item) => item.name);
14358
+ }
14359
+ function scoreLabelsForContent(content, labels) {
14360
+ const contentTokens = tokenize(content.toLowerCase());
14361
+ const scored = labels.map((label) => {
14362
+ const nameTokens = tokenize(label.name.toLowerCase());
14363
+ const descTokens = [...tokenize(stripDescriptionMeta(label.description).toLowerCase())].filter((t2) => t2.length > 3 && !STOP_WORDS.has(t2));
14364
+ let score = 0;
14365
+ for (const token of nameTokens) {
14366
+ if (contentTokens.has(token))
14367
+ score += 3;
14368
+ }
14369
+ const normalizedName = normalizeLabelName(label.name);
14370
+ if (content.toLowerCase().includes(normalizedName)) {
14371
+ score += 5;
14372
+ }
14373
+ for (const token of descTokens) {
14374
+ if (contentTokens.has(token))
14375
+ score += 1;
14376
+ }
14377
+ return { label, score };
14378
+ });
14379
+ return scored.filter((item) => item.score > 0).sort((a2, b2) => b2.score - a2.score);
14380
+ }
14381
+ var STOP_WORDS = new Set([
14382
+ "the",
14383
+ "and",
14384
+ "for",
14385
+ "with",
14386
+ "this",
14387
+ "that",
14388
+ "from",
14389
+ "into",
14390
+ "over",
14391
+ "under",
14392
+ "have",
14393
+ "will",
14394
+ "been",
14395
+ "more",
14396
+ "than",
14397
+ "also",
14398
+ "when",
14399
+ "what",
14400
+ "which",
14401
+ "some",
14402
+ "such",
14403
+ "its",
14404
+ "not",
14405
+ "only",
14406
+ "any",
14407
+ "each",
14408
+ "both"
14409
+ ]);
14410
+ function stripDescriptionMeta(description) {
14411
+ return description.replace(/^\[[\w\s]+\]\s*/u, "").replace(/\s*\[[\w,\s]+\]$/u, "").trim();
14412
+ }
14413
+ function tokenize(text) {
14414
+ return new Set(text.split(/[\s\-_/,.:;!?()[\]{}"']+/).filter((t2) => t2.length > 0));
14415
+ }
14416
+ function toBigrams(text) {
14417
+ const bigrams = new Set;
14418
+ for (let i2 = 0;i2 < text.length - 1; i2++) {
14419
+ bigrams.add(text.slice(i2, i2 + 2));
14420
+ }
14421
+ return bigrams;
14422
+ }
14423
+
14424
+ // src/commands/label.ts
14425
+ async function requireGitRepository() {
14426
+ if (!await isGitRepo()) {
14427
+ error("Not inside a git repository.");
14428
+ process.exit(1);
14429
+ }
14430
+ }
14431
+ async function requireGhCli() {
14432
+ if (!await checkGhInstalled()) {
14433
+ error("GitHub CLI (gh) is required. Install it at https://cli.github.com");
14434
+ process.exit(1);
14435
+ }
14436
+ if (!await checkGhAuth()) {
14437
+ error("Not authenticated with GitHub CLI. Run `gh auth login` first.");
14438
+ process.exit(1);
14439
+ }
14440
+ }
14441
+ function extractLabelsCsv(rawArgs) {
14442
+ const knownFlagsWithValues = new Set(["--issue", "--pr", "-i", "-p"]);
14443
+ const parts = [];
14444
+ let skipNext = false;
14445
+ for (const arg of rawArgs) {
14446
+ if (skipNext) {
14447
+ skipNext = false;
14448
+ continue;
14449
+ }
14450
+ if (knownFlagsWithValues.has(arg)) {
14451
+ skipNext = true;
14452
+ continue;
14453
+ }
14454
+ if (arg.startsWith("-")) {
14455
+ continue;
14456
+ }
14457
+ parts.push(arg);
14458
+ }
14459
+ return parts.join(" ");
14460
+ }
14461
+ function formatSourceNote(source) {
14462
+ return source === "clean-labels" ? "(source: Clean Labels dataset)" : "(source: repo labels)";
14463
+ }
14464
+ var addCommand = defineCommand({
14465
+ meta: {
14466
+ name: "add",
14467
+ description: "Apply existing labels to an issue or pull request"
14468
+ },
14469
+ args: {
14470
+ issue: {
14471
+ type: "string",
14472
+ alias: "i",
14473
+ description: "Issue number to label"
14474
+ },
14475
+ pr: {
14476
+ type: "string",
14477
+ alias: "p",
14478
+ description: "Pull request number to label"
14479
+ },
14480
+ labels: {
14481
+ type: "positional",
14482
+ description: "Comma-separated label names (spaces are part of label names)",
14483
+ required: false
14484
+ }
14485
+ },
14486
+ async run({ args, rawArgs }) {
14487
+ await requireGitRepository();
14488
+ await requireGhCli();
14489
+ await projectHeading("label add", "\uD83C\uDFF7\uFE0F");
14490
+ const hasIssue = Boolean(args.issue);
14491
+ const hasPr = Boolean(args.pr);
14492
+ if (!hasIssue && !hasPr) {
14493
+ error("Provide a target: --issue <number> or --pr <number>");
14494
+ process.exit(1);
13362
14495
  }
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.`)}`);
14496
+ if (hasIssue && hasPr) {
14497
+ error("Use either --issue or --pr, not both.");
14498
+ process.exit(1);
14499
+ }
14500
+ const targetNumber = Number(hasIssue ? args.issue : args.pr);
14501
+ if (!Number.isInteger(targetNumber) || targetNumber <= 0) {
14502
+ error(`Invalid ${hasIssue ? "issue" : "PR"} number: ${String(hasIssue ? args.issue : args.pr)}`);
14503
+ process.exit(1);
14504
+ }
14505
+ const labelsCsv = extractLabelsCsv(rawArgs);
14506
+ if (!labelsCsv) {
14507
+ error("No labels provided. Pass a comma-separated list after the target flag.");
14508
+ info("Example: cn label add --issue 42 bug,enhancement", "");
14509
+ process.exit(1);
14510
+ }
14511
+ const requested = parseLabelsCsv(labelsCsv);
14512
+ if (requested.length === 0) {
14513
+ error("No valid label names found in input.");
14514
+ process.exit(1);
14515
+ }
14516
+ let cache = await getActiveLabels();
14517
+ if (!cache) {
14518
+ error("Could not load repository labels. Make sure you are authenticated with `gh auth login`.");
14519
+ process.exit(1);
14520
+ }
14521
+ let { valid, invalid } = validateLabels(requested, cache.labels);
14522
+ if (invalid.length > 0) {
14523
+ warn(`Unknown label(s) detected \u2014 resyncing label cache\u2026`);
14524
+ const freshCache = await syncLabelCache();
14525
+ if (freshCache) {
14526
+ cache = freshCache;
14527
+ const revalidated = validateLabels(requested, cache.labels);
14528
+ valid = revalidated.valid;
14529
+ invalid = revalidated.invalid;
13374
14530
  }
13375
- if (warnings.length > 0) {
13376
- console.log(` ${import_picocolors12.default.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}.`)}`);
14531
+ }
14532
+ if (invalid.length > 0) {
14533
+ error(`Unknown label(s): ${invalid.map((l2) => import_picocolors14.default.bold(l2)).join(", ")}`);
14534
+ console.log();
14535
+ for (const label of invalid) {
14536
+ const suggestions = findCloseMatches(label, cache.labels);
14537
+ if (suggestions.length > 0) {
14538
+ info(` Did you mean for "${label}": ${suggestions.map((s2) => import_picocolors14.default.cyan(s2)).join(", ")}`, "");
14539
+ }
13377
14540
  }
13378
14541
  console.log();
14542
+ info(`Available labels: ${cache.labels.map((l2) => import_picocolors14.default.dim(l2.name)).join(", ")}`, "");
14543
+ process.exit(1);
14544
+ }
14545
+ const targetLabel = hasIssue ? `issue #${targetNumber}` : `PR #${targetNumber}`;
14546
+ info(`Applying ${valid.length} label(s) to ${import_picocolors14.default.bold(targetLabel)}\u2026`, "\uD83C\uDFF7\uFE0F");
14547
+ const result = hasIssue ? await addLabelsToIssue(targetNumber, valid) : await addLabelsToPR(targetNumber, valid);
14548
+ if (result.exitCode !== 0) {
14549
+ const stderr = result.stderr.trim();
14550
+ const isDrift = /not found|does not exist/i.test(stderr) || /not found|does not exist/i.test(result.stdout);
14551
+ if (isDrift) {
14552
+ warn("Label not found on remote \u2014 resyncing and retrying\u2026");
14553
+ const freshCache = await syncLabelCache();
14554
+ if (freshCache) {
14555
+ const revalidated = validateLabels(requested, freshCache.labels);
14556
+ if (revalidated.invalid.length === 0) {
14557
+ const retry = hasIssue ? await addLabelsToIssue(targetNumber, revalidated.valid) : await addLabelsToPR(targetNumber, revalidated.valid);
14558
+ if (retry.exitCode === 0) {
14559
+ success(`Applied to ${import_picocolors14.default.bold(targetLabel)}: ${revalidated.valid.map((l2) => import_picocolors14.default.cyan(l2)).join(", ")}`);
14560
+ return;
14561
+ }
14562
+ error(`Retry failed: ${retry.stderr.trim() || retry.stdout.trim()}`);
14563
+ process.exit(1);
14564
+ }
14565
+ }
14566
+ }
14567
+ error(`Failed to apply labels: ${stderr || result.stdout.trim()}`);
14568
+ info("Run `cn label add --help` for usage guidance.", "");
14569
+ process.exit(1);
13379
14570
  }
14571
+ success(`Applied to ${import_picocolors14.default.bold(targetLabel)}: ${valid.map((l2) => import_picocolors14.default.cyan(l2)).join(", ")}`);
14572
+ const sourceNote = formatSourceNote(cache.source);
14573
+ info(sourceNote, "");
13380
14574
  }
13381
14575
  });
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({
14576
+ var suggestCommand = defineCommand({
13421
14577
  meta: {
13422
- name: "hook",
13423
- description: "Install or uninstall the commit-msg git hook"
14578
+ name: "suggest",
14579
+ description: "Suggest labels for an issue or pull request based on its content"
13424
14580
  },
13425
14581
  args: {
13426
- action: {
13427
- type: "positional",
13428
- description: "Action to perform: install or uninstall",
13429
- required: true
14582
+ issue: {
14583
+ type: "string",
14584
+ alias: "i",
14585
+ description: "Issue number to suggest labels for"
14586
+ },
14587
+ pr: {
14588
+ type: "string",
14589
+ alias: "p",
14590
+ description: "Pull request number to suggest labels for"
13430
14591
  }
13431
14592
  },
13432
14593
  async run({ args }) {
13433
- if (!await isGitRepo()) {
13434
- error("Not inside a git repository.");
14594
+ await requireGitRepository();
14595
+ await requireGhCli();
14596
+ await projectHeading("label suggest", "\uD83C\uDFF7\uFE0F");
14597
+ const hasIssue = Boolean(args.issue);
14598
+ const hasPr = Boolean(args.pr);
14599
+ if (!hasIssue && !hasPr) {
14600
+ error("Provide a target: --issue <number> or --pr <number>");
13435
14601
  process.exit(1);
13436
14602
  }
13437
- const action = args.action;
13438
- if (action !== "install" && action !== "uninstall") {
13439
- error(`Unknown action "${action}". Use "install" or "uninstall".`);
14603
+ if (hasIssue && hasPr) {
14604
+ error("Use either --issue or --pr, not both.");
13440
14605
  process.exit(1);
13441
14606
  }
13442
- if (action === "install") {
13443
- await installHook();
13444
- } else {
13445
- await uninstallHook();
14607
+ const targetNumber = Number(hasIssue ? args.issue : args.pr);
14608
+ if (!Number.isInteger(targetNumber) || targetNumber <= 0) {
14609
+ error(`Invalid ${hasIssue ? "issue" : "PR"} number: ${String(hasIssue ? args.issue : args.pr)}`);
14610
+ process.exit(1);
13446
14611
  }
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.");
14612
+ const targetLabel = hasIssue ? `issue #${targetNumber}` : `PR #${targetNumber}`;
14613
+ info(`Fetching ${import_picocolors14.default.bold(targetLabel)} content\u2026`, "");
14614
+ const content = hasIssue ? await getIssueContent(targetNumber) : await getPRContent(targetNumber);
14615
+ if (!content) {
14616
+ error(`Could not fetch content for ${targetLabel}. Verify the number and your gh auth.`);
13469
14617
  process.exit(1);
13470
14618
  }
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;
14619
+ const fullText = `${content.title}
14620
+
14621
+ ${content.body}`;
14622
+ const cache = await getActiveLabels();
14623
+ if (!cache) {
14624
+ error("Could not load repository labels. Run `cn label add --help` for setup guidance.");
14625
+ process.exit(1);
14626
+ }
14627
+ const ranked = scoreLabelsForContent(fullText, cache.labels);
14628
+ if (ranked.length === 0) {
14629
+ info(`No label suggestions found for ${import_picocolors14.default.bold(targetLabel)}.`);
14630
+ info(`Total labels available: ${cache.labels.length}`, "");
14631
+ return;
14632
+ }
14633
+ const sourceNote = formatSourceNote(cache.source);
14634
+ console.log();
14635
+ console.log(` ${import_picocolors14.default.bold(`Suggested labels for ${import_picocolors14.default.cyan(targetLabel)}:`)} ${import_picocolors14.default.dim(sourceNote)}`);
14636
+ console.log();
14637
+ const topN = ranked.slice(0, 5);
14638
+ for (const { label, score } of topN) {
14639
+ const descPart = label.description ? import_picocolors14.default.dim(` \u2014 ${label.description}`) : "";
14640
+ const scorePart = import_picocolors14.default.dim(` [score: ${score}]`);
14641
+ console.log(` ${import_picocolors14.default.cyan("\u2022")} ${import_picocolors14.default.bold(label.name)}${descPart}${scorePart}`);
14642
+ }
14643
+ console.log();
14644
+ info(`Apply a label: cn label add --${hasIssue ? "issue" : "pr"} ${targetNumber} <label>`, "");
13488
14645
  }
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);
14646
+ });
14647
+ var label_default = defineCommand({
14648
+ meta: {
14649
+ name: "label",
14650
+ description: "Manage labels on issues and pull requests"
14651
+ },
14652
+ subCommands: {
14653
+ add: addCommand,
14654
+ suggest: suggestCommand
13493
14655
  }
13494
- rmSync2(hookPath);
13495
- success("commit-msg hook removed.");
13496
- }
14656
+ });
13497
14657
 
13498
14658
  // src/commands/log.ts
13499
- var import_picocolors14 = __toESM(require_picocolors(), 1);
14659
+ var import_picocolors15 = __toESM(require_picocolors(), 1);
13500
14660
  function getDefaultOverviewRemoteCommitCount(hasLocalUnpushedCommits) {
13501
14661
  return hasLocalUnpushedCommits ? 10 : 20;
13502
14662
  }
@@ -13594,9 +14754,9 @@ var log_default = defineCommand({
13594
14754
  } else if (mode === "local") {
13595
14755
  if (!compareRef) {
13596
14756
  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.`));
14757
+ console.log(import_picocolors15.default.yellow(" \u26A0 Could not determine a comparison branch."));
14758
+ console.log(import_picocolors15.default.dim(" No upstream tracking set and no remote base branch found."));
14759
+ console.log(import_picocolors15.default.dim(` Use ${import_picocolors15.default.bold("cn log --full")} to see the full commit history instead.`));
13600
14760
  console.log();
13601
14761
  return;
13602
14762
  }
@@ -13615,8 +14775,8 @@ var log_default = defineCommand({
13615
14775
  const remoteBranch = compareRef ?? targetBranch;
13616
14776
  if (!remoteBranch) {
13617
14777
  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."));
14778
+ console.log(import_picocolors15.default.yellow(" \u26A0 Could not determine a remote branch to display."));
14779
+ console.log(import_picocolors15.default.dim(" Set an upstream tracking branch or configure your base branch first."));
13620
14780
  console.log();
13621
14781
  return;
13622
14782
  }
@@ -13675,31 +14835,31 @@ async function getOverviewRemoteCommitCount(currentBranch, compareRef) {
13675
14835
  }
13676
14836
  function printModeHeader(mode, currentBranch, compareRef, usingFallback = false) {
13677
14837
  const branch = currentBranch ?? "HEAD";
13678
- const fallbackNote = usingFallback ? import_picocolors14.default.yellow(" (no upstream \u2014 comparing against base branch)") : "";
14838
+ const fallbackNote = usingFallback ? import_picocolors15.default.yellow(" (no upstream \u2014 comparing against base branch)") : "";
13679
14839
  switch (mode) {
13680
14840
  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);
14841
+ 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
14842
  if (compareRef) {
13683
- console.log(import_picocolors14.default.dim(` remote source: ${import_picocolors14.default.bold(compareRef)}`));
14843
+ console.log(import_picocolors15.default.dim(` remote source: ${import_picocolors15.default.bold(compareRef)}`));
13684
14844
  }
13685
14845
  break;
13686
14846
  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);
14847
+ console.log(import_picocolors15.default.dim(` mode: ${import_picocolors15.default.bold("local")} \u2014 unpushed commits on ${import_picocolors15.default.bold(branch)}`) + fallbackNote);
13688
14848
  if (compareRef) {
13689
- console.log(import_picocolors14.default.dim(` comparing: ${import_picocolors14.default.bold(compareRef)} \u279C ${import_picocolors14.default.bold("HEAD")}`));
14849
+ console.log(import_picocolors15.default.dim(` comparing: ${import_picocolors15.default.bold(compareRef)} \u279C ${import_picocolors15.default.bold("HEAD")}`));
13690
14850
  }
13691
14851
  break;
13692
14852
  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);
14853
+ 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
14854
  if (compareRef) {
13695
- console.log(import_picocolors14.default.dim(` branch: ${import_picocolors14.default.bold(compareRef)}`));
14855
+ console.log(import_picocolors15.default.dim(` branch: ${import_picocolors15.default.bold(compareRef)}`));
13696
14856
  }
13697
14857
  break;
13698
14858
  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)}`));
14859
+ console.log(import_picocolors15.default.dim(` mode: ${import_picocolors15.default.bold("full")} \u2014 complete commit history for ${import_picocolors15.default.bold(branch)}`));
13700
14860
  break;
13701
14861
  case "all":
13702
- console.log(import_picocolors14.default.dim(` mode: ${import_picocolors14.default.bold("all")} \u2014 commits across all branches`));
14862
+ console.log(import_picocolors15.default.dim(` mode: ${import_picocolors15.default.bold("all")} \u2014 commits across all branches`));
13703
14863
  break;
13704
14864
  }
13705
14865
  }
@@ -13723,7 +14883,7 @@ async function renderScopedLog(options) {
13723
14883
  }
13724
14884
  console.log();
13725
14885
  for (const entry of entries) {
13726
- const hashStr = import_picocolors14.default.yellow(entry.hash);
14886
+ const hashStr = import_picocolors15.default.yellow(entry.hash);
13727
14887
  const refsStr = entry.refs ? ` ${colorizeRefs(entry.refs, protectedBranches, currentBranch)}` : "";
13728
14888
  const subjectStr = colorizeSubject(entry.subject);
13729
14889
  console.log(` ${hashStr}${refsStr} ${subjectStr}`);
@@ -13742,10 +14902,10 @@ async function renderOverviewLog(options) {
13742
14902
  usingFallback
13743
14903
  } = options;
13744
14904
  console.log();
13745
- console.log(import_picocolors14.default.bold(import_picocolors14.default.cyan(" Local Unpushed Commits")));
14905
+ console.log(import_picocolors15.default.bold(import_picocolors15.default.cyan(" Local Unpushed Commits")));
13746
14906
  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."));
14907
+ console.log(import_picocolors15.default.dim(" No comparison branch detected for local commit status."));
14908
+ console.log(import_picocolors15.default.dim(" Set an upstream tracking branch or run cn log --full to inspect the current branch history."));
13749
14909
  } else {
13750
14910
  await renderScopedLog({
13751
14911
  mode: "local",
@@ -13757,11 +14917,11 @@ async function renderOverviewLog(options) {
13757
14917
  });
13758
14918
  }
13759
14919
  console.log();
13760
- console.log(import_picocolors14.default.bold(import_picocolors14.default.cyan(" Remote Branch History")));
14920
+ console.log(import_picocolors15.default.bold(import_picocolors15.default.cyan(" Remote Branch History")));
13761
14921
  if (!compareRef) {
13762
- console.log(import_picocolors14.default.dim(" No remote branch detected."));
14922
+ console.log(import_picocolors15.default.dim(" No remote branch detected."));
13763
14923
  if (usingFallback) {
13764
- console.log(import_picocolors14.default.dim(" Configure your base branch or upstream tracking to enable the split view."));
14924
+ console.log(import_picocolors15.default.dim(" Configure your base branch or upstream tracking to enable the split view."));
13765
14925
  }
13766
14926
  return;
13767
14927
  }
@@ -13774,15 +14934,15 @@ async function renderOverviewLog(options) {
13774
14934
  currentBranch
13775
14935
  });
13776
14936
  if (!hasRemoteHistory) {
13777
- console.log(import_picocolors14.default.dim(" No remote history found for the selected branch."));
14937
+ console.log(import_picocolors15.default.dim(" No remote history found for the selected branch."));
13778
14938
  }
13779
14939
  }
13780
14940
  function printEmptyState(mode) {
13781
14941
  console.log();
13782
14942
  if (mode === "local") {
13783
- console.log(import_picocolors14.default.dim(" No local unpushed commits \u2014 you're up to date with remote!"));
14943
+ console.log(import_picocolors15.default.dim(" No local unpushed commits \u2014 you're up to date with remote!"));
13784
14944
  } else {
13785
- console.log(import_picocolors14.default.dim(" No remote-only commits \u2014 your local branch is up to date!"));
14945
+ console.log(import_picocolors15.default.dim(" No remote-only commits \u2014 your local branch is up to date!"));
13786
14946
  }
13787
14947
  console.log();
13788
14948
  }
@@ -13791,7 +14951,7 @@ async function renderFullLog(options) {
13791
14951
  if (showGraph) {
13792
14952
  const lines = await getLogGraph({ count, all, branch: targetBranch });
13793
14953
  if (lines.length === 0) {
13794
- console.log(import_picocolors14.default.dim(" No commits found."));
14954
+ console.log(import_picocolors15.default.dim(" No commits found."));
13795
14955
  console.log();
13796
14956
  return false;
13797
14957
  }
@@ -13802,13 +14962,13 @@ async function renderFullLog(options) {
13802
14962
  } else {
13803
14963
  const entries = await getLogEntries({ count, all, branch: targetBranch });
13804
14964
  if (entries.length === 0) {
13805
- console.log(import_picocolors14.default.dim(" No commits found."));
14965
+ console.log(import_picocolors15.default.dim(" No commits found."));
13806
14966
  console.log();
13807
14967
  return false;
13808
14968
  }
13809
14969
  console.log();
13810
14970
  for (const entry of entries) {
13811
- const hashStr = import_picocolors14.default.yellow(entry.hash);
14971
+ const hashStr = import_picocolors15.default.yellow(entry.hash);
13812
14972
  const refsStr = entry.refs ? ` ${colorizeRefs(entry.refs, protectedBranches, currentBranch)}` : "";
13813
14973
  const subjectStr = colorizeSubject(entry.subject);
13814
14974
  console.log(` ${hashStr}${refsStr} ${subjectStr}`);
@@ -13820,33 +14980,33 @@ function printFooter(mode, count, overviewRemoteCount, targetBranch) {
13820
14980
  console.log();
13821
14981
  switch (mode) {
13822
14982
  case "overview":
13823
- console.log(import_picocolors14.default.dim(` Showing up to ${count} local commits and ${overviewRemoteCount} remote commits`));
14983
+ console.log(import_picocolors15.default.dim(` Showing up to ${count} local commits and ${overviewRemoteCount} remote commits`));
13824
14984
  break;
13825
14985
  case "local":
13826
- console.log(import_picocolors14.default.dim(` Showing up to ${count} unpushed commits`));
14986
+ console.log(import_picocolors15.default.dim(` Showing up to ${count} unpushed commits`));
13827
14987
  break;
13828
14988
  case "remote":
13829
- console.log(import_picocolors14.default.dim(` Showing ${count} most recent commits from the remote branch`));
14989
+ console.log(import_picocolors15.default.dim(` Showing ${count} most recent commits from the remote branch`));
13830
14990
  break;
13831
14991
  case "full":
13832
- console.log(import_picocolors14.default.dim(` Showing ${count} most recent commits${targetBranch ? ` (${targetBranch})` : ""}`));
14992
+ console.log(import_picocolors15.default.dim(` Showing ${count} most recent commits${targetBranch ? ` (${targetBranch})` : ""}`));
13833
14993
  break;
13834
14994
  case "all":
13835
- console.log(import_picocolors14.default.dim(` Showing ${count} most recent commits (all branches)`));
14995
+ console.log(import_picocolors15.default.dim(` Showing ${count} most recent commits (all branches)`));
13836
14996
  break;
13837
14997
  }
13838
14998
  }
13839
14999
  function colorizeGraphLine(line, protectedBranches, currentBranch) {
13840
15000
  const match = line.match(/^([|/\\*\s_.-]*)([a-f0-9]{7,12})(\s+\(([^)]+)\))?\s*(.*)/);
13841
15001
  if (!match) {
13842
- return import_picocolors14.default.cyan(line);
15002
+ return import_picocolors15.default.cyan(line);
13843
15003
  }
13844
15004
  const [, graphPart = "", hash, , refs, subject = ""] = match;
13845
15005
  const parts = [];
13846
15006
  if (graphPart) {
13847
15007
  parts.push(colorizeGraphChars(graphPart));
13848
15008
  }
13849
- parts.push(import_picocolors14.default.yellow(hash));
15009
+ parts.push(import_picocolors15.default.yellow(hash));
13850
15010
  if (refs) {
13851
15011
  parts.push(` (${colorizeRefs(refs, protectedBranches, currentBranch)})`);
13852
15012
  }
@@ -13857,15 +15017,15 @@ function colorizeGraphChars(graphPart) {
13857
15017
  return graphPart.split("").map((ch) => {
13858
15018
  switch (ch) {
13859
15019
  case "*":
13860
- return import_picocolors14.default.green(ch);
15020
+ return import_picocolors15.default.green(ch);
13861
15021
  case "|":
13862
- return import_picocolors14.default.cyan(ch);
15022
+ return import_picocolors15.default.cyan(ch);
13863
15023
  case "/":
13864
15024
  case "\\":
13865
- return import_picocolors14.default.cyan(ch);
15025
+ return import_picocolors15.default.cyan(ch);
13866
15026
  case "-":
13867
15027
  case "_":
13868
- return import_picocolors14.default.cyan(ch);
15028
+ return import_picocolors15.default.cyan(ch);
13869
15029
  default:
13870
15030
  return ch;
13871
15031
  }
@@ -13877,50 +15037,50 @@ function colorizeRefs(refs, protectedBranches, currentBranch) {
13877
15037
  if (trimmed.startsWith("HEAD ->") || trimmed === "HEAD") {
13878
15038
  const branchName = trimmed.replace("HEAD -> ", "");
13879
15039
  if (trimmed === "HEAD") {
13880
- return import_picocolors14.default.bold(import_picocolors14.default.cyan("HEAD"));
15040
+ return import_picocolors15.default.bold(import_picocolors15.default.cyan("HEAD"));
13881
15041
  }
13882
- return `${import_picocolors14.default.bold(import_picocolors14.default.cyan("HEAD"))} ${import_picocolors14.default.dim("->")} ${colorizeRefName(branchName, protectedBranches, currentBranch)}`;
15042
+ return `${import_picocolors15.default.bold(import_picocolors15.default.cyan("HEAD"))} ${import_picocolors15.default.dim("->")} ${colorizeRefName(branchName, protectedBranches, currentBranch)}`;
13883
15043
  }
13884
15044
  if (trimmed.startsWith("tag:")) {
13885
- return import_picocolors14.default.bold(import_picocolors14.default.magenta(trimmed));
15045
+ return import_picocolors15.default.bold(import_picocolors15.default.magenta(trimmed));
13886
15046
  }
13887
15047
  return colorizeRefName(trimmed, protectedBranches, currentBranch);
13888
- }).join(import_picocolors14.default.dim(", "));
15048
+ }).join(import_picocolors15.default.dim(", "));
13889
15049
  }
13890
15050
  function colorizeRefName(name, protectedBranches, currentBranch) {
13891
15051
  const isRemote = name.includes("/");
13892
15052
  const localName = isRemote ? name.split("/").slice(1).join("/") : name;
13893
15053
  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));
15054
+ return isRemote ? import_picocolors15.default.bold(import_picocolors15.default.red(name)) : import_picocolors15.default.bold(import_picocolors15.default.red(name));
13895
15055
  }
13896
15056
  if (localName === currentBranch) {
13897
- return import_picocolors14.default.bold(import_picocolors14.default.green(name));
15057
+ return import_picocolors15.default.bold(import_picocolors15.default.green(name));
13898
15058
  }
13899
15059
  if (isRemote) {
13900
- return import_picocolors14.default.blue(name);
15060
+ return import_picocolors15.default.blue(name);
13901
15061
  }
13902
- return import_picocolors14.default.green(name);
15062
+ return import_picocolors15.default.green(name);
13903
15063
  }
13904
15064
  function colorizeSubject(subject) {
13905
15065
  const emojiMatch = subject.match(/^((?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F)+\s*)/u);
13906
15066
  if (emojiMatch) {
13907
15067
  const emoji = emojiMatch[1];
13908
15068
  const rest = subject.slice(emoji.length);
13909
- return `${emoji}${import_picocolors14.default.white(rest)}`;
15069
+ return `${emoji}${import_picocolors15.default.white(rest)}`;
13910
15070
  }
13911
15071
  if (subject.startsWith("Merge ")) {
13912
- return import_picocolors14.default.dim(subject);
15072
+ return import_picocolors15.default.dim(subject);
13913
15073
  }
13914
- return import_picocolors14.default.white(subject);
15074
+ return import_picocolors15.default.white(subject);
13915
15075
  }
13916
15076
 
13917
15077
  // src/commands/save.ts
13918
15078
  import { execFile as execFileCb4 } from "child_process";
13919
- var import_picocolors15 = __toESM(require_picocolors(), 1);
15079
+ var import_picocolors16 = __toESM(require_picocolors(), 1);
13920
15080
  function gitRun(args) {
13921
- return new Promise((resolve5) => {
15081
+ return new Promise((resolve6) => {
13922
15082
  execFileCb4("git", args, (err, stdout2, stderr) => {
13923
- resolve5({
15083
+ resolve6({
13924
15084
  exitCode: err ? err.code === "ENOENT" ? 127 : err.status ?? 1 : 0,
13925
15085
  stdout: stdout2 ?? "",
13926
15086
  stderr: stderr ?? ""
@@ -13990,8 +15150,8 @@ async function handleSave(message) {
13990
15150
  info("No uncommitted changes to save.");
13991
15151
  return;
13992
15152
  }
13993
- success(`Saved: ${import_picocolors15.default.dim(label)}`);
13994
- info(`Use ${import_picocolors15.default.bold("cn save --restore")} to bring them back.`, "");
15153
+ success(`Saved: ${import_picocolors16.default.dim(label)}`);
15154
+ info(`Use ${import_picocolors16.default.bold("cn save --restore")} to bring them back.`, "");
13995
15155
  }
13996
15156
  async function handleRestore() {
13997
15157
  await projectHeading("save --restore", "\uD83D\uDCBE");
@@ -14007,7 +15167,7 @@ async function handleRestore() {
14007
15167
  warn("You may have conflicts. Resolve them and run `git stash drop` when done.");
14008
15168
  process.exit(1);
14009
15169
  }
14010
- success(`Restored: ${import_picocolors15.default.dim(stashes[0].message)}`);
15170
+ success(`Restored: ${import_picocolors16.default.dim(stashes[0].message)}`);
14011
15171
  return;
14012
15172
  }
14013
15173
  const choices = stashes.map((s2) => `${s2.index} ${s2.message}`);
@@ -14020,7 +15180,7 @@ async function handleRestore() {
14020
15180
  process.exit(1);
14021
15181
  }
14022
15182
  const match = stashes.find((s2) => String(s2.index) === idx);
14023
- success(`Restored: ${import_picocolors15.default.dim(match?.message ?? "saved changes")}`);
15183
+ success(`Restored: ${import_picocolors16.default.dim(match?.message ?? "saved changes")}`);
14024
15184
  }
14025
15185
  async function handleList() {
14026
15186
  await projectHeading("save --list", "\uD83D\uDCBE");
@@ -14031,13 +15191,13 @@ async function handleList() {
14031
15191
  }
14032
15192
  console.log();
14033
15193
  for (const s2 of stashes) {
14034
- const idx = import_picocolors15.default.dim(`[${s2.index}]`);
15194
+ const idx = import_picocolors16.default.dim(`[${s2.index}]`);
14035
15195
  const msg = s2.message;
14036
15196
  console.log(` ${idx} ${msg}`);
14037
15197
  }
14038
15198
  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.`, "");
15199
+ info(`Use ${import_picocolors16.default.bold("cn save --restore")} to bring changes back.`, "");
15200
+ info(`Use ${import_picocolors16.default.bold("cn save --drop")} to discard saved changes.`, "");
14041
15201
  }
14042
15202
  async function handleDrop() {
14043
15203
  await projectHeading("save --drop", "\uD83D\uDCBE");
@@ -14055,7 +15215,7 @@ async function handleDrop() {
14055
15215
  process.exit(1);
14056
15216
  }
14057
15217
  const match = stashes.find((s2) => String(s2.index) === idx);
14058
- success(`Dropped: ${import_picocolors15.default.dim(match?.message ?? "saved changes")}`);
15218
+ success(`Dropped: ${import_picocolors16.default.dim(match?.message ?? "saved changes")}`);
14059
15219
  }
14060
15220
  async function getStashList() {
14061
15221
  const result = await gitRun(["stash", "list"]);
@@ -14072,7 +15232,8 @@ async function getStashList() {
14072
15232
  }
14073
15233
 
14074
15234
  // src/commands/setup.ts
14075
- var import_picocolors16 = __toESM(require_picocolors(), 1);
15235
+ var import_picocolors17 = __toESM(require_picocolors(), 1);
15236
+ init_gh();
14076
15237
  async function shouldContinueSetupWithExistingConfig(options) {
14077
15238
  const { existingConfig, hasConfigFile, confirm, onInfo, onWarn, onSuccess, summary } = options;
14078
15239
  if (existingConfig) {
@@ -14095,6 +15256,43 @@ async function shouldContinueSetupWithExistingConfig(options) {
14095
15256
  }
14096
15257
  return true;
14097
15258
  }
15259
+ async function resolveApiKeyForSetup(options) {
15260
+ const { providerLabel, hasStoredKey, getStoredKey, select, promptSecret } = options;
15261
+ if (hasStoredKey) {
15262
+ const choice = await select(`${providerLabel} API key`, [
15263
+ "Keep existing stored key",
15264
+ "Replace stored key"
15265
+ ]);
15266
+ if (choice === "Keep existing stored key") {
15267
+ const existing = (await getStoredKey())?.trim() || "";
15268
+ if (existing) {
15269
+ return {
15270
+ apiKey: existing,
15271
+ shouldStore: false,
15272
+ reusedStoredKey: true
15273
+ };
15274
+ }
15275
+ }
15276
+ const replacement = (await promptSecret(`Enter your ${providerLabel} API key`)).trim();
15277
+ if (!replacement) {
15278
+ throw new Error(`${providerLabel} API key is required when ${providerLabel} is selected.`);
15279
+ }
15280
+ return {
15281
+ apiKey: replacement,
15282
+ shouldStore: true,
15283
+ reusedStoredKey: false
15284
+ };
15285
+ }
15286
+ const initial = (await promptSecret(`Enter your ${providerLabel} API key`)).trim();
15287
+ if (!initial) {
15288
+ throw new Error(`${providerLabel} API key is required when ${providerLabel} is selected.`);
15289
+ }
15290
+ return {
15291
+ apiKey: initial,
15292
+ shouldStore: true,
15293
+ reusedStoredKey: false
15294
+ };
15295
+ }
14098
15296
  async function promptForOllamaCloudModel(apiKey, host = DEFAULT_OLLAMA_CLOUD_HOST) {
14099
15297
  try {
14100
15298
  info("Fetching available Ollama Cloud models...");
@@ -14121,6 +15319,32 @@ async function promptForOllamaCloudModel(apiKey, host = DEFAULT_OLLAMA_CLOUD_HOS
14121
15319
  }
14122
15320
  return inputPrompt(`Ollama Cloud model (default: ${DEFAULT_OLLAMA_CLOUD_MODEL} \u2014 press Enter to keep)`, DEFAULT_OLLAMA_CLOUD_MODEL);
14123
15321
  }
15322
+ async function promptForOpenRouterModel(apiKey) {
15323
+ try {
15324
+ info("Fetching available OpenRouter models...");
15325
+ const models = prioritizeOpenRouterModels(await fetchOpenRouterModels(apiKey));
15326
+ if (models.length > 0) {
15327
+ const manualChoice = "Enter model manually";
15328
+ const choices = models.map((model) => ({
15329
+ value: model,
15330
+ label: model === DEFAULT_OPENROUTER_MODEL ? `${model} (default)` : model
15331
+ }));
15332
+ const selected = await selectPrompt("Which OpenRouter model should this clone use?", [
15333
+ ...choices.map((choice) => choice.label),
15334
+ manualChoice
15335
+ ]);
15336
+ if (selected !== manualChoice) {
15337
+ return choices.find((choice) => choice.label === selected)?.value ?? DEFAULT_OPENROUTER_MODEL;
15338
+ }
15339
+ } else {
15340
+ warn("OpenRouter returned no available models. Enter the model name manually.");
15341
+ }
15342
+ } catch (err) {
15343
+ const message = err instanceof Error ? err.message : String(err);
15344
+ warn(`Could not fetch OpenRouter models: ${message}`);
15345
+ }
15346
+ return inputPrompt(`OpenRouter model (default: ${DEFAULT_OPENROUTER_MODEL} \u2014 press Enter to keep)`, DEFAULT_OPENROUTER_MODEL);
15347
+ }
14124
15348
  var setup_default = defineCommand({
14125
15349
  meta: {
14126
15350
  name: "setup",
@@ -14155,7 +15379,7 @@ var setup_default = defineCommand({
14155
15379
  workflow = "github-flow";
14156
15380
  else if (workflowChoice.startsWith("Git Flow"))
14157
15381
  workflow = "git-flow";
14158
- info(`Workflow: ${import_picocolors16.default.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
15382
+ info(`Workflow: ${import_picocolors17.default.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
14159
15383
  const conventionChoice = await selectPrompt("Which commit convention should this project use?", [
14160
15384
  `${CONVENTION_DESCRIPTIONS["clean-commit"]} (recommended)`,
14161
15385
  CONVENTION_DESCRIPTIONS.conventional,
@@ -14172,25 +15396,74 @@ var setup_default = defineCommand({
14172
15396
  if (enableAI) {
14173
15397
  const providerChoice = await selectPrompt("Which AI provider should this clone use?", [
14174
15398
  "GitHub Copilot \u2014 use your existing GitHub/Copilot auth",
14175
- "Ollama Cloud \u2014 use an API key stored in the local secrets store"
15399
+ "Ollama Cloud \u2014 use an API key stored in the local secrets store",
15400
+ "OpenRouter \u2014 use an API key stored in the local secrets store"
14176
15401
  ]);
14177
- aiProvider = providerChoice.startsWith("Ollama Cloud") ? "ollama-cloud" : "copilot";
15402
+ if (providerChoice.startsWith("Ollama Cloud")) {
15403
+ aiProvider = "ollama-cloud";
15404
+ } else if (providerChoice.startsWith("OpenRouter")) {
15405
+ aiProvider = "openrouter";
15406
+ } else {
15407
+ aiProvider = "copilot";
15408
+ }
14178
15409
  if (aiProvider === "ollama-cloud") {
14179
- const apiKey = (await passwordPrompt("Enter your Ollama Cloud API key")).trim();
14180
- if (!apiKey) {
14181
- error("Ollama Cloud API key is required when Ollama Cloud is selected.");
15410
+ let resolvedKey;
15411
+ try {
15412
+ resolvedKey = await resolveApiKeyForSetup({
15413
+ providerLabel: "Ollama Cloud",
15414
+ hasStoredKey: await hasOllamaCloudApiKey(),
15415
+ getStoredKey: getOllamaCloudApiKey,
15416
+ select: selectPrompt,
15417
+ promptSecret: passwordPrompt
15418
+ });
15419
+ } catch (err) {
15420
+ const message = err instanceof Error ? err.message : String(err);
15421
+ error(message);
14182
15422
  process.exit(1);
14183
15423
  }
14184
- aiModel = await promptForOllamaCloudModel(apiKey);
15424
+ aiModel = await promptForOllamaCloudModel(resolvedKey.apiKey);
14185
15425
  try {
14186
- await setOllamaCloudApiKey(apiKey);
14187
- success("Stored Ollama Cloud API key in the local secrets store.");
14188
- info(`Secrets path: ${import_picocolors16.default.bold(getSecretsStorePath())}`);
15426
+ if (resolvedKey.shouldStore) {
15427
+ await setOllamaCloudApiKey(resolvedKey.apiKey);
15428
+ success("Stored Ollama Cloud API key in the local secrets store.");
15429
+ info(`Secrets path: ${import_picocolors17.default.bold(getSecretsStorePath())}`);
15430
+ } else {
15431
+ info("Using existing Ollama Cloud API key from the local secrets store.");
15432
+ }
14189
15433
  } catch (err) {
14190
15434
  const message = err instanceof Error ? err.message : String(err);
14191
15435
  error(`Failed to store Ollama Cloud API key: ${message}`);
14192
15436
  process.exit(1);
14193
15437
  }
15438
+ } else if (aiProvider === "openrouter") {
15439
+ let resolvedKey;
15440
+ try {
15441
+ resolvedKey = await resolveApiKeyForSetup({
15442
+ providerLabel: "OpenRouter",
15443
+ hasStoredKey: await hasOpenRouterApiKey(),
15444
+ getStoredKey: getOpenRouterApiKey,
15445
+ select: selectPrompt,
15446
+ promptSecret: passwordPrompt
15447
+ });
15448
+ } catch (err) {
15449
+ const message = err instanceof Error ? err.message : String(err);
15450
+ error(message);
15451
+ process.exit(1);
15452
+ }
15453
+ aiModel = await promptForOpenRouterModel(resolvedKey.apiKey);
15454
+ try {
15455
+ if (resolvedKey.shouldStore) {
15456
+ await setOpenRouterApiKey(resolvedKey.apiKey);
15457
+ success("Stored OpenRouter API key in the local secrets store.");
15458
+ info(`Secrets path: ${import_picocolors17.default.bold(getSecretsStorePath())}`);
15459
+ } else {
15460
+ info("Using existing OpenRouter API key from the local secrets store.");
15461
+ }
15462
+ } catch (err) {
15463
+ const message = err instanceof Error ? err.message : String(err);
15464
+ error(`Failed to store OpenRouter API key: ${message}`);
15465
+ process.exit(1);
15466
+ }
14194
15467
  }
14195
15468
  }
14196
15469
  const showTips = await confirmPrompt("Show beginner quick guides and loading tips in command output?");
@@ -14247,15 +15520,15 @@ var setup_default = defineCommand({
14247
15520
  detectedRole = roleChoice;
14248
15521
  detectionSource = "user selection";
14249
15522
  } 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?`);
15523
+ info(`Detected role: ${import_picocolors17.default.bold(detectedRole)} (via ${detectionSource})`);
15524
+ const confirmed = await confirmPrompt(`Role detected as ${import_picocolors17.default.bold(detectedRole)}. Is this correct?`);
14252
15525
  if (!confirmed) {
14253
15526
  const roleChoice = await selectPrompt("Select your role:", ["maintainer", "contributor"]);
14254
15527
  detectedRole = roleChoice;
14255
15528
  }
14256
15529
  }
14257
15530
  const defaultConfig = getDefaultConfig();
14258
- info(import_picocolors16.default.dim("Tip: press Enter to keep the default branch name shown in each prompt."));
15531
+ info(import_picocolors17.default.dim("Tip: press Enter to keep the default branch name shown in each prompt."));
14259
15532
  const mainBranchDefault = defaultConfig.mainBranch;
14260
15533
  const mainBranch = await inputPrompt(`Main branch name (default: ${mainBranchDefault} \u2014 press Enter to keep)`, mainBranchDefault);
14261
15534
  let devBranch;
@@ -14281,7 +15554,7 @@ var setup_default = defineCommand({
14281
15554
  error("Setup cannot continue without the upstream remote for contributors.");
14282
15555
  process.exit(1);
14283
15556
  }
14284
- success(`Added remote ${import_picocolors16.default.bold(upstreamRemote)} \u2192 ${upstreamUrl}`);
15557
+ success(`Added remote ${import_picocolors17.default.bold(upstreamRemote)} \u2192 ${upstreamUrl}`);
14285
15558
  } else {
14286
15559
  error("An upstream remote URL is required for contributors.");
14287
15560
  info("Add it manually: git remote add upstream <url>", "");
@@ -14304,67 +15577,67 @@ var setup_default = defineCommand({
14304
15577
  showTips
14305
15578
  };
14306
15579
  writeConfig(config);
14307
- success(`Config written to ${import_picocolors16.default.bold(getConfigLocationLabel())}`);
15580
+ success(`Config written to ${import_picocolors17.default.bold(getConfigLocationLabel())}`);
14308
15581
  info("This setup is stored locally for this clone and does not modify tracked files.", "");
14309
15582
  const syncRemote = config.role === "contributor" ? config.upstream : config.origin;
14310
- info(`Fetching ${import_picocolors16.default.bold(syncRemote)} to verify branch configuration...`, "");
15583
+ info(`Fetching ${import_picocolors17.default.bold(syncRemote)} to verify branch configuration...`, "");
14311
15584
  await fetchRemote(syncRemote);
14312
15585
  const mainRef = `${syncRemote}/${config.mainBranch}`;
14313
15586
  if (!await refExists(mainRef)) {
14314
- warn(`Main branch ref ${import_picocolors16.default.bold(mainRef)} not found on remote.`);
15587
+ warn(`Main branch ref ${import_picocolors17.default.bold(mainRef)} not found on remote.`);
14315
15588
  warn("Config was saved \u2014 verify the branch name and re-run setup if needed.");
14316
15589
  }
14317
15590
  if (config.devBranch) {
14318
15591
  const devRef = `${syncRemote}/${config.devBranch}`;
14319
15592
  if (!await refExists(devRef)) {
14320
- warn(`Dev branch ref ${import_picocolors16.default.bold(devRef)} not found on remote.`);
15593
+ warn(`Dev branch ref ${import_picocolors17.default.bold(devRef)} not found on remote.`);
14321
15594
  warn("Config was saved \u2014 verify the branch name and re-run setup if needed.");
14322
15595
  }
14323
15596
  }
14324
15597
  console.log();
14325
15598
  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")}`);
15599
+ info(`Workflow: ${import_picocolors17.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
15600
+ info(`Convention: ${import_picocolors17.default.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
15601
+ info(`AI: ${import_picocolors17.default.bold(isAIEnabled(config) ? "enabled" : "disabled")}`);
14329
15602
  if (isAIEnabled(config)) {
14330
- info(`AI provider: ${import_picocolors16.default.bold(resolvedAIConfig.providerLabel)}`);
15603
+ info(`AI provider: ${import_picocolors17.default.bold(resolvedAIConfig.providerLabel)}`);
14331
15604
  if (resolvedAIConfig.model) {
14332
- info(`AI model: ${import_picocolors16.default.bold(resolvedAIConfig.model)}`);
15605
+ info(`AI model: ${import_picocolors17.default.bold(resolvedAIConfig.model)}`);
14333
15606
  }
14334
15607
  }
14335
- info(`Guides: ${import_picocolors16.default.bold(shouldShowTips(config) ? "shown" : "hidden")}`);
14336
- info(`Role: ${import_picocolors16.default.bold(config.role)}`);
15608
+ info(`Guides: ${import_picocolors17.default.bold(shouldShowTips(config) ? "shown" : "hidden")}`);
15609
+ info(`Role: ${import_picocolors17.default.bold(config.role)}`);
14337
15610
  if (config.devBranch) {
14338
- info(`Main: ${import_picocolors16.default.bold(config.mainBranch)} | Dev: ${import_picocolors16.default.bold(config.devBranch)}`);
15611
+ info(`Main: ${import_picocolors17.default.bold(config.mainBranch)} | Dev: ${import_picocolors17.default.bold(config.devBranch)}`);
14339
15612
  } else {
14340
- info(`Main: ${import_picocolors16.default.bold(config.mainBranch)}`);
15613
+ info(`Main: ${import_picocolors17.default.bold(config.mainBranch)}`);
14341
15614
  }
14342
- info(`Origin: ${import_picocolors16.default.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${import_picocolors16.default.bold(config.upstream)}` : ""}`);
15615
+ info(`Origin: ${import_picocolors17.default.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${import_picocolors17.default.bold(config.upstream)}` : ""}`);
14343
15616
  }
14344
15617
  });
14345
15618
  function logConfigSummary(config) {
14346
15619
  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")}`);
15620
+ info(`Workflow: ${import_picocolors17.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
15621
+ info(`Convention: ${import_picocolors17.default.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
15622
+ info(`AI: ${import_picocolors17.default.bold(isAIEnabled(config) ? "enabled" : "disabled")}`);
14350
15623
  if (isAIEnabled(config)) {
14351
- info(`AI provider: ${import_picocolors16.default.bold(aiConfig.providerLabel)}`);
15624
+ info(`AI provider: ${import_picocolors17.default.bold(aiConfig.providerLabel)}`);
14352
15625
  if (aiConfig.model) {
14353
- info(`AI model: ${import_picocolors16.default.bold(aiConfig.model)}`);
15626
+ info(`AI model: ${import_picocolors17.default.bold(aiConfig.model)}`);
14354
15627
  }
14355
15628
  }
14356
- info(`Guides: ${import_picocolors16.default.bold(shouldShowTips(config) ? "shown" : "hidden")}`);
14357
- info(`Role: ${import_picocolors16.default.bold(config.role)}`);
15629
+ info(`Guides: ${import_picocolors17.default.bold(shouldShowTips(config) ? "shown" : "hidden")}`);
15630
+ info(`Role: ${import_picocolors17.default.bold(config.role)}`);
14358
15631
  if (config.devBranch) {
14359
- info(`Main: ${import_picocolors16.default.bold(config.mainBranch)} | Dev: ${import_picocolors16.default.bold(config.devBranch)}`);
15632
+ info(`Main: ${import_picocolors17.default.bold(config.mainBranch)} | Dev: ${import_picocolors17.default.bold(config.devBranch)}`);
14360
15633
  } else {
14361
- info(`Main: ${import_picocolors16.default.bold(config.mainBranch)}`);
15634
+ info(`Main: ${import_picocolors17.default.bold(config.mainBranch)}`);
14362
15635
  }
14363
- info(`Origin: ${import_picocolors16.default.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${import_picocolors16.default.bold(config.upstream)}` : ""}`);
15636
+ info(`Origin: ${import_picocolors17.default.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${import_picocolors17.default.bold(config.upstream)}` : ""}`);
14364
15637
  }
14365
15638
 
14366
15639
  // src/commands/start.ts
14367
- var import_picocolors17 = __toESM(require_picocolors(), 1);
15640
+ var import_picocolors18 = __toESM(require_picocolors(), 1);
14368
15641
  var start_default = defineCommand({
14369
15642
  meta: {
14370
15643
  name: "start",
@@ -14416,16 +15689,16 @@ var start_default = defineCommand({
14416
15689
  warn("Start cancelled.");
14417
15690
  process.exit(0);
14418
15691
  }
14419
- info(`Creating branch: ${import_picocolors17.default.bold(branchName)}`);
15692
+ info(`Creating branch: ${import_picocolors18.default.bold(branchName)}`);
14420
15693
  await fetchRemote(syncSource.remote);
14421
15694
  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)}.`);
15695
+ warn(`Remote ref ${import_picocolors18.default.bold(syncSource.ref)} not found. Creating branch from local ${import_picocolors18.default.bold(baseBranch)}.`);
14423
15696
  }
14424
15697
  const currentBranch = await getCurrentBranch();
14425
15698
  if (currentBranch === baseBranch && await refExists(syncSource.ref)) {
14426
15699
  const ahead = await countCommitsAhead(baseBranch, syncSource.ref);
14427
15700
  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)}.`);
15701
+ 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
15702
  info(" Syncing will discard those commits. Consider backing them up first (e.g. create a branch).");
14430
15703
  const proceed = await confirmPrompt("Discard local commits and sync to remote?");
14431
15704
  if (!proceed) {
@@ -14442,10 +15715,10 @@ var start_default = defineCommand({
14442
15715
  error(`Failed to create branch: ${result2.stderr}`);
14443
15716
  process.exit(1);
14444
15717
  }
14445
- success(`Created ${import_picocolors17.default.bold(branchName)} from ${import_picocolors17.default.bold(syncSource.ref)}`);
15718
+ success(`Created ${import_picocolors18.default.bold(branchName)} from ${import_picocolors18.default.bold(syncSource.ref)}`);
14446
15719
  return;
14447
15720
  }
14448
- error(`Failed to update ${import_picocolors17.default.bold(baseBranch)}: ${updateResult.stderr}`);
15721
+ error(`Failed to update ${import_picocolors18.default.bold(baseBranch)}: ${updateResult.stderr}`);
14449
15722
  info("Make sure your base branch exists locally or the remote ref is available.", "");
14450
15723
  process.exit(1);
14451
15724
  }
@@ -14454,12 +15727,13 @@ var start_default = defineCommand({
14454
15727
  error(`Failed to create branch: ${result.stderr}`);
14455
15728
  process.exit(1);
14456
15729
  }
14457
- success(`Created ${import_picocolors17.default.bold(branchName)} from latest ${import_picocolors17.default.bold(baseBranch)}`);
15730
+ success(`Created ${import_picocolors18.default.bold(branchName)} from latest ${import_picocolors18.default.bold(baseBranch)}`);
14458
15731
  }
14459
15732
  });
14460
15733
 
14461
15734
  // src/commands/status.ts
14462
- var import_picocolors18 = __toESM(require_picocolors(), 1);
15735
+ var import_picocolors19 = __toESM(require_picocolors(), 1);
15736
+ init_gh();
14463
15737
  var status_default = defineCommand({
14464
15738
  meta: {
14465
15739
  name: "status",
@@ -14476,8 +15750,8 @@ var status_default = defineCommand({
14476
15750
  process.exit(1);
14477
15751
  }
14478
15752
  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)}`);
15753
+ console.log(` ${import_picocolors19.default.dim("Workflow:")} ${import_picocolors19.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
15754
+ console.log(` ${import_picocolors19.default.dim("Role:")} ${import_picocolors19.default.bold(config.role)}`);
14481
15755
  console.log();
14482
15756
  await fetchAll();
14483
15757
  const currentBranch = await getCurrentBranch();
@@ -14486,7 +15760,7 @@ var status_default = defineCommand({
14486
15760
  const isContributor = config.role === "contributor";
14487
15761
  const [dirty, fileStatus] = await Promise.all([hasUncommittedChanges(), getFileStatus()]);
14488
15762
  if (dirty) {
14489
- console.log(` ${import_picocolors18.default.yellow("\u26A0")} ${import_picocolors18.default.yellow("Uncommitted changes in working tree")}`);
15763
+ console.log(` ${import_picocolors19.default.yellow("\u26A0")} ${import_picocolors19.default.yellow("Uncommitted changes in working tree")}`);
14490
15764
  console.log();
14491
15765
  }
14492
15766
  const mainRemote = `${origin}/${mainBranch}`;
@@ -14505,16 +15779,16 @@ var status_default = defineCommand({
14505
15779
  if (isFeatureBranch) {
14506
15780
  const branchDiv = await getDivergence(currentBranch, baseBranch);
14507
15781
  const branchLine = formatStatus(currentBranch, baseBranch, branchDiv.ahead, branchDiv.behind);
14508
- console.log(branchLine + import_picocolors18.default.dim(` (current ${import_picocolors18.default.green("*")})`));
15782
+ console.log(branchLine + import_picocolors19.default.dim(` (current ${import_picocolors19.default.green("*")})`));
14509
15783
  branchStatus = await detectBranchStatus(currentBranch, baseBranch);
14510
15784
  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")}`);
15785
+ 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
15786
  }
14513
15787
  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`)}`);
15788
+ 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
15789
  }
14516
15790
  } else if (currentBranch) {
14517
- console.log(import_picocolors18.default.dim(` (on ${import_picocolors18.default.bold(currentBranch)} branch)`));
15791
+ console.log(import_picocolors19.default.dim(` (on ${import_picocolors19.default.bold(currentBranch)} branch)`));
14518
15792
  }
14519
15793
  let branchesAligned = true;
14520
15794
  {
@@ -14551,20 +15825,20 @@ var status_default = defineCommand({
14551
15825
  }
14552
15826
  branchesAligned = groups.size === 1;
14553
15827
  console.log();
14554
- console.log(` ${import_picocolors18.default.bold("\uD83D\uDD17 Branch Alignment")}`);
15828
+ console.log(` ${import_picocolors19.default.bold("\uD83D\uDD17 Branch Alignment")}`);
14555
15829
  for (const [hash, names] of groups) {
14556
15830
  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}`);
15831
+ const nameStr = names.map((n2) => import_picocolors19.default.bold(n2)).join(import_picocolors19.default.dim(" \xB7 "));
15832
+ console.log(` ${import_picocolors19.default.yellow(short)} ${import_picocolors19.default.dim("\u2500\u2500")} ${nameStr}`);
14559
15833
  const subject = await getCommitSubject(hash);
14560
15834
  if (subject) {
14561
- console.log(` ${import_picocolors18.default.dim(subject)}`);
15835
+ console.log(` ${import_picocolors19.default.dim(subject)}`);
14562
15836
  }
14563
15837
  }
14564
15838
  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")}`);
15839
+ console.log(` ${import_picocolors19.default.green("\u2713")} ${import_picocolors19.default.green("All branches aligned")} ${import_picocolors19.default.dim("\u2014 ready to start")}`);
14566
15840
  } else {
14567
- console.log(` ${import_picocolors18.default.yellow("\u26A0")} ${import_picocolors18.default.yellow("Branches are not fully aligned")}`);
15841
+ console.log(` ${import_picocolors19.default.yellow("\u26A0")} ${import_picocolors19.default.yellow("Branches are not fully aligned")}`);
14568
15842
  }
14569
15843
  }
14570
15844
  }
@@ -14572,41 +15846,41 @@ var status_default = defineCommand({
14572
15846
  if (hasFiles) {
14573
15847
  console.log();
14574
15848
  if (fileStatus.staged.length > 0) {
14575
- console.log(` ${import_picocolors18.default.green("Staged for commit:")}`);
15849
+ console.log(` ${import_picocolors19.default.green("Staged for commit:")}`);
14576
15850
  for (const { file, status } of fileStatus.staged) {
14577
- console.log(` ${import_picocolors18.default.green("+")} ${import_picocolors18.default.dim(`${status}:`)} ${file}`);
15851
+ console.log(` ${import_picocolors19.default.green("+")} ${import_picocolors19.default.dim(`${status}:`)} ${file}`);
14578
15852
  }
14579
15853
  }
14580
15854
  if (fileStatus.modified.length > 0) {
14581
- console.log(` ${import_picocolors18.default.yellow("Unstaged changes:")}`);
15855
+ console.log(` ${import_picocolors19.default.yellow("Unstaged changes:")}`);
14582
15856
  for (const { file, status } of fileStatus.modified) {
14583
- console.log(` ${import_picocolors18.default.yellow("~")} ${import_picocolors18.default.dim(`${status}:`)} ${file}`);
15857
+ console.log(` ${import_picocolors19.default.yellow("~")} ${import_picocolors19.default.dim(`${status}:`)} ${file}`);
14584
15858
  }
14585
15859
  }
14586
15860
  if (fileStatus.untracked.length > 0) {
14587
- console.log(` ${import_picocolors18.default.red("Untracked files:")}`);
15861
+ console.log(` ${import_picocolors19.default.red("Untracked files:")}`);
14588
15862
  for (const file of fileStatus.untracked) {
14589
- console.log(` ${import_picocolors18.default.red("?")} ${file}`);
15863
+ console.log(` ${import_picocolors19.default.red("?")} ${file}`);
14590
15864
  }
14591
15865
  }
14592
15866
  } else if (!dirty) {
14593
- console.log(` ${import_picocolors18.default.green("\u2713")} ${import_picocolors18.default.dim("Working tree clean")}`);
15867
+ console.log(` ${import_picocolors19.default.green("\u2713")} ${import_picocolors19.default.dim("Working tree clean")}`);
14594
15868
  }
14595
15869
  console.log();
14596
15870
  }
14597
15871
  });
14598
15872
  function formatStatus(branch, base, ahead, behind) {
14599
- const label = import_picocolors18.default.bold(branch.padEnd(20));
15873
+ const label = import_picocolors19.default.bold(branch.padEnd(20));
14600
15874
  if (ahead === 0 && behind === 0) {
14601
- return ` ${import_picocolors18.default.green("\u2713")} ${label} ${import_picocolors18.default.dim(`in sync with ${base}`)}`;
15875
+ return ` ${import_picocolors19.default.green("\u2713")} ${label} ${import_picocolors19.default.dim(`in sync with ${base}`)}`;
14602
15876
  }
14603
15877
  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}`)}`;
15878
+ return ` ${import_picocolors19.default.yellow("\u2191")} ${label} ${import_picocolors19.default.yellow(`${ahead} commit${ahead !== 1 ? "s" : ""} ahead of ${base}`)}`;
14605
15879
  }
14606
15880
  if (behind > 0 && ahead === 0) {
14607
- return ` ${import_picocolors18.default.red("\u2193")} ${label} ${import_picocolors18.default.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
15881
+ return ` ${import_picocolors19.default.red("\u2193")} ${label} ${import_picocolors19.default.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
14608
15882
  }
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)}`;
15883
+ 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
15884
  }
14611
15885
  var STALE_THRESHOLD_DAYS = 14;
14612
15886
  async function detectBranchStatus(branch, baseBranch) {
@@ -14657,15 +15931,16 @@ async function detectBranchStatus(branch, baseBranch) {
14657
15931
  }
14658
15932
 
14659
15933
  // src/commands/submit.ts
14660
- var import_picocolors19 = __toESM(require_picocolors(), 1);
15934
+ var import_picocolors20 = __toESM(require_picocolors(), 1);
15935
+ init_gh();
14661
15936
  async function performSquashMerge(origin, baseBranch, featureBranch, options) {
14662
- info(`Checking out ${import_picocolors19.default.bold(baseBranch)}...`);
15937
+ info(`Checking out ${import_picocolors20.default.bold(baseBranch)}...`);
14663
15938
  const coResult = await checkoutBranch(baseBranch);
14664
15939
  if (coResult.exitCode !== 0) {
14665
15940
  error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
14666
15941
  process.exit(1);
14667
15942
  }
14668
- info(`Squash merging ${import_picocolors19.default.bold(featureBranch)} into ${import_picocolors19.default.bold(baseBranch)}...`);
15943
+ info(`Squash merging ${import_picocolors20.default.bold(featureBranch)} into ${import_picocolors20.default.bold(baseBranch)}...`);
14669
15944
  const mergeResult = await mergeSquash(featureBranch);
14670
15945
  if (mergeResult.exitCode !== 0) {
14671
15946
  error(`Squash merge failed: ${mergeResult.stderr}`);
@@ -14685,7 +15960,7 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
14685
15960
  message = aiMsg;
14686
15961
  spinner.success("AI commit message generated.");
14687
15962
  console.log(`
14688
- ${import_picocolors19.default.dim("AI suggestion:")} ${import_picocolors19.default.bold(import_picocolors19.default.cyan(message))}`);
15963
+ ${import_picocolors20.default.dim("AI suggestion:")} ${import_picocolors20.default.bold(import_picocolors20.default.cyan(message))}`);
14689
15964
  break;
14690
15965
  }
14691
15966
  spinner.fail("AI did not return a commit message.");
@@ -14725,7 +16000,7 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
14725
16000
  message = regen;
14726
16001
  spinner.success("Commit message regenerated.");
14727
16002
  console.log(`
14728
- ${import_picocolors19.default.dim("AI suggestion:")} ${import_picocolors19.default.bold(import_picocolors19.default.cyan(regen))}`);
16003
+ ${import_picocolors20.default.dim("AI suggestion:")} ${import_picocolors20.default.bold(import_picocolors20.default.cyan(regen))}`);
14729
16004
  } else {
14730
16005
  spinner.fail("Regeneration failed.");
14731
16006
  }
@@ -14741,13 +16016,13 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
14741
16016
  error(`Commit failed: ${commitResult.stderr}`);
14742
16017
  process.exit(1);
14743
16018
  }
14744
- info(`Pushing ${import_picocolors19.default.bold(baseBranch)} to ${origin}...`);
16019
+ info(`Pushing ${import_picocolors20.default.bold(baseBranch)} to ${origin}...`);
14745
16020
  const pushResult = await pushBranch(origin, baseBranch);
14746
16021
  if (pushResult.exitCode !== 0) {
14747
16022
  error(`Failed to push ${baseBranch}: ${pushResult.stderr}`);
14748
16023
  process.exit(1);
14749
16024
  }
14750
- info(`Deleting local branch ${import_picocolors19.default.bold(featureBranch)}...`);
16025
+ info(`Deleting local branch ${import_picocolors20.default.bold(featureBranch)}...`);
14751
16026
  const delLocal = await forceDeleteBranch(featureBranch);
14752
16027
  if (delLocal.exitCode !== 0) {
14753
16028
  warn(`Could not delete local branch: ${delLocal.stderr.trim()}`);
@@ -14755,14 +16030,14 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
14755
16030
  const remoteBranchRef = `${origin}/${featureBranch}`;
14756
16031
  const remoteExists = await branchExists(remoteBranchRef);
14757
16032
  if (remoteExists) {
14758
- info(`Deleting remote branch ${import_picocolors19.default.bold(featureBranch)}...`);
16033
+ info(`Deleting remote branch ${import_picocolors20.default.bold(featureBranch)}...`);
14759
16034
  const delRemote = await deleteRemoteBranch(origin, featureBranch);
14760
16035
  if (delRemote.exitCode !== 0) {
14761
16036
  warn(`Could not delete remote branch: ${delRemote.stderr.trim()}`);
14762
16037
  }
14763
16038
  }
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.`, "");
16039
+ success(`Squash merged ${import_picocolors20.default.bold(featureBranch)} into ${import_picocolors20.default.bold(baseBranch)} and pushed.`);
16040
+ info(`Run ${import_picocolors20.default.bold("cn start")} to begin a new feature.`, "");
14766
16041
  }
14767
16042
  var submit_default = defineCommand({
14768
16043
  meta: {
@@ -14819,7 +16094,7 @@ var submit_default = defineCommand({
14819
16094
  }
14820
16095
  if (protectedBranches.includes(currentBranch)) {
14821
16096
  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.`);
16097
+ warn(`You're on ${import_picocolors20.default.bold(currentBranch)}, which is a protected branch. PRs should come from feature branches.`);
14823
16098
  await fetchAll();
14824
16099
  const remoteRef = `${origin}/${currentBranch}`;
14825
16100
  const localWork = await hasLocalWork(origin, currentBranch);
@@ -14828,11 +16103,11 @@ var submit_default = defineCommand({
14828
16103
  const hasAnything = hasCommits || dirty;
14829
16104
  if (!hasAnything) {
14830
16105
  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.`, "");
16106
+ info(` Run ${import_picocolors20.default.bold("cn start")} to create a new feature branch.`, "");
14832
16107
  process.exit(1);
14833
16108
  }
14834
16109
  if (hasCommits) {
14835
- info(`Found ${import_picocolors19.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${import_picocolors19.default.bold(currentBranch)}.`);
16110
+ info(`Found ${import_picocolors20.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${import_picocolors20.default.bold(currentBranch)}.`);
14836
16111
  }
14837
16112
  if (dirty) {
14838
16113
  info("You also have uncommitted changes in the working tree.");
@@ -14862,12 +16137,12 @@ var submit_default = defineCommand({
14862
16137
  error(`Failed to create branch: ${branchResult.stderr}`);
14863
16138
  process.exit(1);
14864
16139
  }
14865
- success(`Created ${import_picocolors19.default.bold(newBranchName)} with your changes.`);
16140
+ success(`Created ${import_picocolors20.default.bold(newBranchName)} with your changes.`);
14866
16141
  await updateLocalBranch(currentBranch, remoteRef);
14867
- info(`Reset ${import_picocolors19.default.bold(currentBranch)} back to ${import_picocolors19.default.bold(remoteRef)} \u2014 no damage done.`, "");
16142
+ info(`Reset ${import_picocolors20.default.bold(currentBranch)} back to ${import_picocolors20.default.bold(remoteRef)} \u2014 no damage done.`, "");
14868
16143
  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.`, "");
16144
+ success(`You're now on ${import_picocolors20.default.bold(newBranchName)} with all your work intact.`);
16145
+ info(`Run ${import_picocolors20.default.bold("cn submit")} again to push and create your PR.`, "");
14871
16146
  return;
14872
16147
  }
14873
16148
  await projectHeading("submit", "\uD83D\uDE80");
@@ -14876,7 +16151,7 @@ var submit_default = defineCommand({
14876
16151
  if (ghInstalled && ghAuthed) {
14877
16152
  const mergedPR = await getMergedPRForBranch(currentBranch);
14878
16153
  if (mergedPR) {
14879
- warn(`PR #${mergedPR.number} (${import_picocolors19.default.bold(mergedPR.title)}) was already merged.`);
16154
+ warn(`PR #${mergedPR.number} (${import_picocolors20.default.bold(mergedPR.title)}) was already merged.`);
14880
16155
  const localWork = await hasLocalWork(origin, currentBranch);
14881
16156
  const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
14882
16157
  if (hasWork) {
@@ -14884,7 +16159,7 @@ var submit_default = defineCommand({
14884
16159
  warn("You have uncommitted changes in your working tree.");
14885
16160
  }
14886
16161
  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.`);
16162
+ warn(`You have ${import_picocolors20.default.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not in the merged PR.`);
14888
16163
  }
14889
16164
  const SAVE_NEW_BRANCH = "Save changes to a new branch";
14890
16165
  const DISCARD = "Discard all changes and clean up";
@@ -14911,10 +16186,10 @@ var submit_default = defineCommand({
14911
16186
  error(`Failed to rename branch: ${renameResult.stderr}`);
14912
16187
  process.exit(1);
14913
16188
  }
14914
- success(`Renamed ${import_picocolors19.default.bold(currentBranch)} \u2192 ${import_picocolors19.default.bold(newBranchName)}`);
16189
+ success(`Renamed ${import_picocolors20.default.bold(currentBranch)} \u2192 ${import_picocolors20.default.bold(newBranchName)}`);
14915
16190
  await unsetUpstream();
14916
16191
  const syncSource2 = getSyncSource(config);
14917
- info(`Syncing ${import_picocolors19.default.bold(newBranchName)} with latest ${import_picocolors19.default.bold(baseBranch)}...`);
16192
+ info(`Syncing ${import_picocolors20.default.bold(newBranchName)} with latest ${import_picocolors20.default.bold(baseBranch)}...`);
14918
16193
  await fetchRemote(syncSource2.remote);
14919
16194
  let rebaseResult;
14920
16195
  if (staleUpstreamHash) {
@@ -14925,17 +16200,17 @@ var submit_default = defineCommand({
14925
16200
  }
14926
16201
  if (rebaseResult.exitCode !== 0) {
14927
16202
  warn("Rebase encountered conflicts. Resolve them manually, then run:");
14928
- info(` ${import_picocolors19.default.bold("git rebase --continue")}`, "");
16203
+ info(` ${import_picocolors20.default.bold("git rebase --continue")}`, "");
14929
16204
  } else {
14930
- success(`Rebased ${import_picocolors19.default.bold(newBranchName)} onto ${import_picocolors19.default.bold(syncSource2.ref)}.`);
16205
+ success(`Rebased ${import_picocolors20.default.bold(newBranchName)} onto ${import_picocolors20.default.bold(syncSource2.ref)}.`);
14931
16206
  }
14932
- info(`All your changes are preserved. Run ${import_picocolors19.default.bold("cn submit")} when ready to create a new PR.`, "");
16207
+ info(`All your changes are preserved. Run ${import_picocolors20.default.bold("cn submit")} when ready to create a new PR.`, "");
14933
16208
  return;
14934
16209
  }
14935
16210
  warn("Discarding local changes...");
14936
16211
  }
14937
16212
  const syncSource = getSyncSource(config);
14938
- info(`Switching to ${import_picocolors19.default.bold(baseBranch)} and syncing...`);
16213
+ info(`Switching to ${import_picocolors20.default.bold(baseBranch)} and syncing...`);
14939
16214
  await fetchRemote(syncSource.remote);
14940
16215
  await resetHard("HEAD");
14941
16216
  const coResult = await checkoutBranch(baseBranch);
@@ -14944,23 +16219,23 @@ var submit_default = defineCommand({
14944
16219
  process.exit(1);
14945
16220
  }
14946
16221
  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)}...`);
16222
+ success(`Synced ${import_picocolors20.default.bold(baseBranch)} with ${import_picocolors20.default.bold(syncSource.ref)}.`);
16223
+ info(`Deleting stale branch ${import_picocolors20.default.bold(currentBranch)}...`);
14949
16224
  const delResult = await forceDeleteBranch(currentBranch);
14950
16225
  if (delResult.exitCode === 0) {
14951
- success(`Deleted ${import_picocolors19.default.bold(currentBranch)}.`);
16226
+ success(`Deleted ${import_picocolors20.default.bold(currentBranch)}.`);
14952
16227
  } else {
14953
16228
  warn(`Could not delete branch: ${delResult.stderr.trim()}`);
14954
16229
  }
14955
16230
  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.`);
16231
+ info(`You're now on ${import_picocolors20.default.bold(baseBranch)}. Run ${import_picocolors20.default.bold("cn start")} to begin a new feature.`);
14957
16232
  return;
14958
16233
  }
14959
16234
  }
14960
16235
  if (ghInstalled && ghAuthed) {
14961
16236
  const existingPR = await getPRForBranch(currentBranch);
14962
16237
  if (existingPR) {
14963
- info(`Pushing ${import_picocolors19.default.bold(currentBranch)} to ${origin}...`);
16238
+ info(`Pushing ${import_picocolors20.default.bold(currentBranch)} to ${origin}...`);
14964
16239
  const pushResult2 = await pushSetUpstream(origin, currentBranch);
14965
16240
  if (pushResult2.exitCode !== 0) {
14966
16241
  error(`Failed to push: ${pushResult2.stderr}`);
@@ -14971,8 +16246,8 @@ var submit_default = defineCommand({
14971
16246
  }
14972
16247
  process.exit(1);
14973
16248
  }
14974
- success(`Pushed changes to existing PR #${existingPR.number}: ${import_picocolors19.default.bold(existingPR.title)}`);
14975
- console.log(` ${import_picocolors19.default.cyan(existingPR.url)}`);
16249
+ success(`Pushed changes to existing PR #${existingPR.number}: ${import_picocolors20.default.bold(existingPR.title)}`);
16250
+ console.log(` ${import_picocolors20.default.cyan(existingPR.url)}`);
14976
16251
  return;
14977
16252
  }
14978
16253
  }
@@ -14994,10 +16269,10 @@ var submit_default = defineCommand({
14994
16269
  prBody = result.body;
14995
16270
  spinner.success("PR description generated.");
14996
16271
  console.log(`
14997
- ${import_picocolors19.default.dim("AI title:")} ${import_picocolors19.default.bold(import_picocolors19.default.cyan(prTitle))}`);
16272
+ ${import_picocolors20.default.dim("AI title:")} ${import_picocolors20.default.bold(import_picocolors20.default.cyan(prTitle))}`);
14998
16273
  console.log(`
14999
- ${import_picocolors19.default.dim("AI body preview:")}`);
15000
- console.log(import_picocolors19.default.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
16274
+ ${import_picocolors20.default.dim("AI body preview:")}`);
16275
+ console.log(import_picocolors20.default.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
15001
16276
  } else {
15002
16277
  spinner.fail("AI did not return a PR description.");
15003
16278
  }
@@ -15108,7 +16383,7 @@ ${import_picocolors19.default.dim("AI body preview:")}`);
15108
16383
  warn("Submit cancelled.");
15109
16384
  return;
15110
16385
  }
15111
- info(`Pushing ${import_picocolors19.default.bold(currentBranch)} to ${origin}...`);
16386
+ info(`Pushing ${import_picocolors20.default.bold(currentBranch)} to ${origin}...`);
15112
16387
  const pushResult = await pushSetUpstream(origin, currentBranch);
15113
16388
  if (pushResult.exitCode !== 0) {
15114
16389
  error(`Failed to push: ${pushResult.stderr}`);
@@ -15127,7 +16402,7 @@ ${import_picocolors19.default.dim("AI body preview:")}`);
15127
16402
  const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/compare/${baseBranch}...${currentBranch}?expand=1`;
15128
16403
  console.log();
15129
16404
  info("Create your PR manually:", "");
15130
- console.log(` ${import_picocolors19.default.cyan(prUrl)}`);
16405
+ console.log(` ${import_picocolors20.default.cyan(prUrl)}`);
15131
16406
  } else {
15132
16407
  info("gh CLI not available. Create your PR manually on GitHub.", "");
15133
16408
  }
@@ -15161,7 +16436,7 @@ ${import_picocolors19.default.dim("AI body preview:")}`);
15161
16436
  });
15162
16437
 
15163
16438
  // src/commands/switch.ts
15164
- var import_picocolors20 = __toESM(require_picocolors(), 1);
16439
+ var import_picocolors21 = __toESM(require_picocolors(), 1);
15165
16440
  var switch_default = defineCommand({
15166
16441
  meta: {
15167
16442
  name: "switch",
@@ -15193,11 +16468,11 @@ var switch_default = defineCommand({
15193
16468
  const choices = localBranches.filter((b2) => b2.name !== currentBranch).map((b2) => {
15194
16469
  const labels = [];
15195
16470
  if (protectedBranches.includes(b2.name))
15196
- labels.push(import_picocolors20.default.red("protected"));
16471
+ labels.push(import_picocolors21.default.red("protected"));
15197
16472
  if (b2.upstream)
15198
- labels.push(import_picocolors20.default.dim(`\u2192 ${b2.upstream}`));
16473
+ labels.push(import_picocolors21.default.dim(`\u2192 ${b2.upstream}`));
15199
16474
  if (b2.gone)
15200
- labels.push(import_picocolors20.default.red("remote gone"));
16475
+ labels.push(import_picocolors21.default.red("remote gone"));
15201
16476
  const suffix = labels.length > 0 ? ` ${labels.join(" \xB7 ")}` : "";
15202
16477
  return `${b2.name}${suffix}`;
15203
16478
  });
@@ -15209,7 +16484,7 @@ var switch_default = defineCommand({
15209
16484
  targetBranch = selected.split(/\s{2,}/)[0].trim();
15210
16485
  }
15211
16486
  if (targetBranch === currentBranch) {
15212
- info(`Already on ${import_picocolors20.default.bold(targetBranch)}.`);
16487
+ info(`Already on ${import_picocolors21.default.bold(targetBranch)}.`);
15213
16488
  return;
15214
16489
  }
15215
16490
  if (await hasUncommittedChanges()) {
@@ -15228,7 +16503,7 @@ var switch_default = defineCommand({
15228
16503
  const stashMsg = `contrib-save: auto-save from ${currentBranch}`;
15229
16504
  try {
15230
16505
  await exec("git", ["stash", "push", "-m", stashMsg]);
15231
- info(`Saved changes: ${import_picocolors20.default.dim(stashMsg)}`);
16506
+ info(`Saved changes: ${import_picocolors21.default.dim(stashMsg)}`);
15232
16507
  } catch {
15233
16508
  error("Failed to save changes. Please commit or save manually.");
15234
16509
  process.exit(1);
@@ -15244,9 +16519,9 @@ var switch_default = defineCommand({
15244
16519
  }
15245
16520
  process.exit(1);
15246
16521
  }
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.`, "");
16522
+ success(`Switched to ${import_picocolors21.default.bold(targetBranch)}`);
16523
+ info(`Your changes from ${import_picocolors21.default.bold(currentBranch ?? "previous branch")} are saved.`, "");
16524
+ info(`Use ${import_picocolors21.default.bold("cn save --restore")} to bring them back.`, "");
15250
16525
  return;
15251
16526
  }
15252
16527
  const result = await checkoutBranch(targetBranch);
@@ -15254,12 +16529,12 @@ var switch_default = defineCommand({
15254
16529
  error(`Failed to switch to ${targetBranch}: ${result.stderr}`);
15255
16530
  process.exit(1);
15256
16531
  }
15257
- success(`Switched to ${import_picocolors20.default.bold(targetBranch)}`);
16532
+ success(`Switched to ${import_picocolors21.default.bold(targetBranch)}`);
15258
16533
  }
15259
16534
  });
15260
16535
 
15261
16536
  // src/commands/sync.ts
15262
- var import_picocolors21 = __toESM(require_picocolors(), 1);
16537
+ var import_picocolors22 = __toESM(require_picocolors(), 1);
15263
16538
  var sync_default = defineCommand({
15264
16539
  meta: {
15265
16540
  name: "sync",
@@ -15311,24 +16586,24 @@ var sync_default = defineCommand({
15311
16586
  await fetchRemote(origin);
15312
16587
  }
15313
16588
  if (!await refExists(syncSource.ref)) {
15314
- error(`Remote ref ${import_picocolors21.default.bold(syncSource.ref)} does not exist.`);
16589
+ error(`Remote ref ${import_picocolors22.default.bold(syncSource.ref)} does not exist.`);
15315
16590
  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")}.`, "");
16591
+ info(`Check your config: the base branch may need updating via ${import_picocolors22.default.bold("cn setup")}.`, "");
15317
16592
  process.exit(1);
15318
16593
  }
15319
16594
  let allowMergeCommit = false;
15320
16595
  const div = await getDivergence(baseBranch, syncSource.ref);
15321
16596
  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}`);
16597
+ 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
16598
  } else {
15324
- info(`${import_picocolors21.default.bold(baseBranch)} is already in sync with ${syncSource.ref}`);
16599
+ info(`${import_picocolors22.default.bold(baseBranch)} is already in sync with ${syncSource.ref}`);
15325
16600
  }
15326
16601
  if (div.ahead > 0) {
15327
16602
  const currentBranch = await getCurrentBranch();
15328
16603
  const protectedBranches = getProtectedBranches(config);
15329
16604
  const isOnProtected = currentBranch && protectedBranches.includes(currentBranch);
15330
16605
  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.`);
16606
+ 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
16607
  info("Pulling now could create a merge commit, which breaks clean history.");
15333
16608
  console.log();
15334
16609
  const MOVE_BRANCH = "Move my commits to a new feature branch, then sync";
@@ -15358,7 +16633,7 @@ var sync_default = defineCommand({
15358
16633
  error(`Failed to create branch: ${branchResult.stderr}`);
15359
16634
  process.exit(1);
15360
16635
  }
15361
- success(`Created ${import_picocolors21.default.bold(newBranchName)} with your commits.`);
16636
+ success(`Created ${import_picocolors22.default.bold(newBranchName)} with your commits.`);
15362
16637
  const coResult2 = await checkoutBranch(baseBranch);
15363
16638
  if (coResult2.exitCode !== 0) {
15364
16639
  error(`Failed to checkout ${baseBranch}: ${coResult2.stderr}`);
@@ -15366,11 +16641,11 @@ var sync_default = defineCommand({
15366
16641
  }
15367
16642
  const remoteRef = syncSource.ref;
15368
16643
  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}`);
16644
+ success(`Reset ${import_picocolors22.default.bold(baseBranch)} to ${import_picocolors22.default.bold(remoteRef)}.`);
16645
+ success(`${import_picocolors22.default.bold(baseBranch)} is now in sync with ${syncSource.ref}`);
15371
16646
  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)}.`, "");
16647
+ info(`Your commits are safe on ${import_picocolors22.default.bold(newBranchName)}.`, "");
16648
+ 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
16649
  return;
15375
16650
  }
15376
16651
  allowMergeCommit = true;
@@ -15378,7 +16653,7 @@ var sync_default = defineCommand({
15378
16653
  }
15379
16654
  }
15380
16655
  if (!args.yes) {
15381
- const ok = await confirmPrompt(`This will pull ${import_picocolors21.default.bold(syncSource.ref)} into local ${import_picocolors21.default.bold(baseBranch)}.`);
16656
+ const ok = await confirmPrompt(`This will pull ${import_picocolors22.default.bold(syncSource.ref)} into local ${import_picocolors22.default.bold(baseBranch)}.`);
15382
16657
  if (!ok)
15383
16658
  process.exit(0);
15384
16659
  }
@@ -15392,8 +16667,8 @@ var sync_default = defineCommand({
15392
16667
  if (allowMergeCommit) {
15393
16668
  error(`Pull failed: ${pullResult.stderr.trim()}`);
15394
16669
  } 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.`, "");
16670
+ error(`Fast-forward pull failed. Your local ${import_picocolors22.default.bold(baseBranch)} may have diverged.`);
16671
+ info(`Use ${import_picocolors22.default.bold("cn sync")} again and choose "Move my commits to a new feature branch" to fix this.`, "");
15397
16672
  }
15398
16673
  process.exit(1);
15399
16674
  }
@@ -15401,7 +16676,7 @@ var sync_default = defineCommand({
15401
16676
  if (hasDevBranch(workflow) && role === "maintainer") {
15402
16677
  const mainDiv = await getDivergence(config.mainBranch, `${origin}/${config.mainBranch}`);
15403
16678
  if (mainDiv.behind > 0) {
15404
- info(`Also syncing ${import_picocolors21.default.bold(config.mainBranch)}...`);
16679
+ info(`Also syncing ${import_picocolors22.default.bold(config.mainBranch)}...`);
15405
16680
  const mainCoResult = await checkoutBranch(config.mainBranch);
15406
16681
  if (mainCoResult.exitCode === 0) {
15407
16682
  const mainPullResult = await pullFastForwardOnly(origin, config.mainBranch);
@@ -15442,20 +16717,20 @@ var sync_default = defineCommand({
15442
16717
  }
15443
16718
  }
15444
16719
  console.log();
15445
- console.log(` ${import_picocolors21.default.bold("\uD83D\uDD17 Branch Alignment")}`);
16720
+ console.log(` ${import_picocolors22.default.bold("\uD83D\uDD17 Branch Alignment")}`);
15446
16721
  for (const [hash, names] of groups) {
15447
16722
  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}`);
16723
+ const nameStr = names.map((n2) => import_picocolors22.default.bold(n2)).join(import_picocolors22.default.dim(" \xB7 "));
16724
+ console.log(` ${import_picocolors22.default.yellow(short)} ${import_picocolors22.default.dim("\u2500\u2500")} ${nameStr}`);
15450
16725
  const subject = await getCommitSubject(hash);
15451
16726
  if (subject) {
15452
- console.log(` ${import_picocolors21.default.dim(subject)}`);
16727
+ console.log(` ${import_picocolors22.default.dim(subject)}`);
15453
16728
  }
15454
16729
  }
15455
16730
  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")}`);
16731
+ console.log(` ${import_picocolors22.default.green("\u2713")} ${import_picocolors22.default.green("All branches aligned")} ${import_picocolors22.default.dim("\u2014 ready to start")}`);
15457
16732
  } else {
15458
- console.log(` ${import_picocolors21.default.yellow("\u26A0")} ${import_picocolors21.default.yellow("Branches are not fully aligned")}`);
16733
+ console.log(` ${import_picocolors22.default.yellow("\u26A0")} ${import_picocolors22.default.yellow("Branches are not fully aligned")}`);
15459
16734
  }
15460
16735
  }
15461
16736
  }
@@ -15463,8 +16738,9 @@ var sync_default = defineCommand({
15463
16738
  });
15464
16739
 
15465
16740
  // src/commands/update.ts
15466
- import { readFileSync as readFileSync6 } from "fs";
15467
- var import_picocolors22 = __toESM(require_picocolors(), 1);
16741
+ import { readFileSync as readFileSync7 } from "fs";
16742
+ var import_picocolors23 = __toESM(require_picocolors(), 1);
16743
+ init_gh();
15468
16744
  function hasStaleBranchWorkToPreserve(uniqueCommitsAheadOfBase, hasUncommittedChanges2) {
15469
16745
  return hasUncommittedChanges2 || uniqueCommitsAheadOfBase > 0;
15470
16746
  }
@@ -15505,7 +16781,7 @@ var update_default = defineCommand({
15505
16781
  }
15506
16782
  if (protectedBranches.includes(currentBranch)) {
15507
16783
  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.`);
16784
+ warn(`You're on ${import_picocolors23.default.bold(currentBranch)}, which is a protected branch. Updates (rebase) apply to feature branches.`);
15509
16785
  await fetchAll();
15510
16786
  const { origin } = config;
15511
16787
  const remoteRef = `${origin}/${currentBranch}`;
@@ -15514,12 +16790,12 @@ var update_default = defineCommand({
15514
16790
  const hasCommits = localWork.unpushedCommits > 0;
15515
16791
  const hasAnything = hasCommits || dirty;
15516
16792
  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.`);
16793
+ info(`No local changes found on ${import_picocolors23.default.bold(currentBranch)}.`);
16794
+ 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
16795
  process.exit(1);
15520
16796
  }
15521
16797
  if (hasCommits) {
15522
- info(`Found ${import_picocolors22.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${import_picocolors22.default.bold(currentBranch)}.`);
16798
+ info(`Found ${import_picocolors23.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${import_picocolors23.default.bold(currentBranch)}.`);
15523
16799
  }
15524
16800
  if (dirty) {
15525
16801
  info("You also have uncommitted changes in the working tree.");
@@ -15549,12 +16825,12 @@ var update_default = defineCommand({
15549
16825
  error(`Failed to create branch: ${branchResult.stderr}`);
15550
16826
  process.exit(1);
15551
16827
  }
15552
- success(`Created ${import_picocolors22.default.bold(newBranchName)} with your changes.`);
16828
+ success(`Created ${import_picocolors23.default.bold(newBranchName)} with your changes.`);
15553
16829
  await updateLocalBranch(currentBranch, remoteRef);
15554
- info(`Reset ${import_picocolors22.default.bold(currentBranch)} back to ${import_picocolors22.default.bold(remoteRef)} \u2014 no damage done.`, "");
16830
+ info(`Reset ${import_picocolors23.default.bold(currentBranch)} back to ${import_picocolors23.default.bold(remoteRef)} \u2014 no damage done.`, "");
15555
16831
  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)}.`, "");
16832
+ success(`You're now on ${import_picocolors23.default.bold(newBranchName)} with all your work intact.`);
16833
+ info(`Run ${import_picocolors23.default.bold("cn update")} again to rebase onto latest ${import_picocolors23.default.bold(baseBranch)}.`, "");
15558
16834
  return;
15559
16835
  }
15560
16836
  if (await hasUncommittedChanges()) {
@@ -15564,8 +16840,8 @@ var update_default = defineCommand({
15564
16840
  await projectHeading("update", "\uD83D\uDD03");
15565
16841
  const mergedPR = await getMergedPRForBranch(currentBranch);
15566
16842
  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)}`, "");
16843
+ warn(`PR #${mergedPR.number} (${import_picocolors23.default.bold(mergedPR.title)}) has already been merged.`);
16844
+ info(`Link: ${import_picocolors23.default.underline(mergedPR.url)}`, "");
15569
16845
  const uniqueCommitsAheadOfBase = await countCommitsAhead(currentBranch, syncSource.ref);
15570
16846
  const dirty = await hasUncommittedChanges();
15571
16847
  const hasWork = hasStaleBranchWorkToPreserve(uniqueCommitsAheadOfBase, dirty);
@@ -15574,12 +16850,12 @@ var update_default = defineCommand({
15574
16850
  info("You have uncommitted local changes.");
15575
16851
  }
15576
16852
  if (uniqueCommitsAheadOfBase > 0) {
15577
- info(`You have ${uniqueCommitsAheadOfBase} local commit(s) not in ${import_picocolors22.default.bold(syncSource.ref)}.`);
16853
+ info(`You have ${uniqueCommitsAheadOfBase} local commit(s) not in ${import_picocolors23.default.bold(syncSource.ref)}.`);
15578
16854
  }
15579
16855
  const SAVE_NEW_BRANCH = "Save changes to a new branch";
15580
16856
  const DISCARD = "Discard all changes and clean up";
15581
16857
  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]);
16858
+ 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
16859
  if (action === CANCEL) {
15584
16860
  info("No changes made. You are still on your current branch.");
15585
16861
  return;
@@ -15601,7 +16877,7 @@ var update_default = defineCommand({
15601
16877
  error(`Failed to rename branch: ${renameResult.stderr}`);
15602
16878
  process.exit(1);
15603
16879
  }
15604
- success(`Renamed ${import_picocolors22.default.bold(currentBranch)} \u2192 ${import_picocolors22.default.bold(newBranchName)}`);
16880
+ success(`Renamed ${import_picocolors23.default.bold(currentBranch)} \u2192 ${import_picocolors23.default.bold(newBranchName)}`);
15605
16881
  await unsetUpstream();
15606
16882
  await fetchRemote(syncSource.remote);
15607
16883
  let rebaseResult2;
@@ -15613,11 +16889,11 @@ var update_default = defineCommand({
15613
16889
  }
15614
16890
  if (rebaseResult2.exitCode !== 0) {
15615
16891
  warn("Rebase encountered conflicts. Resolve them manually, then run:");
15616
- info(` ${import_picocolors22.default.bold("git rebase --continue")}`, "");
16892
+ info(` ${import_picocolors23.default.bold("git rebase --continue")}`, "");
15617
16893
  } else {
15618
- success(`Rebased ${import_picocolors22.default.bold(newBranchName)} onto ${import_picocolors22.default.bold(syncSource.ref)}.`);
16894
+ success(`Rebased ${import_picocolors23.default.bold(newBranchName)} onto ${import_picocolors23.default.bold(syncSource.ref)}.`);
15619
16895
  }
15620
- info(`All your changes are preserved. Run ${import_picocolors22.default.bold("cn submit")} when ready to create a new PR.`, "");
16896
+ info(`All your changes are preserved. Run ${import_picocolors23.default.bold("cn submit")} when ready to create a new PR.`, "");
15621
16897
  return;
15622
16898
  }
15623
16899
  warn("Discarding local changes...");
@@ -15636,24 +16912,24 @@ var update_default = defineCommand({
15636
16912
  process.exit(1);
15637
16913
  }
15638
16914
  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)}...`);
16915
+ success(`Synced ${import_picocolors23.default.bold(baseBranch)} with ${import_picocolors23.default.bold(syncSource.ref)}.`);
16916
+ info(`Deleting stale branch ${import_picocolors23.default.bold(currentBranch)}...`);
15641
16917
  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.`, "");
16918
+ success(`Deleted ${import_picocolors23.default.bold(currentBranch)}.`);
16919
+ info(`Run ${import_picocolors23.default.bold("cn start")} to begin a new feature branch.`, "");
15644
16920
  return;
15645
16921
  }
15646
- info(`Updating ${import_picocolors22.default.bold(currentBranch)} with latest ${import_picocolors22.default.bold(baseBranch)}...`);
16922
+ info(`Updating ${import_picocolors23.default.bold(currentBranch)} with latest ${import_picocolors23.default.bold(baseBranch)}...`);
15647
16923
  await fetchRemote(syncSource.remote);
15648
16924
  if (!await refExists(syncSource.ref)) {
15649
- error(`Remote ref ${import_picocolors22.default.bold(syncSource.ref)} does not exist.`);
16925
+ error(`Remote ref ${import_picocolors23.default.bold(syncSource.ref)} does not exist.`);
15650
16926
  error("Run `git fetch --all` and verify your remote configuration.");
15651
16927
  process.exit(1);
15652
16928
  }
15653
16929
  await updateLocalBranch(baseBranch, syncSource.ref);
15654
16930
  const rebaseStrategy = await determineRebaseStrategy(currentBranch, syncSource.ref);
15655
16931
  if (rebaseStrategy.strategy === "onto" && rebaseStrategy.ontoOldBase) {
15656
- info(import_picocolors22.default.dim(`Using --onto rebase (branch was based on a different ref)`));
16932
+ info(import_picocolors23.default.dim(`Using --onto rebase (branch was based on a different ref)`));
15657
16933
  }
15658
16934
  const rebaseResult = rebaseStrategy.strategy === "onto" && rebaseStrategy.ontoOldBase ? await rebaseOnto(syncSource.ref, rebaseStrategy.ontoOldBase) : await rebase(syncSource.ref);
15659
16935
  if (rebaseResult.exitCode !== 0) {
@@ -15667,7 +16943,7 @@ var update_default = defineCommand({
15667
16943
  let conflictDiff = "";
15668
16944
  for (const file of conflictFiles.slice(0, 3)) {
15669
16945
  try {
15670
- const content = readFileSync6(file, "utf-8");
16946
+ const content = readFileSync7(file, "utf-8");
15671
16947
  if (content.includes("<<<<<<<")) {
15672
16948
  conflictDiff += `
15673
16949
  --- ${file} ---
@@ -15684,10 +16960,10 @@ ${content.slice(0, 2000)}
15684
16960
  if (suggestion) {
15685
16961
  spinner.success("AI conflict guidance ready.");
15686
16962
  console.log(`
15687
- ${import_picocolors22.default.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
15688
- console.log(import_picocolors22.default.dim("\u2500".repeat(60)));
16963
+ ${import_picocolors23.default.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
16964
+ console.log(import_picocolors23.default.dim("\u2500".repeat(60)));
15689
16965
  console.log(suggestion);
15690
- console.log(import_picocolors22.default.dim("\u2500".repeat(60)));
16966
+ console.log(import_picocolors23.default.dim("\u2500".repeat(60)));
15691
16967
  console.log();
15692
16968
  } else {
15693
16969
  spinner.fail("AI could not analyze the conflicts.");
@@ -15695,21 +16971,21 @@ ${import_picocolors22.default.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance
15695
16971
  }
15696
16972
  }
15697
16973
  }
15698
- console.log(import_picocolors22.default.bold("To resolve:"));
16974
+ console.log(import_picocolors23.default.bold("To resolve:"));
15699
16975
  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")}`);
16976
+ console.log(` 2. ${import_picocolors23.default.cyan("git add <resolved-files>")}`);
16977
+ console.log(` 3. ${import_picocolors23.default.cyan("git rebase --continue")}`);
15702
16978
  console.log();
15703
- console.log(` Or abort: ${import_picocolors22.default.cyan("git rebase --abort")}`);
16979
+ console.log(` Or abort: ${import_picocolors23.default.cyan("git rebase --abort")}`);
15704
16980
  process.exit(1);
15705
16981
  }
15706
- success(`${import_picocolors22.default.bold(currentBranch)} has been rebased onto latest ${import_picocolors22.default.bold(baseBranch)}`);
16982
+ success(`${import_picocolors23.default.bold(currentBranch)} has been rebased onto latest ${import_picocolors23.default.bold(baseBranch)}`);
15707
16983
  }
15708
16984
  });
15709
16985
 
15710
16986
  // src/commands/validate.ts
15711
- import { readFileSync as readFileSync7 } from "fs";
15712
- var import_picocolors23 = __toESM(require_picocolors(), 1);
16987
+ import { readFileSync as readFileSync8 } from "fs";
16988
+ var import_picocolors24 = __toESM(require_picocolors(), 1);
15713
16989
  var validate_default = defineCommand({
15714
16990
  meta: {
15715
16991
  name: "validate",
@@ -15738,7 +17014,7 @@ var validate_default = defineCommand({
15738
17014
  info('Commit convention is set to "none". All messages are accepted.');
15739
17015
  process.exit(0);
15740
17016
  }
15741
- const message = args.file ? readFileSync7(args.file, "utf-8").split(/\r?\n/, 1)[0] ?? "" : args.message;
17017
+ const message = args.file ? readFileSync8(args.file, "utf-8").split(/\r?\n/, 1)[0] ?? "" : args.message;
15742
17018
  if (!message) {
15743
17019
  error("No commit message provided. Pass a message or use --file <path>.");
15744
17020
  process.exit(1);
@@ -15749,14 +17025,14 @@ var validate_default = defineCommand({
15749
17025
  }
15750
17026
  const errors = getValidationError(convention);
15751
17027
  for (const line of errors) {
15752
- console.error(import_picocolors23.default.red(` \u2717 ${line}`));
17028
+ console.error(import_picocolors24.default.red(` \u2717 ${line}`));
15753
17029
  }
15754
17030
  process.exit(1);
15755
17031
  }
15756
17032
  });
15757
17033
 
15758
17034
  // src/ui/banner.ts
15759
- var import_picocolors24 = __toESM(require_picocolors(), 1);
17035
+ var import_picocolors25 = __toESM(require_picocolors(), 1);
15760
17036
 
15761
17037
  // src/data/announcements.json
15762
17038
  var announcements_default = [
@@ -15817,9 +17093,9 @@ function getAuthor() {
15817
17093
  return typeof package_default.author === "string" ? package_default.author : "unknown";
15818
17094
  }
15819
17095
  function showBanner(variant = "small") {
15820
- console.log(import_picocolors24.default.cyan(`
17096
+ console.log(import_picocolors25.default.cyan(`
15821
17097
  ${LOGO}`));
15822
- console.log(` ${import_picocolors24.default.dim(`v${getVersion()}`)} ${import_picocolors24.default.dim("\u2014")} ${import_picocolors24.default.dim(`Built by ${getAuthor()}`)}`);
17098
+ console.log(` ${import_picocolors25.default.dim(`v${getVersion()}`)} ${import_picocolors25.default.dim("\u2014")} ${import_picocolors25.default.dim(`Built by ${getAuthor()}`)}`);
15823
17099
  const announcements = getActiveAnnouncements();
15824
17100
  if (announcements.length > 0) {
15825
17101
  console.log();
@@ -15828,27 +17104,27 @@ ${LOGO}`));
15828
17104
  if (variant === "big") {
15829
17105
  const panelLines = [
15830
17106
  {
15831
- label: import_picocolors24.default.bold(import_picocolors24.default.cyan("Getting Started")),
17107
+ label: import_picocolors25.default.bold(import_picocolors25.default.cyan("Getting Started")),
15832
17108
  rawLabel: "Getting Started",
15833
17109
  value: "",
15834
17110
  rawValue: ""
15835
17111
  },
15836
17112
  {
15837
- label: import_picocolors24.default.cyan("cn setup"),
17113
+ label: import_picocolors25.default.cyan("cn setup"),
15838
17114
  rawLabel: "cn setup",
15839
- value: import_picocolors24.default.dim("configure workflow, remotes, and defaults"),
17115
+ value: import_picocolors25.default.dim("configure workflow, remotes, and defaults"),
15840
17116
  rawValue: "configure workflow, remotes, and defaults"
15841
17117
  },
15842
17118
  {
15843
- label: import_picocolors24.default.cyan("cn doctor"),
17119
+ label: import_picocolors25.default.cyan("cn doctor"),
15844
17120
  rawLabel: "cn doctor",
15845
- value: import_picocolors24.default.dim("verify your environment before doing any work"),
17121
+ value: import_picocolors25.default.dim("verify your environment before doing any work"),
15846
17122
  rawValue: "verify your environment before doing any work"
15847
17123
  },
15848
17124
  {
15849
- label: import_picocolors24.default.cyan("cn start"),
17125
+ label: import_picocolors25.default.cyan("cn start"),
15850
17126
  rawLabel: "cn start",
15851
- value: import_picocolors24.default.dim("create a branch and begin the next task"),
17127
+ value: import_picocolors25.default.dim("create a branch and begin the next task"),
15852
17128
  rawValue: "create a branch and begin the next task"
15853
17129
  },
15854
17130
  {
@@ -15858,13 +17134,13 @@ ${LOGO}`));
15858
17134
  rawValue: ""
15859
17135
  },
15860
17136
  {
15861
- label: import_picocolors24.default.bold(import_picocolors24.default.cyan("Workflow")),
17137
+ label: import_picocolors25.default.bold(import_picocolors25.default.cyan("Workflow")),
15862
17138
  rawLabel: "Workflow",
15863
17139
  value: "",
15864
17140
  rawValue: ""
15865
17141
  },
15866
17142
  {
15867
- label: import_picocolors24.default.dim("cn setup \u2192 cn commit \u2192 cn update \u2192 cn submit"),
17143
+ label: import_picocolors25.default.dim("cn setup \u2192 cn commit \u2192 cn update \u2192 cn submit"),
15868
17144
  rawLabel: "cn setup \u2192 cn commit \u2192 cn update \u2192 cn submit",
15869
17145
  value: "",
15870
17146
  rawValue: ""
@@ -15889,22 +17165,22 @@ ${LOGO}`));
15889
17165
  return Math.max(max, lineLength);
15890
17166
  }, 0));
15891
17167
  console.log();
15892
- console.log(` ${import_picocolors24.default.dim(`\u250C${"\u2500".repeat(contentWidth + 2)}\u2510`)}`);
17168
+ console.log(` ${import_picocolors25.default.dim(`\u250C${"\u2500".repeat(contentWidth + 2)}\u2510`)}`);
15893
17169
  for (const line of rows) {
15894
17170
  if (!line.rawLabel && !line.rawValue) {
15895
- console.log(` ${import_picocolors24.default.dim("\u2502")} ${" ".repeat(contentWidth)} ${import_picocolors24.default.dim("\u2502")}`);
17171
+ console.log(` ${import_picocolors25.default.dim("\u2502")} ${" ".repeat(contentWidth)} ${import_picocolors25.default.dim("\u2502")}`);
15896
17172
  continue;
15897
17173
  }
15898
17174
  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) : "";
17175
+ const value = line.rawValue ? import_picocolors25.default.dim(line.rawValue) : "";
15900
17176
  const rawLength = line.rawValue ? labelWidth + 2 + line.rawValue.length : line.rawLabel.length;
15901
17177
  const trailing = " ".repeat(Math.max(0, contentWidth - rawLength));
15902
- console.log(` ${import_picocolors24.default.dim("\u2502")} ${left}${value}${trailing} ${import_picocolors24.default.dim("\u2502")}`);
17178
+ console.log(` ${import_picocolors25.default.dim("\u2502")} ${left}${value}${trailing} ${import_picocolors25.default.dim("\u2502")}`);
15903
17179
  }
15904
- console.log(` ${import_picocolors24.default.dim(`\u2514${"\u2500".repeat(contentWidth + 2)}\u2518`)}`);
17180
+ console.log(` ${import_picocolors25.default.dim(`\u2514${"\u2500".repeat(contentWidth + 2)}\u2518`)}`);
15905
17181
  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"))}`);
17182
+ 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"))}`);
17183
+ console.log(` ${import_picocolors25.default.dim("Sponsor:")} ${import_picocolors25.default.dim(linkify("warengonzaga.com/sponsor", "https://warengonzaga.com/sponsor"))}`);
15908
17184
  }
15909
17185
  console.log();
15910
17186
  }
@@ -15936,7 +17212,7 @@ function renderAnnouncementBanner(announcement) {
15936
17212
  console.log(` ${tone.border(`\u250C${"\u2500".repeat(rawWidth + 2)}\u2510`)}`);
15937
17213
  for (const line of lines) {
15938
17214
  const trailing = " ".repeat(Math.max(0, rawWidth - line.length));
15939
- const content = line === title ? tone.title(line) : import_picocolors24.default.dim(line);
17215
+ const content = line === title ? tone.title(line) : import_picocolors25.default.dim(line);
15940
17216
  console.log(` ${tone.border("\u2502")} ${content}${trailing} ${tone.border("\u2502")}`);
15941
17217
  }
15942
17218
  console.log(` ${tone.border(`\u2514${"\u2500".repeat(rawWidth + 2)}\u2518`)}`);
@@ -15972,20 +17248,20 @@ function getAnnouncementTone(kind) {
15972
17248
  case "info":
15973
17249
  return {
15974
17250
  emoji: "\u2139",
15975
- border: import_picocolors24.default.blue,
15976
- title: (value) => import_picocolors24.default.bold(import_picocolors24.default.blue(value))
17251
+ border: import_picocolors25.default.blue,
17252
+ title: (value) => import_picocolors25.default.bold(import_picocolors25.default.blue(value))
15977
17253
  };
15978
17254
  case "warning":
15979
17255
  return {
15980
17256
  emoji: "\uD83D\uDEA8",
15981
- border: import_picocolors24.default.red,
15982
- title: (value) => import_picocolors24.default.bold(import_picocolors24.default.red(value))
17257
+ border: import_picocolors25.default.red,
17258
+ title: (value) => import_picocolors25.default.bold(import_picocolors25.default.red(value))
15983
17259
  };
15984
17260
  default:
15985
17261
  return {
15986
17262
  emoji: "\u26A0",
15987
- border: import_picocolors24.default.yellow,
15988
- title: (value) => import_picocolors24.default.bold(import_picocolors24.default.yellow(value))
17263
+ border: import_picocolors25.default.yellow,
17264
+ title: (value) => import_picocolors25.default.bold(import_picocolors25.default.yellow(value))
15989
17265
  };
15990
17266
  }
15991
17267
  }
@@ -16024,7 +17300,8 @@ if (!isVersion) {
16024
17300
  "branch",
16025
17301
  "hook",
16026
17302
  "validate",
16027
- "doctor"
17303
+ "doctor",
17304
+ "label"
16028
17305
  ];
16029
17306
  const isHelp = process.argv.includes("--help") || process.argv.includes("-h");
16030
17307
  const hasSubCommand = subCommands.some((cmd) => process.argv.includes(cmd));
@@ -16061,7 +17338,8 @@ var main = defineCommand({
16061
17338
  log: log_default,
16062
17339
  hook: hook_default,
16063
17340
  validate: validate_default,
16064
- doctor: doctor_default
17341
+ doctor: doctor_default,
17342
+ label: label_default
16065
17343
  },
16066
17344
  run({ args }) {
16067
17345
  if (args.version) {