contribute-now 0.6.2-dev.b908626 → 0.6.2-dev.d6e92ac

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 +10 -3
  2. package/dist/index.js +1876 -924
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2355,8 +2355,8 @@ var require_semaphore = __commonJS((exports) => {
2355
2355
  this._waiting = [];
2356
2356
  }
2357
2357
  lock(thunk) {
2358
- return new Promise((resolve2, reject) => {
2359
- this._waiting.push({ thunk, resolve: resolve2, reject });
2358
+ return new Promise((resolve3, reject) => {
2359
+ this._waiting.push({ thunk, resolve: resolve3, reject });
2360
2360
  this.runNext();
2361
2361
  });
2362
2362
  }
@@ -3847,9 +3847,9 @@ ${JSON.stringify(message, null, 4)}`);
3847
3847
  if (typeof cancellationStrategy.sender.enableCancellation === "function") {
3848
3848
  cancellationStrategy.sender.enableCancellation(requestMessage);
3849
3849
  }
3850
- return new Promise(async (resolve2, reject) => {
3850
+ return new Promise(async (resolve3, reject) => {
3851
3851
  const resolveWithCleanup = (r3) => {
3852
- resolve2(r3);
3852
+ resolve3(r3);
3853
3853
  cancellationStrategy.sender.cleanup(id);
3854
3854
  disposable?.dispose();
3855
3855
  };
@@ -4258,10 +4258,10 @@ var require_ril = __commonJS((exports) => {
4258
4258
  return api_1.Disposable.create(() => this.stream.off("end", listener));
4259
4259
  }
4260
4260
  write(data, encoding) {
4261
- return new Promise((resolve2, reject) => {
4261
+ return new Promise((resolve3, reject) => {
4262
4262
  const callback = (error2) => {
4263
4263
  if (error2 === undefined || error2 === null) {
4264
- resolve2();
4264
+ resolve3();
4265
4265
  } else {
4266
4266
  reject(error2);
4267
4267
  }
@@ -4520,10 +4520,10 @@ var require_main = __commonJS((exports) => {
4520
4520
  exports.generateRandomPipeName = generateRandomPipeName;
4521
4521
  function createClientPipeTransport(pipeName, encoding = "utf-8") {
4522
4522
  let connectResolve;
4523
- const connected = new Promise((resolve2, _reject) => {
4524
- connectResolve = resolve2;
4523
+ const connected = new Promise((resolve3, _reject) => {
4524
+ connectResolve = resolve3;
4525
4525
  });
4526
- return new Promise((resolve2, reject) => {
4526
+ return new Promise((resolve3, reject) => {
4527
4527
  let server = (0, net_1.createServer)((socket) => {
4528
4528
  server.close();
4529
4529
  connectResolve([
@@ -4534,7 +4534,7 @@ var require_main = __commonJS((exports) => {
4534
4534
  server.on("error", reject);
4535
4535
  server.listen(pipeName, () => {
4536
4536
  server.removeListener("error", reject);
4537
- resolve2({
4537
+ resolve3({
4538
4538
  onConnected: () => {
4539
4539
  return connected;
4540
4540
  }
@@ -4553,10 +4553,10 @@ var require_main = __commonJS((exports) => {
4553
4553
  exports.createServerPipeTransport = createServerPipeTransport;
4554
4554
  function createClientSocketTransport(port, encoding = "utf-8") {
4555
4555
  let connectResolve;
4556
- const connected = new Promise((resolve2, _reject) => {
4557
- connectResolve = resolve2;
4556
+ const connected = new Promise((resolve3, _reject) => {
4557
+ connectResolve = resolve3;
4558
4558
  });
4559
- return new Promise((resolve2, reject) => {
4559
+ return new Promise((resolve3, reject) => {
4560
4560
  const server = (0, net_1.createServer)((socket) => {
4561
4561
  server.close();
4562
4562
  connectResolve([
@@ -4567,7 +4567,7 @@ var require_main = __commonJS((exports) => {
4567
4567
  server.on("error", reject);
4568
4568
  server.listen(port, "127.0.0.1", () => {
4569
4569
  server.removeListener("error", reject);
4570
- resolve2({
4570
+ resolve3({
4571
4571
  onConnected: () => {
4572
4572
  return connected;
4573
4573
  }
@@ -4691,8 +4691,8 @@ class CopilotSession {
4691
4691
  const effectiveTimeout = timeout ?? 60000;
4692
4692
  let resolveIdle;
4693
4693
  let rejectWithError;
4694
- const idlePromise = new Promise((resolve2, reject) => {
4695
- resolveIdle = resolve2;
4694
+ const idlePromise = new Promise((resolve3, reject) => {
4695
+ resolveIdle = resolve3;
4696
4696
  rejectWithError = reject;
4697
4697
  });
4698
4698
  let lastAssistantMessage;
@@ -4857,7 +4857,7 @@ var init_session = __esm(() => {
4857
4857
  import { spawn } from "node:child_process";
4858
4858
  import { existsSync as existsSync3 } from "node:fs";
4859
4859
  import { Socket } from "node:net";
4860
- import { dirname as dirname2, join as join3 } from "node:path";
4860
+ import { dirname as dirname3, join as join3 } from "node:path";
4861
4861
  import { fileURLToPath } from "node:url";
4862
4862
  function isZodSchema(value) {
4863
4863
  return value != null && typeof value === "object" && "toJSONSchema" in value && typeof value.toJSONSchema === "function";
@@ -4879,7 +4879,7 @@ function getNodeExecPath() {
4879
4879
  function getBundledCliPath() {
4880
4880
  const sdkUrl = import.meta.resolve("@github/copilot/sdk");
4881
4881
  const sdkPath = fileURLToPath(sdkUrl);
4882
- return join3(dirname2(dirname2(sdkPath)), "index.js");
4882
+ return join3(dirname3(dirname3(sdkPath)), "index.js");
4883
4883
  }
4884
4884
 
4885
4885
  class CopilotClient {
@@ -4984,7 +4984,7 @@ class CopilotClient {
4984
4984
  lastError = error2 instanceof Error ? error2 : new Error(String(error2));
4985
4985
  if (attempt < 3) {
4986
4986
  const delay = 100 * Math.pow(2, attempt - 1);
4987
- await new Promise((resolve2) => setTimeout(resolve2, delay));
4987
+ await new Promise((resolve3) => setTimeout(resolve3, delay));
4988
4988
  }
4989
4989
  }
4990
4990
  }
@@ -5184,8 +5184,8 @@ class CopilotClient {
5184
5184
  }
5185
5185
  await this.modelsCacheLock;
5186
5186
  let resolveLock;
5187
- this.modelsCacheLock = new Promise((resolve2) => {
5188
- resolveLock = resolve2;
5187
+ this.modelsCacheLock = new Promise((resolve3) => {
5188
+ resolveLock = resolve3;
5189
5189
  });
5190
5190
  try {
5191
5191
  if (this.modelsCache !== null) {
@@ -5290,7 +5290,7 @@ class CopilotClient {
5290
5290
  };
5291
5291
  }
5292
5292
  async startCLIServer() {
5293
- return new Promise((resolve2, reject) => {
5293
+ return new Promise((resolve3, reject) => {
5294
5294
  this.stderrBuffer = "";
5295
5295
  const args = [
5296
5296
  ...this.options.cliArgs,
@@ -5339,7 +5339,7 @@ class CopilotClient {
5339
5339
  let resolved = false;
5340
5340
  if (this.options.useStdio) {
5341
5341
  resolved = true;
5342
- resolve2();
5342
+ resolve3();
5343
5343
  } else {
5344
5344
  this.cliProcess.stdout?.on("data", (data) => {
5345
5345
  stdout2 += data.toString();
@@ -5347,7 +5347,7 @@ class CopilotClient {
5347
5347
  if (match && !resolved) {
5348
5348
  this.actualPort = parseInt(match[1], 10);
5349
5349
  resolved = true;
5350
- resolve2();
5350
+ resolve3();
5351
5351
  }
5352
5352
  });
5353
5353
  }
@@ -5434,13 +5434,13 @@ stderr: ${stderrOutput}`));
5434
5434
  if (!this.actualPort) {
5435
5435
  throw new Error("Server port not available");
5436
5436
  }
5437
- return new Promise((resolve2, reject) => {
5437
+ return new Promise((resolve3, reject) => {
5438
5438
  this.socket = new Socket;
5439
5439
  this.socket.connect(this.actualPort, this.actualHost, () => {
5440
5440
  this.connection = import_node.createMessageConnection(new import_node.StreamMessageReader(this.socket), new import_node.StreamMessageWriter(this.socket));
5441
5441
  this.attachConnectionHandlers();
5442
5442
  this.connection.listen();
5443
- resolve2();
5443
+ resolve3();
5444
5444
  });
5445
5445
  this.socket.on("error", (error2) => {
5446
5446
  reject(new Error(`Failed to connect to CLI server: ${error2.message}`));
@@ -7047,22 +7047,56 @@ async function runMain(cmd, opts = {}) {
7047
7047
  var import_picocolors2 = __toESM(require_picocolors(), 1);
7048
7048
 
7049
7049
  // src/utils/config.ts
7050
- import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
7051
- import { join } from "node:path";
7050
+ import {
7051
+ appendFileSync,
7052
+ existsSync,
7053
+ mkdirSync,
7054
+ readFileSync,
7055
+ statSync,
7056
+ writeFileSync
7057
+ } from "node:fs";
7058
+ import { dirname, join, resolve } from "node:path";
7052
7059
  var CONFIG_FILENAME = ".contributerc.json";
7053
- function getConfigPath(cwd = process.cwd()) {
7054
- return join(cwd, CONFIG_FILENAME);
7055
- }
7056
- function configExists(cwd = process.cwd()) {
7057
- return existsSync(getConfigPath(cwd));
7060
+ var LOCAL_CONFIG_DIRNAME = "contribute-now";
7061
+ var LOCAL_CONFIG_FILENAME = "config.json";
7062
+ function findRepoRoot(cwd = process.cwd()) {
7063
+ let current = resolve(cwd);
7064
+ while (true) {
7065
+ if (existsSync(join(current, ".git"))) {
7066
+ return current;
7067
+ }
7068
+ const parent = dirname(current);
7069
+ if (parent === current) {
7070
+ return null;
7071
+ }
7072
+ current = parent;
7073
+ }
7058
7074
  }
7059
- var VALID_WORKFLOWS = ["clean-flow", "github-flow", "git-flow"];
7060
- var VALID_ROLES = ["maintainer", "contributor"];
7061
- var VALID_CONVENTIONS = ["conventional", "clean-commit", "none"];
7062
- function readConfig(cwd = process.cwd()) {
7063
- const path = getConfigPath(cwd);
7064
- if (!existsSync(path))
7075
+ function resolveGitDir(cwd = process.cwd()) {
7076
+ const repoRoot = findRepoRoot(cwd);
7077
+ if (!repoRoot) {
7078
+ return null;
7079
+ }
7080
+ const dotGitPath = join(repoRoot, ".git");
7081
+ try {
7082
+ const stat = statSync(dotGitPath);
7083
+ if (stat.isDirectory()) {
7084
+ return dotGitPath;
7085
+ }
7086
+ if (!stat.isFile()) {
7087
+ return null;
7088
+ }
7089
+ const content = readFileSync(dotGitPath, "utf-8").trim();
7090
+ const match = /^gitdir:\s*(.+)$/i.exec(content);
7091
+ if (!match) {
7092
+ return null;
7093
+ }
7094
+ return resolve(repoRoot, match[1].trim());
7095
+ } catch {
7065
7096
  return null;
7097
+ }
7098
+ }
7099
+ function parseConfigFile(path) {
7066
7100
  try {
7067
7101
  const raw = readFileSync(path, "utf-8");
7068
7102
  const parsed = JSON.parse(raw);
@@ -7070,44 +7104,112 @@ function readConfig(cwd = process.cwd()) {
7070
7104
  return null;
7071
7105
  }
7072
7106
  if (!VALID_WORKFLOWS.includes(parsed.workflow)) {
7073
- console.error(`Invalid workflow "${parsed.workflow}" in .contributerc.json. Valid: ${VALID_WORKFLOWS.join(", ")}`);
7107
+ console.error(`Invalid workflow "${parsed.workflow}" in ${path.endsWith(CONFIG_FILENAME) ? CONFIG_FILENAME : LOCAL_CONFIG_FILENAME}. Valid: ${VALID_WORKFLOWS.join(", ")}`);
7074
7108
  return null;
7075
7109
  }
7076
7110
  if (!VALID_ROLES.includes(parsed.role)) {
7077
- console.error(`Invalid role "${parsed.role}" in .contributerc.json. Valid: ${VALID_ROLES.join(", ")}`);
7111
+ console.error(`Invalid role "${parsed.role}" in ${path.endsWith(CONFIG_FILENAME) ? CONFIG_FILENAME : LOCAL_CONFIG_FILENAME}. Valid: ${VALID_ROLES.join(", ")}`);
7078
7112
  return null;
7079
7113
  }
7080
7114
  if (!VALID_CONVENTIONS.includes(parsed.commitConvention)) {
7081
- console.error(`Invalid commitConvention "${parsed.commitConvention}" in .contributerc.json. Valid: ${VALID_CONVENTIONS.join(", ")}`);
7115
+ console.error(`Invalid commitConvention "${parsed.commitConvention}" in ${path.endsWith(CONFIG_FILENAME) ? CONFIG_FILENAME : LOCAL_CONFIG_FILENAME}. Valid: ${VALID_CONVENTIONS.join(", ")}`);
7082
7116
  return null;
7083
7117
  }
7084
7118
  if (!parsed.mainBranch.trim()) {
7085
- console.error("Invalid .contributerc.json: mainBranch must not be empty.");
7119
+ console.error(`Invalid config (${path}): mainBranch must not be empty.`);
7086
7120
  return null;
7087
7121
  }
7088
7122
  if (!parsed.origin.trim()) {
7089
- console.error("Invalid .contributerc.json: origin must not be empty.");
7123
+ console.error(`Invalid config (${path}): origin must not be empty.`);
7090
7124
  return null;
7091
7125
  }
7092
7126
  if (parsed.role === "contributor" && !parsed.upstream.trim()) {
7093
- console.error("Invalid .contributerc.json: upstream must not be empty for contributors.");
7127
+ console.error(`Invalid config (${path}): upstream must not be empty for contributors.`);
7094
7128
  return null;
7095
7129
  }
7096
7130
  if (parsed.branchPrefixes.length === 0) {
7097
- console.error("Invalid .contributerc.json: branchPrefixes must not be empty.");
7131
+ console.error(`Invalid config (${path}): branchPrefixes must not be empty.`);
7098
7132
  return null;
7099
7133
  }
7100
7134
  if (!parsed.branchPrefixes.every((p) => typeof p === "string" && p.trim().length > 0)) {
7101
- console.error("Invalid .contributerc.json: all branchPrefixes must be non-empty strings.");
7135
+ console.error(`Invalid config (${path}): all branchPrefixes must be non-empty strings.`);
7102
7136
  return null;
7103
7137
  }
7104
- return parsed;
7138
+ return {
7139
+ ...parsed,
7140
+ aiEnabled: parsed.aiEnabled !== false,
7141
+ showTips: parsed.showTips !== false,
7142
+ guideRotation: parsed.guideRotation && typeof parsed.guideRotation === "object" ? parsed.guideRotation : {}
7143
+ };
7105
7144
  } catch {
7106
7145
  return null;
7107
7146
  }
7108
7147
  }
7148
+ function getConfigPath(cwd = process.cwd()) {
7149
+ const legacyPath = getLegacyConfigPath(cwd);
7150
+ if (existsSync(legacyPath)) {
7151
+ return legacyPath;
7152
+ }
7153
+ return getLocalConfigPath(cwd) ?? legacyPath;
7154
+ }
7155
+ function getLegacyConfigPath(cwd = process.cwd()) {
7156
+ return join(findRepoRoot(cwd) ?? cwd, CONFIG_FILENAME);
7157
+ }
7158
+ function getLocalConfigPath(cwd = process.cwd()) {
7159
+ const gitDir = resolveGitDir(cwd);
7160
+ if (!gitDir) {
7161
+ return null;
7162
+ }
7163
+ return join(gitDir, LOCAL_CONFIG_DIRNAME, LOCAL_CONFIG_FILENAME);
7164
+ }
7165
+ function getConfigSource(cwd = process.cwd()) {
7166
+ if (existsSync(getLegacyConfigPath(cwd))) {
7167
+ return "legacy";
7168
+ }
7169
+ const localPath = getLocalConfigPath(cwd);
7170
+ if (localPath && existsSync(localPath)) {
7171
+ return "local";
7172
+ }
7173
+ return null;
7174
+ }
7175
+ function hasLegacyConfig(cwd = process.cwd()) {
7176
+ return existsSync(getLegacyConfigPath(cwd));
7177
+ }
7178
+ function hasLocalConfig(cwd = process.cwd()) {
7179
+ const localPath = getLocalConfigPath(cwd);
7180
+ return !!localPath && existsSync(localPath);
7181
+ }
7182
+ function getConfigLocationLabel(cwd = process.cwd()) {
7183
+ const source = getConfigSource(cwd);
7184
+ if (source === "legacy") {
7185
+ return CONFIG_FILENAME;
7186
+ }
7187
+ return getLocalConfigPath(cwd) ? `.git/${LOCAL_CONFIG_DIRNAME}/${LOCAL_CONFIG_FILENAME}` : CONFIG_FILENAME;
7188
+ }
7189
+ function configExists(cwd = process.cwd()) {
7190
+ return getConfigSource(cwd) !== null;
7191
+ }
7192
+ var VALID_WORKFLOWS = ["clean-flow", "github-flow", "git-flow"];
7193
+ var VALID_ROLES = ["maintainer", "contributor"];
7194
+ var VALID_CONVENTIONS = ["conventional", "clean-commit", "none"];
7195
+ function isAIEnabled(config, cliNoAI = false) {
7196
+ return config.aiEnabled !== false && !cliNoAI;
7197
+ }
7198
+ function shouldShowTips(config) {
7199
+ return config?.showTips !== false;
7200
+ }
7201
+ function readConfig(cwd = process.cwd()) {
7202
+ const source = getConfigSource(cwd);
7203
+ if (!source)
7204
+ return null;
7205
+ const path = source === "local" ? getLocalConfigPath(cwd) : getLegacyConfigPath(cwd);
7206
+ if (!path)
7207
+ return null;
7208
+ return parseConfigFile(path);
7209
+ }
7109
7210
  function writeConfig(config, cwd = process.cwd()) {
7110
7211
  const path = getConfigPath(cwd);
7212
+ mkdirSync(dirname(path), { recursive: true });
7111
7213
  writeFileSync(path, `${JSON.stringify(config, null, 2)}
7112
7214
  `, "utf-8");
7113
7215
  }
@@ -7123,23 +7225,6 @@ function isGitignored(cwd = process.cwd()) {
7123
7225
  return false;
7124
7226
  }
7125
7227
  }
7126
- function ensureGitignored(cwd = process.cwd()) {
7127
- if (isGitignored(cwd))
7128
- return false;
7129
- const gitignorePath = join(cwd, ".gitignore");
7130
- const line = `${CONFIG_FILENAME}
7131
- `;
7132
- if (!existsSync(gitignorePath)) {
7133
- writeFileSync(gitignorePath, line, "utf-8");
7134
- return true;
7135
- }
7136
- const content = readFileSync(gitignorePath, "utf-8");
7137
- const needsLeadingNewline = content.length > 0 && !content.endsWith(`
7138
- `);
7139
- appendFileSync(gitignorePath, `${needsLeadingNewline ? `
7140
- ` : ""}${line}`, "utf-8");
7141
- return true;
7142
- }
7143
7228
  function getDefaultConfig() {
7144
7229
  return {
7145
7230
  workflow: "clean-flow",
@@ -7149,7 +7234,10 @@ function getDefaultConfig() {
7149
7234
  upstream: "upstream",
7150
7235
  origin: "origin",
7151
7236
  branchPrefixes: ["feature", "fix", "docs", "chore", "test", "refactor"],
7152
- commitConvention: "clean-commit"
7237
+ commitConvention: "clean-commit",
7238
+ aiEnabled: true,
7239
+ showTips: true,
7240
+ guideRotation: {}
7153
7241
  };
7154
7242
  }
7155
7243
 
@@ -7158,9 +7246,9 @@ import { execFile as execFileCb } from "node:child_process";
7158
7246
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
7159
7247
  import { join as join2 } from "node:path";
7160
7248
  function run(args) {
7161
- return new Promise((resolve) => {
7249
+ return new Promise((resolve2) => {
7162
7250
  execFileCb("git", args, (error, stdout2, stderr) => {
7163
- resolve({
7251
+ resolve2({
7164
7252
  exitCode: error ? error.code === "ENOENT" ? 127 : error.status ?? 1 : 0,
7165
7253
  stdout: stdout2 ?? "",
7166
7254
  stderr: stderr ?? ""
@@ -7625,41 +7713,6 @@ async function getLocalCommitsEntries(options) {
7625
7713
  return { hash: hash.trim(), subject: subject.trim(), refs: refs.trim() };
7626
7714
  });
7627
7715
  }
7628
- async function getRemoteOnlyCommitsGraph(options) {
7629
- const count = options?.count ?? 20;
7630
- const upstream = options?.upstream;
7631
- if (!upstream)
7632
- return [];
7633
- const args = [
7634
- "log",
7635
- "--oneline",
7636
- "--graph",
7637
- "--decorate",
7638
- `--max-count=${count}`,
7639
- "--color=never",
7640
- `HEAD..${upstream}`
7641
- ];
7642
- const { exitCode, stdout: stdout2 } = await run(args);
7643
- if (exitCode !== 0)
7644
- return [];
7645
- return stdout2.trimEnd().split(`
7646
- `).filter(Boolean);
7647
- }
7648
- async function getRemoteOnlyCommitsEntries(options) {
7649
- const count = options?.count ?? 20;
7650
- const upstream = options?.upstream;
7651
- if (!upstream)
7652
- return [];
7653
- const args = ["log", `--format=%h||%s||%D`, `--max-count=${count}`, `HEAD..${upstream}`];
7654
- const { exitCode, stdout: stdout2 } = await run(args);
7655
- if (exitCode !== 0)
7656
- return [];
7657
- return stdout2.trimEnd().split(`
7658
- `).filter(Boolean).map((line) => {
7659
- const [hash = "", subject = "", refs = ""] = line.split("||");
7660
- return { hash: hash.trim(), subject: subject.trim(), refs: refs.trim() };
7661
- });
7662
- }
7663
7716
  async function getLocalBranches() {
7664
7717
  const { exitCode, stdout: stdout2 } = await run(["branch", "-vv", "--no-color"]);
7665
7718
  if (exitCode !== 0)
@@ -8756,6 +8809,233 @@ var LogEngine = {
8756
8809
 
8757
8810
  // src/utils/logger.ts
8758
8811
  var import_picocolors = __toESM(require_picocolors(), 1);
8812
+
8813
+ // src/utils/tips.ts
8814
+ var COMMAND_GUIDES = {
8815
+ setup: {
8816
+ summary: "Initialize workflow rules, remotes, AI settings, and personal repo defaults.",
8817
+ examples: [
8818
+ { command: "cn setup --help", description: "learn all setup options and prompts" },
8819
+ { command: "cn setup", description: "initialize workflow, conventions, and remotes" },
8820
+ {
8821
+ command: "cn setup",
8822
+ description: "re-run setup to update repo preferences for this clone"
8823
+ },
8824
+ {
8825
+ command: "cn setup",
8826
+ description: "migrate legacy repo-root config into local Git storage"
8827
+ }
8828
+ ]
8829
+ },
8830
+ start: {
8831
+ summary: "Create a new working branch from the correct base branch for your workflow.",
8832
+ examples: [
8833
+ { command: "cn start --help", description: "learn branch naming and creation flags" },
8834
+ {
8835
+ command: "cn start feature/user-auth",
8836
+ description: "create a branch from an explicit name"
8837
+ },
8838
+ {
8839
+ command: 'cn start "fix login timeout"',
8840
+ description: "describe the work and let the CLI help"
8841
+ },
8842
+ {
8843
+ command: 'cn start "fix login timeout" --no-ai',
8844
+ description: "skip AI and keep control manual"
8845
+ }
8846
+ ]
8847
+ },
8848
+ sync: {
8849
+ summary: "Sync your protected branches with the right upstream source for your role.",
8850
+ examples: [
8851
+ { command: "cn sync --help", description: "learn sync modes and confirmation flags" },
8852
+ { command: "cn sync", description: "pull the right base branch for your workflow" },
8853
+ { command: "cn sync --yes", description: "skip the confirmation prompt" },
8854
+ { command: "cn sync", description: "refresh local protected branches before feature work" }
8855
+ ]
8856
+ },
8857
+ commit: {
8858
+ summary: "Stage changes, validate the message format, and create one or more commits.",
8859
+ examples: [
8860
+ { command: "cn commit --help", description: "learn commit generation and grouping flags" },
8861
+ { command: "cn commit", description: "stage and create one commit" },
8862
+ { command: "cn commit --no-ai", description: "write the commit message yourself" },
8863
+ { command: "cn commit --group", description: "split a large changeset into atomic commits" }
8864
+ ]
8865
+ },
8866
+ update: {
8867
+ summary: "Rebase your current feature branch onto the latest configured base branch.",
8868
+ examples: [
8869
+ { command: "cn update --help", description: "learn rebase and conflict guidance options" },
8870
+ { command: "cn update", description: "rebase your branch onto the latest base branch" },
8871
+ { command: "cn update --no-ai", description: "skip AI conflict guidance" },
8872
+ { command: "cn update", description: "refresh your branch before pushing or opening a PR" }
8873
+ ]
8874
+ },
8875
+ submit: {
8876
+ summary: "Push your branch and submit it through a pull request or local merge flow.",
8877
+ examples: [
8878
+ { command: "cn submit --help", description: "learn PR and local submit modes" },
8879
+ { command: "cn submit", description: "push and create or update a PR" },
8880
+ { command: "cn submit --pullrequest", description: "go straight to the PR flow" },
8881
+ { command: "cn submit -l", description: "maintainers can squash-merge locally" }
8882
+ ]
8883
+ },
8884
+ switch: {
8885
+ summary: "Switch branches safely and protect uncommitted work before moving around.",
8886
+ examples: [
8887
+ { command: "cn switch --help", description: "learn interactive and direct switch usage" },
8888
+ { command: "cn switch", description: "pick a branch interactively" },
8889
+ { command: "cn switch feature/login-fix", description: "switch directly to a named branch" },
8890
+ { command: "cn switch dev", description: "jump back to a protected branch directly" }
8891
+ ]
8892
+ },
8893
+ save: {
8894
+ summary: "Store uncommitted work for later and restore or delete saved change sets.",
8895
+ examples: [
8896
+ { command: "cn save --help", description: "learn save, restore, list, and drop actions" },
8897
+ { command: "cn save", description: "stash current uncommitted work" },
8898
+ { command: "cn save --restore", description: "bring back a saved change set" },
8899
+ { command: "cn save --list", description: "review what you saved before restoring" },
8900
+ { command: "cn save --drop", description: "discard a saved change set you no longer need" }
8901
+ ]
8902
+ },
8903
+ clean: {
8904
+ summary: "Delete merged or stale branches and keep the local repo tidy.",
8905
+ examples: [
8906
+ { command: "cn clean --help", description: "learn cleanup behavior and shortcuts" },
8907
+ { command: "cn clean", description: "review merged and stale branches before deleting" },
8908
+ { command: "cn clean --yes", description: "skip the confirmation prompt" },
8909
+ { command: "cn clean", description: "remove stale local branches after merge cleanup" }
8910
+ ]
8911
+ },
8912
+ status: {
8913
+ summary: "Inspect branch alignment, working tree state, and next recommended actions.",
8914
+ examples: [
8915
+ { command: "cn status --help", description: "learn what the status dashboard shows" },
8916
+ { command: "cn status", description: "see branch alignment and working tree state" },
8917
+ { command: "cn status", description: "check whether protected branches are aligned" },
8918
+ { command: "cn status", description: "review staged, modified, and untracked files" }
8919
+ ]
8920
+ },
8921
+ log: {
8922
+ summary: "View commit history for your current branch, remote diffs, or the full repo graph.",
8923
+ examples: [
8924
+ { command: "cn log --help", description: "learn all log views and filtering flags" },
8925
+ { command: "cn log", description: "show local and remote history in one split view" },
8926
+ { command: "cn log --local", description: "show only local unpushed commits" },
8927
+ { command: "cn log --remote", description: "show only remote branch history" },
8928
+ { command: "cn log --full", description: "show full history for the current branch" },
8929
+ { command: "cn log --all", description: "show history across all branches" },
8930
+ { command: "cn log -b dev", description: "inspect a specific branch" }
8931
+ ]
8932
+ },
8933
+ branch: {
8934
+ summary: "List branches with workflow-aware labels and local or remote tracking details.",
8935
+ examples: [
8936
+ { command: "cn branch --help", description: "learn branch list modes and filters" },
8937
+ { command: "cn branch", description: "list local branches and tracking info" },
8938
+ { command: "cn branch --all", description: "include local and remote branches" },
8939
+ { command: "cn branch --remote", description: "show only remote branches" }
8940
+ ]
8941
+ },
8942
+ hook: {
8943
+ summary: "Install or remove a managed commit-msg hook for commit convention validation.",
8944
+ examples: [
8945
+ { command: "cn hook --help", description: "learn hook install and uninstall usage" },
8946
+ { command: "cn hook install", description: "validate commit messages automatically" },
8947
+ { command: "cn hook uninstall", description: "remove the managed git hook" },
8948
+ {
8949
+ command: "cn hook install",
8950
+ description: "keep commit convention checks active in this clone"
8951
+ }
8952
+ ]
8953
+ },
8954
+ validate: {
8955
+ summary: "Check a commit message against the repository commit convention rules.",
8956
+ examples: [
8957
+ {
8958
+ command: "cn validate --help",
8959
+ description: "learn direct and file-based validation usage"
8960
+ },
8961
+ {
8962
+ command: 'cn validate "\uD83D\uDD27 update: tidy config"',
8963
+ description: "validate one message inline"
8964
+ },
8965
+ {
8966
+ command: "cn validate --file .git/COMMIT_EDITMSG",
8967
+ description: "validate a commit message file"
8968
+ },
8969
+ {
8970
+ command: 'cn validate "\uD83D\uDCE6 new: add API client"',
8971
+ description: "check another message before committing"
8972
+ }
8973
+ ]
8974
+ },
8975
+ doctor: {
8976
+ summary: "Run environment, dependency, config, and workflow diagnostics for the CLI.",
8977
+ examples: [
8978
+ { command: "cn doctor --help", description: "learn human and JSON output modes" },
8979
+ { command: "cn doctor", description: "run a full environment and config check" },
8980
+ { command: "cn doctor --json", description: "export machine-readable diagnostics" },
8981
+ {
8982
+ command: "cn doctor",
8983
+ description: "check config, remotes, and workflow resolution together"
8984
+ }
8985
+ ]
8986
+ }
8987
+ };
8988
+ var LOADING_TIPS = [
8989
+ "Manual commit mode: cn commit --no-ai",
8990
+ 'Disable AI for this clone: set "aiEnabled": false',
8991
+ 'Describe work for naming help: cn start "fix login timeout"',
8992
+ "Remote-only history: cn log --remote",
8993
+ "Skip PR mode prompt: cn submit --pullrequest"
8994
+ ];
8995
+ function getVisibleCommandGuide(command, rotationIndex = 0) {
8996
+ const key = normalizeCommandKey(command);
8997
+ const guide = COMMAND_GUIDES[key];
8998
+ if (!guide)
8999
+ return null;
9000
+ const helpExample = guide.examples.find((example) => example.command.endsWith("--help")) ?? null;
9001
+ const rotatingExamples = dedupeGuideExamples(helpExample ? guide.examples.filter((example) => example !== helpExample) : [...guide.examples]);
9002
+ const visibleExamples = [];
9003
+ if (helpExample) {
9004
+ visibleExamples.push(helpExample);
9005
+ }
9006
+ const remainingSlots = Math.max(0, 3 - visibleExamples.length);
9007
+ if (rotatingExamples.length <= remainingSlots) {
9008
+ visibleExamples.push(...rotatingExamples);
9009
+ } else {
9010
+ for (let index = 0;index < remainingSlots; index++) {
9011
+ visibleExamples.push(rotatingExamples[(rotationIndex + index) % rotatingExamples.length]);
9012
+ }
9013
+ }
9014
+ return {
9015
+ guide,
9016
+ examples: visibleExamples.slice(0, 3),
9017
+ rotatableCount: rotatingExamples.length,
9018
+ key
9019
+ };
9020
+ }
9021
+ function normalizeCommandKey(command) {
9022
+ const normalized = command.replace(/\s*\(.+\)$/, "").trim();
9023
+ return normalized.split(/\s+/)[0] ?? normalized;
9024
+ }
9025
+ function dedupeGuideExamples(examples) {
9026
+ const seen = new Set;
9027
+ const deduped = [];
9028
+ for (const example of examples) {
9029
+ if (seen.has(example.command)) {
9030
+ continue;
9031
+ }
9032
+ seen.add(example.command);
9033
+ deduped.push(example);
9034
+ }
9035
+ return deduped;
9036
+ }
9037
+
9038
+ // src/utils/logger.ts
8759
9039
  LogEngine.configure({
8760
9040
  mode: LogMode.INFO,
8761
9041
  format: {
@@ -8776,9 +9056,63 @@ function warn(msg, emoji = "⚠️") {
8776
9056
  function info(msg, emoji = "ℹ️") {
8777
9057
  LogEngine.info(msg, undefined, { emoji });
8778
9058
  }
8779
- function heading(msg) {
8780
- console.log(`
8781
- ${import_picocolors.default.bold(msg)}`);
9059
+ function projectHeading(command, emoji) {
9060
+ const prefix = emoji ? `${emoji} ` : "";
9061
+ console.log(` ${import_picocolors.default.bold(import_picocolors.default.cyan(`${prefix}${command}`))}`);
9062
+ const config = readConfig();
9063
+ const rotationIndex = config?.guideRotation?.[normalizeGuideKey(command)] ?? 0;
9064
+ const visibleGuide = getVisibleCommandGuide(command, rotationIndex);
9065
+ if (visibleGuide) {
9066
+ console.log(` ${import_picocolors.default.dim(visibleGuide.guide.summary)}`);
9067
+ }
9068
+ if (!shouldShowTips(config)) {
9069
+ return;
9070
+ }
9071
+ if (!visibleGuide || visibleGuide.examples.length === 0) {
9072
+ return;
9073
+ }
9074
+ if (config && visibleGuide.rotatableCount > 0) {
9075
+ config.guideRotation = config.guideRotation ?? {};
9076
+ config.guideRotation[visibleGuide.key] = (rotationIndex + 1) % visibleGuide.rotatableCount;
9077
+ writeConfig(config);
9078
+ }
9079
+ console.log();
9080
+ const terminalWidth = process.stdout.columns ?? 80;
9081
+ const maxContentWidth = Math.max(28, terminalWidth - 8);
9082
+ const commandWidth = visibleGuide.examples.reduce((max, example) => Math.max(max, example.command.length), 0);
9083
+ const descriptionWidth = Math.max(12, maxContentWidth - commandWidth - 2);
9084
+ const rows = visibleGuide.examples.map((example) => {
9085
+ const description = truncateText(example.description, descriptionWidth);
9086
+ const commandText = example.command.padEnd(commandWidth + 2);
9087
+ return {
9088
+ commandText,
9089
+ description,
9090
+ rawLength: commandText.length + description.length
9091
+ };
9092
+ });
9093
+ const contentWidth = Math.min(maxContentWidth, Math.max(28, ...rows.map((row) => row.rawLength)));
9094
+ const label = "─ quick guide ";
9095
+ const topBorder = `┌${label}${"─".repeat(Math.max(1, contentWidth - label.length + 1))}┐`;
9096
+ console.log(` ${import_picocolors.default.dim(topBorder)}`);
9097
+ for (const row of rows) {
9098
+ const left = import_picocolors.default.cyan(row.commandText);
9099
+ const right = import_picocolors.default.dim(row.description);
9100
+ const trailing = " ".repeat(Math.max(0, contentWidth - row.rawLength));
9101
+ console.log(` ${import_picocolors.default.dim("│")} ${left}${right}${trailing}${import_picocolors.default.dim("│")}`);
9102
+ }
9103
+ console.log(` ${import_picocolors.default.dim(`└${"─".repeat(contentWidth + 1)}┘`)}`);
9104
+ }
9105
+ function normalizeGuideKey(command) {
9106
+ return command.replace(/\s*\(.+\)$/, "").trim().split(/\s+/)[0] ?? command;
9107
+ }
9108
+ function truncateText(text, maxWidth) {
9109
+ if (text.length <= maxWidth) {
9110
+ return text;
9111
+ }
9112
+ if (maxWidth <= 1) {
9113
+ return text.slice(0, maxWidth);
9114
+ }
9115
+ return `${text.slice(0, maxWidth - 1)}…`;
8782
9116
  }
8783
9117
 
8784
9118
  // src/utils/workflow.ts
@@ -8871,7 +9205,7 @@ var branch_default = defineCommand({
8871
9205
  const currentBranch = await getCurrentBranch();
8872
9206
  const showRemoteOnly = args.remote;
8873
9207
  const showAll = args.all;
8874
- heading("\uD83C\uDF3F branches");
9208
+ projectHeading("branch", "\uD83C\uDF3F");
8875
9209
  console.log();
8876
9210
  if (!showRemoteOnly) {
8877
9211
  const localBranches = await getLocalBranches();
@@ -8926,20 +9260,6 @@ var branch_default = defineCommand({
8926
9260
  }
8927
9261
  }
8928
9262
  }
8929
- const tips = [];
8930
- if (!showAll && !showRemoteOnly) {
8931
- tips.push(`Use ${import_picocolors2.default.bold("contrib branch -a")} to include remote branches`);
8932
- }
8933
- if (!showRemoteOnly) {
8934
- tips.push(`Use ${import_picocolors2.default.bold("contrib start")} to create a new feature branch`);
8935
- tips.push(`Use ${import_picocolors2.default.bold("contrib clean")} to remove merged/stale branches`);
8936
- }
8937
- if (tips.length > 0) {
8938
- console.log(` ${import_picocolors2.default.dim("\uD83D\uDCA1 Tip:")}`);
8939
- for (const tip of tips) {
8940
- console.log(` ${import_picocolors2.default.dim(tip)}`);
8941
- }
8942
- }
8943
9263
  console.log();
8944
9264
  }
8945
9265
  });
@@ -8981,6 +9301,9 @@ function groupByRemote(branches) {
8981
9301
  }
8982
9302
 
8983
9303
  // src/commands/clean.ts
9304
+ var import_picocolors8 = __toESM(require_picocolors(), 1);
9305
+
9306
+ // src/utils/branchPrompt.ts
8984
9307
  var import_picocolors7 = __toESM(require_picocolors(), 1);
8985
9308
 
8986
9309
  // src/utils/branch.ts
@@ -10103,12 +10426,12 @@ async function multiSelectPrompt(message, choices) {
10103
10426
  init_dist();
10104
10427
  var CONVENTIONAL_COMMIT_SYSTEM_PROMPT = `Git commit message generator. Format: <type>[!][(<scope>)]: <description>
10105
10428
  Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
10106
- Rules: breaking (!) only for feat/fix/refactor/perf; imperative mood; max 72 chars; lowercase start; scope optional camelCase/kebab-case. Return ONLY the message line.
10429
+ Rules: breaking (!) only for feat/fix/refactor/perf; imperative mood; max 72 chars; lowercase start; scope optional camelCase/kebab-case. Do NOT use backticks, quotes, or markdown formatting around filenames, functions, or identifiers. Return ONLY the message line.
10107
10430
  Examples: feat: add user auth | fix(auth): resolve token expiry | feat!: redesign auth API`;
10108
10431
  var CLEAN_COMMIT_SYSTEM_PROMPT = `Git commit message generator. EXACT format: <emoji> <type>[!][ (<scope>)]: <description>
10109
10432
  Spacing: EMOJI SPACE TYPE [SPACE OPENPAREN SCOPE CLOSEPAREN] COLON SPACE DESCRIPTION
10110
10433
  Types: \uD83D\uDCE6 new, \uD83D\uDD27 update, \uD83D\uDDD1️ remove, \uD83D\uDD12 security, ⚙️ setup, ☕ chore, \uD83E\uDDEA test, \uD83D\uDCD6 docs, \uD83D\uDE80 release
10111
- Rules: breaking (!) only for new/update/remove/security; imperative mood; max 72 chars; lowercase start; scope optional. Return ONLY the message line.
10434
+ Rules: breaking (!) only for new/update/remove/security; imperative mood; max 72 chars; lowercase start; scope optional. Do NOT use backticks, quotes, or markdown formatting around filenames, functions, or identifiers. Return ONLY the message line.
10112
10435
  Correct: \uD83D\uDCE6 new: add user auth | \uD83D\uDD27 update (api): improve error handling | ⚙️ setup (ci): configure github actions
10113
10436
  WRONG: ⚙️setup(ci): ... | \uD83D\uDD27 update(api): ... ← always space before scope parenthesis`;
10114
10437
  function getGroupingSystemPrompt(convention) {
@@ -10133,6 +10456,7 @@ Rules:
10133
10456
  - Each group should represent ONE logical change
10134
10457
  - Every file must appear in exactly one group
10135
10458
  - Commit messages must follow the convention, be concise, imperative, max 72 chars
10459
+ - Do not use backticks, quotes, or markdown formatting in commit messages
10136
10460
  - Order groups so foundational changes come first (types, utils) and consumers come after
10137
10461
  - Return ONLY the JSON array, nothing else`;
10138
10462
  }
@@ -10167,11 +10491,11 @@ function suppressSubprocessWarnings() {
10167
10491
  process.env.NODE_NO_WARNINGS = "1";
10168
10492
  }
10169
10493
  function withTimeout(promise, ms) {
10170
- return new Promise((resolve2, reject) => {
10494
+ return new Promise((resolve3, reject) => {
10171
10495
  const timer = setTimeout(() => reject(new Error(`Copilot request timed out after ${ms / 1000}s`)), ms);
10172
10496
  promise.then((val) => {
10173
10497
  clearTimeout(timer);
10174
- resolve2(val);
10498
+ resolve3(val);
10175
10499
  }, (err) => {
10176
10500
  clearTimeout(timer);
10177
10501
  reject(err);
@@ -10182,10 +10506,84 @@ var COPILOT_TIMEOUT_MS = 30000;
10182
10506
  var COPILOT_LONG_TIMEOUT_MS = 90000;
10183
10507
  var BATCH_CONFIG = {
10184
10508
  LARGE_CHANGESET_THRESHOLD: 15,
10509
+ DIRECT_BATCH_THRESHOLD: 40,
10185
10510
  COMPACT_PER_FILE_CHARS: 300,
10186
10511
  MAX_COMPACT_PAYLOAD: 1e4,
10187
- FALLBACK_BATCH_SIZE: 15
10512
+ FALLBACK_BATCH_SIZE: 8
10188
10513
  };
10514
+ function getRecoveryGroupKey(file) {
10515
+ const parts = file.split("/").filter(Boolean);
10516
+ const [topLevel = "", secondLevel = ""] = parts;
10517
+ const extension = parts.at(-1)?.split(".").at(-1)?.toLowerCase() ?? "";
10518
+ if (topLevel === "src" || topLevel === "tests") {
10519
+ return `${topLevel}/${secondLevel || "root"}`;
10520
+ }
10521
+ if (topLevel === "docs" || topLevel === "landing") {
10522
+ return topLevel;
10523
+ }
10524
+ if (!topLevel.includes(".")) {
10525
+ return topLevel || "root";
10526
+ }
10527
+ if (extension === "md" || extension === "mdx") {
10528
+ return "root-docs";
10529
+ }
10530
+ if (["json", "yaml", "yml", "toml"].includes(extension)) {
10531
+ return "root-config";
10532
+ }
10533
+ return "root";
10534
+ }
10535
+ function createFallbackMessageForGroup(files, key, convention) {
10536
+ const countLabel = files.length === 1 ? "file" : "files";
10537
+ if (key === "docs" || key === "root-docs") {
10538
+ return convention === "conventional" ? `docs: update ${files.length} documentation ${countLabel}` : convention === "clean-commit" ? `\uD83D\uDCD6 docs: update ${files.length} documentation ${countLabel}` : `update ${files.length} documentation ${countLabel}`;
10539
+ }
10540
+ if (key.startsWith("tests/")) {
10541
+ const scope = key.split("/")[1];
10542
+ return convention === "conventional" ? `test${scope && scope !== "root" ? `(${scope})` : ""}: update ${files.length} test ${countLabel}` : convention === "clean-commit" ? `\uD83E\uDDEA test${scope && scope !== "root" ? ` (${scope})` : ""}: update ${files.length} test ${countLabel}` : `update ${files.length} test ${countLabel}`;
10543
+ }
10544
+ if (key === "landing") {
10545
+ return convention === "conventional" ? `chore(ui): update ${files.length} landing ${countLabel}` : convention === "clean-commit" ? `\uD83D\uDD27 update (ui): update ${files.length} landing ${countLabel}` : `update ${files.length} landing ${countLabel}`;
10546
+ }
10547
+ if (key === "root-config") {
10548
+ return convention === "conventional" ? `chore(config): update ${files.length} config ${countLabel}` : convention === "clean-commit" ? `⚙️ setup (config): update ${files.length} config ${countLabel}` : `update ${files.length} config ${countLabel}`;
10549
+ }
10550
+ if (key.startsWith("src/")) {
10551
+ const scope = key.split("/")[1];
10552
+ const scopeLabel = scope && scope !== "root" ? scope : "code";
10553
+ return convention === "conventional" ? `chore(${scopeLabel}): update ${files.length} source ${countLabel}` : convention === "clean-commit" ? `\uD83D\uDD27 update (${scopeLabel}): update ${files.length} source ${countLabel}` : `update ${files.length} source ${countLabel}`;
10554
+ }
10555
+ return convention === "conventional" ? `chore: update ${files.length} repo ${countLabel}` : convention === "clean-commit" ? `☕ chore: update ${files.length} repo ${countLabel}` : `update ${files.length} repo ${countLabel}`;
10556
+ }
10557
+ function createRecoveryCommitGroups(files, convention = "clean-commit") {
10558
+ if (files.length === 0) {
10559
+ return [];
10560
+ }
10561
+ const grouped = new Map;
10562
+ for (const file of files) {
10563
+ const key = getRecoveryGroupKey(file);
10564
+ const entry = grouped.get(key);
10565
+ if (entry) {
10566
+ entry.push(file);
10567
+ } else {
10568
+ grouped.set(key, [file]);
10569
+ }
10570
+ }
10571
+ const result = [];
10572
+ for (const [key, groupedFiles] of grouped.entries()) {
10573
+ for (let index = 0;index < groupedFiles.length; index += BATCH_CONFIG.FALLBACK_BATCH_SIZE) {
10574
+ const chunk = groupedFiles.slice(index, index + BATCH_CONFIG.FALLBACK_BATCH_SIZE);
10575
+ result.push({
10576
+ files: chunk,
10577
+ message: createFallbackMessageForGroup(chunk, key, convention)
10578
+ });
10579
+ }
10580
+ }
10581
+ return result;
10582
+ }
10583
+ function hasIncompleteDiffCoverage(files, rawDiff) {
10584
+ const diffSections = parseDiffByFile(rawDiff);
10585
+ return files.some((file) => !diffSections.has(file));
10586
+ }
10189
10587
  function parseDiffByFile(rawDiff) {
10190
10588
  const sections = new Map;
10191
10589
  const headerPattern = /^diff --git a\/(.+?) b\/(.+?)$/gm;
@@ -10253,7 +10651,7 @@ ${truncated}
10253
10651
  return result.length > maxTotalChars ? `${result.slice(0, maxTotalChars - 15)}
10254
10652
  ...(truncated)` : result;
10255
10653
  }
10256
- async function checkCopilotAvailable() {
10654
+ async function checkCopilotAvailable2() {
10257
10655
  try {
10258
10656
  const client = await getManagedClient();
10259
10657
  try {
@@ -10351,16 +10749,20 @@ function extractJson(raw) {
10351
10749
  }
10352
10750
  return text;
10353
10751
  }
10752
+ function sanitizeGeneratedCommitMessage(message) {
10753
+ return message.replace(/`+/g, "").replace(/\s+/g, " ").trim();
10754
+ }
10354
10755
  async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit", context) {
10355
10756
  try {
10356
10757
  const isLarge = stagedFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
10758
+ const hasMissingDiffCoverage = hasIncompleteDiffCoverage(stagedFiles, diff);
10357
10759
  const multiFileHint = stagedFiles.length > 1 ? `
10358
10760
 
10359
10761
  IMPORTANT: Multiple files are staged. Generate ONE commit message that captures the high-level purpose of ALL changes together. Focus on the overall intent, not individual file changes. Be specific but concise — do not list every file.` : "";
10360
10762
  const squashHint = context === "squash-merge" ? `
10361
10763
 
10362
10764
  CONTEXT: This is a squash merge of an entire feature branch into the base branch. All commits are being combined into ONE single commit. Generate a single high-level summary that describes the overall feature or change — NOT a list of individual commits. Think: what capability was added or what problem was solved? Be specific but concise.` : "";
10363
- const diffContent = isLarge ? createCompactDiff(stagedFiles, diff) : diff.slice(0, 4000);
10765
+ const diffContent = isLarge || hasMissingDiffCoverage ? createCompactDiff(stagedFiles, diff) : diff.slice(0, 4000);
10364
10766
  const userMessage = `Generate a commit message for these staged changes:
10365
10767
 
10366
10768
  Files (${stagedFiles.length}): ${stagedFiles.join(", ")}
@@ -10368,7 +10770,7 @@ Files (${stagedFiles.length}): ${stagedFiles.join(", ")}
10368
10770
  Diff:
10369
10771
  ${diffContent}${multiFileHint}${squashHint}`;
10370
10772
  const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model, isLarge ? COPILOT_LONG_TIMEOUT_MS : COPILOT_TIMEOUT_MS);
10371
- return result?.trim() ?? null;
10773
+ return result ? sanitizeGeneratedCommitMessage(result) : null;
10372
10774
  } catch {
10373
10775
  return null;
10374
10776
  }
@@ -10415,9 +10817,49 @@ ${conflictDiff.slice(0, 4000)}`;
10415
10817
  return null;
10416
10818
  }
10417
10819
  }
10418
- async function generateCommitGroups(files, diffs, model, convention = "clean-commit") {
10820
+ function normalizeCommitGroups(changedFiles, groups) {
10821
+ const changedSet = new Set(changedFiles);
10822
+ const assignedFiles = new Set;
10823
+ const unknownFiles = new Set;
10824
+ const duplicateFiles = new Set;
10825
+ const normalizedGroups = groups.map((group) => {
10826
+ const uniqueFiles = new Set;
10827
+ const files = [];
10828
+ for (const file of group.files) {
10829
+ if (!changedSet.has(file)) {
10830
+ unknownFiles.add(file);
10831
+ continue;
10832
+ }
10833
+ if (uniqueFiles.has(file) || assignedFiles.has(file)) {
10834
+ duplicateFiles.add(file);
10835
+ continue;
10836
+ }
10837
+ uniqueFiles.add(file);
10838
+ assignedFiles.add(file);
10839
+ files.push(file);
10840
+ }
10841
+ return {
10842
+ ...group,
10843
+ files
10844
+ };
10845
+ }).filter((group) => group.files.length > 0);
10846
+ const unassignedFiles = changedFiles.filter((file) => !assignedFiles.has(file));
10847
+ return {
10848
+ groups: normalizedGroups,
10849
+ unknownFiles: [...unknownFiles],
10850
+ duplicateFiles: [...duplicateFiles],
10851
+ unassignedFiles
10852
+ };
10853
+ }
10854
+ async function generateCommitGroups(files, diffs, model, convention = "clean-commit", onProgress) {
10419
10855
  const isLarge = files.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
10420
- const diffContent = isLarge ? createCompactDiff(files, diffs) : diffs.slice(0, 6000);
10856
+ const shouldBatchImmediately = files.length >= BATCH_CONFIG.DIRECT_BATCH_THRESHOLD;
10857
+ const hasMissingDiffCoverage = hasIncompleteDiffCoverage(files, diffs);
10858
+ if (shouldBatchImmediately) {
10859
+ onProgress?.(`Large changeset detected. Grouping in focused batches of ${BATCH_CONFIG.FALLBACK_BATCH_SIZE} files...`);
10860
+ return generateCommitGroupsInBatches(files, diffs, model, convention, onProgress);
10861
+ }
10862
+ const diffContent = isLarge || hasMissingDiffCoverage ? createCompactDiff(files, diffs) : diffs.slice(0, 6000);
10421
10863
  const largeHint = isLarge ? `
10422
10864
 
10423
10865
  NOTE: This is a large changeset (${files.length} files). Compact diffs are provided for every file. Focus on creating well-organized logical groups.` : "";
@@ -10429,10 +10871,22 @@ ${files.join(`
10429
10871
 
10430
10872
  Diffs:
10431
10873
  ${diffContent}${largeHint}`;
10432
- const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
10874
+ let result = null;
10875
+ try {
10876
+ onProgress?.(`Analyzing ${files.length} files together before batching fallback...`);
10877
+ result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
10878
+ } catch {
10879
+ if (isLarge) {
10880
+ onProgress?.(`Initial grouping timed out. Switching to focused batches of ${BATCH_CONFIG.FALLBACK_BATCH_SIZE} files...`);
10881
+ return generateCommitGroupsInBatches(files, diffs, model, convention, onProgress);
10882
+ }
10883
+ throw new Error("AI grouping failed before a response was returned");
10884
+ }
10433
10885
  if (!result) {
10434
- if (isLarge)
10435
- return generateCommitGroupsInBatches(files, diffs, model, convention);
10886
+ if (isLarge) {
10887
+ onProgress?.(`AI returned an empty response. Switching to focused batches...`);
10888
+ return generateCommitGroupsInBatches(files, diffs, model, convention, onProgress);
10889
+ }
10436
10890
  throw new Error("AI returned an empty response");
10437
10891
  }
10438
10892
  const cleaned = extractJson(result);
@@ -10440,14 +10894,18 @@ ${diffContent}${largeHint}`;
10440
10894
  try {
10441
10895
  parsed = JSON.parse(cleaned);
10442
10896
  } catch {
10443
- if (isLarge)
10444
- return generateCommitGroupsInBatches(files, diffs, model, convention);
10897
+ if (isLarge) {
10898
+ onProgress?.("AI returned invalid JSON for the full changeset. Switching to focused batches...");
10899
+ return generateCommitGroupsInBatches(files, diffs, model, convention, onProgress);
10900
+ }
10445
10901
  throw new Error(`AI response is not valid JSON. Raw start: "${result.slice(0, 120)}..."`);
10446
10902
  }
10447
10903
  const groups = parsed;
10448
10904
  if (!Array.isArray(groups) || groups.length === 0) {
10449
- if (isLarge)
10450
- return generateCommitGroupsInBatches(files, diffs, model, convention);
10905
+ if (isLarge) {
10906
+ onProgress?.("AI returned no usable groups for the full changeset. Switching to focused batches...");
10907
+ return generateCommitGroupsInBatches(files, diffs, model, convention, onProgress);
10908
+ }
10451
10909
  throw new Error("AI response was not a valid JSON array of commit groups");
10452
10910
  }
10453
10911
  for (const group of groups) {
@@ -10455,19 +10913,23 @@ ${diffContent}${largeHint}`;
10455
10913
  throw new Error("AI returned groups with invalid structure (missing files or message)");
10456
10914
  }
10457
10915
  }
10458
- return groups;
10916
+ return groups.map((group) => ({
10917
+ ...group,
10918
+ message: sanitizeGeneratedCommitMessage(group.message)
10919
+ }));
10459
10920
  }
10460
- async function generateCommitGroupsInBatches(files, diffs, model, convention = "clean-commit") {
10921
+ async function generateCommitGroupsInBatches(files, diffs, model, convention = "clean-commit", onProgress) {
10461
10922
  const batchSize = BATCH_CONFIG.FALLBACK_BATCH_SIZE;
10462
10923
  const allGroups = [];
10463
10924
  const diffSections = parseDiffByFile(diffs);
10925
+ const totalBatches = Math.ceil(files.length / batchSize);
10464
10926
  for (let i2 = 0;i2 < files.length; i2 += batchSize) {
10465
10927
  const batchFiles = files.slice(i2, i2 + batchSize);
10466
10928
  const batchDiff = batchFiles.map((f3) => diffSections.get(f3) ?? "").filter(Boolean).join(`
10467
10929
  `);
10468
- const batchDiffContent = batchFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD ? createCompactDiff(batchFiles, batchDiff) : batchDiff.slice(0, 6000);
10930
+ const batchDiffContent = batchFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD || hasIncompleteDiffCoverage(batchFiles, batchDiff) ? createCompactDiff(batchFiles, batchDiff) : batchDiff.slice(0, 6000);
10469
10931
  const batchNum = Math.floor(i2 / batchSize) + 1;
10470
- const totalBatches = Math.ceil(files.length / batchSize);
10932
+ onProgress?.(`Grouping batch ${batchNum}/${totalBatches} (${batchFiles.length} files)...`);
10471
10933
  const userMessage = `Group these changed files into logical atomic commits:
10472
10934
 
10473
10935
  Files:
@@ -10490,7 +10952,11 @@ NOTE: Processing batch ${batchNum}/${totalBatches} of a large changeset. Group o
10490
10952
  const batchFileSet = new Set(batchFiles);
10491
10953
  const filteredFiles = group.files.filter((f3) => batchFileSet.has(f3));
10492
10954
  if (filteredFiles.length > 0) {
10493
- allGroups.push({ ...group, files: filteredFiles });
10955
+ allGroups.push({
10956
+ ...group,
10957
+ files: filteredFiles,
10958
+ message: sanitizeGeneratedCommitMessage(group.message)
10959
+ });
10494
10960
  }
10495
10961
  }
10496
10962
  }
@@ -10500,10 +10966,7 @@ NOTE: Processing batch ${batchNum}/${totalBatches} of a large changeset. Group o
10500
10966
  const groupedFiles = new Set(allGroups.flatMap((g3) => g3.files));
10501
10967
  const ungrouped = files.filter((f3) => !groupedFiles.has(f3));
10502
10968
  if (ungrouped.length > 0) {
10503
- allGroups.push({
10504
- files: ungrouped,
10505
- message: `chore: update ${ungrouped.length} remaining file${ungrouped.length !== 1 ? "s" : ""}`
10506
- });
10969
+ allGroups.push(...createRecoveryCommitGroups(ungrouped, convention));
10507
10970
  }
10508
10971
  if (allGroups.length === 0) {
10509
10972
  throw new Error("AI could not group any files even with batch processing");
@@ -10533,7 +10996,7 @@ ${diffContent}`;
10533
10996
  return groups;
10534
10997
  return groups.map((g3, i2) => ({
10535
10998
  files: g3.files,
10536
- message: typeof parsed[i2]?.message === "string" ? parsed[i2].message : g3.message
10999
+ message: typeof parsed[i2]?.message === "string" ? sanitizeGeneratedCommitMessage(parsed[i2].message) : g3.message
10537
11000
  }));
10538
11001
  } catch {
10539
11002
  return groups;
@@ -10550,60 +11013,303 @@ Files: ${files.join(", ")}
10550
11013
  Diff:
10551
11014
  ${diffContent}`;
10552
11015
  const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
10553
- return result?.trim() ?? null;
11016
+ return result ? sanitizeGeneratedCommitMessage(result) : null;
10554
11017
  } catch {
10555
11018
  return null;
10556
11019
  }
10557
11020
  }
10558
11021
 
10559
- // src/utils/gh.ts
10560
- import { execFile as execFileCb2 } from "node:child_process";
10561
- function run2(args) {
10562
- return new Promise((resolve2) => {
10563
- execFileCb2("gh", args, (error2, stdout2, stderr) => {
10564
- resolve2({
10565
- exitCode: error2 ? error2.code === "ENOENT" ? 127 : error2.status ?? 1 : 0,
10566
- stdout: stdout2 ?? "",
10567
- stderr: stderr ?? ""
10568
- });
10569
- });
10570
- });
10571
- }
10572
- async function checkGhInstalled() {
10573
- try {
10574
- const { exitCode } = await run2(["--version"]);
10575
- return exitCode === 0;
10576
- } catch {
10577
- return false;
10578
- }
10579
- }
10580
- async function checkGhAuth() {
10581
- try {
10582
- const { exitCode } = await run2(["auth", "status"]);
10583
- return exitCode === 0;
10584
- } catch {
10585
- return false;
11022
+ // src/utils/spinner.ts
11023
+ var import_picocolors6 = __toESM(require_picocolors(), 1);
11024
+ var FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
11025
+ var MIN_LINE_WIDTH = 20;
11026
+ var DEFAULT_TIP_INTERVAL_MS = 3960;
11027
+ function formatSpinnerLines(text, tip, maxWidth) {
11028
+ if (maxWidth <= 0) {
11029
+ return [];
10586
11030
  }
10587
- }
10588
- var SAFE_SLUG = /^[\w.-]+$/;
10589
- async function checkRepoPermissions(owner, repo) {
10590
- if (!SAFE_SLUG.test(owner) || !SAFE_SLUG.test(repo))
10591
- return null;
10592
- const { exitCode, stdout: stdout2 } = await run2(["api", `repos/${owner}/${repo}`, "--jq", ".permissions"]);
10593
- if (exitCode !== 0)
10594
- return null;
10595
- try {
10596
- return JSON.parse(stdout2.trim());
10597
- } catch {
10598
- return null;
11031
+ const normalizedText = text.trim();
11032
+ const normalizedTip = formatSpinnerTip(tip);
11033
+ const primary = truncateText2(normalizedText, Math.max(MIN_LINE_WIDTH, maxWidth));
11034
+ if (!normalizedTip) {
11035
+ return [primary];
10599
11036
  }
11037
+ const secondary = truncateText2(normalizedTip, Math.max(MIN_LINE_WIDTH, maxWidth - 2));
11038
+ return [primary, secondary];
10600
11039
  }
10601
- async function isRepoFork() {
10602
- const { exitCode, stdout: stdout2 } = await run2(["repo", "view", "--json", "isFork", "-q", ".isFork"]);
10603
- if (exitCode !== 0)
10604
- return null;
10605
- const val = stdout2.trim();
10606
- if (val === "true")
11040
+ function createSpinner(text, options = {}) {
11041
+ let frameIdx = 0;
11042
+ let currentText = text;
11043
+ let stopped = false;
11044
+ let tipIdx = 0;
11045
+ let renderedLineCount = 0;
11046
+ let lastPrimaryLine = "";
11047
+ let lastSecondaryLine = "";
11048
+ const tips = options.tips?.filter(Boolean) ?? [];
11049
+ const tipIntervalMs = options.tipIntervalMs ?? DEFAULT_TIP_INTERVAL_MS;
11050
+ const clearBlock = () => {
11051
+ if (renderedLineCount === 0) {
11052
+ return;
11053
+ }
11054
+ for (let index = 0;index < renderedLineCount; index++) {
11055
+ process.stderr.write("\r\x1B[2K");
11056
+ if (index < renderedLineCount - 1) {
11057
+ process.stderr.write("\x1B[1A");
11058
+ }
11059
+ }
11060
+ process.stderr.write("\r");
11061
+ lastPrimaryLine = "";
11062
+ lastSecondaryLine = "";
11063
+ };
11064
+ const renderNextState = (primaryLine, secondaryLine) => {
11065
+ if (renderedLineCount === 0) {
11066
+ process.stderr.write(primaryLine);
11067
+ if (secondaryLine) {
11068
+ process.stderr.write(`
11069
+ ${secondaryLine}`);
11070
+ renderedLineCount = 2;
11071
+ } else {
11072
+ renderedLineCount = 1;
11073
+ }
11074
+ lastPrimaryLine = primaryLine;
11075
+ lastSecondaryLine = secondaryLine ?? "";
11076
+ return;
11077
+ }
11078
+ if (renderedLineCount === 1) {
11079
+ if (lastPrimaryLine !== primaryLine) {
11080
+ process.stderr.write(`\r\x1B[2K${primaryLine}`);
11081
+ }
11082
+ if (secondaryLine) {
11083
+ process.stderr.write(`
11084
+ ${secondaryLine}`);
11085
+ renderedLineCount = 2;
11086
+ }
11087
+ lastPrimaryLine = primaryLine;
11088
+ lastSecondaryLine = secondaryLine ?? "";
11089
+ return;
11090
+ }
11091
+ process.stderr.write(`\x1B[1A\r\x1B[2K${primaryLine}
11092
+ `);
11093
+ if (secondaryLine) {
11094
+ if (lastSecondaryLine !== secondaryLine) {
11095
+ process.stderr.write(`\r\x1B[2K${secondaryLine}`);
11096
+ }
11097
+ renderedLineCount = 2;
11098
+ } else {
11099
+ process.stderr.write("\r\x1B[2K\x1B[1A\r");
11100
+ renderedLineCount = 1;
11101
+ }
11102
+ lastPrimaryLine = primaryLine;
11103
+ lastSecondaryLine = secondaryLine ?? "";
11104
+ };
11105
+ const render = () => {
11106
+ if (stopped)
11107
+ return;
11108
+ const frame = import_picocolors6.default.cyan(FRAMES[frameIdx % FRAMES.length]);
11109
+ const width = Math.max(MIN_LINE_WIDTH, (process.stderr.columns ?? process.stdout.columns ?? 100) - 4);
11110
+ const lines = formatSpinnerLines(currentText, tips[tipIdx % tips.length], width);
11111
+ renderNextState(`${frame} ${import_picocolors6.default.cyan(lines[0] ?? "")}`, lines[1] ? ` ${import_picocolors6.default.dim(lines[1])}` : undefined);
11112
+ frameIdx++;
11113
+ };
11114
+ const timer = setInterval(render, 80);
11115
+ const tipTimer = tips.length > 1 ? setInterval(() => {
11116
+ tipIdx = (tipIdx + 1) % tips.length;
11117
+ }, tipIntervalMs) : null;
11118
+ render();
11119
+ const stop = () => {
11120
+ if (stopped)
11121
+ return;
11122
+ stopped = true;
11123
+ clearInterval(timer);
11124
+ if (tipTimer)
11125
+ clearInterval(tipTimer);
11126
+ clearBlock();
11127
+ renderedLineCount = 0;
11128
+ };
11129
+ return {
11130
+ update(newText) {
11131
+ currentText = newText;
11132
+ },
11133
+ success(msg) {
11134
+ stop();
11135
+ process.stderr.write(`${import_picocolors6.default.green("✔")} ${msg}
11136
+ `);
11137
+ },
11138
+ fail(msg) {
11139
+ stop();
11140
+ process.stderr.write(`${import_picocolors6.default.red("✖")} ${msg}
11141
+ `);
11142
+ },
11143
+ stop() {
11144
+ stop();
11145
+ }
11146
+ };
11147
+ }
11148
+ function formatSpinnerTip(tip) {
11149
+ const normalizedTip = tip?.trim() ?? "";
11150
+ return normalizedTip ? `\uD83D\uDCA1 TIP: ${normalizedTip}` : "";
11151
+ }
11152
+ function truncateText2(text, maxWidth) {
11153
+ if (text.length <= maxWidth) {
11154
+ return text;
11155
+ }
11156
+ if (maxWidth <= 1) {
11157
+ return text.slice(0, maxWidth);
11158
+ }
11159
+ return `${text.slice(0, maxWidth - 1)}…`;
11160
+ }
11161
+
11162
+ // src/utils/branchPrompt.ts
11163
+ async function promptForBranchName(options) {
11164
+ const promptMessage = options.promptMessage ?? "What are you going to work on?";
11165
+ let branchInput = options.initialValue?.trim() ?? "";
11166
+ while (!branchInput) {
11167
+ branchInput = (await inputPrompt(promptMessage)).trim();
11168
+ if (branchInput)
11169
+ break;
11170
+ warn("A branch name or description is required.");
11171
+ const action = await selectPrompt("What would you like to do?", ["Try again", "Cancel"]);
11172
+ if (action === "Cancel")
11173
+ return null;
11174
+ }
11175
+ let branchName = branchInput;
11176
+ const useAI = options.useAI !== false && looksLikeNaturalLanguage(branchInput);
11177
+ if (useAI) {
11178
+ const copilotError = await checkCopilotAvailable2();
11179
+ if (copilotError) {
11180
+ warn(`AI unavailable: ${copilotError}`);
11181
+ } else {
11182
+ while (true) {
11183
+ const spinner = createSpinner("Generating branch name suggestion...", {
11184
+ tips: LOADING_TIPS
11185
+ });
11186
+ const suggested = await suggestBranchName(branchInput, options.model);
11187
+ if (suggested) {
11188
+ spinner.success("Branch name suggestion ready.");
11189
+ console.log(`
11190
+ ${import_picocolors7.default.dim("AI suggestion:")} ${import_picocolors7.default.bold(import_picocolors7.default.cyan(suggested))}`);
11191
+ const action2 = await selectPrompt("What would you like to do with this branch name?", [
11192
+ "Use this suggestion",
11193
+ "Try again with AI",
11194
+ "Enter branch name manually",
11195
+ "Use my original description",
11196
+ "Cancel"
11197
+ ]);
11198
+ if (action2 === "Use this suggestion") {
11199
+ branchName = suggested;
11200
+ break;
11201
+ }
11202
+ if (action2 === "Try again with AI") {
11203
+ continue;
11204
+ }
11205
+ if (action2 === "Enter branch name manually") {
11206
+ branchName = (await inputPrompt("Enter branch name", branchInput)).trim();
11207
+ break;
11208
+ }
11209
+ if (action2 === "Use my original description") {
11210
+ branchName = branchInput;
11211
+ break;
11212
+ }
11213
+ return null;
11214
+ }
11215
+ spinner.fail("AI did not return a branch name suggestion.");
11216
+ const action = await selectPrompt("AI could not generate a branch name. What would you like to do?", [
11217
+ "Try again with AI",
11218
+ "Enter branch name manually",
11219
+ "Use my original description",
11220
+ "Cancel"
11221
+ ]);
11222
+ if (action === "Try again with AI") {
11223
+ continue;
11224
+ }
11225
+ if (action === "Enter branch name manually") {
11226
+ branchName = (await inputPrompt("Enter branch name", branchInput)).trim();
11227
+ break;
11228
+ }
11229
+ if (action === "Use my original description") {
11230
+ branchName = branchInput;
11231
+ break;
11232
+ }
11233
+ return null;
11234
+ }
11235
+ }
11236
+ }
11237
+ while (true) {
11238
+ if (!branchName) {
11239
+ branchName = (await inputPrompt("Enter branch name", branchInput)).trim();
11240
+ if (!branchName) {
11241
+ const action = await selectPrompt("What would you like to do?", ["Try again", "Cancel"]);
11242
+ if (action === "Cancel")
11243
+ return null;
11244
+ continue;
11245
+ }
11246
+ }
11247
+ if (!hasPrefix(branchName, options.branchPrefixes)) {
11248
+ const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors7.default.bold(branchName)}:`, options.branchPrefixes);
11249
+ branchName = formatBranchName(prefix, branchName);
11250
+ }
11251
+ if (!isValidBranchName(branchName)) {
11252
+ warn("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
11253
+ branchName = (await inputPrompt("Enter branch name", branchName)).trim();
11254
+ continue;
11255
+ }
11256
+ if (await branchExists(branchName)) {
11257
+ warn(`Branch ${import_picocolors7.default.bold(branchName)} already exists. Choose a different name.`);
11258
+ branchName = (await inputPrompt("Enter branch name", branchName)).trim();
11259
+ continue;
11260
+ }
11261
+ return branchName;
11262
+ }
11263
+ }
11264
+
11265
+ // src/utils/gh.ts
11266
+ import { execFile as execFileCb2 } from "node:child_process";
11267
+ function run2(args) {
11268
+ return new Promise((resolve3) => {
11269
+ execFileCb2("gh", args, (error2, stdout2, stderr) => {
11270
+ resolve3({
11271
+ exitCode: error2 ? error2.code === "ENOENT" ? 127 : error2.status ?? 1 : 0,
11272
+ stdout: stdout2 ?? "",
11273
+ stderr: stderr ?? ""
11274
+ });
11275
+ });
11276
+ });
11277
+ }
11278
+ async function checkGhInstalled() {
11279
+ try {
11280
+ const { exitCode } = await run2(["--version"]);
11281
+ return exitCode === 0;
11282
+ } catch {
11283
+ return false;
11284
+ }
11285
+ }
11286
+ async function checkGhAuth() {
11287
+ try {
11288
+ const { exitCode } = await run2(["auth", "status"]);
11289
+ return exitCode === 0;
11290
+ } catch {
11291
+ return false;
11292
+ }
11293
+ }
11294
+ var SAFE_SLUG = /^[\w.-]+$/;
11295
+ async function checkRepoPermissions(owner, repo) {
11296
+ if (!SAFE_SLUG.test(owner) || !SAFE_SLUG.test(repo))
11297
+ return null;
11298
+ const { exitCode, stdout: stdout2 } = await run2(["api", `repos/${owner}/${repo}`, "--jq", ".permissions"]);
11299
+ if (exitCode !== 0)
11300
+ return null;
11301
+ try {
11302
+ return JSON.parse(stdout2.trim());
11303
+ } catch {
11304
+ return null;
11305
+ }
11306
+ }
11307
+ async function isRepoFork() {
11308
+ const { exitCode, stdout: stdout2 } = await run2(["repo", "view", "--json", "isFork", "-q", ".isFork"]);
11309
+ if (exitCode !== 0)
11310
+ return null;
11311
+ const val = stdout2.trim();
11312
+ if (val === "true")
10607
11313
  return true;
10608
11314
  if (val === "false")
10609
11315
  return false;
@@ -10694,53 +11400,6 @@ async function getMergedPRForBranch(headBranch) {
10694
11400
  }
10695
11401
  }
10696
11402
 
10697
- // src/utils/spinner.ts
10698
- var import_picocolors6 = __toESM(require_picocolors(), 1);
10699
- var FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
10700
- function createSpinner(text) {
10701
- let frameIdx = 0;
10702
- let currentText = text;
10703
- let stopped = false;
10704
- const clearLine = () => {
10705
- process.stderr.write("\r\x1B[K");
10706
- };
10707
- const render = () => {
10708
- if (stopped)
10709
- return;
10710
- const frame = import_picocolors6.default.cyan(FRAMES[frameIdx % FRAMES.length]);
10711
- clearLine();
10712
- process.stderr.write(`${frame} ${currentText}`);
10713
- frameIdx++;
10714
- };
10715
- const timer = setInterval(render, 80);
10716
- render();
10717
- const stop = () => {
10718
- if (stopped)
10719
- return;
10720
- stopped = true;
10721
- clearInterval(timer);
10722
- clearLine();
10723
- };
10724
- return {
10725
- update(newText) {
10726
- currentText = newText;
10727
- },
10728
- success(msg) {
10729
- stop();
10730
- process.stderr.write(`${import_picocolors6.default.green("✔")} ${msg}
10731
- `);
10732
- },
10733
- fail(msg) {
10734
- stop();
10735
- process.stderr.write(`${import_picocolors6.default.red("✖")} ${msg}
10736
- `);
10737
- },
10738
- stop() {
10739
- stop();
10740
- }
10741
- };
10742
- }
10743
-
10744
11403
  // src/commands/clean.ts
10745
11404
  async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
10746
11405
  if (!config)
@@ -10753,44 +11412,22 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
10753
11412
  warn("You have uncommitted changes in your working tree.");
10754
11413
  }
10755
11414
  if (localWork.unpushedCommits > 0) {
10756
- warn(`You have ${import_picocolors7.default.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not pushed.`);
11415
+ warn(`You have ${import_picocolors8.default.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not pushed.`);
10757
11416
  }
10758
11417
  const SAVE_NEW_BRANCH = "Save changes to a new branch";
10759
11418
  const DISCARD = "Discard all changes and clean up";
10760
11419
  const CANCEL = "Skip this branch";
10761
- const action = await selectPrompt(`${import_picocolors7.default.bold(currentBranch)} has local changes. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
11420
+ const action = await selectPrompt(`${import_picocolors8.default.bold(currentBranch)} has local changes. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
10762
11421
  if (action === CANCEL)
10763
11422
  return "skipped";
10764
11423
  if (action === SAVE_NEW_BRANCH) {
10765
11424
  if (!config)
10766
11425
  return "skipped";
10767
- info(import_picocolors7.default.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
10768
- const description = await inputPrompt("What are you going to work on?");
10769
- let newBranchName = description;
10770
- if (looksLikeNaturalLanguage(description)) {
10771
- const spinner = createSpinner("Generating branch name suggestion...");
10772
- const suggested = await suggestBranchName(description);
10773
- if (suggested) {
10774
- spinner.success("Branch name suggestion ready.");
10775
- console.log(`
10776
- ${import_picocolors7.default.dim("AI suggestion:")} ${import_picocolors7.default.bold(import_picocolors7.default.cyan(suggested))}`);
10777
- const accepted = await confirmPrompt(`Use ${import_picocolors7.default.bold(suggested)} as your branch name?`);
10778
- newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
10779
- } else {
10780
- spinner.fail("AI did not return a suggestion.");
10781
- newBranchName = await inputPrompt("Enter branch name", description);
10782
- }
10783
- }
10784
- if (!hasPrefix(newBranchName, config.branchPrefixes)) {
10785
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors7.default.bold(newBranchName)}:`, config.branchPrefixes);
10786
- newBranchName = formatBranchName(prefix, newBranchName);
10787
- }
10788
- if (!isValidBranchName(newBranchName)) {
10789
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
10790
- return "skipped";
10791
- }
10792
- if (await branchExists(newBranchName)) {
10793
- error(`Branch ${import_picocolors7.default.bold(newBranchName)} already exists. Choose a different name.`);
11426
+ const newBranchName = await promptForBranchName({
11427
+ branchPrefixes: config.branchPrefixes,
11428
+ useAI: isAIEnabled(config)
11429
+ });
11430
+ if (!newBranchName) {
10794
11431
  return "skipped";
10795
11432
  }
10796
11433
  const renameResult = await renameBranch(currentBranch, newBranchName);
@@ -10798,7 +11435,7 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
10798
11435
  error(`Failed to rename branch: ${renameResult.stderr}`);
10799
11436
  return "skipped";
10800
11437
  }
10801
- success(`Renamed ${import_picocolors7.default.bold(currentBranch)} → ${import_picocolors7.default.bold(newBranchName)}`);
11438
+ success(`Renamed ${import_picocolors8.default.bold(currentBranch)} → ${import_picocolors8.default.bold(newBranchName)}`);
10802
11439
  const syncSource2 = getSyncSource(config);
10803
11440
  await fetchRemote(syncSource2.remote);
10804
11441
  const savedUpstreamRef = await getUpstreamRef();
@@ -10806,10 +11443,10 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
10806
11443
  if (rebaseResult.exitCode !== 0) {
10807
11444
  await rebaseAbort();
10808
11445
  warn("Rebase had conflicts — aborted to keep the repo in a clean state.");
10809
- info(`Your work is saved on ${import_picocolors7.default.bold(newBranchName)}. After cleanup, rebase manually:`, "");
10810
- info(` ${import_picocolors7.default.bold(`git checkout ${newBranchName} && git rebase ${syncSource2.ref}`)}`, "");
11446
+ info(`Your work is saved on ${import_picocolors8.default.bold(newBranchName)}. After cleanup, rebase manually:`, "");
11447
+ info(` ${import_picocolors8.default.bold(`git checkout ${newBranchName} && git rebase ${syncSource2.ref}`)}`, "");
10811
11448
  } else {
10812
- success(`Rebased ${import_picocolors7.default.bold(newBranchName)} onto ${import_picocolors7.default.bold(syncSource2.ref)}.`);
11449
+ success(`Rebased ${import_picocolors8.default.bold(newBranchName)} onto ${import_picocolors8.default.bold(syncSource2.ref)}.`);
10813
11450
  }
10814
11451
  const coResult2 = await checkoutBranch(baseBranch);
10815
11452
  if (coResult2.exitCode !== 0) {
@@ -10817,12 +11454,12 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
10817
11454
  return "saved";
10818
11455
  }
10819
11456
  await updateLocalBranch(baseBranch, syncSource2.ref);
10820
- success(`Synced ${import_picocolors7.default.bold(baseBranch)} with ${import_picocolors7.default.bold(syncSource2.ref)}.`);
11457
+ success(`Synced ${import_picocolors8.default.bold(baseBranch)} with ${import_picocolors8.default.bold(syncSource2.ref)}.`);
10821
11458
  return "saved";
10822
11459
  }
10823
11460
  }
10824
11461
  const syncSource = getSyncSource(config);
10825
- info(`Switching to ${import_picocolors7.default.bold(baseBranch)} and syncing...`);
11462
+ info(`Switching to ${import_picocolors8.default.bold(baseBranch)} and syncing...`);
10826
11463
  await fetchRemote(syncSource.remote);
10827
11464
  await resetHard("HEAD");
10828
11465
  const coResult = await checkoutBranch(baseBranch);
@@ -10831,7 +11468,7 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
10831
11468
  return "skipped";
10832
11469
  }
10833
11470
  await updateLocalBranch(baseBranch, syncSource.ref);
10834
- success(`Synced ${import_picocolors7.default.bold(baseBranch)} with ${import_picocolors7.default.bold(syncSource.ref)}.`);
11471
+ success(`Synced ${import_picocolors8.default.bold(baseBranch)} with ${import_picocolors8.default.bold(syncSource.ref)}.`);
10835
11472
  return "switched";
10836
11473
  }
10837
11474
  var clean_default = defineCommand({
@@ -10855,13 +11492,13 @@ var clean_default = defineCommand({
10855
11492
  await assertCleanGitState("cleaning");
10856
11493
  const config = readConfig();
10857
11494
  if (!config) {
10858
- error("No .contributerc.json found. Run `contrib setup` first.");
11495
+ error("No repo config found. Run `contrib setup` first.");
10859
11496
  process.exit(1);
10860
11497
  }
10861
11498
  const { origin } = config;
10862
11499
  const baseBranch = getBaseBranch(config);
10863
11500
  let currentBranch = await getCurrentBranch();
10864
- heading("\uD83E\uDDF9 contrib clean");
11501
+ projectHeading("clean", "\uD83E\uDDF9");
10865
11502
  info(`Pruning ${origin} remote refs...`);
10866
11503
  const pruneResult = await pruneRemote(origin);
10867
11504
  if (pruneResult.exitCode === 0) {
@@ -10881,21 +11518,21 @@ var clean_default = defineCommand({
10881
11518
  if (ghInstalled && ghAuthed) {
10882
11519
  const mergedPR = await getMergedPRForBranch(currentBranch);
10883
11520
  if (mergedPR) {
10884
- warn(`PR #${mergedPR.number} (${import_picocolors7.default.bold(mergedPR.title)}) has already been merged.`);
10885
- info(`Link: ${import_picocolors7.default.underline(mergedPR.url)}`, "");
11521
+ warn(`PR #${mergedPR.number} (${import_picocolors8.default.bold(mergedPR.title)}) has already been merged.`);
11522
+ info(`Link: ${import_picocolors8.default.underline(mergedPR.url)}`, "");
10886
11523
  goneCandidates.push(currentBranch);
10887
11524
  }
10888
11525
  }
10889
11526
  }
10890
11527
  if (mergedCandidates.length > 0) {
10891
11528
  console.log(`
10892
- ${import_picocolors7.default.bold("Merged branches to delete:")}`);
11529
+ ${import_picocolors8.default.bold("Merged branches to delete:")}`);
10893
11530
  for (const b2 of mergedCandidates) {
10894
- const marker = b2 === currentBranch ? import_picocolors7.default.yellow(" (current)") : "";
10895
- console.log(` ${import_picocolors7.default.dim("•")} ${b2}${marker}`);
11531
+ const marker = b2 === currentBranch ? import_picocolors8.default.yellow(" (current)") : "";
11532
+ console.log(` ${import_picocolors8.default.dim("•")} ${b2}${marker}`);
10896
11533
  }
10897
11534
  console.log();
10898
- const ok = args.yes || await confirmPrompt(`Delete ${import_picocolors7.default.bold(String(mergedCandidates.length))} merged branch${mergedCandidates.length !== 1 ? "es" : ""}?`);
11535
+ const ok = args.yes || await confirmPrompt(`Delete ${import_picocolors8.default.bold(String(mergedCandidates.length))} merged branch${mergedCandidates.length !== 1 ? "es" : ""}?`);
10899
11536
  if (ok) {
10900
11537
  for (const branch of mergedCandidates) {
10901
11538
  if (branch === currentBranch) {
@@ -10912,7 +11549,7 @@ ${import_picocolors7.default.bold("Merged branches to delete:")}`);
10912
11549
  }
10913
11550
  const result = await deleteBranch(branch);
10914
11551
  if (result.exitCode === 0) {
10915
- success(` Deleted ${import_picocolors7.default.bold(branch)}`);
11552
+ success(` Deleted ${import_picocolors8.default.bold(branch)}`);
10916
11553
  } else {
10917
11554
  warn(` Failed to delete ${branch}: ${result.stderr.trim()}`);
10918
11555
  }
@@ -10923,13 +11560,13 @@ ${import_picocolors7.default.bold("Merged branches to delete:")}`);
10923
11560
  }
10924
11561
  if (goneCandidates.length > 0) {
10925
11562
  console.log(`
10926
- ${import_picocolors7.default.bold("Stale branches (remote deleted, likely squash-merged):")}`);
11563
+ ${import_picocolors8.default.bold("Stale branches (remote deleted, likely squash-merged):")}`);
10927
11564
  for (const b2 of goneCandidates) {
10928
- const marker = b2 === currentBranch ? import_picocolors7.default.yellow(" (current)") : "";
10929
- console.log(` ${import_picocolors7.default.dim("•")} ${b2}${marker}`);
11565
+ const marker = b2 === currentBranch ? import_picocolors8.default.yellow(" (current)") : "";
11566
+ console.log(` ${import_picocolors8.default.dim("•")} ${b2}${marker}`);
10930
11567
  }
10931
11568
  console.log();
10932
- const ok = args.yes || await confirmPrompt(`Delete ${import_picocolors7.default.bold(String(goneCandidates.length))} stale branch${goneCandidates.length !== 1 ? "es" : ""}?`);
11569
+ const ok = args.yes || await confirmPrompt(`Delete ${import_picocolors8.default.bold(String(goneCandidates.length))} stale branch${goneCandidates.length !== 1 ? "es" : ""}?`);
10933
11570
  if (ok) {
10934
11571
  for (const branch of goneCandidates) {
10935
11572
  if (branch === currentBranch) {
@@ -10946,7 +11583,7 @@ ${import_picocolors7.default.bold("Stale branches (remote deleted, likely squash
10946
11583
  }
10947
11584
  const result = await forceDeleteBranch(branch);
10948
11585
  if (result.exitCode === 0) {
10949
- success(` Deleted ${import_picocolors7.default.bold(branch)}`);
11586
+ success(` Deleted ${import_picocolors8.default.bold(branch)}`);
10950
11587
  } else {
10951
11588
  warn(` Failed to delete ${branch}: ${result.stderr.trim()}`);
10952
11589
  }
@@ -10961,13 +11598,13 @@ ${import_picocolors7.default.bold("Stale branches (remote deleted, likely squash
10961
11598
  const finalBranch = await getCurrentBranch();
10962
11599
  if (finalBranch && protectedBranches.has(finalBranch)) {
10963
11600
  console.log();
10964
- info(`You're on ${import_picocolors7.default.bold(finalBranch)}. Run ${import_picocolors7.default.bold("contrib start")} to begin a new feature.`);
11601
+ info(`You're on ${import_picocolors8.default.bold(finalBranch)}. Run ${import_picocolors8.default.bold("contrib start")} to begin a new feature.`);
10965
11602
  }
10966
11603
  }
10967
11604
  });
10968
11605
 
10969
11606
  // src/commands/commit.ts
10970
- var import_picocolors8 = __toESM(require_picocolors(), 1);
11607
+ var import_picocolors9 = __toESM(require_picocolors(), 1);
10971
11608
 
10972
11609
  // src/utils/convention.ts
10973
11610
  var CLEAN_COMMIT_PATTERN = /^(📦|🔧|🗑\uFE0F?|🔒|⚙\uFE0F?|☕|🧪|📖|🚀) (new|update|remove|security|setup|chore|test|docs|release)(!?)( \([a-zA-Z0-9][a-zA-Z0-9-]*\))?: .{1,72}$/u;
@@ -10986,17 +11623,24 @@ var CONVENTION_FORMAT_HINTS = {
10986
11623
  conventional: [
10987
11624
  "Format: <type>[!][(<scope>)]: <description>",
10988
11625
  "Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert",
10989
- "Examples: feat: add login page | fix(auth): resolve token expiry | docs: update README"
11626
+ "Examples: feat: add login page | fix(auth): resolve token expiry | docs: update README",
11627
+ "Do not use backticks or markdown formatting in the message."
10990
11628
  ],
10991
11629
  "clean-commit": [
10992
11630
  "Format: <emoji> <type>[!][(<scope>)]: <description>",
10993
11631
  "Types: \uD83D\uDCE6 new | \uD83D\uDD27 update | \uD83D\uDDD1️ remove | \uD83D\uDD12 security | ⚙️ setup | ☕ chore | \uD83E\uDDEA test | \uD83D\uDCD6 docs | \uD83D\uDE80 release",
10994
- "Examples: \uD83D\uDCE6 new: user auth | \uD83D\uDD27 update (api): improve errors | ⚙️ setup (ci): add workflow"
11632
+ "Examples: \uD83D\uDCE6 new: user auth | \uD83D\uDD27 update (api): improve errors | ⚙️ setup (ci): add workflow",
11633
+ "Do not use backticks or markdown formatting in the message."
10995
11634
  ]
10996
11635
  };
11636
+ function hasUnsupportedCommitMessageChars(message) {
11637
+ return message.includes("`");
11638
+ }
10997
11639
  function validateCommitMessage(message, convention) {
10998
11640
  if (convention === "none")
10999
11641
  return true;
11642
+ if (hasUnsupportedCommitMessageChars(message))
11643
+ return false;
11000
11644
  if (convention === "clean-commit")
11001
11645
  return CLEAN_COMMIT_PATTERN.test(message);
11002
11646
  if (convention === "conventional")
@@ -11008,11 +11652,15 @@ function getValidationError(convention) {
11008
11652
  return [];
11009
11653
  return [
11010
11654
  `Commit message does not follow ${CONVENTION_LABELS[convention]} format.`,
11655
+ "Do not use backticks or markdown formatting in commit messages.",
11011
11656
  ...CONVENTION_FORMAT_HINTS[convention]
11012
11657
  ];
11013
11658
  }
11014
11659
 
11015
11660
  // src/commands/commit.ts
11661
+ function isEmptyGroupCommitResult(detail) {
11662
+ return /no changes added to commit|nothing to commit/i.test(detail);
11663
+ }
11016
11664
  var commit_default = defineCommand({
11017
11665
  meta: {
11018
11666
  name: "commit",
@@ -11042,11 +11690,16 @@ var commit_default = defineCommand({
11042
11690
  await assertCleanGitState("committing");
11043
11691
  const config = readConfig();
11044
11692
  if (!config) {
11045
- error("No .contributerc.json found. Run `contrib setup` first.");
11693
+ error("No repo config found. Run `contrib setup` first.");
11046
11694
  process.exit(1);
11047
11695
  }
11048
- heading("\uD83D\uDCBE contrib commit");
11696
+ projectHeading("commit", "\uD83D\uDCBE");
11697
+ const aiEnabled = isAIEnabled(config, args["no-ai"]);
11049
11698
  if (args.group) {
11699
+ if (!aiEnabled) {
11700
+ error("AI group commit is unavailable because AI is disabled. Re-run without --group or enable AI in your repo config.");
11701
+ process.exit(1);
11702
+ }
11050
11703
  await runGroupCommit(args.model, config);
11051
11704
  return;
11052
11705
  }
@@ -11058,9 +11711,9 @@ var commit_default = defineCommand({
11058
11711
  process.exit(1);
11059
11712
  }
11060
11713
  console.log(`
11061
- ${import_picocolors8.default.bold("Changed files:")}`);
11714
+ ${import_picocolors9.default.bold("Changed files:")}`);
11062
11715
  for (const f3 of changedFiles) {
11063
- console.log(` ${import_picocolors8.default.dim("•")} ${f3}`);
11716
+ console.log(` ${import_picocolors9.default.dim("•")} ${f3}`);
11064
11717
  }
11065
11718
  const stageAction = await selectPrompt("No staged changes. How would you like to stage?", [
11066
11719
  "Stage all changes",
@@ -11102,8 +11755,8 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11102
11755
  const dirs = new Set(stagedFiles.map((f3) => f3.split("/")[0]));
11103
11756
  if (dirs.size > 1) {
11104
11757
  console.log();
11105
- warn(`You're staging ${import_picocolors8.default.bold(String(stagedFiles.length))} files across ${import_picocolors8.default.bold(String(dirs.size))} directories in a single commit.`);
11106
- info(import_picocolors8.default.dim("Large commits mixing different topics make history harder to read and bisect. " + "For cleaner history, consider splitting into atomic commits."));
11758
+ 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.`);
11759
+ 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."));
11107
11760
  const choice = await selectPrompt("How would you like to proceed?", [
11108
11761
  "Continue as single commit",
11109
11762
  "Switch to group mode (AI splits into atomic commits)",
@@ -11119,20 +11772,22 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11119
11772
  }
11120
11773
  }
11121
11774
  let commitMessage = null;
11122
- const useAI = !args["no-ai"];
11775
+ const useAI = aiEnabled;
11123
11776
  if (useAI) {
11124
- const [copilotError, diff] = await Promise.all([checkCopilotAvailable(), getStagedDiff()]);
11777
+ const [copilotError, diff] = await Promise.all([checkCopilotAvailable2(), getStagedDiff()]);
11125
11778
  if (copilotError) {
11126
11779
  warn(`AI unavailable: ${copilotError}`);
11127
11780
  warn("Falling back to manual commit message entry.");
11128
11781
  } else {
11129
11782
  const spinnerMsg = stagedFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD ? `Generating commit message with AI (${stagedFiles.length} files — using optimized batching)...` : "Generating commit message with AI...";
11130
- const spinner = createSpinner(spinnerMsg);
11783
+ const spinner = createSpinner(spinnerMsg, {
11784
+ tips: LOADING_TIPS
11785
+ });
11131
11786
  commitMessage = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
11132
11787
  if (commitMessage) {
11133
11788
  spinner.success("AI commit message generated.");
11134
11789
  console.log(`
11135
- ${import_picocolors8.default.dim("AI suggestion:")} ${import_picocolors8.default.bold(import_picocolors8.default.cyan(commitMessage))}`);
11790
+ ${import_picocolors9.default.dim("AI suggestion:")} ${import_picocolors9.default.bold(import_picocolors9.default.cyan(commitMessage))}`);
11136
11791
  } else {
11137
11792
  spinner.fail("AI did not return a commit message.");
11138
11793
  warn("Falling back to manual entry.");
@@ -11152,13 +11807,15 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11152
11807
  } else if (action === "Edit this message") {
11153
11808
  finalMessage = await inputPrompt("Edit commit message", commitMessage);
11154
11809
  } else if (action === "Regenerate") {
11155
- const spinner = createSpinner("Regenerating commit message...");
11810
+ const spinner = createSpinner("Regenerating commit message...", {
11811
+ tips: LOADING_TIPS
11812
+ });
11156
11813
  const diff = await getStagedDiff();
11157
11814
  const regen = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
11158
11815
  if (regen) {
11159
11816
  spinner.success("Commit message regenerated.");
11160
11817
  console.log(`
11161
- ${import_picocolors8.default.dim("AI suggestion:")} ${import_picocolors8.default.bold(import_picocolors8.default.cyan(regen))}`);
11818
+ ${import_picocolors9.default.dim("AI suggestion:")} ${import_picocolors9.default.bold(import_picocolors9.default.cyan(regen))}`);
11162
11819
  const ok = await confirmPrompt("Use this message?");
11163
11820
  finalMessage = ok ? regen : await inputPrompt("Enter commit message manually");
11164
11821
  } else {
@@ -11173,7 +11830,7 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11173
11830
  if (convention2 !== "none") {
11174
11831
  console.log();
11175
11832
  for (const hint of CONVENTION_FORMAT_HINTS[convention2]) {
11176
- console.log(import_picocolors8.default.dim(hint));
11833
+ console.log(import_picocolors9.default.dim(hint));
11177
11834
  }
11178
11835
  console.log();
11179
11836
  }
@@ -11197,12 +11854,12 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11197
11854
  error(`Failed to commit: ${result.stderr}`);
11198
11855
  process.exit(1);
11199
11856
  }
11200
- success(`Committed: ${import_picocolors8.default.bold(finalMessage)}`);
11857
+ success(`Committed: ${import_picocolors9.default.bold(finalMessage)}`);
11201
11858
  }
11202
11859
  });
11203
11860
  async function runGroupCommit(model, config) {
11204
11861
  const [copilotError, changedFiles] = await Promise.all([
11205
- checkCopilotAvailable(),
11862
+ checkCopilotAvailable2(),
11206
11863
  getChangedFiles()
11207
11864
  ]);
11208
11865
  if (copilotError) {
@@ -11214,11 +11871,13 @@ async function runGroupCommit(model, config) {
11214
11871
  process.exit(1);
11215
11872
  }
11216
11873
  console.log(`
11217
- ${import_picocolors8.default.bold("Changed files:")}`);
11874
+ ${import_picocolors9.default.bold("Changed files:")}`);
11218
11875
  for (const f3 of changedFiles) {
11219
- console.log(` ${import_picocolors8.default.dim("•")} ${f3}`);
11876
+ console.log(` ${import_picocolors9.default.dim("•")} ${f3}`);
11220
11877
  }
11221
- 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...`);
11878
+ 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...`, {
11879
+ tips: LOADING_TIPS
11880
+ });
11222
11881
  const diffs = await getFullDiffForFiles(changedFiles);
11223
11882
  if (!diffs.trim()) {
11224
11883
  spinner.stop();
@@ -11226,7 +11885,7 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11226
11885
  }
11227
11886
  let groups;
11228
11887
  try {
11229
- groups = await generateCommitGroups(changedFiles, diffs, model, config.commitConvention);
11888
+ groups = await generateCommitGroups(changedFiles, diffs, model, config.commitConvention, (message) => spinner.update(message));
11230
11889
  spinner.success(`AI generated ${groups.length} commit group(s).`);
11231
11890
  } catch (err) {
11232
11891
  const reason = err instanceof Error ? err.message : String(err);
@@ -11237,15 +11896,21 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11237
11896
  error("AI could not produce commit groups. Try committing files manually.");
11238
11897
  process.exit(1);
11239
11898
  }
11240
- const changedSet = new Set(changedFiles);
11241
- for (const group of groups) {
11242
- const invalid = group.files.filter((f3) => !changedSet.has(f3));
11243
- if (invalid.length > 0) {
11244
- warn(`AI suggested unknown file(s): ${invalid.join(", ")} — removed from group.`);
11245
- }
11246
- group.files = group.files.filter((f3) => changedSet.has(f3));
11899
+ const normalized = normalizeCommitGroups(changedFiles, groups);
11900
+ if (normalized.unknownFiles.length > 0) {
11901
+ warn(`AI suggested unknown file(s): ${normalized.unknownFiles.join(", ")} removed from groups.`);
11902
+ }
11903
+ if (normalized.duplicateFiles.length > 0) {
11904
+ warn(`AI assigned duplicate file(s) across groups: ${normalized.duplicateFiles.join(", ")} — keeping the first assignment only.`);
11905
+ }
11906
+ let validGroups = normalized.groups;
11907
+ if (normalized.unassignedFiles.length > 0) {
11908
+ warn(`AI left ${normalized.unassignedFiles.length} file(s) ungrouped: ${normalized.unassignedFiles.join(", ")}. Auto-resolving recovery groups.`);
11909
+ validGroups = [
11910
+ ...validGroups,
11911
+ ...createRecoveryCommitGroups(normalized.unassignedFiles, config.commitConvention)
11912
+ ];
11247
11913
  }
11248
- let validGroups = groups.filter((g3) => g3.files.length > 0);
11249
11914
  if (validGroups.length === 0) {
11250
11915
  error("No valid groups remain after validation. Try committing files manually.");
11251
11916
  process.exit(1);
@@ -11254,13 +11919,13 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11254
11919
  let commitAll = false;
11255
11920
  while (!proceedToCommit) {
11256
11921
  console.log(`
11257
- ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit group(s):`)}
11922
+ ${import_picocolors9.default.bold(`AI suggested ${validGroups.length} commit group(s):`)}
11258
11923
  `);
11259
11924
  for (let i2 = 0;i2 < validGroups.length; i2++) {
11260
11925
  const g3 = validGroups[i2];
11261
- console.log(` ${import_picocolors8.default.cyan(`Group ${i2 + 1}:`)} ${import_picocolors8.default.bold(g3.message)}`);
11926
+ console.log(` ${import_picocolors9.default.cyan(`Group ${i2 + 1}:`)} ${import_picocolors9.default.bold(g3.message)}`);
11262
11927
  for (const f3 of g3.files) {
11263
- console.log(` ${import_picocolors8.default.dim("•")} ${f3}`);
11928
+ console.log(` ${import_picocolors9.default.dim("•")} ${f3}`);
11264
11929
  }
11265
11930
  console.log();
11266
11931
  }
@@ -11275,7 +11940,9 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11275
11940
  process.exit(0);
11276
11941
  }
11277
11942
  if (summaryAction === "Regenerate all messages") {
11278
- const regenSpinner = createSpinner("Regenerating all commit messages...");
11943
+ const regenSpinner = createSpinner("Regenerating all commit messages...", {
11944
+ tips: LOADING_TIPS
11945
+ });
11279
11946
  try {
11280
11947
  validGroups = await regenerateAllGroupMessages(validGroups, diffs, model, config.commitConvention);
11281
11948
  regenSpinner.success("All commit messages regenerated.");
@@ -11291,29 +11958,49 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11291
11958
  if (commitAll) {
11292
11959
  for (let i2 = 0;i2 < validGroups.length; i2++) {
11293
11960
  const group = validGroups[i2];
11294
- const stageResult = await stageFiles(group.files);
11961
+ const remainingChangedFiles = new Set(await getChangedFiles());
11962
+ const stageableFiles = group.files.filter((file) => remainingChangedFiles.has(file));
11963
+ const skippedFiles = group.files.filter((file) => !remainingChangedFiles.has(file));
11964
+ if (skippedFiles.length > 0) {
11965
+ warn(`Group ${i2 + 1} file(s) no longer have changes: ${skippedFiles.join(", ")}`);
11966
+ }
11967
+ if (stageableFiles.length === 0) {
11968
+ warn(`Skipped group ${i2 + 1}: no files remain to commit.`);
11969
+ continue;
11970
+ }
11971
+ const stageResult = await stageFiles(stageableFiles);
11295
11972
  if (stageResult.exitCode !== 0) {
11296
11973
  error(`Failed to stage group ${i2 + 1}: ${stageResult.stderr}`);
11297
11974
  continue;
11298
11975
  }
11976
+ const stagedFiles = new Set(await getStagedFiles());
11977
+ const stagedGroupFiles = stageableFiles.filter((file) => stagedFiles.has(file));
11978
+ if (stagedGroupFiles.length === 0) {
11979
+ warn(`Skipped group ${i2 + 1}: no files were staged for commit.`);
11980
+ continue;
11981
+ }
11299
11982
  const commitResult = await commitWithMessage(group.message);
11300
11983
  if (commitResult.exitCode !== 0) {
11301
11984
  const detail = (commitResult.stderr || commitResult.stdout).trim();
11985
+ if (isEmptyGroupCommitResult(detail)) {
11986
+ warn(`Skipped group ${i2 + 1}: nothing remained to commit.`);
11987
+ continue;
11988
+ }
11302
11989
  error(`Failed to commit group ${i2 + 1}: ${detail}`);
11303
- await unstageFiles(group.files);
11990
+ await unstageFiles(stageableFiles);
11304
11991
  continue;
11305
11992
  }
11306
11993
  committed++;
11307
- success(`Committed group ${i2 + 1}: ${import_picocolors8.default.bold(group.message)}`);
11994
+ success(`Committed group ${i2 + 1}: ${import_picocolors9.default.bold(group.message)}`);
11308
11995
  }
11309
11996
  } else {
11310
11997
  for (let i2 = 0;i2 < validGroups.length; i2++) {
11311
11998
  const group = validGroups[i2];
11312
- console.log(import_picocolors8.default.bold(`
11999
+ console.log(import_picocolors9.default.bold(`
11313
12000
  ── Group ${i2 + 1}/${validGroups.length} ──`));
11314
- console.log(` ${import_picocolors8.default.cyan(group.message)}`);
12001
+ console.log(` ${import_picocolors9.default.cyan(group.message)}`);
11315
12002
  for (const f3 of group.files) {
11316
- console.log(` ${import_picocolors8.default.dim("•")} ${f3}`);
12003
+ console.log(` ${import_picocolors9.default.dim("•")} ${f3}`);
11317
12004
  }
11318
12005
  let message = group.message;
11319
12006
  let actionDone = false;
@@ -11330,12 +12017,14 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11330
12017
  continue;
11331
12018
  }
11332
12019
  if (action === "Regenerate message") {
11333
- const regenSpinner = createSpinner("Regenerating commit message for this group...");
12020
+ const regenSpinner = createSpinner("Regenerating commit message for this group...", {
12021
+ tips: LOADING_TIPS
12022
+ });
11334
12023
  const newMsg = await regenerateGroupMessage(group.files, diffs, model, config.commitConvention);
11335
12024
  if (newMsg) {
11336
12025
  message = newMsg;
11337
12026
  group.message = newMsg;
11338
- regenSpinner.success(`New message: ${import_picocolors8.default.bold(message)}`);
12027
+ regenSpinner.success(`New message: ${import_picocolors9.default.bold(message)}`);
11339
12028
  } else {
11340
12029
  regenSpinner.fail("AI could not generate a new message. Keeping current one.");
11341
12030
  }
@@ -11360,22 +12049,45 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11360
12049
  continue;
11361
12050
  }
11362
12051
  }
11363
- const stageResult = await stageFiles(group.files);
12052
+ const remainingChangedFiles = new Set(await getChangedFiles());
12053
+ const stageableFiles = group.files.filter((file) => remainingChangedFiles.has(file));
12054
+ const skippedFiles = group.files.filter((file) => !remainingChangedFiles.has(file));
12055
+ if (skippedFiles.length > 0) {
12056
+ warn(`Group ${i2 + 1} file(s) no longer have changes: ${skippedFiles.join(", ")}`);
12057
+ }
12058
+ if (stageableFiles.length === 0) {
12059
+ warn(`Skipped group ${i2 + 1}: no files remain to commit.`);
12060
+ actionDone = true;
12061
+ continue;
12062
+ }
12063
+ const stageResult = await stageFiles(stageableFiles);
11364
12064
  if (stageResult.exitCode !== 0) {
11365
12065
  error(`Failed to stage group ${i2 + 1}: ${stageResult.stderr}`);
11366
12066
  actionDone = true;
11367
12067
  continue;
11368
12068
  }
12069
+ const stagedFiles = new Set(await getStagedFiles());
12070
+ const stagedGroupFiles = stageableFiles.filter((file) => stagedFiles.has(file));
12071
+ if (stagedGroupFiles.length === 0) {
12072
+ warn(`Skipped group ${i2 + 1}: no files were staged for commit.`);
12073
+ actionDone = true;
12074
+ continue;
12075
+ }
11369
12076
  const commitResult = await commitWithMessage(message);
11370
12077
  if (commitResult.exitCode !== 0) {
11371
12078
  const detail = (commitResult.stderr || commitResult.stdout).trim();
12079
+ if (isEmptyGroupCommitResult(detail)) {
12080
+ warn(`Skipped group ${i2 + 1}: nothing remained to commit.`);
12081
+ actionDone = true;
12082
+ continue;
12083
+ }
11372
12084
  error(`Failed to commit group ${i2 + 1}: ${detail}`);
11373
- await unstageFiles(group.files);
12085
+ await unstageFiles(stageableFiles);
11374
12086
  actionDone = true;
11375
12087
  continue;
11376
12088
  }
11377
12089
  committed++;
11378
- success(`Committed group ${i2 + 1}: ${import_picocolors8.default.bold(message)}`);
12090
+ success(`Committed group ${i2 + 1}: ${import_picocolors9.default.bold(message)}`);
11379
12091
  actionDone = true;
11380
12092
  }
11381
12093
  }
@@ -11391,11 +12103,11 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11391
12103
 
11392
12104
  // src/commands/doctor.ts
11393
12105
  import { execFile as execFileCb3 } from "node:child_process";
11394
- var import_picocolors9 = __toESM(require_picocolors(), 1);
12106
+ var import_picocolors10 = __toESM(require_picocolors(), 1);
11395
12107
  // package.json
11396
12108
  var package_default = {
11397
12109
  name: "contribute-now",
11398
- version: "0.6.2-dev.b908626",
12110
+ version: "0.6.2-dev.d6e92ac",
11399
12111
  description: "Developer CLI that automates git workflows — branching, syncing, committing, and PRs — with multi-workflow and commit convention support.",
11400
12112
  type: "module",
11401
12113
  bin: {
@@ -11486,16 +12198,16 @@ async function getRepoInfoFromRemote(remote = "origin") {
11486
12198
  }
11487
12199
 
11488
12200
  // src/commands/doctor.ts
11489
- var PASS = ` ${import_picocolors9.default.green("✔")} `;
11490
- var FAIL = ` ${import_picocolors9.default.red("✗")} `;
11491
- var WARN = ` ${import_picocolors9.default.yellow("⚠")} `;
12201
+ var PASS = ` ${import_picocolors10.default.green("✔")} `;
12202
+ var FAIL = ` ${import_picocolors10.default.red("✗")} `;
12203
+ var WARN = ` ${import_picocolors10.default.yellow("⚠")} `;
11492
12204
  function printReport(report) {
11493
12205
  for (const section of report.sections) {
11494
12206
  console.log(`
11495
- ${import_picocolors9.default.bold(import_picocolors9.default.underline(section.title))}`);
12207
+ ${import_picocolors10.default.bold(import_picocolors10.default.underline(section.title))}`);
11496
12208
  for (const check of section.checks) {
11497
12209
  const prefix = check.ok ? check.warning ? WARN : PASS : FAIL;
11498
- const text = check.detail ? `${check.label} ${import_picocolors9.default.dim(`— ${check.detail}`)}` : check.label;
12210
+ const text = check.detail ? `${check.label} ${import_picocolors10.default.dim(`— ${check.detail}`)}` : check.label;
11499
12211
  console.log(`${prefix}${text}`);
11500
12212
  }
11501
12213
  }
@@ -11513,9 +12225,9 @@ function toJson(report) {
11513
12225
  })), null, 2);
11514
12226
  }
11515
12227
  function runCmd(cmd, args) {
11516
- return new Promise((resolve2) => {
12228
+ return new Promise((resolve3) => {
11517
12229
  execFileCb3(cmd, args, (error2, stdout2) => {
11518
- resolve2({
12230
+ resolve3({
11519
12231
  ok: !error2,
11520
12232
  stdout: (stdout2 ?? "").trim()
11521
12233
  });
@@ -11577,18 +12289,32 @@ async function configSection() {
11577
12289
  const exists = configExists();
11578
12290
  if (!exists) {
11579
12291
  checks.push({
11580
- label: ".contributerc.json not found",
12292
+ label: "Repo config not found",
11581
12293
  ok: false,
11582
- detail: "run `contrib setup` to create it"
12294
+ detail: "run `contrib setup` to create local config for this clone"
11583
12295
  });
11584
12296
  return { title: "Config", checks };
11585
12297
  }
11586
12298
  const config = readConfig();
11587
12299
  if (!config) {
11588
- checks.push({ label: ".contributerc.json found but invalid", ok: false });
12300
+ checks.push({ label: "Repo config found but invalid", ok: false });
11589
12301
  return { title: "Config", checks };
11590
12302
  }
11591
- checks.push({ label: ".contributerc.json found and valid", ok: true });
12303
+ const configSource = getConfigSource();
12304
+ const hasBothConfigSources = hasLegacyConfig() && hasLocalConfig();
12305
+ checks.push({
12306
+ label: `${configSource === "local" ? "Local Git config" : "Legacy repo config"} found and valid`,
12307
+ ok: true,
12308
+ detail: getConfigLocationLabel()
12309
+ });
12310
+ if (hasBothConfigSources) {
12311
+ checks.push({
12312
+ label: "Both legacy and local config files exist",
12313
+ ok: true,
12314
+ warning: true,
12315
+ detail: "legacy .contributerc.json currently takes precedence"
12316
+ });
12317
+ }
11592
12318
  const desc = WORKFLOW_DESCRIPTIONS[config.workflow] ?? config.workflow;
11593
12319
  checks.push({
11594
12320
  label: `Workflow: ${config.workflow}`,
@@ -11603,13 +12329,21 @@ async function configSection() {
11603
12329
  ok: !!config.devBranch
11604
12330
  });
11605
12331
  }
11606
- const ignored = isGitignored();
11607
- checks.push({
11608
- label: ignored ? ".contributerc.json in .gitignore" : ".contributerc.json NOT in .gitignore",
11609
- ok: true,
11610
- warning: !ignored,
11611
- detail: ignored ? undefined : "consider adding it to .gitignore"
11612
- });
12332
+ if (configSource === "legacy") {
12333
+ const ignored = isGitignored();
12334
+ checks.push({
12335
+ label: ignored ? ".contributerc.json in .gitignore" : ".contributerc.json NOT in .gitignore",
12336
+ ok: true,
12337
+ warning: !ignored,
12338
+ detail: ignored ? undefined : "consider adding it to .gitignore"
12339
+ });
12340
+ } else {
12341
+ checks.push({
12342
+ label: "Config stored in local Git metadata",
12343
+ ok: true,
12344
+ detail: "does not modify tracked files or require .gitignore changes"
12345
+ });
12346
+ }
11613
12347
  return { title: "Config", checks };
11614
12348
  }
11615
12349
  async function gitSection() {
@@ -11758,20 +12492,20 @@ var doctor_default = defineCommand({
11758
12492
  console.log(toJson(report));
11759
12493
  return;
11760
12494
  }
11761
- heading("\uD83E\uDE7A contribute-now doctor");
12495
+ projectHeading("doctor", "\uD83E\uDE7A");
11762
12496
  printReport(report);
11763
12497
  const total = report.sections.flatMap((s2) => s2.checks);
11764
12498
  const failures = total.filter((c3) => !c3.ok);
11765
12499
  const warnings = total.filter((c3) => c3.ok && c3.warning);
11766
12500
  if (failures.length === 0 && warnings.length === 0) {
11767
- console.log(` ${import_picocolors9.default.green("All checks passed!")} No issues detected.
12501
+ console.log(` ${import_picocolors10.default.green("All checks passed!")} No issues detected.
11768
12502
  `);
11769
12503
  } else {
11770
12504
  if (failures.length > 0) {
11771
- console.log(` ${import_picocolors9.default.red(`${failures.length} issue${failures.length !== 1 ? "s" : ""} found.`)}`);
12505
+ console.log(` ${import_picocolors10.default.red(`${failures.length} issue${failures.length !== 1 ? "s" : ""} found.`)}`);
11772
12506
  }
11773
12507
  if (warnings.length > 0) {
11774
- console.log(` ${import_picocolors9.default.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}.`)}`);
12508
+ console.log(` ${import_picocolors10.default.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}.`)}`);
11775
12509
  }
11776
12510
  console.log();
11777
12511
  }
@@ -11779,9 +12513,9 @@ var doctor_default = defineCommand({
11779
12513
  });
11780
12514
 
11781
12515
  // src/commands/hook.ts
11782
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
12516
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
11783
12517
  import { join as join4 } from "node:path";
11784
- var import_picocolors10 = __toESM(require_picocolors(), 1);
12518
+ var import_picocolors11 = __toESM(require_picocolors(), 1);
11785
12519
  var HOOK_MARKER = "# managed by contribute-now";
11786
12520
  function getHooksDir(cwd = process.cwd()) {
11787
12521
  return join4(cwd, ".git", "hooks");
@@ -11806,13 +12540,13 @@ esac
11806
12540
 
11807
12541
  # Detect available package runner
11808
12542
  if command -v contrib >/dev/null 2>&1; then
11809
- contrib validate "$commit_msg"
12543
+ contrib validate --file "$commit_msg_file"
11810
12544
  elif command -v bunx >/dev/null 2>&1; then
11811
- bunx contrib validate "$commit_msg"
12545
+ bunx contrib validate --file "$commit_msg_file"
11812
12546
  elif command -v pnpx >/dev/null 2>&1; then
11813
- pnpx contrib validate "$commit_msg"
12547
+ pnpx contrib validate --file "$commit_msg_file"
11814
12548
  elif command -v npx >/dev/null 2>&1; then
11815
- npx contrib validate "$commit_msg"
12549
+ npx contrib validate --file "$commit_msg_file"
11816
12550
  else
11817
12551
  echo "Warning: No package runner found. Skipping commit message validation."
11818
12552
  exit 0
@@ -11849,10 +12583,10 @@ var hook_default = defineCommand({
11849
12583
  }
11850
12584
  });
11851
12585
  async function installHook() {
11852
- heading("\uD83E\uDE9D hook install");
12586
+ projectHeading("hook install", "\uD83E\uDE9D");
11853
12587
  const config = readConfig();
11854
12588
  if (!config) {
11855
- error("No .contributerc.json found. Run `contrib setup` first.");
12589
+ error("No repo config found. Run `contrib setup` first.");
11856
12590
  process.exit(1);
11857
12591
  }
11858
12592
  if (config.commitConvention === "none") {
@@ -11873,16 +12607,16 @@ async function installHook() {
11873
12607
  info("Updating existing contribute-now hook...");
11874
12608
  }
11875
12609
  if (!existsSync4(hooksDir)) {
11876
- mkdirSync2(hooksDir, { recursive: true });
12610
+ mkdirSync3(hooksDir, { recursive: true });
11877
12611
  }
11878
12612
  writeFileSync3(hookPath, generateHookScript(), { mode: 493 });
11879
12613
  success(`commit-msg hook installed.`);
11880
- info(`Convention: ${import_picocolors10.default.bold(CONVENTION_LABELS[config.commitConvention])}`, "");
11881
- info(`Path: ${import_picocolors10.default.dim(hookPath)}`, "");
12614
+ info(`Convention: ${import_picocolors11.default.bold(CONVENTION_LABELS[config.commitConvention])}`, "");
12615
+ info(`Path: ${import_picocolors11.default.dim(hookPath)}`, "");
11882
12616
  warn("Note: hooks can be bypassed with `git commit --no-verify`.");
11883
12617
  }
11884
12618
  async function uninstallHook() {
11885
- heading("\uD83E\uDE9D hook uninstall");
12619
+ projectHeading("hook uninstall", "\uD83E\uDE9D");
11886
12620
  const hookPath = getHookPath();
11887
12621
  if (!existsSync4(hookPath)) {
11888
12622
  info("No commit-msg hook found. Nothing to uninstall.");
@@ -11898,7 +12632,10 @@ async function uninstallHook() {
11898
12632
  }
11899
12633
 
11900
12634
  // src/commands/log.ts
11901
- var import_picocolors11 = __toESM(require_picocolors(), 1);
12635
+ var import_picocolors12 = __toESM(require_picocolors(), 1);
12636
+ function getDefaultOverviewRemoteCommitCount(hasLocalUnpushedCommits) {
12637
+ return hasLocalUnpushedCommits ? 10 : 20;
12638
+ }
11902
12639
  var log_default = defineCommand({
11903
12640
  meta: {
11904
12641
  name: "log",
@@ -11919,7 +12656,13 @@ var log_default = defineCommand({
11919
12656
  remote: {
11920
12657
  type: "boolean",
11921
12658
  alias: "r",
11922
- description: "Show only remote commits not yet pulled locally",
12659
+ description: "Show only the remote branch history",
12660
+ default: false
12661
+ },
12662
+ local: {
12663
+ type: "boolean",
12664
+ alias: "l",
12665
+ description: "Show only local unpushed commits",
11923
12666
  default: false
11924
12667
  },
11925
12668
  full: {
@@ -11946,12 +12689,15 @@ var log_default = defineCommand({
11946
12689
  process.exit(1);
11947
12690
  }
11948
12691
  const config = readConfig();
11949
- const count = args.count ? Number.parseInt(args.count, 10) : 20;
12692
+ const requestedCount = args.count ? Number.parseInt(args.count, 10) : null;
12693
+ const localCount = requestedCount ?? 20;
11950
12694
  const showGraph = args.graph;
11951
12695
  const targetBranch = args.branch;
11952
- let mode = "local";
12696
+ let mode = "overview";
11953
12697
  if (args.all)
11954
12698
  mode = "all";
12699
+ else if (args.local)
12700
+ mode = "local";
11955
12701
  else if (args.remote)
11956
12702
  mode = "remote";
11957
12703
  else if (args.full || targetBranch)
@@ -11967,33 +12713,74 @@ var log_default = defineCommand({
11967
12713
  compareRef = fallback;
11968
12714
  usingFallback = true;
11969
12715
  }
11970
- }
11971
- heading("\uD83D\uDCDC commit log");
11972
- printModeHeader(mode, currentBranch, compareRef, usingFallback);
11973
- if (mode === "local" || mode === "remote") {
11974
- if (!compareRef) {
12716
+ }
12717
+ const overviewRemoteCount = requestedCount ?? await getOverviewRemoteCommitCount(currentBranch, compareRef);
12718
+ projectHeading("log", "\uD83D\uDCDC");
12719
+ printModeHeader(mode, currentBranch, compareRef, usingFallback);
12720
+ if (mode === "overview") {
12721
+ await renderOverviewLog({
12722
+ localCount,
12723
+ remoteCount: overviewRemoteCount,
12724
+ compareRef,
12725
+ showGraph,
12726
+ protectedBranches,
12727
+ currentBranch,
12728
+ usingFallback
12729
+ });
12730
+ } else if (mode === "local") {
12731
+ if (!compareRef) {
12732
+ console.log();
12733
+ console.log(import_picocolors12.default.yellow(" ⚠ Could not determine a comparison branch."));
12734
+ console.log(import_picocolors12.default.dim(" No upstream tracking set and no remote base branch found."));
12735
+ console.log(import_picocolors12.default.dim(` Use ${import_picocolors12.default.bold("contrib log --full")} to see the full commit history instead.`));
12736
+ console.log();
12737
+ return;
12738
+ }
12739
+ const hasCommits = await renderScopedLog({
12740
+ mode,
12741
+ count: localCount,
12742
+ upstream: compareRef,
12743
+ showGraph,
12744
+ protectedBranches,
12745
+ currentBranch
12746
+ });
12747
+ if (!hasCommits) {
12748
+ return;
12749
+ }
12750
+ } else if (mode === "remote") {
12751
+ const remoteBranch = compareRef ?? targetBranch;
12752
+ if (!remoteBranch) {
11975
12753
  console.log();
11976
- console.log(import_picocolors11.default.yellow(" ⚠ Could not determine a comparison branch."));
11977
- console.log(import_picocolors11.default.dim(" No upstream tracking set and no remote base branch found."));
11978
- console.log(import_picocolors11.default.dim(` Use ${import_picocolors11.default.bold("contrib log --full")} to see the full commit history instead.`));
12754
+ console.log(import_picocolors12.default.yellow(" ⚠ Could not determine a remote branch to display."));
12755
+ console.log(import_picocolors12.default.dim(" Set an upstream tracking branch or configure your base branch first."));
11979
12756
  console.log();
11980
- printGuidance();
11981
12757
  return;
11982
12758
  }
11983
- const hasCommits = await renderScopedLog({ mode, count, upstream: compareRef, showGraph, protectedBranches, currentBranch });
12759
+ const hasCommits = await renderFullLog({
12760
+ count: localCount,
12761
+ all: false,
12762
+ showGraph,
12763
+ targetBranch: remoteBranch,
12764
+ protectedBranches,
12765
+ currentBranch
12766
+ });
11984
12767
  if (!hasCommits) {
11985
- printGuidance();
11986
12768
  return;
11987
12769
  }
11988
12770
  } else {
11989
- const hasCommits = await renderFullLog({ count, all: mode === "all", showGraph, targetBranch, protectedBranches, currentBranch });
12771
+ const hasCommits = await renderFullLog({
12772
+ count: localCount,
12773
+ all: mode === "all",
12774
+ showGraph,
12775
+ targetBranch,
12776
+ protectedBranches,
12777
+ currentBranch
12778
+ });
11990
12779
  if (!hasCommits) {
11991
- printGuidance();
11992
12780
  return;
11993
12781
  }
11994
12782
  }
11995
- printFooter(mode, count, targetBranch);
11996
- printGuidance();
12783
+ printFooter(mode, localCount, overviewRemoteCount, targetBranch);
11997
12784
  }
11998
12785
  });
11999
12786
  async function resolveBaseBranchRef(config) {
@@ -12015,38 +12802,49 @@ async function resolveBaseBranchRef(config) {
12015
12802
  }
12016
12803
  return null;
12017
12804
  }
12805
+ async function getOverviewRemoteCommitCount(currentBranch, compareRef) {
12806
+ if (!currentBranch || !compareRef) {
12807
+ return getDefaultOverviewRemoteCommitCount(false);
12808
+ }
12809
+ const divergence = await getDivergence(currentBranch, compareRef);
12810
+ return getDefaultOverviewRemoteCommitCount(divergence.ahead > 0);
12811
+ }
12018
12812
  function printModeHeader(mode, currentBranch, compareRef, usingFallback = false) {
12019
12813
  const branch = currentBranch ?? "HEAD";
12020
- const fallbackNote = usingFallback ? import_picocolors11.default.yellow(" (no upstream — comparing against base branch)") : "";
12021
- console.log();
12814
+ const fallbackNote = usingFallback ? import_picocolors12.default.yellow(" (no upstream — comparing against base branch)") : "";
12022
12815
  switch (mode) {
12816
+ case "overview":
12817
+ console.log(import_picocolors12.default.dim(` mode: ${import_picocolors12.default.bold("overview")} — local unpushed commits and remote branch history for ${import_picocolors12.default.bold(branch)}`) + fallbackNote);
12818
+ if (compareRef) {
12819
+ console.log(import_picocolors12.default.dim(` remote source: ${import_picocolors12.default.bold(compareRef)}`));
12820
+ }
12821
+ break;
12023
12822
  case "local":
12024
- console.log(import_picocolors11.default.dim(` mode: ${import_picocolors11.default.bold("local")} — unpushed commits on ${import_picocolors11.default.bold(branch)}`) + fallbackNote);
12823
+ console.log(import_picocolors12.default.dim(` mode: ${import_picocolors12.default.bold("local")} — unpushed commits on ${import_picocolors12.default.bold(branch)}`) + fallbackNote);
12025
12824
  if (compareRef) {
12026
- console.log(import_picocolors11.default.dim(` comparing: ${import_picocolors11.default.bold(compareRef)} ➜ ${import_picocolors11.default.bold("HEAD")}`));
12825
+ console.log(import_picocolors12.default.dim(` comparing: ${import_picocolors12.default.bold(compareRef)} ➜ ${import_picocolors12.default.bold("HEAD")}`));
12027
12826
  }
12028
12827
  break;
12029
12828
  case "remote":
12030
- console.log(import_picocolors11.default.dim(` mode: ${import_picocolors11.default.bold("remote")} — commits on remote not yet pulled into ${import_picocolors11.default.bold(branch)}`) + fallbackNote);
12829
+ console.log(import_picocolors12.default.dim(` mode: ${import_picocolors12.default.bold("remote")} — remote branch history relevant to ${import_picocolors12.default.bold(branch)}`) + fallbackNote);
12031
12830
  if (compareRef) {
12032
- console.log(import_picocolors11.default.dim(` comparing: ${import_picocolors11.default.bold("HEAD")} ➜ ${import_picocolors11.default.bold(compareRef)}`));
12831
+ console.log(import_picocolors12.default.dim(` branch: ${import_picocolors12.default.bold(compareRef)}`));
12033
12832
  }
12034
12833
  break;
12035
12834
  case "full":
12036
- console.log(import_picocolors11.default.dim(` mode: ${import_picocolors11.default.bold("full")} — complete commit history for ${import_picocolors11.default.bold(branch)}`));
12835
+ console.log(import_picocolors12.default.dim(` mode: ${import_picocolors12.default.bold("full")} — complete commit history for ${import_picocolors12.default.bold(branch)}`));
12037
12836
  break;
12038
12837
  case "all":
12039
- console.log(import_picocolors11.default.dim(` mode: ${import_picocolors11.default.bold("all")} — commits across all branches`));
12838
+ console.log(import_picocolors12.default.dim(` mode: ${import_picocolors12.default.bold("all")} — commits across all branches`));
12040
12839
  break;
12041
12840
  }
12042
12841
  }
12043
12842
  async function renderScopedLog(options) {
12044
- const { mode, count, upstream, showGraph, protectedBranches, currentBranch } = options;
12843
+ const { count, upstream, showGraph, protectedBranches, currentBranch } = options;
12045
12844
  if (showGraph) {
12046
- const graphFn = mode === "local" ? getLocalCommitsGraph : getRemoteOnlyCommitsGraph;
12047
- const lines = await graphFn({ count, upstream });
12845
+ const lines = await getLocalCommitsGraph({ count, upstream });
12048
12846
  if (lines.length === 0) {
12049
- printEmptyState(mode);
12847
+ printEmptyState("local");
12050
12848
  return false;
12051
12849
  }
12052
12850
  console.log();
@@ -12054,15 +12852,14 @@ async function renderScopedLog(options) {
12054
12852
  console.log(` ${colorizeGraphLine(line, protectedBranches, currentBranch)}`);
12055
12853
  }
12056
12854
  } else {
12057
- const entryFn = mode === "local" ? getLocalCommitsEntries : getRemoteOnlyCommitsEntries;
12058
- const entries = await entryFn({ count, upstream });
12855
+ const entries = await getLocalCommitsEntries({ count, upstream });
12059
12856
  if (entries.length === 0) {
12060
- printEmptyState(mode);
12857
+ printEmptyState("local");
12061
12858
  return false;
12062
12859
  }
12063
12860
  console.log();
12064
12861
  for (const entry of entries) {
12065
- const hashStr = import_picocolors11.default.yellow(entry.hash);
12862
+ const hashStr = import_picocolors12.default.yellow(entry.hash);
12066
12863
  const refsStr = entry.refs ? ` ${colorizeRefs(entry.refs, protectedBranches, currentBranch)}` : "";
12067
12864
  const subjectStr = colorizeSubject(entry.subject);
12068
12865
  console.log(` ${hashStr}${refsStr} ${subjectStr}`);
@@ -12070,12 +12867,58 @@ async function renderScopedLog(options) {
12070
12867
  }
12071
12868
  return true;
12072
12869
  }
12870
+ async function renderOverviewLog(options) {
12871
+ const {
12872
+ localCount,
12873
+ remoteCount,
12874
+ compareRef,
12875
+ showGraph,
12876
+ protectedBranches,
12877
+ currentBranch,
12878
+ usingFallback
12879
+ } = options;
12880
+ console.log();
12881
+ console.log(import_picocolors12.default.bold(import_picocolors12.default.cyan(" Local Unpushed Commits")));
12882
+ if (!compareRef) {
12883
+ console.log(import_picocolors12.default.dim(" No comparison branch detected for local commit status."));
12884
+ console.log(import_picocolors12.default.dim(" Set an upstream tracking branch or run cn log --full to inspect the current branch history."));
12885
+ } else {
12886
+ await renderScopedLog({
12887
+ mode: "local",
12888
+ count: localCount,
12889
+ upstream: compareRef,
12890
+ showGraph,
12891
+ protectedBranches,
12892
+ currentBranch
12893
+ });
12894
+ }
12895
+ console.log();
12896
+ console.log(import_picocolors12.default.bold(import_picocolors12.default.cyan(" Remote Branch History")));
12897
+ if (!compareRef) {
12898
+ console.log(import_picocolors12.default.dim(" No remote branch detected."));
12899
+ if (usingFallback) {
12900
+ console.log(import_picocolors12.default.dim(" Configure your base branch or upstream tracking to enable the split view."));
12901
+ }
12902
+ return;
12903
+ }
12904
+ const hasRemoteHistory = await renderFullLog({
12905
+ count: remoteCount,
12906
+ all: false,
12907
+ showGraph,
12908
+ targetBranch: compareRef,
12909
+ protectedBranches,
12910
+ currentBranch
12911
+ });
12912
+ if (!hasRemoteHistory) {
12913
+ console.log(import_picocolors12.default.dim(" No remote history found for the selected branch."));
12914
+ }
12915
+ }
12073
12916
  function printEmptyState(mode) {
12074
12917
  console.log();
12075
12918
  if (mode === "local") {
12076
- console.log(import_picocolors11.default.dim(" No local unpushed commits — you're up to date with remote!"));
12919
+ console.log(import_picocolors12.default.dim(" No local unpushed commits — you're up to date with remote!"));
12077
12920
  } else {
12078
- console.log(import_picocolors11.default.dim(" No remote-only commits — your local branch is up to date!"));
12921
+ console.log(import_picocolors12.default.dim(" No remote-only commits — your local branch is up to date!"));
12079
12922
  }
12080
12923
  console.log();
12081
12924
  }
@@ -12084,7 +12927,7 @@ async function renderFullLog(options) {
12084
12927
  if (showGraph) {
12085
12928
  const lines = await getLogGraph({ count, all, branch: targetBranch });
12086
12929
  if (lines.length === 0) {
12087
- console.log(import_picocolors11.default.dim(" No commits found."));
12930
+ console.log(import_picocolors12.default.dim(" No commits found."));
12088
12931
  console.log();
12089
12932
  return false;
12090
12933
  }
@@ -12095,13 +12938,13 @@ async function renderFullLog(options) {
12095
12938
  } else {
12096
12939
  const entries = await getLogEntries({ count, all, branch: targetBranch });
12097
12940
  if (entries.length === 0) {
12098
- console.log(import_picocolors11.default.dim(" No commits found."));
12941
+ console.log(import_picocolors12.default.dim(" No commits found."));
12099
12942
  console.log();
12100
12943
  return false;
12101
12944
  }
12102
12945
  console.log();
12103
12946
  for (const entry of entries) {
12104
- const hashStr = import_picocolors11.default.yellow(entry.hash);
12947
+ const hashStr = import_picocolors12.default.yellow(entry.hash);
12105
12948
  const refsStr = entry.refs ? ` ${colorizeRefs(entry.refs, protectedBranches, currentBranch)}` : "";
12106
12949
  const subjectStr = colorizeSubject(entry.subject);
12107
12950
  console.log(` ${hashStr}${refsStr} ${subjectStr}`);
@@ -12109,46 +12952,37 @@ async function renderFullLog(options) {
12109
12952
  }
12110
12953
  return true;
12111
12954
  }
12112
- function printFooter(mode, count, targetBranch) {
12955
+ function printFooter(mode, count, overviewRemoteCount, targetBranch) {
12113
12956
  console.log();
12114
12957
  switch (mode) {
12958
+ case "overview":
12959
+ console.log(import_picocolors12.default.dim(` Showing up to ${count} local commits and ${overviewRemoteCount} remote commits`));
12960
+ break;
12115
12961
  case "local":
12116
- console.log(import_picocolors11.default.dim(` Showing up to ${count} unpushed commits`));
12962
+ console.log(import_picocolors12.default.dim(` Showing up to ${count} unpushed commits`));
12117
12963
  break;
12118
12964
  case "remote":
12119
- console.log(import_picocolors11.default.dim(` Showing up to ${count} remote-only commits`));
12965
+ console.log(import_picocolors12.default.dim(` Showing ${count} most recent commits from the remote branch`));
12120
12966
  break;
12121
12967
  case "full":
12122
- console.log(import_picocolors11.default.dim(` Showing ${count} most recent commits${targetBranch ? ` (${targetBranch})` : ""}`));
12968
+ console.log(import_picocolors12.default.dim(` Showing ${count} most recent commits${targetBranch ? ` (${targetBranch})` : ""}`));
12123
12969
  break;
12124
12970
  case "all":
12125
- console.log(import_picocolors11.default.dim(` Showing ${count} most recent commits (all branches)`));
12971
+ console.log(import_picocolors12.default.dim(` Showing ${count} most recent commits (all branches)`));
12126
12972
  break;
12127
12973
  }
12128
12974
  }
12129
- function printGuidance() {
12130
- console.log();
12131
- console.log(import_picocolors11.default.dim(" ─── quick guide ───"));
12132
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log")} local unpushed commits (default)`));
12133
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log --remote")} commits on remote not yet pulled`));
12134
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log --full")} full history for the current branch`));
12135
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log --all")} commits across all branches`));
12136
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log -n 50")} change the commit limit (default: 20)`));
12137
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log -b dev")} view log for a specific branch`));
12138
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log --no-graph")} flat list without graph lines`));
12139
- console.log();
12140
- }
12141
12975
  function colorizeGraphLine(line, protectedBranches, currentBranch) {
12142
12976
  const match = line.match(/^([|/\\*\s_.-]*)([a-f0-9]{7,12})(\s+\(([^)]+)\))?\s*(.*)/);
12143
12977
  if (!match) {
12144
- return import_picocolors11.default.cyan(line);
12978
+ return import_picocolors12.default.cyan(line);
12145
12979
  }
12146
12980
  const [, graphPart = "", hash, , refs, subject = ""] = match;
12147
12981
  const parts = [];
12148
12982
  if (graphPart) {
12149
12983
  parts.push(colorizeGraphChars(graphPart));
12150
12984
  }
12151
- parts.push(import_picocolors11.default.yellow(hash));
12985
+ parts.push(import_picocolors12.default.yellow(hash));
12152
12986
  if (refs) {
12153
12987
  parts.push(` (${colorizeRefs(refs, protectedBranches, currentBranch)})`);
12154
12988
  }
@@ -12159,15 +12993,15 @@ function colorizeGraphChars(graphPart) {
12159
12993
  return graphPart.split("").map((ch) => {
12160
12994
  switch (ch) {
12161
12995
  case "*":
12162
- return import_picocolors11.default.green(ch);
12996
+ return import_picocolors12.default.green(ch);
12163
12997
  case "|":
12164
- return import_picocolors11.default.cyan(ch);
12998
+ return import_picocolors12.default.cyan(ch);
12165
12999
  case "/":
12166
13000
  case "\\":
12167
- return import_picocolors11.default.cyan(ch);
13001
+ return import_picocolors12.default.cyan(ch);
12168
13002
  case "-":
12169
13003
  case "_":
12170
- return import_picocolors11.default.cyan(ch);
13004
+ return import_picocolors12.default.cyan(ch);
12171
13005
  default:
12172
13006
  return ch;
12173
13007
  }
@@ -12179,50 +13013,50 @@ function colorizeRefs(refs, protectedBranches, currentBranch) {
12179
13013
  if (trimmed.startsWith("HEAD ->") || trimmed === "HEAD") {
12180
13014
  const branchName = trimmed.replace("HEAD -> ", "");
12181
13015
  if (trimmed === "HEAD") {
12182
- return import_picocolors11.default.bold(import_picocolors11.default.cyan("HEAD"));
13016
+ return import_picocolors12.default.bold(import_picocolors12.default.cyan("HEAD"));
12183
13017
  }
12184
- return `${import_picocolors11.default.bold(import_picocolors11.default.cyan("HEAD"))} ${import_picocolors11.default.dim("->")} ${colorizeRefName(branchName, protectedBranches, currentBranch)}`;
13018
+ return `${import_picocolors12.default.bold(import_picocolors12.default.cyan("HEAD"))} ${import_picocolors12.default.dim("->")} ${colorizeRefName(branchName, protectedBranches, currentBranch)}`;
12185
13019
  }
12186
13020
  if (trimmed.startsWith("tag:")) {
12187
- return import_picocolors11.default.bold(import_picocolors11.default.magenta(trimmed));
13021
+ return import_picocolors12.default.bold(import_picocolors12.default.magenta(trimmed));
12188
13022
  }
12189
13023
  return colorizeRefName(trimmed, protectedBranches, currentBranch);
12190
- }).join(import_picocolors11.default.dim(", "));
13024
+ }).join(import_picocolors12.default.dim(", "));
12191
13025
  }
12192
13026
  function colorizeRefName(name, protectedBranches, currentBranch) {
12193
13027
  const isRemote = name.includes("/");
12194
13028
  const localName = isRemote ? name.split("/").slice(1).join("/") : name;
12195
13029
  if (protectedBranches.includes(localName)) {
12196
- return isRemote ? import_picocolors11.default.bold(import_picocolors11.default.red(name)) : import_picocolors11.default.bold(import_picocolors11.default.red(name));
13030
+ return isRemote ? import_picocolors12.default.bold(import_picocolors12.default.red(name)) : import_picocolors12.default.bold(import_picocolors12.default.red(name));
12197
13031
  }
12198
13032
  if (localName === currentBranch) {
12199
- return import_picocolors11.default.bold(import_picocolors11.default.green(name));
13033
+ return import_picocolors12.default.bold(import_picocolors12.default.green(name));
12200
13034
  }
12201
13035
  if (isRemote) {
12202
- return import_picocolors11.default.blue(name);
13036
+ return import_picocolors12.default.blue(name);
12203
13037
  }
12204
- return import_picocolors11.default.green(name);
13038
+ return import_picocolors12.default.green(name);
12205
13039
  }
12206
13040
  function colorizeSubject(subject) {
12207
13041
  const emojiMatch = subject.match(/^((?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F)+\s*)/u);
12208
13042
  if (emojiMatch) {
12209
13043
  const emoji = emojiMatch[1];
12210
13044
  const rest = subject.slice(emoji.length);
12211
- return `${emoji}${import_picocolors11.default.white(rest)}`;
13045
+ return `${emoji}${import_picocolors12.default.white(rest)}`;
12212
13046
  }
12213
13047
  if (subject.startsWith("Merge ")) {
12214
- return import_picocolors11.default.dim(subject);
13048
+ return import_picocolors12.default.dim(subject);
12215
13049
  }
12216
- return import_picocolors11.default.white(subject);
13050
+ return import_picocolors12.default.white(subject);
12217
13051
  }
12218
13052
 
12219
13053
  // src/commands/save.ts
12220
- var import_picocolors12 = __toESM(require_picocolors(), 1);
12221
13054
  import { execFile as execFileCb4 } from "node:child_process";
13055
+ var import_picocolors13 = __toESM(require_picocolors(), 1);
12222
13056
  function gitRun(args) {
12223
- return new Promise((resolve2) => {
13057
+ return new Promise((resolve3) => {
12224
13058
  execFileCb4("git", args, (err, stdout2, stderr) => {
12225
- resolve2({
13059
+ resolve3({
12226
13060
  exitCode: err ? err.code === "ENOENT" ? 127 : err.status ?? 1 : 0,
12227
13061
  stdout: stdout2 ?? "",
12228
13062
  stderr: stderr ?? ""
@@ -12279,7 +13113,7 @@ var save_default = defineCommand({
12279
13113
  }
12280
13114
  });
12281
13115
  async function handleSave(message) {
12282
- heading("\uD83D\uDCBE contrib save");
13116
+ projectHeading("save", "\uD83D\uDCBE");
12283
13117
  const currentBranch = await getCurrentBranch();
12284
13118
  const label = message ?? `work-in-progress on ${currentBranch ?? "unknown"}`;
12285
13119
  const stashMsg = `contrib-save: ${label}`;
@@ -12292,11 +13126,11 @@ async function handleSave(message) {
12292
13126
  info("No uncommitted changes to save.");
12293
13127
  return;
12294
13128
  }
12295
- success(`Saved: ${import_picocolors12.default.dim(label)}`);
12296
- info(`Use ${import_picocolors12.default.bold("contrib save --restore")} to bring them back.`, "");
13129
+ success(`Saved: ${import_picocolors13.default.dim(label)}`);
13130
+ info(`Use ${import_picocolors13.default.bold("contrib save --restore")} to bring them back.`, "");
12297
13131
  }
12298
13132
  async function handleRestore() {
12299
- heading("\uD83D\uDCBE contrib save --restore");
13133
+ projectHeading("save --restore", "\uD83D\uDCBE");
12300
13134
  const stashes = await getStashList();
12301
13135
  if (stashes.length === 0) {
12302
13136
  info("No saved changes found.");
@@ -12309,7 +13143,7 @@ async function handleRestore() {
12309
13143
  warn("You may have conflicts. Resolve them and run `git stash drop` when done.");
12310
13144
  process.exit(1);
12311
13145
  }
12312
- success(`Restored: ${import_picocolors12.default.dim(stashes[0].message)}`);
13146
+ success(`Restored: ${import_picocolors13.default.dim(stashes[0].message)}`);
12313
13147
  return;
12314
13148
  }
12315
13149
  const choices = stashes.map((s2) => `${s2.index} ${s2.message}`);
@@ -12322,10 +13156,10 @@ async function handleRestore() {
12322
13156
  process.exit(1);
12323
13157
  }
12324
13158
  const match = stashes.find((s2) => String(s2.index) === idx);
12325
- success(`Restored: ${import_picocolors12.default.dim(match?.message ?? "saved changes")}`);
13159
+ success(`Restored: ${import_picocolors13.default.dim(match?.message ?? "saved changes")}`);
12326
13160
  }
12327
13161
  async function handleList() {
12328
- heading("\uD83D\uDCBE contrib save --list");
13162
+ projectHeading("save --list", "\uD83D\uDCBE");
12329
13163
  const stashes = await getStashList();
12330
13164
  if (stashes.length === 0) {
12331
13165
  info("No saved changes.");
@@ -12333,16 +13167,16 @@ async function handleList() {
12333
13167
  }
12334
13168
  console.log();
12335
13169
  for (const s2 of stashes) {
12336
- const idx = import_picocolors12.default.dim(`[${s2.index}]`);
13170
+ const idx = import_picocolors13.default.dim(`[${s2.index}]`);
12337
13171
  const msg = s2.message;
12338
13172
  console.log(` ${idx} ${msg}`);
12339
13173
  }
12340
13174
  console.log();
12341
- info(`Use ${import_picocolors12.default.bold("contrib save --restore")} to bring changes back.`, "");
12342
- info(`Use ${import_picocolors12.default.bold("contrib save --drop")} to discard saved changes.`, "");
13175
+ info(`Use ${import_picocolors13.default.bold("contrib save --restore")} to bring changes back.`, "");
13176
+ info(`Use ${import_picocolors13.default.bold("contrib save --drop")} to discard saved changes.`, "");
12343
13177
  }
12344
13178
  async function handleDrop() {
12345
- heading("\uD83D\uDCBE contrib save --drop");
13179
+ projectHeading("save --drop", "\uD83D\uDCBE");
12346
13180
  const stashes = await getStashList();
12347
13181
  if (stashes.length === 0) {
12348
13182
  info("No saved changes to drop.");
@@ -12357,7 +13191,7 @@ async function handleDrop() {
12357
13191
  process.exit(1);
12358
13192
  }
12359
13193
  const match = stashes.find((s2) => String(s2.index) === idx);
12360
- success(`Dropped: ${import_picocolors12.default.dim(match?.message ?? "saved changes")}`);
13194
+ success(`Dropped: ${import_picocolors13.default.dim(match?.message ?? "saved changes")}`);
12361
13195
  }
12362
13196
  async function getStashList() {
12363
13197
  const result = await gitRun(["stash", "list"]);
@@ -12374,38 +13208,23 @@ async function getStashList() {
12374
13208
  }
12375
13209
 
12376
13210
  // src/commands/setup.ts
12377
- var import_picocolors13 = __toESM(require_picocolors(), 1);
13211
+ var import_picocolors14 = __toESM(require_picocolors(), 1);
12378
13212
  async function shouldContinueSetupWithExistingConfig(options) {
12379
- const {
12380
- existingConfig,
12381
- hasConfigFile,
12382
- confirm,
12383
- ensureIgnored,
12384
- onInfo,
12385
- onWarn,
12386
- onSuccess,
12387
- summary
12388
- } = options;
13213
+ const { existingConfig, hasConfigFile, confirm, onInfo, onWarn, onSuccess, summary } = options;
12389
13214
  if (existingConfig) {
12390
- onInfo("Existing .contributerc.json detected:");
13215
+ onInfo("Existing repo config detected:");
12391
13216
  summary(existingConfig);
12392
13217
  const shouldContinue = await confirm("Continue setup and overwrite existing config?");
12393
13218
  if (!shouldContinue) {
12394
- if (ensureIgnored()) {
12395
- onInfo("Added .contributerc.json to .gitignore to avoid committing personal config.");
12396
- }
12397
13219
  onSuccess("Keeping existing setup.");
12398
13220
  return false;
12399
13221
  }
12400
13222
  return true;
12401
13223
  }
12402
13224
  if (hasConfigFile) {
12403
- onWarn("Found .contributerc.json but it appears invalid.");
13225
+ onWarn("Found an existing repo config but it appears invalid.");
12404
13226
  const shouldContinue = await confirm("Continue setup and overwrite invalid config?");
12405
13227
  if (!shouldContinue) {
12406
- if (ensureIgnored()) {
12407
- onInfo("Added .contributerc.json to .gitignore to avoid committing personal config.");
12408
- }
12409
13228
  onInfo("Keeping existing file. Run setup again when ready to repair it.");
12410
13229
  return false;
12411
13230
  }
@@ -12415,20 +13234,19 @@ async function shouldContinueSetupWithExistingConfig(options) {
12415
13234
  var setup_default = defineCommand({
12416
13235
  meta: {
12417
13236
  name: "setup",
12418
- description: "Initialize contribute-now config for this repo (.contributerc.json)"
13237
+ description: "Initialize contribute-now config for this repo using local Git storage"
12419
13238
  },
12420
13239
  async run() {
12421
13240
  if (!await isGitRepo()) {
12422
13241
  error("Not inside a git repository. Run this command from within a git repo.");
12423
13242
  process.exit(1);
12424
13243
  }
12425
- heading("\uD83D\uDD27 contribute-now setup");
13244
+ projectHeading("setup", "\uD83D\uDD27");
12426
13245
  const existingConfig = readConfig();
12427
13246
  const shouldContinue = await shouldContinueSetupWithExistingConfig({
12428
13247
  existingConfig,
12429
13248
  hasConfigFile: configExists(),
12430
13249
  confirm: confirmPrompt,
12431
- ensureIgnored: ensureGitignored,
12432
13250
  onInfo: info,
12433
13251
  onWarn: warn,
12434
13252
  onSuccess: success,
@@ -12447,7 +13265,7 @@ var setup_default = defineCommand({
12447
13265
  workflow = "github-flow";
12448
13266
  else if (workflowChoice.startsWith("Git Flow"))
12449
13267
  workflow = "git-flow";
12450
- info(`Workflow: ${import_picocolors13.default.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
13268
+ info(`Workflow: ${import_picocolors14.default.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
12451
13269
  const conventionChoice = await selectPrompt("Which commit convention should this project use?", [
12452
13270
  `${CONVENTION_DESCRIPTIONS["clean-commit"]} (recommended)`,
12453
13271
  CONVENTION_DESCRIPTIONS.conventional,
@@ -12458,6 +13276,8 @@ var setup_default = defineCommand({
12458
13276
  commitConvention = "conventional";
12459
13277
  else if (conventionChoice.includes("No commit"))
12460
13278
  commitConvention = "none";
13279
+ const enableAI = await confirmPrompt("Enable AI-assisted features like commit messages, branch naming, PR text, and conflict guidance?");
13280
+ const showTips = await confirmPrompt("Show beginner quick guides and loading tips in command output?");
12461
13281
  const remotes = await getRemotes();
12462
13282
  if (remotes.length === 0) {
12463
13283
  error("No git remotes found. Add a remote first (e.g., git remote add origin <url>).");
@@ -12511,15 +13331,15 @@ var setup_default = defineCommand({
12511
13331
  detectedRole = roleChoice;
12512
13332
  detectionSource = "user selection";
12513
13333
  } else {
12514
- info(`Detected role: ${import_picocolors13.default.bold(detectedRole)} (via ${detectionSource})`);
12515
- const confirmed = await confirmPrompt(`Role detected as ${import_picocolors13.default.bold(detectedRole)}. Is this correct?`);
13334
+ info(`Detected role: ${import_picocolors14.default.bold(detectedRole)} (via ${detectionSource})`);
13335
+ const confirmed = await confirmPrompt(`Role detected as ${import_picocolors14.default.bold(detectedRole)}. Is this correct?`);
12516
13336
  if (!confirmed) {
12517
13337
  const roleChoice = await selectPrompt("Select your role:", ["maintainer", "contributor"]);
12518
13338
  detectedRole = roleChoice;
12519
13339
  }
12520
13340
  }
12521
13341
  const defaultConfig = getDefaultConfig();
12522
- info(import_picocolors13.default.dim("Tip: press Enter to keep the default branch name shown in each prompt."));
13342
+ info(import_picocolors14.default.dim("Tip: press Enter to keep the default branch name shown in each prompt."));
12523
13343
  const mainBranchDefault = defaultConfig.mainBranch;
12524
13344
  const mainBranch = await inputPrompt(`Main branch name (default: ${mainBranchDefault} — press Enter to keep)`, mainBranchDefault);
12525
13345
  let devBranch;
@@ -12545,7 +13365,7 @@ var setup_default = defineCommand({
12545
13365
  error("Setup cannot continue without the upstream remote for contributors.");
12546
13366
  process.exit(1);
12547
13367
  }
12548
- success(`Added remote ${import_picocolors13.default.bold(upstreamRemote)} → ${upstreamUrl}`);
13368
+ success(`Added remote ${import_picocolors14.default.bold(upstreamRemote)} → ${upstreamUrl}`);
12549
13369
  } else {
12550
13370
  error("An upstream remote URL is required for contributors.");
12551
13371
  info("Add it manually: git remote add upstream <url>", "");
@@ -12561,54 +13381,58 @@ var setup_default = defineCommand({
12561
13381
  upstream: upstreamRemote,
12562
13382
  origin: originRemote,
12563
13383
  branchPrefixes: defaultConfig.branchPrefixes,
12564
- commitConvention
13384
+ commitConvention,
13385
+ aiEnabled: enableAI,
13386
+ showTips
12565
13387
  };
12566
13388
  writeConfig(config);
12567
- success(`Config written to .contributerc.json`);
13389
+ success(`Config written to ${import_picocolors14.default.bold(getConfigLocationLabel())}`);
13390
+ info("This setup is stored locally for this clone and does not modify tracked files.", "");
12568
13391
  const syncRemote = config.role === "contributor" ? config.upstream : config.origin;
12569
- info(`Fetching ${import_picocolors13.default.bold(syncRemote)} to verify branch configuration...`, "");
13392
+ info(`Fetching ${import_picocolors14.default.bold(syncRemote)} to verify branch configuration...`, "");
12570
13393
  await fetchRemote(syncRemote);
12571
13394
  const mainRef = `${syncRemote}/${config.mainBranch}`;
12572
13395
  if (!await refExists(mainRef)) {
12573
- warn(`Main branch ref ${import_picocolors13.default.bold(mainRef)} not found on remote.`);
13396
+ warn(`Main branch ref ${import_picocolors14.default.bold(mainRef)} not found on remote.`);
12574
13397
  warn("Config was saved — verify the branch name and re-run setup if needed.");
12575
13398
  }
12576
13399
  if (config.devBranch) {
12577
13400
  const devRef = `${syncRemote}/${config.devBranch}`;
12578
13401
  if (!await refExists(devRef)) {
12579
- warn(`Dev branch ref ${import_picocolors13.default.bold(devRef)} not found on remote.`);
13402
+ warn(`Dev branch ref ${import_picocolors14.default.bold(devRef)} not found on remote.`);
12580
13403
  warn("Config was saved — verify the branch name and re-run setup if needed.");
12581
13404
  }
12582
13405
  }
12583
- if (ensureGitignored()) {
12584
- info("Added .contributerc.json to .gitignore to avoid committing personal config.");
12585
- }
12586
13406
  console.log();
12587
- info(`Workflow: ${import_picocolors13.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
12588
- info(`Convention: ${import_picocolors13.default.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
12589
- info(`Role: ${import_picocolors13.default.bold(config.role)}`);
13407
+ info(`Workflow: ${import_picocolors14.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
13408
+ info(`Convention: ${import_picocolors14.default.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
13409
+ info(`AI: ${import_picocolors14.default.bold(isAIEnabled(config) ? "enabled" : "disabled")}`);
13410
+ info(`Guides: ${import_picocolors14.default.bold(shouldShowTips(config) ? "shown" : "hidden")}`);
13411
+ info(`Role: ${import_picocolors14.default.bold(config.role)}`);
12590
13412
  if (config.devBranch) {
12591
- info(`Main: ${import_picocolors13.default.bold(config.mainBranch)} | Dev: ${import_picocolors13.default.bold(config.devBranch)}`);
13413
+ info(`Main: ${import_picocolors14.default.bold(config.mainBranch)} | Dev: ${import_picocolors14.default.bold(config.devBranch)}`);
12592
13414
  } else {
12593
- info(`Main: ${import_picocolors13.default.bold(config.mainBranch)}`);
13415
+ info(`Main: ${import_picocolors14.default.bold(config.mainBranch)}`);
12594
13416
  }
12595
- info(`Origin: ${import_picocolors13.default.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${import_picocolors13.default.bold(config.upstream)}` : ""}`);
13417
+ info(`Origin: ${import_picocolors14.default.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${import_picocolors14.default.bold(config.upstream)}` : ""}`);
12596
13418
  }
12597
13419
  });
12598
13420
  function logConfigSummary(config) {
12599
- info(`Workflow: ${import_picocolors13.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
12600
- info(`Convention: ${import_picocolors13.default.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
12601
- info(`Role: ${import_picocolors13.default.bold(config.role)}`);
13421
+ info(`Workflow: ${import_picocolors14.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
13422
+ info(`Convention: ${import_picocolors14.default.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
13423
+ info(`AI: ${import_picocolors14.default.bold(isAIEnabled(config) ? "enabled" : "disabled")}`);
13424
+ info(`Guides: ${import_picocolors14.default.bold(shouldShowTips(config) ? "shown" : "hidden")}`);
13425
+ info(`Role: ${import_picocolors14.default.bold(config.role)}`);
12602
13426
  if (config.devBranch) {
12603
- info(`Main: ${import_picocolors13.default.bold(config.mainBranch)} | Dev: ${import_picocolors13.default.bold(config.devBranch)}`);
13427
+ info(`Main: ${import_picocolors14.default.bold(config.mainBranch)} | Dev: ${import_picocolors14.default.bold(config.devBranch)}`);
12604
13428
  } else {
12605
- info(`Main: ${import_picocolors13.default.bold(config.mainBranch)}`);
13429
+ info(`Main: ${import_picocolors14.default.bold(config.mainBranch)}`);
12606
13430
  }
12607
- info(`Origin: ${import_picocolors13.default.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${import_picocolors13.default.bold(config.upstream)}` : ""}`);
13431
+ info(`Origin: ${import_picocolors14.default.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${import_picocolors14.default.bold(config.upstream)}` : ""}`);
12608
13432
  }
12609
13433
 
12610
13434
  // src/commands/start.ts
12611
- var import_picocolors14 = __toESM(require_picocolors(), 1);
13435
+ var import_picocolors15 = __toESM(require_picocolors(), 1);
12612
13436
  var start_default = defineCommand({
12613
13437
  meta: {
12614
13438
  name: "start",
@@ -12638,7 +13462,7 @@ var start_default = defineCommand({
12638
13462
  await assertCleanGitState("starting a new branch");
12639
13463
  const config = readConfig();
12640
13464
  if (!config) {
12641
- error("No .contributerc.json found. Run `contrib setup` first.");
13465
+ error("No repo config found. Run `contrib setup` first.");
12642
13466
  process.exit(1);
12643
13467
  }
12644
13468
  if (await hasUncommittedChanges()) {
@@ -12648,57 +13472,28 @@ var start_default = defineCommand({
12648
13472
  const { branchPrefixes } = config;
12649
13473
  const baseBranch = getBaseBranch(config);
12650
13474
  const syncSource = getSyncSource(config);
12651
- let branchName = args.name;
12652
- heading("\uD83C\uDF3F contrib start");
13475
+ let branchName = args.name?.trim();
13476
+ projectHeading("start", "\uD83C\uDF3F");
13477
+ branchName = await promptForBranchName({
13478
+ initialValue: branchName,
13479
+ branchPrefixes,
13480
+ useAI: isAIEnabled(config, args["no-ai"]),
13481
+ model: args.model
13482
+ });
12653
13483
  if (!branchName) {
12654
- branchName = await inputPrompt("What are you going to work on?");
12655
- if (!branchName || branchName.trim().length === 0) {
12656
- error("A branch name or description is required.");
12657
- process.exit(1);
12658
- }
12659
- branchName = branchName.trim();
12660
- }
12661
- const useAI = !args["no-ai"] && looksLikeNaturalLanguage(branchName);
12662
- if (useAI) {
12663
- const spinner = createSpinner("Generating branch name suggestion...");
12664
- const suggested = await suggestBranchName(branchName, args.model);
12665
- if (suggested) {
12666
- spinner.success("Branch name suggestion ready.");
12667
- console.log(`
12668
- ${import_picocolors14.default.dim("AI suggestion:")} ${import_picocolors14.default.bold(import_picocolors14.default.cyan(suggested))}`);
12669
- const accepted = await confirmPrompt(`Use ${import_picocolors14.default.bold(suggested)} as your branch name?`);
12670
- if (accepted) {
12671
- branchName = suggested;
12672
- } else {
12673
- branchName = await inputPrompt("Enter branch name", branchName);
12674
- }
12675
- } else {
12676
- spinner.fail("AI did not return a branch name suggestion.");
12677
- }
12678
- }
12679
- if (!hasPrefix(branchName, branchPrefixes)) {
12680
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors14.default.bold(branchName)}:`, branchPrefixes);
12681
- branchName = formatBranchName(prefix, branchName);
12682
- }
12683
- if (!isValidBranchName(branchName)) {
12684
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
12685
- process.exit(1);
12686
- }
12687
- info(`Creating branch: ${import_picocolors14.default.bold(branchName)}`);
12688
- if (await branchExists(branchName)) {
12689
- error(`Branch ${import_picocolors14.default.bold(branchName)} already exists.`);
12690
- info(` Use ${import_picocolors14.default.bold(`git checkout ${branchName}`)} to switch to it, or choose a different name.`, "");
12691
- process.exit(1);
13484
+ warn("Start cancelled.");
13485
+ process.exit(0);
12692
13486
  }
13487
+ info(`Creating branch: ${import_picocolors15.default.bold(branchName)}`);
12693
13488
  await fetchRemote(syncSource.remote);
12694
13489
  if (!await refExists(syncSource.ref)) {
12695
- warn(`Remote ref ${import_picocolors14.default.bold(syncSource.ref)} not found. Creating branch from local ${import_picocolors14.default.bold(baseBranch)}.`);
13490
+ warn(`Remote ref ${import_picocolors15.default.bold(syncSource.ref)} not found. Creating branch from local ${import_picocolors15.default.bold(baseBranch)}.`);
12696
13491
  }
12697
13492
  const currentBranch = await getCurrentBranch();
12698
13493
  if (currentBranch === baseBranch && await refExists(syncSource.ref)) {
12699
13494
  const ahead = await countCommitsAhead(baseBranch, syncSource.ref);
12700
13495
  if (ahead > 0) {
12701
- warn(`You are on ${import_picocolors14.default.bold(baseBranch)} with ${import_picocolors14.default.bold(String(ahead))} local commit${ahead > 1 ? "s" : ""} not in ${import_picocolors14.default.bold(syncSource.ref)}.`);
13496
+ warn(`You are on ${import_picocolors15.default.bold(baseBranch)} with ${import_picocolors15.default.bold(String(ahead))} local commit${ahead > 1 ? "s" : ""} not in ${import_picocolors15.default.bold(syncSource.ref)}.`);
12702
13497
  info(" Syncing will discard those commits. Consider backing them up first (e.g. create a branch).");
12703
13498
  const proceed = await confirmPrompt("Discard local commits and sync to remote?");
12704
13499
  if (!proceed) {
@@ -12715,10 +13510,10 @@ var start_default = defineCommand({
12715
13510
  error(`Failed to create branch: ${result2.stderr}`);
12716
13511
  process.exit(1);
12717
13512
  }
12718
- success(`Created ${import_picocolors14.default.bold(branchName)} from ${import_picocolors14.default.bold(syncSource.ref)}`);
13513
+ success(`Created ${import_picocolors15.default.bold(branchName)} from ${import_picocolors15.default.bold(syncSource.ref)}`);
12719
13514
  return;
12720
13515
  }
12721
- error(`Failed to update ${import_picocolors14.default.bold(baseBranch)}: ${updateResult.stderr}`);
13516
+ error(`Failed to update ${import_picocolors15.default.bold(baseBranch)}: ${updateResult.stderr}`);
12722
13517
  info("Make sure your base branch exists locally or the remote ref is available.", "");
12723
13518
  process.exit(1);
12724
13519
  }
@@ -12727,12 +13522,12 @@ var start_default = defineCommand({
12727
13522
  error(`Failed to create branch: ${result.stderr}`);
12728
13523
  process.exit(1);
12729
13524
  }
12730
- success(`Created ${import_picocolors14.default.bold(branchName)} from latest ${import_picocolors14.default.bold(baseBranch)}`);
13525
+ success(`Created ${import_picocolors15.default.bold(branchName)} from latest ${import_picocolors15.default.bold(baseBranch)}`);
12731
13526
  }
12732
13527
  });
12733
13528
 
12734
13529
  // src/commands/status.ts
12735
- var import_picocolors15 = __toESM(require_picocolors(), 1);
13530
+ var import_picocolors16 = __toESM(require_picocolors(), 1);
12736
13531
  var status_default = defineCommand({
12737
13532
  meta: {
12738
13533
  name: "status",
@@ -12745,12 +13540,12 @@ var status_default = defineCommand({
12745
13540
  }
12746
13541
  const config = readConfig();
12747
13542
  if (!config) {
12748
- error("No .contributerc.json found. Run `contrib setup` first.");
13543
+ error("No repo config found. Run `contrib setup` first.");
12749
13544
  process.exit(1);
12750
13545
  }
12751
- heading("\uD83D\uDCCA contribute-now status");
12752
- console.log(` ${import_picocolors15.default.dim("Workflow:")} ${import_picocolors15.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
12753
- console.log(` ${import_picocolors15.default.dim("Role:")} ${import_picocolors15.default.bold(config.role)}`);
13546
+ projectHeading("status", "\uD83D\uDCCA");
13547
+ console.log(` ${import_picocolors16.default.dim("Workflow:")} ${import_picocolors16.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
13548
+ console.log(` ${import_picocolors16.default.dim("Role:")} ${import_picocolors16.default.bold(config.role)}`);
12754
13549
  console.log();
12755
13550
  await fetchAll();
12756
13551
  const currentBranch = await getCurrentBranch();
@@ -12759,7 +13554,7 @@ var status_default = defineCommand({
12759
13554
  const isContributor = config.role === "contributor";
12760
13555
  const [dirty, fileStatus] = await Promise.all([hasUncommittedChanges(), getFileStatus()]);
12761
13556
  if (dirty) {
12762
- console.log(` ${import_picocolors15.default.yellow("⚠")} ${import_picocolors15.default.yellow("Uncommitted changes in working tree")}`);
13557
+ console.log(` ${import_picocolors16.default.yellow("⚠")} ${import_picocolors16.default.yellow("Uncommitted changes in working tree")}`);
12763
13558
  console.log();
12764
13559
  }
12765
13560
  const mainRemote = `${origin}/${mainBranch}`;
@@ -12778,28 +13573,32 @@ var status_default = defineCommand({
12778
13573
  if (isFeatureBranch) {
12779
13574
  const branchDiv = await getDivergence(currentBranch, baseBranch);
12780
13575
  const branchLine = formatStatus(currentBranch, baseBranch, branchDiv.ahead, branchDiv.behind);
12781
- console.log(branchLine + import_picocolors15.default.dim(` (current ${import_picocolors15.default.green("*")})`));
13576
+ console.log(branchLine + import_picocolors16.default.dim(` (current ${import_picocolors16.default.green("*")})`));
12782
13577
  branchStatus = await detectBranchStatus(currentBranch, baseBranch);
12783
13578
  if (branchStatus.merged) {
12784
- console.log(` ${import_picocolors15.default.green("✓")} ${import_picocolors15.default.green("Branch merged")} — ${import_picocolors15.default.dim(branchStatus.mergedReason ?? "all commits reachable from base")}`);
13579
+ console.log(` ${import_picocolors16.default.green("✓")} ${import_picocolors16.default.green("Branch merged")} — ${import_picocolors16.default.dim(branchStatus.mergedReason ?? "all commits reachable from base")}`);
12785
13580
  }
12786
13581
  if (branchStatus.stale) {
12787
- console.log(` ${import_picocolors15.default.yellow("⏳")} ${import_picocolors15.default.yellow("Branch is stale")} — ${import_picocolors15.default.dim(`last commit ${branchStatus.staleDaysAgo} days ago`)}`);
13582
+ console.log(` ${import_picocolors16.default.yellow("⏳")} ${import_picocolors16.default.yellow("Branch is stale")} — ${import_picocolors16.default.dim(`last commit ${branchStatus.staleDaysAgo} days ago`)}`);
12788
13583
  }
12789
13584
  } else if (currentBranch) {
12790
- console.log(import_picocolors15.default.dim(` (on ${import_picocolors15.default.bold(currentBranch)} branch)`));
13585
+ console.log(import_picocolors16.default.dim(` (on ${import_picocolors16.default.bold(currentBranch)} branch)`));
12791
13586
  }
12792
13587
  let branchesAligned = true;
12793
13588
  {
12794
13589
  const alignRefs = [];
12795
13590
  const devRemote = isContributor ? upstream : origin;
13591
+ const devBranch = hasDevBranch(workflow) ? config.devBranch : undefined;
12796
13592
  const hashResults = await Promise.all([
12797
13593
  getCommitHash(mainBranch).then((h2) => ({ name: mainBranch, hash: h2 })),
12798
- getCommitHash(`${origin}/${mainBranch}`).then((h2) => ({ name: `${origin}/${mainBranch}`, hash: h2 })),
12799
- ...hasDevBranch(workflow) && config.devBranch ? [
12800
- getCommitHash(config.devBranch).then((h2) => ({ name: config.devBranch, hash: h2 })),
12801
- getCommitHash(`${devRemote}/${config.devBranch}`).then((h2) => ({
12802
- name: `${devRemote}/${config.devBranch}`,
13594
+ getCommitHash(`${origin}/${mainBranch}`).then((h2) => ({
13595
+ name: `${origin}/${mainBranch}`,
13596
+ hash: h2
13597
+ })),
13598
+ ...devBranch ? [
13599
+ getCommitHash(devBranch).then((h2) => ({ name: devBranch, hash: h2 })),
13600
+ getCommitHash(`${devRemote}/${devBranch}`).then((h2) => ({
13601
+ name: `${devRemote}/${devBranch}`,
12803
13602
  hash: h2
12804
13603
  }))
12805
13604
  ] : []
@@ -12813,24 +13612,27 @@ var status_default = defineCommand({
12813
13612
  for (const { name, hash } of alignRefs) {
12814
13613
  if (!groups.has(hash))
12815
13614
  groups.set(hash, []);
12816
- groups.get(hash).push(name);
13615
+ const group = groups.get(hash);
13616
+ if (group) {
13617
+ group.push(name);
13618
+ }
12817
13619
  }
12818
13620
  branchesAligned = groups.size === 1;
12819
13621
  console.log();
12820
- console.log(` ${import_picocolors15.default.bold("\uD83D\uDD17 Branch Alignment")}`);
13622
+ console.log(` ${import_picocolors16.default.bold("\uD83D\uDD17 Branch Alignment")}`);
12821
13623
  for (const [hash, names] of groups) {
12822
13624
  const short = hash.slice(0, 7);
12823
- const nameStr = names.map((n2) => import_picocolors15.default.bold(n2)).join(import_picocolors15.default.dim(" · "));
12824
- console.log(` ${import_picocolors15.default.yellow(short)} ${import_picocolors15.default.dim("──")} ${nameStr}`);
13625
+ const nameStr = names.map((n2) => import_picocolors16.default.bold(n2)).join(import_picocolors16.default.dim(" · "));
13626
+ console.log(` ${import_picocolors16.default.yellow(short)} ${import_picocolors16.default.dim("──")} ${nameStr}`);
12825
13627
  const subject = await getCommitSubject(hash);
12826
13628
  if (subject) {
12827
- console.log(` ${import_picocolors15.default.dim(subject)}`);
13629
+ console.log(` ${import_picocolors16.default.dim(subject)}`);
12828
13630
  }
12829
13631
  }
12830
13632
  if (branchesAligned) {
12831
- console.log(` ${import_picocolors15.default.green("✓")} ${import_picocolors15.default.green("All branches aligned")} ${import_picocolors15.default.dim("— ready to start")}`);
13633
+ console.log(` ${import_picocolors16.default.green("✓")} ${import_picocolors16.default.green("All branches aligned")} ${import_picocolors16.default.dim("— ready to start")}`);
12832
13634
  } else {
12833
- console.log(` ${import_picocolors15.default.yellow("⚠")} ${import_picocolors15.default.yellow("Branches are not fully aligned")}`);
13635
+ console.log(` ${import_picocolors16.default.yellow("⚠")} ${import_picocolors16.default.yellow("Branches are not fully aligned")}`);
12834
13636
  }
12835
13637
  }
12836
13638
  }
@@ -12838,74 +13640,50 @@ var status_default = defineCommand({
12838
13640
  if (hasFiles) {
12839
13641
  console.log();
12840
13642
  if (fileStatus.staged.length > 0) {
12841
- console.log(` ${import_picocolors15.default.green("Staged for commit:")}`);
13643
+ console.log(` ${import_picocolors16.default.green("Staged for commit:")}`);
12842
13644
  for (const { file, status } of fileStatus.staged) {
12843
- console.log(` ${import_picocolors15.default.green("+")} ${import_picocolors15.default.dim(`${status}:`)} ${file}`);
13645
+ console.log(` ${import_picocolors16.default.green("+")} ${import_picocolors16.default.dim(`${status}:`)} ${file}`);
12844
13646
  }
12845
13647
  }
12846
13648
  if (fileStatus.modified.length > 0) {
12847
- console.log(` ${import_picocolors15.default.yellow("Unstaged changes:")}`);
13649
+ console.log(` ${import_picocolors16.default.yellow("Unstaged changes:")}`);
12848
13650
  for (const { file, status } of fileStatus.modified) {
12849
- console.log(` ${import_picocolors15.default.yellow("~")} ${import_picocolors15.default.dim(`${status}:`)} ${file}`);
13651
+ console.log(` ${import_picocolors16.default.yellow("~")} ${import_picocolors16.default.dim(`${status}:`)} ${file}`);
12850
13652
  }
12851
13653
  }
12852
13654
  if (fileStatus.untracked.length > 0) {
12853
- console.log(` ${import_picocolors15.default.red("Untracked files:")}`);
13655
+ console.log(` ${import_picocolors16.default.red("Untracked files:")}`);
12854
13656
  for (const file of fileStatus.untracked) {
12855
- console.log(` ${import_picocolors15.default.red("?")} ${file}`);
13657
+ console.log(` ${import_picocolors16.default.red("?")} ${file}`);
12856
13658
  }
12857
13659
  }
12858
13660
  } else if (!dirty) {
12859
- console.log(` ${import_picocolors15.default.green("✓")} ${import_picocolors15.default.dim("Working tree clean")}`);
12860
- }
12861
- const tips = [];
12862
- if (!branchesAligned) {
12863
- tips.push(`Run ${import_picocolors15.default.bold("contrib sync")} to align your local branches with the remote`);
12864
- }
12865
- if (fileStatus.staged.length > 0) {
12866
- tips.push(`Run ${import_picocolors15.default.bold("contrib commit")} to commit staged changes`);
12867
- }
12868
- if (fileStatus.modified.length > 0 || fileStatus.untracked.length > 0) {
12869
- tips.push(`Run ${import_picocolors15.default.bold("contrib commit")} to stage and commit changes`);
12870
- }
12871
- if (isFeatureBranch && branchStatus) {
12872
- if (branchStatus.merged) {
12873
- tips.push(`Run ${import_picocolors15.default.bold("contrib clean")} to delete this merged branch`);
12874
- } else if (branchStatus.stale) {
12875
- tips.push(`Run ${import_picocolors15.default.bold("contrib sync")} to rebase on latest changes, or ${import_picocolors15.default.bold("contrib clean")} if no longer needed`);
12876
- } else if (fileStatus.staged.length === 0 && fileStatus.modified.length === 0 && fileStatus.untracked.length === 0) {
12877
- const branchDiv = await getDivergence(currentBranch, `${origin}/${currentBranch}`);
12878
- if (branchDiv.ahead > 0) {
12879
- tips.push(`Run ${import_picocolors15.default.bold("contrib submit")} to push and create/update your PR`);
12880
- }
12881
- }
12882
- }
12883
- if (tips.length > 0) {
12884
- console.log();
12885
- console.log(` ${import_picocolors15.default.dim("\uD83D\uDCA1 Tip:")}`);
12886
- for (const tip of tips) {
12887
- console.log(` ${import_picocolors15.default.dim(tip)}`);
12888
- }
13661
+ console.log(` ${import_picocolors16.default.green("✓")} ${import_picocolors16.default.dim("Working tree clean")}`);
12889
13662
  }
12890
13663
  console.log();
12891
13664
  }
12892
13665
  });
12893
13666
  function formatStatus(branch, base, ahead, behind) {
12894
- const label = import_picocolors15.default.bold(branch.padEnd(20));
13667
+ const label = import_picocolors16.default.bold(branch.padEnd(20));
12895
13668
  if (ahead === 0 && behind === 0) {
12896
- return ` ${import_picocolors15.default.green("✓")} ${label} ${import_picocolors15.default.dim(`in sync with ${base}`)}`;
13669
+ return ` ${import_picocolors16.default.green("✓")} ${label} ${import_picocolors16.default.dim(`in sync with ${base}`)}`;
12897
13670
  }
12898
13671
  if (ahead > 0 && behind === 0) {
12899
- return ` ${import_picocolors15.default.yellow("↑")} ${label} ${import_picocolors15.default.yellow(`${ahead} commit${ahead !== 1 ? "s" : ""} ahead of ${base}`)}`;
13672
+ return ` ${import_picocolors16.default.yellow("↑")} ${label} ${import_picocolors16.default.yellow(`${ahead} commit${ahead !== 1 ? "s" : ""} ahead of ${base}`)}`;
12900
13673
  }
12901
13674
  if (behind > 0 && ahead === 0) {
12902
- return ` ${import_picocolors15.default.red("↓")} ${label} ${import_picocolors15.default.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
13675
+ return ` ${import_picocolors16.default.red("↓")} ${label} ${import_picocolors16.default.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
12903
13676
  }
12904
- return ` ${import_picocolors15.default.red("⚡")} ${label} ${import_picocolors15.default.yellow(`${ahead} ahead`)}${import_picocolors15.default.dim(", ")}${import_picocolors15.default.red(`${behind} behind`)} ${import_picocolors15.default.dim(base)}`;
13677
+ return ` ${import_picocolors16.default.red("⚡")} ${label} ${import_picocolors16.default.yellow(`${ahead} ahead`)}${import_picocolors16.default.dim(", ")}${import_picocolors16.default.red(`${behind} behind`)} ${import_picocolors16.default.dim(base)}`;
12905
13678
  }
12906
13679
  var STALE_THRESHOLD_DAYS = 14;
12907
13680
  async function detectBranchStatus(branch, baseBranch) {
12908
- const result = { merged: false, mergedReason: null, stale: false, staleDaysAgo: null };
13681
+ const result = {
13682
+ merged: false,
13683
+ mergedReason: null,
13684
+ stale: false,
13685
+ staleDaysAgo: null
13686
+ };
12909
13687
  const div = await getDivergence(branch, baseBranch);
12910
13688
  const hasWork = div.ahead > 0;
12911
13689
  if (hasWork) {
@@ -12947,34 +13725,47 @@ async function detectBranchStatus(branch, baseBranch) {
12947
13725
  }
12948
13726
 
12949
13727
  // src/commands/submit.ts
12950
- var import_picocolors16 = __toESM(require_picocolors(), 1);
13728
+ var import_picocolors17 = __toESM(require_picocolors(), 1);
12951
13729
  async function performSquashMerge(origin, baseBranch, featureBranch, options) {
12952
- info(`Checking out ${import_picocolors16.default.bold(baseBranch)}...`);
13730
+ info(`Checking out ${import_picocolors17.default.bold(baseBranch)}...`);
12953
13731
  const coResult = await checkoutBranch(baseBranch);
12954
13732
  if (coResult.exitCode !== 0) {
12955
13733
  error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
12956
13734
  process.exit(1);
12957
13735
  }
12958
- info(`Squash merging ${import_picocolors16.default.bold(featureBranch)} into ${import_picocolors16.default.bold(baseBranch)}...`);
13736
+ info(`Squash merging ${import_picocolors17.default.bold(featureBranch)} into ${import_picocolors17.default.bold(baseBranch)}...`);
12959
13737
  const mergeResult = await mergeSquash(featureBranch);
12960
13738
  if (mergeResult.exitCode !== 0) {
12961
13739
  error(`Squash merge failed: ${mergeResult.stderr}`);
12962
13740
  process.exit(1);
12963
13741
  }
12964
13742
  let message = options?.defaultMsg;
12965
- if (!message) {
12966
- const copilotError = await checkCopilotAvailable();
13743
+ if (!message && options?.useAI !== false) {
13744
+ const copilotError = await checkCopilotAvailable2();
12967
13745
  if (!copilotError) {
12968
- const spinner = createSpinner("Generating AI commit message for squash merge...");
12969
- const [stagedDiff, stagedFiles] = await Promise.all([getStagedDiff(), getStagedFiles()]);
12970
- const aiMsg = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit", "squash-merge");
12971
- if (aiMsg) {
12972
- message = aiMsg;
12973
- spinner.success("AI commit message generated.");
12974
- console.log(`
12975
- ${import_picocolors16.default.dim("AI suggestion:")} ${import_picocolors16.default.bold(import_picocolors16.default.cyan(message))}`);
12976
- } else {
13746
+ while (!message) {
13747
+ const spinner = createSpinner("Generating AI commit message for squash merge...", {
13748
+ tips: LOADING_TIPS
13749
+ });
13750
+ const [stagedDiff, stagedFiles] = await Promise.all([getStagedDiff(), getStagedFiles()]);
13751
+ const aiMsg = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit", "squash-merge");
13752
+ if (aiMsg) {
13753
+ message = aiMsg;
13754
+ spinner.success("AI commit message generated.");
13755
+ console.log(`
13756
+ ${import_picocolors17.default.dim("AI suggestion:")} ${import_picocolors17.default.bold(import_picocolors17.default.cyan(message))}`);
13757
+ break;
13758
+ }
12977
13759
  spinner.fail("AI did not return a commit message.");
13760
+ const retryAction = await selectPrompt("AI could not generate a commit message. What would you like to do?", ["Try again with AI", "Write manually", "Cancel"]);
13761
+ if (retryAction === "Try again with AI") {
13762
+ continue;
13763
+ }
13764
+ if (retryAction === "Cancel") {
13765
+ warn("Squash merge commit cancelled.");
13766
+ process.exit(0);
13767
+ }
13768
+ break;
12978
13769
  }
12979
13770
  } else {
12980
13771
  warn(`AI unavailable: ${copilotError}`);
@@ -12983,28 +13774,28 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
12983
13774
  let finalMsg = null;
12984
13775
  if (message) {
12985
13776
  while (!finalMsg) {
12986
- const action = await selectPrompt("What would you like to do?", [
12987
- "Accept this message",
12988
- "Edit this message",
12989
- "Regenerate",
12990
- "Write manually"
12991
- ]);
13777
+ const actions = ["Accept this message", "Edit this message", "Write manually"];
13778
+ if (options?.useAI !== false) {
13779
+ actions.splice(2, 0, "Regenerate");
13780
+ }
13781
+ const action = await selectPrompt("What would you like to do?", actions);
12992
13782
  if (action === "Accept this message") {
12993
13783
  finalMsg = message;
12994
13784
  } else if (action === "Edit this message") {
12995
13785
  finalMsg = await inputPrompt("Edit commit message", message);
12996
13786
  } else if (action === "Regenerate") {
12997
- const spinner = createSpinner("Regenerating commit message...");
13787
+ const spinner = createSpinner("Regenerating commit message...", {
13788
+ tips: LOADING_TIPS
13789
+ });
12998
13790
  const [stagedDiff, stagedFiles] = await Promise.all([getStagedDiff(), getStagedFiles()]);
12999
13791
  const regen = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit", "squash-merge");
13000
13792
  if (regen) {
13001
13793
  message = regen;
13002
13794
  spinner.success("Commit message regenerated.");
13003
13795
  console.log(`
13004
- ${import_picocolors16.default.dim("AI suggestion:")} ${import_picocolors16.default.bold(import_picocolors16.default.cyan(regen))}`);
13796
+ ${import_picocolors17.default.dim("AI suggestion:")} ${import_picocolors17.default.bold(import_picocolors17.default.cyan(regen))}`);
13005
13797
  } else {
13006
13798
  spinner.fail("Regeneration failed.");
13007
- finalMsg = await inputPrompt("Enter commit message");
13008
13799
  }
13009
13800
  } else {
13010
13801
  finalMsg = await inputPrompt("Enter commit message");
@@ -13018,13 +13809,13 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
13018
13809
  error(`Commit failed: ${commitResult.stderr}`);
13019
13810
  process.exit(1);
13020
13811
  }
13021
- info(`Pushing ${import_picocolors16.default.bold(baseBranch)} to ${origin}...`);
13812
+ info(`Pushing ${import_picocolors17.default.bold(baseBranch)} to ${origin}...`);
13022
13813
  const pushResult = await pushBranch(origin, baseBranch);
13023
13814
  if (pushResult.exitCode !== 0) {
13024
13815
  error(`Failed to push ${baseBranch}: ${pushResult.stderr}`);
13025
13816
  process.exit(1);
13026
13817
  }
13027
- info(`Deleting local branch ${import_picocolors16.default.bold(featureBranch)}...`);
13818
+ info(`Deleting local branch ${import_picocolors17.default.bold(featureBranch)}...`);
13028
13819
  const delLocal = await forceDeleteBranch(featureBranch);
13029
13820
  if (delLocal.exitCode !== 0) {
13030
13821
  warn(`Could not delete local branch: ${delLocal.stderr.trim()}`);
@@ -13032,14 +13823,14 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
13032
13823
  const remoteBranchRef = `${origin}/${featureBranch}`;
13033
13824
  const remoteExists = await branchExists(remoteBranchRef);
13034
13825
  if (remoteExists) {
13035
- info(`Deleting remote branch ${import_picocolors16.default.bold(featureBranch)}...`);
13826
+ info(`Deleting remote branch ${import_picocolors17.default.bold(featureBranch)}...`);
13036
13827
  const delRemote = await deleteRemoteBranch(origin, featureBranch);
13037
13828
  if (delRemote.exitCode !== 0) {
13038
13829
  warn(`Could not delete remote branch: ${delRemote.stderr.trim()}`);
13039
13830
  }
13040
13831
  }
13041
- success(`Squash merged ${import_picocolors16.default.bold(featureBranch)} into ${import_picocolors16.default.bold(baseBranch)} and pushed.`);
13042
- info(`Run ${import_picocolors16.default.bold("contrib start")} to begin a new feature.`, "");
13832
+ success(`Squash merged ${import_picocolors17.default.bold(featureBranch)} into ${import_picocolors17.default.bold(baseBranch)} and pushed.`);
13833
+ info(`Run ${import_picocolors17.default.bold("contrib start")} to begin a new feature.`, "");
13043
13834
  }
13044
13835
  var submit_default = defineCommand({
13045
13836
  meta: {
@@ -13052,6 +13843,18 @@ var submit_default = defineCommand({
13052
13843
  description: "Create PR as draft",
13053
13844
  default: false
13054
13845
  },
13846
+ pullrequest: {
13847
+ type: "boolean",
13848
+ alias: "pr",
13849
+ description: "Submit directly to PR flow without prompting for mode",
13850
+ default: false
13851
+ },
13852
+ local: {
13853
+ type: "boolean",
13854
+ alias: "l",
13855
+ description: "Squash merge locally without PR (maintainers only)",
13856
+ default: false
13857
+ },
13055
13858
  "no-ai": {
13056
13859
  type: "boolean",
13057
13860
  description: "Skip AI PR description generation",
@@ -13070,10 +13873,11 @@ var submit_default = defineCommand({
13070
13873
  await assertCleanGitState("submitting");
13071
13874
  const config = readConfig();
13072
13875
  if (!config) {
13073
- error("No .contributerc.json found. Run `contrib setup` first.");
13876
+ error("No repo config found. Run `contrib setup` first.");
13074
13877
  process.exit(1);
13075
13878
  }
13076
13879
  const { origin } = config;
13880
+ const aiEnabled = isAIEnabled(config, args["no-ai"]);
13077
13881
  const baseBranch = getBaseBranch(config);
13078
13882
  const protectedBranches = getProtectedBranches(config);
13079
13883
  const currentBranch = await getCurrentBranch();
@@ -13082,8 +13886,8 @@ var submit_default = defineCommand({
13082
13886
  process.exit(1);
13083
13887
  }
13084
13888
  if (protectedBranches.includes(currentBranch)) {
13085
- heading("\uD83D\uDE80 contrib submit");
13086
- warn(`You're on ${import_picocolors16.default.bold(currentBranch)}, which is a protected branch. PRs should come from feature branches.`);
13889
+ projectHeading("submit", "\uD83D\uDE80");
13890
+ warn(`You're on ${import_picocolors17.default.bold(currentBranch)}, which is a protected branch. PRs should come from feature branches.`);
13087
13891
  await fetchAll();
13088
13892
  const remoteRef = `${origin}/${currentBranch}`;
13089
13893
  const localWork = await hasLocalWork(origin, currentBranch);
@@ -13092,11 +13896,11 @@ var submit_default = defineCommand({
13092
13896
  const hasAnything = hasCommits || dirty;
13093
13897
  if (!hasAnything) {
13094
13898
  error("No local changes or commits to move. Switch to a feature branch first.");
13095
- info(` Run ${import_picocolors16.default.bold("contrib start")} to create a new feature branch.`, "");
13899
+ info(` Run ${import_picocolors17.default.bold("contrib start")} to create a new feature branch.`, "");
13096
13900
  process.exit(1);
13097
13901
  }
13098
13902
  if (hasCommits) {
13099
- info(`Found ${import_picocolors16.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${import_picocolors16.default.bold(currentBranch)}.`);
13903
+ info(`Found ${import_picocolors17.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${import_picocolors17.default.bold(currentBranch)}.`);
13100
13904
  }
13101
13905
  if (dirty) {
13102
13906
  info("You also have uncommitted changes in the working tree.");
@@ -13112,58 +13916,35 @@ var submit_default = defineCommand({
13112
13916
  info("No changes made. You are still on your current branch.");
13113
13917
  return;
13114
13918
  }
13115
- info(import_picocolors16.default.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
13116
- const description = await inputPrompt("What are you going to work on?");
13117
- let newBranchName = description;
13118
- if (looksLikeNaturalLanguage(description)) {
13119
- const copilotError = await checkCopilotAvailable();
13120
- if (!copilotError) {
13121
- const spinner = createSpinner("Generating branch name suggestion...");
13122
- const suggested = await suggestBranchName(description, args.model);
13123
- if (suggested) {
13124
- spinner.success("Branch name suggestion ready.");
13125
- console.log(`
13126
- ${import_picocolors16.default.dim("AI suggestion:")} ${import_picocolors16.default.bold(import_picocolors16.default.cyan(suggested))}`);
13127
- const accepted = await confirmPrompt(`Use ${import_picocolors16.default.bold(suggested)} as your branch name?`);
13128
- newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
13129
- } else {
13130
- spinner.fail("AI did not return a suggestion.");
13131
- newBranchName = await inputPrompt("Enter branch name", description);
13132
- }
13133
- }
13134
- }
13135
- if (!hasPrefix(newBranchName, config.branchPrefixes)) {
13136
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors16.default.bold(newBranchName)}:`, config.branchPrefixes);
13137
- newBranchName = formatBranchName(prefix, newBranchName);
13138
- }
13139
- if (!isValidBranchName(newBranchName)) {
13140
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
13141
- process.exit(1);
13142
- }
13143
- if (await branchExists(newBranchName)) {
13144
- error(`Branch ${import_picocolors16.default.bold(newBranchName)} already exists. Choose a different name.`);
13145
- process.exit(1);
13919
+ const newBranchName = await promptForBranchName({
13920
+ branchPrefixes: config.branchPrefixes,
13921
+ useAI: aiEnabled,
13922
+ model: args.model
13923
+ });
13924
+ if (!newBranchName) {
13925
+ info("No changes made. You are still on your current branch.");
13926
+ return;
13146
13927
  }
13147
13928
  const branchResult = await createBranch(newBranchName);
13148
13929
  if (branchResult.exitCode !== 0) {
13149
13930
  error(`Failed to create branch: ${branchResult.stderr}`);
13150
13931
  process.exit(1);
13151
13932
  }
13152
- success(`Created ${import_picocolors16.default.bold(newBranchName)} with your changes.`);
13933
+ success(`Created ${import_picocolors17.default.bold(newBranchName)} with your changes.`);
13153
13934
  await updateLocalBranch(currentBranch, remoteRef);
13154
- info(`Reset ${import_picocolors16.default.bold(currentBranch)} back to ${import_picocolors16.default.bold(remoteRef)} — no damage done.`, "");
13935
+ info(`Reset ${import_picocolors17.default.bold(currentBranch)} back to ${import_picocolors17.default.bold(remoteRef)} — no damage done.`, "");
13155
13936
  console.log();
13156
- success(`You're now on ${import_picocolors16.default.bold(newBranchName)} with all your work intact.`);
13157
- info(`Run ${import_picocolors16.default.bold("contrib submit")} again to push and create your PR.`, "");
13937
+ success(`You're now on ${import_picocolors17.default.bold(newBranchName)} with all your work intact.`);
13938
+ info(`Run ${import_picocolors17.default.bold("contrib submit")} again to push and create your PR.`, "");
13158
13939
  return;
13159
13940
  }
13160
- heading("\uD83D\uDE80 contrib submit");
13941
+ projectHeading("submit", "\uD83D\uDE80");
13161
13942
  const ghInstalled = await checkGhInstalled();
13162
13943
  const ghAuthed = ghInstalled && await checkGhAuth();
13163
13944
  if (ghInstalled && ghAuthed) {
13164
13945
  const mergedPR = await getMergedPRForBranch(currentBranch);
13165
13946
  if (mergedPR) {
13166
- warn(`PR #${mergedPR.number} (${import_picocolors16.default.bold(mergedPR.title)}) was already merged.`);
13947
+ warn(`PR #${mergedPR.number} (${import_picocolors17.default.bold(mergedPR.title)}) was already merged.`);
13167
13948
  const localWork = await hasLocalWork(origin, currentBranch);
13168
13949
  const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
13169
13950
  if (hasWork) {
@@ -13171,7 +13952,7 @@ var submit_default = defineCommand({
13171
13952
  warn("You have uncommitted changes in your working tree.");
13172
13953
  }
13173
13954
  if (localWork.unpushedCommits > 0) {
13174
- warn(`You have ${import_picocolors16.default.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not in the merged PR.`);
13955
+ warn(`You have ${import_picocolors17.default.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not in the merged PR.`);
13175
13956
  }
13176
13957
  const SAVE_NEW_BRANCH = "Save changes to a new branch";
13177
13958
  const DISCARD = "Discard all changes and clean up";
@@ -13182,46 +13963,26 @@ var submit_default = defineCommand({
13182
13963
  return;
13183
13964
  }
13184
13965
  if (action === SAVE_NEW_BRANCH) {
13185
- info(import_picocolors16.default.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
13186
- const description = await inputPrompt("What are you going to work on?");
13187
- let newBranchName = description;
13188
- if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
13189
- const spinner = createSpinner("Generating branch name suggestion...");
13190
- const suggested = await suggestBranchName(description, args.model);
13191
- if (suggested) {
13192
- spinner.success("Branch name suggestion ready.");
13193
- console.log(`
13194
- ${import_picocolors16.default.dim("AI suggestion:")} ${import_picocolors16.default.bold(import_picocolors16.default.cyan(suggested))}`);
13195
- const accepted = await confirmPrompt(`Use ${import_picocolors16.default.bold(suggested)} as your branch name?`);
13196
- newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
13197
- } else {
13198
- spinner.fail("AI did not return a suggestion.");
13199
- newBranchName = await inputPrompt("Enter branch name", description);
13200
- }
13201
- }
13202
- if (!hasPrefix(newBranchName, config.branchPrefixes)) {
13203
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors16.default.bold(newBranchName)}:`, config.branchPrefixes);
13204
- newBranchName = formatBranchName(prefix, newBranchName);
13205
- }
13206
- if (!isValidBranchName(newBranchName)) {
13207
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
13208
- process.exit(1);
13966
+ const newBranchName = await promptForBranchName({
13967
+ branchPrefixes: config.branchPrefixes,
13968
+ useAI: aiEnabled,
13969
+ model: args.model
13970
+ });
13971
+ if (!newBranchName) {
13972
+ info("No changes made. You are still on your current branch.");
13973
+ return;
13209
13974
  }
13210
13975
  const staleUpstream = await getUpstreamRef();
13211
13976
  const staleUpstreamHash = staleUpstream ? await getCommitHash(staleUpstream) : null;
13212
- if (await branchExists(newBranchName)) {
13213
- error(`Branch ${import_picocolors16.default.bold(newBranchName)} already exists. Choose a different name.`);
13214
- process.exit(1);
13215
- }
13216
13977
  const renameResult = await renameBranch(currentBranch, newBranchName);
13217
13978
  if (renameResult.exitCode !== 0) {
13218
13979
  error(`Failed to rename branch: ${renameResult.stderr}`);
13219
13980
  process.exit(1);
13220
13981
  }
13221
- success(`Renamed ${import_picocolors16.default.bold(currentBranch)} → ${import_picocolors16.default.bold(newBranchName)}`);
13982
+ success(`Renamed ${import_picocolors17.default.bold(currentBranch)} → ${import_picocolors17.default.bold(newBranchName)}`);
13222
13983
  await unsetUpstream();
13223
13984
  const syncSource2 = getSyncSource(config);
13224
- info(`Syncing ${import_picocolors16.default.bold(newBranchName)} with latest ${import_picocolors16.default.bold(baseBranch)}...`);
13985
+ info(`Syncing ${import_picocolors17.default.bold(newBranchName)} with latest ${import_picocolors17.default.bold(baseBranch)}...`);
13225
13986
  await fetchRemote(syncSource2.remote);
13226
13987
  let rebaseResult;
13227
13988
  if (staleUpstreamHash) {
@@ -13232,17 +13993,17 @@ var submit_default = defineCommand({
13232
13993
  }
13233
13994
  if (rebaseResult.exitCode !== 0) {
13234
13995
  warn("Rebase encountered conflicts. Resolve them manually, then run:");
13235
- info(` ${import_picocolors16.default.bold("git rebase --continue")}`, "");
13996
+ info(` ${import_picocolors17.default.bold("git rebase --continue")}`, "");
13236
13997
  } else {
13237
- success(`Rebased ${import_picocolors16.default.bold(newBranchName)} onto ${import_picocolors16.default.bold(syncSource2.ref)}.`);
13998
+ success(`Rebased ${import_picocolors17.default.bold(newBranchName)} onto ${import_picocolors17.default.bold(syncSource2.ref)}.`);
13238
13999
  }
13239
- info(`All your changes are preserved. Run ${import_picocolors16.default.bold("contrib submit")} when ready to create a new PR.`, "");
14000
+ info(`All your changes are preserved. Run ${import_picocolors17.default.bold("contrib submit")} when ready to create a new PR.`, "");
13240
14001
  return;
13241
14002
  }
13242
14003
  warn("Discarding local changes...");
13243
14004
  }
13244
14005
  const syncSource = getSyncSource(config);
13245
- info(`Switching to ${import_picocolors16.default.bold(baseBranch)} and syncing...`);
14006
+ info(`Switching to ${import_picocolors17.default.bold(baseBranch)} and syncing...`);
13246
14007
  await fetchRemote(syncSource.remote);
13247
14008
  await resetHard("HEAD");
13248
14009
  const coResult = await checkoutBranch(baseBranch);
@@ -13251,23 +14012,23 @@ var submit_default = defineCommand({
13251
14012
  process.exit(1);
13252
14013
  }
13253
14014
  await updateLocalBranch(baseBranch, syncSource.ref);
13254
- success(`Synced ${import_picocolors16.default.bold(baseBranch)} with ${import_picocolors16.default.bold(syncSource.ref)}.`);
13255
- info(`Deleting stale branch ${import_picocolors16.default.bold(currentBranch)}...`);
14015
+ success(`Synced ${import_picocolors17.default.bold(baseBranch)} with ${import_picocolors17.default.bold(syncSource.ref)}.`);
14016
+ info(`Deleting stale branch ${import_picocolors17.default.bold(currentBranch)}...`);
13256
14017
  const delResult = await forceDeleteBranch(currentBranch);
13257
14018
  if (delResult.exitCode === 0) {
13258
- success(`Deleted ${import_picocolors16.default.bold(currentBranch)}.`);
14019
+ success(`Deleted ${import_picocolors17.default.bold(currentBranch)}.`);
13259
14020
  } else {
13260
14021
  warn(`Could not delete branch: ${delResult.stderr.trim()}`);
13261
14022
  }
13262
14023
  console.log();
13263
- info(`You're now on ${import_picocolors16.default.bold(baseBranch)}. Run ${import_picocolors16.default.bold("contrib start")} to begin a new feature.`);
14024
+ info(`You're now on ${import_picocolors17.default.bold(baseBranch)}. Run ${import_picocolors17.default.bold("contrib start")} to begin a new feature.`);
13264
14025
  return;
13265
14026
  }
13266
14027
  }
13267
14028
  if (ghInstalled && ghAuthed) {
13268
14029
  const existingPR = await getPRForBranch(currentBranch);
13269
14030
  if (existingPR) {
13270
- info(`Pushing ${import_picocolors16.default.bold(currentBranch)} to ${origin}...`);
14031
+ info(`Pushing ${import_picocolors17.default.bold(currentBranch)} to ${origin}...`);
13271
14032
  const pushResult2 = await pushSetUpstream(origin, currentBranch);
13272
14033
  if (pushResult2.exitCode !== 0) {
13273
14034
  error(`Failed to push: ${pushResult2.stderr}`);
@@ -13278,8 +14039,8 @@ var submit_default = defineCommand({
13278
14039
  }
13279
14040
  process.exit(1);
13280
14041
  }
13281
- success(`Pushed changes to existing PR #${existingPR.number}: ${import_picocolors16.default.bold(existingPR.title)}`);
13282
- console.log(` ${import_picocolors16.default.cyan(existingPR.url)}`);
14042
+ success(`Pushed changes to existing PR #${existingPR.number}: ${import_picocolors17.default.bold(existingPR.title)}`);
14043
+ console.log(` ${import_picocolors17.default.cyan(existingPR.url)}`);
13283
14044
  return;
13284
14045
  }
13285
14046
  }
@@ -13287,22 +14048,24 @@ var submit_default = defineCommand({
13287
14048
  let prBody = null;
13288
14049
  async function tryGenerateAI() {
13289
14050
  const [copilotError, commits, diff] = await Promise.all([
13290
- checkCopilotAvailable(),
14051
+ checkCopilotAvailable2(),
13291
14052
  getLog(baseBranch, "HEAD"),
13292
14053
  getLogDiff(baseBranch, "HEAD")
13293
14054
  ]);
13294
14055
  if (!copilotError) {
13295
- const spinner = createSpinner("Generating AI PR description...");
14056
+ const spinner = createSpinner("Generating AI PR description...", {
14057
+ tips: LOADING_TIPS
14058
+ });
13296
14059
  const result = await generatePRDescription(commits, diff, args.model, config.commitConvention);
13297
14060
  if (result) {
13298
14061
  prTitle = result.title;
13299
14062
  prBody = result.body;
13300
14063
  spinner.success("PR description generated.");
13301
14064
  console.log(`
13302
- ${import_picocolors16.default.dim("AI title:")} ${import_picocolors16.default.bold(import_picocolors16.default.cyan(prTitle))}`);
14065
+ ${import_picocolors17.default.dim("AI title:")} ${import_picocolors17.default.bold(import_picocolors17.default.cyan(prTitle))}`);
13303
14066
  console.log(`
13304
- ${import_picocolors16.default.dim("AI body preview:")}`);
13305
- console.log(import_picocolors16.default.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
14067
+ ${import_picocolors17.default.dim("AI body preview:")}`);
14068
+ console.log(import_picocolors17.default.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
13306
14069
  } else {
13307
14070
  spinner.fail("AI did not return a PR description.");
13308
14071
  }
@@ -13315,8 +14078,28 @@ ${import_picocolors16.default.dim("AI body preview:")}`);
13315
14078
  const REGENERATE = "Regenerate AI description";
13316
14079
  let submitAction = "cancel";
13317
14080
  const isMaintainer = config.role === "maintainer";
13318
- if (isMaintainer) {
13319
- const maintainerChoice = await selectPrompt("How would you like to submit your changes?", ["Create a PR", SQUASH_LOCAL, CANCEL]);
14081
+ if (args.pullrequest && args.local) {
14082
+ error("Use only one submit mode flag at a time: --pullrequest/--pr/-pr or -l for local squash merge.");
14083
+ process.exit(1);
14084
+ }
14085
+ if (args.local && !isMaintainer) {
14086
+ error("The -l flag is only available for maintainers. Contributors must submit via PR.");
14087
+ process.exit(1);
14088
+ }
14089
+ if (args.local) {
14090
+ await performSquashMerge(origin, baseBranch, currentBranch, {
14091
+ model: args.model,
14092
+ convention: config.commitConvention,
14093
+ useAI: aiEnabled
14094
+ });
14095
+ return;
14096
+ }
14097
+ if (isMaintainer && !args.pullrequest) {
14098
+ const maintainerChoice = await selectPrompt("How would you like to submit your changes?", [
14099
+ "Create a PR",
14100
+ SQUASH_LOCAL,
14101
+ CANCEL
14102
+ ]);
13320
14103
  if (maintainerChoice === CANCEL) {
13321
14104
  warn("Submit cancelled.");
13322
14105
  return;
@@ -13324,12 +14107,13 @@ ${import_picocolors16.default.dim("AI body preview:")}`);
13324
14107
  if (maintainerChoice === SQUASH_LOCAL) {
13325
14108
  await performSquashMerge(origin, baseBranch, currentBranch, {
13326
14109
  model: args.model,
13327
- convention: config.commitConvention
14110
+ convention: config.commitConvention,
14111
+ useAI: aiEnabled
13328
14112
  });
13329
14113
  return;
13330
14114
  }
13331
14115
  }
13332
- if (!args["no-ai"]) {
14116
+ if (aiEnabled) {
13333
14117
  await tryGenerateAI();
13334
14118
  }
13335
14119
  let actionResolved = false;
@@ -13368,7 +14152,7 @@ ${import_picocolors16.default.dim("AI body preview:")}`);
13368
14152
  }
13369
14153
  } else {
13370
14154
  const choices = [];
13371
- if (!args["no-ai"])
14155
+ if (aiEnabled)
13372
14156
  choices.push(REGENERATE);
13373
14157
  choices.push("Write title & body manually", "Use gh --fill (auto-fill from commits)", CANCEL);
13374
14158
  const action = await selectPrompt("How would you like to create the PR?", choices);
@@ -13392,7 +14176,7 @@ ${import_picocolors16.default.dim("AI body preview:")}`);
13392
14176
  warn("Submit cancelled.");
13393
14177
  return;
13394
14178
  }
13395
- info(`Pushing ${import_picocolors16.default.bold(currentBranch)} to ${origin}...`);
14179
+ info(`Pushing ${import_picocolors17.default.bold(currentBranch)} to ${origin}...`);
13396
14180
  const pushResult = await pushSetUpstream(origin, currentBranch);
13397
14181
  if (pushResult.exitCode !== 0) {
13398
14182
  error(`Failed to push: ${pushResult.stderr}`);
@@ -13411,7 +14195,7 @@ ${import_picocolors16.default.dim("AI body preview:")}`);
13411
14195
  const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/compare/${baseBranch}...${currentBranch}?expand=1`;
13412
14196
  console.log();
13413
14197
  info("Create your PR manually:", "");
13414
- console.log(` ${import_picocolors16.default.cyan(prUrl)}`);
14198
+ console.log(` ${import_picocolors17.default.cyan(prUrl)}`);
13415
14199
  } else {
13416
14200
  info("gh CLI not available. Create your PR manually on GitHub.", "");
13417
14201
  }
@@ -13445,7 +14229,7 @@ ${import_picocolors16.default.dim("AI body preview:")}`);
13445
14229
  });
13446
14230
 
13447
14231
  // src/commands/switch.ts
13448
- var import_picocolors17 = __toESM(require_picocolors(), 1);
14232
+ var import_picocolors18 = __toESM(require_picocolors(), 1);
13449
14233
  var switch_default = defineCommand({
13450
14234
  meta: {
13451
14235
  name: "switch",
@@ -13466,7 +14250,7 @@ var switch_default = defineCommand({
13466
14250
  const config = readConfig();
13467
14251
  const protectedBranches = config ? getProtectedBranches(config) : ["main", "master"];
13468
14252
  const currentBranch = await getCurrentBranch();
13469
- heading("\uD83D\uDD00 contrib switch");
14253
+ projectHeading("switch", "\uD83D\uDD00");
13470
14254
  let targetBranch = args.name;
13471
14255
  if (!targetBranch) {
13472
14256
  const localBranches = await getLocalBranches();
@@ -13477,11 +14261,11 @@ var switch_default = defineCommand({
13477
14261
  const choices = localBranches.filter((b2) => b2.name !== currentBranch).map((b2) => {
13478
14262
  const labels = [];
13479
14263
  if (protectedBranches.includes(b2.name))
13480
- labels.push(import_picocolors17.default.red("protected"));
14264
+ labels.push(import_picocolors18.default.red("protected"));
13481
14265
  if (b2.upstream)
13482
- labels.push(import_picocolors17.default.dim(`→ ${b2.upstream}`));
14266
+ labels.push(import_picocolors18.default.dim(`→ ${b2.upstream}`));
13483
14267
  if (b2.gone)
13484
- labels.push(import_picocolors17.default.red("remote gone"));
14268
+ labels.push(import_picocolors18.default.red("remote gone"));
13485
14269
  const suffix = labels.length > 0 ? ` ${labels.join(" · ")}` : "";
13486
14270
  return `${b2.name}${suffix}`;
13487
14271
  });
@@ -13493,7 +14277,7 @@ var switch_default = defineCommand({
13493
14277
  targetBranch = selected.split(/\s{2,}/)[0].trim();
13494
14278
  }
13495
14279
  if (targetBranch === currentBranch) {
13496
- info(`Already on ${import_picocolors17.default.bold(targetBranch)}.`);
14280
+ info(`Already on ${import_picocolors18.default.bold(targetBranch)}.`);
13497
14281
  return;
13498
14282
  }
13499
14283
  if (await hasUncommittedChanges()) {
@@ -13512,7 +14296,7 @@ var switch_default = defineCommand({
13512
14296
  const stashMsg = `contrib-save: auto-save from ${currentBranch}`;
13513
14297
  try {
13514
14298
  await exec("git", ["stash", "push", "-m", stashMsg]);
13515
- info(`Saved changes: ${import_picocolors17.default.dim(stashMsg)}`);
14299
+ info(`Saved changes: ${import_picocolors18.default.dim(stashMsg)}`);
13516
14300
  } catch {
13517
14301
  error("Failed to save changes. Please commit or save manually.");
13518
14302
  process.exit(1);
@@ -13528,9 +14312,9 @@ var switch_default = defineCommand({
13528
14312
  }
13529
14313
  process.exit(1);
13530
14314
  }
13531
- success(`Switched to ${import_picocolors17.default.bold(targetBranch)}`);
13532
- info(`Your changes from ${import_picocolors17.default.bold(currentBranch ?? "previous branch")} are saved.`, "");
13533
- info(`Use ${import_picocolors17.default.bold("contrib save --restore")} to bring them back.`, "");
14315
+ success(`Switched to ${import_picocolors18.default.bold(targetBranch)}`);
14316
+ info(`Your changes from ${import_picocolors18.default.bold(currentBranch ?? "previous branch")} are saved.`, "");
14317
+ info(`Use ${import_picocolors18.default.bold("contrib save --restore")} to bring them back.`, "");
13534
14318
  return;
13535
14319
  }
13536
14320
  const result = await checkoutBranch(targetBranch);
@@ -13538,12 +14322,12 @@ var switch_default = defineCommand({
13538
14322
  error(`Failed to switch to ${targetBranch}: ${result.stderr}`);
13539
14323
  process.exit(1);
13540
14324
  }
13541
- success(`Switched to ${import_picocolors17.default.bold(targetBranch)}`);
14325
+ success(`Switched to ${import_picocolors18.default.bold(targetBranch)}`);
13542
14326
  }
13543
14327
  });
13544
14328
 
13545
14329
  // src/commands/sync.ts
13546
- var import_picocolors18 = __toESM(require_picocolors(), 1);
14330
+ var import_picocolors19 = __toESM(require_picocolors(), 1);
13547
14331
  var sync_default = defineCommand({
13548
14332
  meta: {
13549
14333
  name: "sync",
@@ -13574,7 +14358,7 @@ var sync_default = defineCommand({
13574
14358
  await assertCleanGitState("syncing");
13575
14359
  const config = readConfig();
13576
14360
  if (!config) {
13577
- error("No .contributerc.json found. Run `contrib setup` first.");
14361
+ error("No repo config found. Run `contrib setup` first.");
13578
14362
  process.exit(1);
13579
14363
  }
13580
14364
  const { workflow, role, origin } = config;
@@ -13582,7 +14366,7 @@ var sync_default = defineCommand({
13582
14366
  error("You have uncommitted changes. Please commit or stash them before syncing.");
13583
14367
  process.exit(1);
13584
14368
  }
13585
- heading(`\uD83D\uDD04 contrib sync (${workflow}, ${role})`);
14369
+ projectHeading(`sync (${workflow}, ${role})`, "\uD83D\uDD04");
13586
14370
  const baseBranch = getBaseBranch(config);
13587
14371
  const syncSource = getSyncSource(config);
13588
14372
  info(`Fetching ${syncSource.remote}...`);
@@ -13595,24 +14379,24 @@ var sync_default = defineCommand({
13595
14379
  await fetchRemote(origin);
13596
14380
  }
13597
14381
  if (!await refExists(syncSource.ref)) {
13598
- error(`Remote ref ${import_picocolors18.default.bold(syncSource.ref)} does not exist.`);
14382
+ error(`Remote ref ${import_picocolors19.default.bold(syncSource.ref)} does not exist.`);
13599
14383
  info("This can happen if the branch was renamed or deleted on the remote.", "");
13600
- info(`Check your config: the base branch may need updating via ${import_picocolors18.default.bold("contrib setup")}.`, "");
14384
+ info(`Check your config: the base branch may need updating via ${import_picocolors19.default.bold("contrib setup")}.`, "");
13601
14385
  process.exit(1);
13602
14386
  }
13603
14387
  let allowMergeCommit = false;
13604
14388
  const div = await getDivergence(baseBranch, syncSource.ref);
13605
14389
  if (div.ahead > 0 || div.behind > 0) {
13606
- info(`${import_picocolors18.default.bold(baseBranch)} is ${import_picocolors18.default.yellow(`${div.ahead} ahead`)} and ${import_picocolors18.default.red(`${div.behind} behind`)} ${syncSource.ref}`);
14390
+ info(`${import_picocolors19.default.bold(baseBranch)} is ${import_picocolors19.default.yellow(`${div.ahead} ahead`)} and ${import_picocolors19.default.red(`${div.behind} behind`)} ${syncSource.ref}`);
13607
14391
  } else {
13608
- info(`${import_picocolors18.default.bold(baseBranch)} is already in sync with ${syncSource.ref}`);
14392
+ info(`${import_picocolors19.default.bold(baseBranch)} is already in sync with ${syncSource.ref}`);
13609
14393
  }
13610
14394
  if (div.ahead > 0) {
13611
14395
  const currentBranch = await getCurrentBranch();
13612
14396
  const protectedBranches = getProtectedBranches(config);
13613
14397
  const isOnProtected = currentBranch && protectedBranches.includes(currentBranch);
13614
14398
  if (isOnProtected) {
13615
- warn(`You have ${import_picocolors18.default.bold(String(div.ahead))} local commit${div.ahead !== 1 ? "s" : ""} on ${import_picocolors18.default.bold(baseBranch)} that aren't on the remote.`);
14399
+ warn(`You have ${import_picocolors19.default.bold(String(div.ahead))} local commit${div.ahead !== 1 ? "s" : ""} on ${import_picocolors19.default.bold(baseBranch)} that aren't on the remote.`);
13616
14400
  info("Pulling now could create a merge commit, which breaks clean history.");
13617
14401
  console.log();
13618
14402
  const MOVE_BRANCH = "Move my commits to a new feature branch, then sync";
@@ -13628,44 +14412,21 @@ var sync_default = defineCommand({
13628
14412
  return;
13629
14413
  }
13630
14414
  if (action === MOVE_BRANCH) {
13631
- info(import_picocolors18.default.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
13632
- const description = await inputPrompt("What are you going to work on?");
13633
- let newBranchName = description;
13634
- if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
13635
- const copilotError = await checkCopilotAvailable();
13636
- if (!copilotError) {
13637
- const spinner = createSpinner("Generating branch name suggestion...");
13638
- const suggested = await suggestBranchName(description, args.model);
13639
- if (suggested) {
13640
- spinner.success("Branch name suggestion ready.");
13641
- console.log(`
13642
- ${import_picocolors18.default.dim("AI suggestion:")} ${import_picocolors18.default.bold(import_picocolors18.default.cyan(suggested))}`);
13643
- const accepted = await confirmPrompt(`Use ${import_picocolors18.default.bold(suggested)} as your branch name?`);
13644
- newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
13645
- } else {
13646
- spinner.fail("AI did not return a suggestion.");
13647
- newBranchName = await inputPrompt("Enter branch name", description);
13648
- }
13649
- }
13650
- }
13651
- if (!hasPrefix(newBranchName, config.branchPrefixes)) {
13652
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors18.default.bold(newBranchName)}:`, config.branchPrefixes);
13653
- newBranchName = formatBranchName(prefix, newBranchName);
13654
- }
13655
- if (!isValidBranchName(newBranchName)) {
13656
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
13657
- process.exit(1);
13658
- }
13659
- if (await branchExists(newBranchName)) {
13660
- error(`Branch ${import_picocolors18.default.bold(newBranchName)} already exists. Choose a different name.`);
13661
- process.exit(1);
14415
+ const newBranchName = await promptForBranchName({
14416
+ branchPrefixes: config.branchPrefixes,
14417
+ useAI: isAIEnabled(config, args["no-ai"]),
14418
+ model: args.model
14419
+ });
14420
+ if (!newBranchName) {
14421
+ info("No changes made.");
14422
+ return;
13662
14423
  }
13663
14424
  const branchResult = await createBranch(newBranchName);
13664
14425
  if (branchResult.exitCode !== 0) {
13665
14426
  error(`Failed to create branch: ${branchResult.stderr}`);
13666
14427
  process.exit(1);
13667
14428
  }
13668
- success(`Created ${import_picocolors18.default.bold(newBranchName)} with your commits.`);
14429
+ success(`Created ${import_picocolors19.default.bold(newBranchName)} with your commits.`);
13669
14430
  const coResult2 = await checkoutBranch(baseBranch);
13670
14431
  if (coResult2.exitCode !== 0) {
13671
14432
  error(`Failed to checkout ${baseBranch}: ${coResult2.stderr}`);
@@ -13673,11 +14434,11 @@ var sync_default = defineCommand({
13673
14434
  }
13674
14435
  const remoteRef = syncSource.ref;
13675
14436
  await updateLocalBranch(baseBranch, remoteRef);
13676
- success(`Reset ${import_picocolors18.default.bold(baseBranch)} to ${import_picocolors18.default.bold(remoteRef)}.`);
13677
- success(`${import_picocolors18.default.bold(baseBranch)} is now in sync with ${syncSource.ref}`);
14437
+ success(`Reset ${import_picocolors19.default.bold(baseBranch)} to ${import_picocolors19.default.bold(remoteRef)}.`);
14438
+ success(`${import_picocolors19.default.bold(baseBranch)} is now in sync with ${syncSource.ref}`);
13678
14439
  console.log();
13679
- info(`Your commits are safe on ${import_picocolors18.default.bold(newBranchName)}.`, "");
13680
- info(`Run ${import_picocolors18.default.bold(`git checkout ${newBranchName}`)} then ${import_picocolors18.default.bold("contrib update")} to rebase onto the synced ${import_picocolors18.default.bold(baseBranch)}.`, "");
14440
+ info(`Your commits are safe on ${import_picocolors19.default.bold(newBranchName)}.`, "");
14441
+ info(`Run ${import_picocolors19.default.bold(`git checkout ${newBranchName}`)} then ${import_picocolors19.default.bold("contrib update")} to rebase onto the synced ${import_picocolors19.default.bold(baseBranch)}.`, "");
13681
14442
  return;
13682
14443
  }
13683
14444
  allowMergeCommit = true;
@@ -13685,7 +14446,7 @@ var sync_default = defineCommand({
13685
14446
  }
13686
14447
  }
13687
14448
  if (!args.yes) {
13688
- const ok = await confirmPrompt(`This will pull ${import_picocolors18.default.bold(syncSource.ref)} into local ${import_picocolors18.default.bold(baseBranch)}.`);
14449
+ const ok = await confirmPrompt(`This will pull ${import_picocolors19.default.bold(syncSource.ref)} into local ${import_picocolors19.default.bold(baseBranch)}.`);
13689
14450
  if (!ok)
13690
14451
  process.exit(0);
13691
14452
  }
@@ -13699,8 +14460,8 @@ var sync_default = defineCommand({
13699
14460
  if (allowMergeCommit) {
13700
14461
  error(`Pull failed: ${pullResult.stderr.trim()}`);
13701
14462
  } else {
13702
- error(`Fast-forward pull failed. Your local ${import_picocolors18.default.bold(baseBranch)} may have diverged.`);
13703
- info(`Use ${import_picocolors18.default.bold("contrib sync")} again and choose "Move my commits to a new feature branch" to fix this.`, "");
14463
+ error(`Fast-forward pull failed. Your local ${import_picocolors19.default.bold(baseBranch)} may have diverged.`);
14464
+ info(`Use ${import_picocolors19.default.bold("contrib sync")} again and choose "Move my commits to a new feature branch" to fix this.`, "");
13704
14465
  }
13705
14466
  process.exit(1);
13706
14467
  }
@@ -13708,7 +14469,7 @@ var sync_default = defineCommand({
13708
14469
  if (hasDevBranch(workflow) && role === "maintainer") {
13709
14470
  const mainDiv = await getDivergence(config.mainBranch, `${origin}/${config.mainBranch}`);
13710
14471
  if (mainDiv.behind > 0) {
13711
- info(`Also syncing ${import_picocolors18.default.bold(config.mainBranch)}...`);
14472
+ info(`Also syncing ${import_picocolors19.default.bold(config.mainBranch)}...`);
13712
14473
  const mainCoResult = await checkoutBranch(config.mainBranch);
13713
14474
  if (mainCoResult.exitCode === 0) {
13714
14475
  const mainPullResult = await pullFastForwardOnly(origin, config.mainBranch);
@@ -13743,23 +14504,26 @@ var sync_default = defineCommand({
13743
14504
  for (const { name, hash } of refs) {
13744
14505
  if (!groups.has(hash))
13745
14506
  groups.set(hash, []);
13746
- groups.get(hash).push(name);
14507
+ const group = groups.get(hash);
14508
+ if (group) {
14509
+ group.push(name);
14510
+ }
13747
14511
  }
13748
14512
  console.log();
13749
- console.log(` ${import_picocolors18.default.bold("\uD83D\uDD17 Branch Alignment")}`);
14513
+ console.log(` ${import_picocolors19.default.bold("\uD83D\uDD17 Branch Alignment")}`);
13750
14514
  for (const [hash, names] of groups) {
13751
14515
  const short = hash.slice(0, 7);
13752
- const nameStr = names.map((n2) => import_picocolors18.default.bold(n2)).join(import_picocolors18.default.dim(" · "));
13753
- console.log(` ${import_picocolors18.default.yellow(short)} ${import_picocolors18.default.dim("──")} ${nameStr}`);
14516
+ const nameStr = names.map((n2) => import_picocolors19.default.bold(n2)).join(import_picocolors19.default.dim(" · "));
14517
+ console.log(` ${import_picocolors19.default.yellow(short)} ${import_picocolors19.default.dim("──")} ${nameStr}`);
13754
14518
  const subject = await getCommitSubject(hash);
13755
14519
  if (subject) {
13756
- console.log(` ${import_picocolors18.default.dim(subject)}`);
14520
+ console.log(` ${import_picocolors19.default.dim(subject)}`);
13757
14521
  }
13758
14522
  }
13759
14523
  if (groups.size === 1) {
13760
- console.log(` ${import_picocolors18.default.green("✓")} ${import_picocolors18.default.green("All branches aligned")} ${import_picocolors18.default.dim("— ready to start")}`);
14524
+ console.log(` ${import_picocolors19.default.green("✓")} ${import_picocolors19.default.green("All branches aligned")} ${import_picocolors19.default.dim("— ready to start")}`);
13761
14525
  } else {
13762
- console.log(` ${import_picocolors18.default.yellow("⚠")} ${import_picocolors18.default.yellow("Branches are not fully aligned")}`);
14526
+ console.log(` ${import_picocolors19.default.yellow("⚠")} ${import_picocolors19.default.yellow("Branches are not fully aligned")}`);
13763
14527
  }
13764
14528
  }
13765
14529
  }
@@ -13768,7 +14532,10 @@ var sync_default = defineCommand({
13768
14532
 
13769
14533
  // src/commands/update.ts
13770
14534
  import { readFileSync as readFileSync4 } from "node:fs";
13771
- var import_picocolors19 = __toESM(require_picocolors(), 1);
14535
+ var import_picocolors20 = __toESM(require_picocolors(), 1);
14536
+ function hasStaleBranchWorkToPreserve(uniqueCommitsAheadOfBase, hasUncommittedChanges2) {
14537
+ return hasUncommittedChanges2 || uniqueCommitsAheadOfBase > 0;
14538
+ }
13772
14539
  var update_default = defineCommand({
13773
14540
  meta: {
13774
14541
  name: "update",
@@ -13793,7 +14560,7 @@ var update_default = defineCommand({
13793
14560
  await assertCleanGitState("updating");
13794
14561
  const config = readConfig();
13795
14562
  if (!config) {
13796
- error("No .contributerc.json found. Run `contrib setup` first.");
14563
+ error("No repo config found. Run `contrib setup` first.");
13797
14564
  process.exit(1);
13798
14565
  }
13799
14566
  const baseBranch = getBaseBranch(config);
@@ -13805,8 +14572,8 @@ var update_default = defineCommand({
13805
14572
  process.exit(1);
13806
14573
  }
13807
14574
  if (protectedBranches.includes(currentBranch)) {
13808
- heading("\uD83D\uDD03 contrib update");
13809
- warn(`You're on ${import_picocolors19.default.bold(currentBranch)}, which is a protected branch. Updates (rebase) apply to feature branches.`);
14575
+ projectHeading("update", "\uD83D\uDD03");
14576
+ warn(`You're on ${import_picocolors20.default.bold(currentBranch)}, which is a protected branch. Updates (rebase) apply to feature branches.`);
13810
14577
  await fetchAll();
13811
14578
  const { origin } = config;
13812
14579
  const remoteRef = `${origin}/${currentBranch}`;
@@ -13815,12 +14582,12 @@ var update_default = defineCommand({
13815
14582
  const hasCommits = localWork.unpushedCommits > 0;
13816
14583
  const hasAnything = hasCommits || dirty;
13817
14584
  if (!hasAnything) {
13818
- info(`No local changes found on ${import_picocolors19.default.bold(currentBranch)}.`);
13819
- info(`Use ${import_picocolors19.default.bold("contrib sync")} to sync protected branches, or ${import_picocolors19.default.bold("contrib start")} to create a feature branch.`);
14585
+ info(`No local changes found on ${import_picocolors20.default.bold(currentBranch)}.`);
14586
+ info(`Use ${import_picocolors20.default.bold("contrib sync")} to sync protected branches, or ${import_picocolors20.default.bold("contrib start")} to create a feature branch.`);
13820
14587
  process.exit(1);
13821
14588
  }
13822
14589
  if (hasCommits) {
13823
- info(`Found ${import_picocolors19.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${import_picocolors19.default.bold(currentBranch)}.`);
14590
+ info(`Found ${import_picocolors20.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${import_picocolors20.default.bold(currentBranch)}.`);
13824
14591
  }
13825
14592
  if (dirty) {
13826
14593
  info("You also have uncommitted changes in the working tree.");
@@ -13836,111 +14603,73 @@ var update_default = defineCommand({
13836
14603
  info("No changes made. You are still on your current branch.");
13837
14604
  return;
13838
14605
  }
13839
- info(import_picocolors19.default.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
13840
- const description = await inputPrompt("What are you going to work on?");
13841
- let newBranchName = description;
13842
- if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
13843
- const copilotError = await checkCopilotAvailable();
13844
- if (!copilotError) {
13845
- const spinner = createSpinner("Generating branch name suggestion...");
13846
- const suggested = await suggestBranchName(description, args.model);
13847
- if (suggested) {
13848
- spinner.success("Branch name suggestion ready.");
13849
- console.log(`
13850
- ${import_picocolors19.default.dim("AI suggestion:")} ${import_picocolors19.default.bold(import_picocolors19.default.cyan(suggested))}`);
13851
- const accepted = await confirmPrompt(`Use ${import_picocolors19.default.bold(suggested)} as your branch name?`);
13852
- newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
13853
- } else {
13854
- spinner.fail("AI did not return a suggestion.");
13855
- newBranchName = await inputPrompt("Enter branch name", description);
13856
- }
13857
- }
13858
- }
13859
- if (!hasPrefix(newBranchName, config.branchPrefixes)) {
13860
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors19.default.bold(newBranchName)}:`, config.branchPrefixes);
13861
- newBranchName = formatBranchName(prefix, newBranchName);
13862
- }
13863
- if (!isValidBranchName(newBranchName)) {
13864
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
13865
- process.exit(1);
14606
+ const newBranchName = await promptForBranchName({
14607
+ branchPrefixes: config.branchPrefixes,
14608
+ useAI: isAIEnabled(config, args["no-ai"]),
14609
+ model: args.model
14610
+ });
14611
+ if (!newBranchName) {
14612
+ info("No changes made. You are still on your current branch.");
14613
+ return;
13866
14614
  }
13867
14615
  const branchResult = await createBranch(newBranchName);
13868
14616
  if (branchResult.exitCode !== 0) {
13869
14617
  error(`Failed to create branch: ${branchResult.stderr}`);
13870
14618
  process.exit(1);
13871
14619
  }
13872
- success(`Created ${import_picocolors19.default.bold(newBranchName)} with your changes.`);
14620
+ success(`Created ${import_picocolors20.default.bold(newBranchName)} with your changes.`);
13873
14621
  await updateLocalBranch(currentBranch, remoteRef);
13874
- info(`Reset ${import_picocolors19.default.bold(currentBranch)} back to ${import_picocolors19.default.bold(remoteRef)} — no damage done.`, "");
14622
+ info(`Reset ${import_picocolors20.default.bold(currentBranch)} back to ${import_picocolors20.default.bold(remoteRef)} — no damage done.`, "");
13875
14623
  console.log();
13876
- success(`You're now on ${import_picocolors19.default.bold(newBranchName)} with all your work intact.`);
13877
- info(`Run ${import_picocolors19.default.bold("contrib update")} again to rebase onto latest ${import_picocolors19.default.bold(baseBranch)}.`, "");
14624
+ success(`You're now on ${import_picocolors20.default.bold(newBranchName)} with all your work intact.`);
14625
+ info(`Run ${import_picocolors20.default.bold("contrib update")} again to rebase onto latest ${import_picocolors20.default.bold(baseBranch)}.`, "");
13878
14626
  return;
13879
14627
  }
13880
14628
  if (await hasUncommittedChanges()) {
13881
14629
  error("You have uncommitted changes. Please commit or stash them first.");
13882
14630
  process.exit(1);
13883
14631
  }
13884
- heading("\uD83D\uDD03 contrib update");
14632
+ projectHeading("update", "\uD83D\uDD03");
13885
14633
  const mergedPR = await getMergedPRForBranch(currentBranch);
13886
14634
  if (mergedPR) {
13887
- warn(`PR #${mergedPR.number} (${import_picocolors19.default.bold(mergedPR.title)}) has already been merged.`);
13888
- info(`Link: ${import_picocolors19.default.underline(mergedPR.url)}`, "");
13889
- const localWork = await hasLocalWork(syncSource.remote, currentBranch);
13890
- const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
14635
+ warn(`PR #${mergedPR.number} (${import_picocolors20.default.bold(mergedPR.title)}) has already been merged.`);
14636
+ info(`Link: ${import_picocolors20.default.underline(mergedPR.url)}`, "");
14637
+ const uniqueCommitsAheadOfBase = await countCommitsAhead(currentBranch, syncSource.ref);
14638
+ const dirty = await hasUncommittedChanges();
14639
+ const hasWork = hasStaleBranchWorkToPreserve(uniqueCommitsAheadOfBase, dirty);
13891
14640
  if (hasWork) {
13892
- if (localWork.uncommitted) {
14641
+ if (dirty) {
13893
14642
  info("You have uncommitted local changes.");
13894
14643
  }
13895
- if (localWork.unpushedCommits > 0) {
13896
- info(`You have ${localWork.unpushedCommits} unpushed commit(s).`);
14644
+ if (uniqueCommitsAheadOfBase > 0) {
14645
+ info(`You have ${uniqueCommitsAheadOfBase} local commit(s) not in ${import_picocolors20.default.bold(syncSource.ref)}.`);
13897
14646
  }
13898
14647
  const SAVE_NEW_BRANCH = "Save changes to a new branch";
13899
14648
  const DISCARD = "Discard all changes and clean up";
13900
14649
  const CANCEL = "Cancel";
13901
- const action = await selectPrompt(`${import_picocolors19.default.bold(currentBranch)} is stale but has local work. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
14650
+ const action = await selectPrompt(`${import_picocolors20.default.bold(currentBranch)} is stale but has local work. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
13902
14651
  if (action === CANCEL) {
13903
14652
  info("No changes made. You are still on your current branch.");
13904
14653
  return;
13905
14654
  }
13906
14655
  if (action === SAVE_NEW_BRANCH) {
13907
- info(import_picocolors19.default.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
13908
- const description = await inputPrompt("What are you going to work on?");
13909
- let newBranchName = description;
13910
- if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
13911
- const spinner = createSpinner("Generating branch name suggestion...");
13912
- const suggested = await suggestBranchName(description, args.model);
13913
- if (suggested) {
13914
- spinner.success("Branch name suggestion ready.");
13915
- console.log(`
13916
- ${import_picocolors19.default.dim("AI suggestion:")} ${import_picocolors19.default.bold(import_picocolors19.default.cyan(suggested))}`);
13917
- const accepted = await confirmPrompt(`Use ${import_picocolors19.default.bold(suggested)} as your branch name?`);
13918
- newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
13919
- } else {
13920
- spinner.fail("AI did not return a suggestion.");
13921
- newBranchName = await inputPrompt("Enter branch name", description);
13922
- }
13923
- }
13924
- if (!hasPrefix(newBranchName, config.branchPrefixes)) {
13925
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors19.default.bold(newBranchName)}:`, config.branchPrefixes);
13926
- newBranchName = formatBranchName(prefix, newBranchName);
13927
- }
13928
- if (!isValidBranchName(newBranchName)) {
13929
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
13930
- process.exit(1);
14656
+ const newBranchName = await promptForBranchName({
14657
+ branchPrefixes: config.branchPrefixes,
14658
+ useAI: isAIEnabled(config, args["no-ai"]),
14659
+ model: args.model
14660
+ });
14661
+ if (!newBranchName) {
14662
+ info("No changes made. You are still on your current branch.");
14663
+ return;
13931
14664
  }
13932
14665
  const staleUpstream = await getUpstreamRef();
13933
14666
  const staleUpstreamHash = staleUpstream ? await getCommitHash(staleUpstream) : null;
13934
- if (await branchExists(newBranchName)) {
13935
- error(`Branch ${import_picocolors19.default.bold(newBranchName)} already exists. Choose a different name.`);
13936
- process.exit(1);
13937
- }
13938
14667
  const renameResult = await renameBranch(currentBranch, newBranchName);
13939
14668
  if (renameResult.exitCode !== 0) {
13940
14669
  error(`Failed to rename branch: ${renameResult.stderr}`);
13941
14670
  process.exit(1);
13942
14671
  }
13943
- success(`Renamed ${import_picocolors19.default.bold(currentBranch)} → ${import_picocolors19.default.bold(newBranchName)}`);
14672
+ success(`Renamed ${import_picocolors20.default.bold(currentBranch)} → ${import_picocolors20.default.bold(newBranchName)}`);
13944
14673
  await unsetUpstream();
13945
14674
  await fetchRemote(syncSource.remote);
13946
14675
  let rebaseResult2;
@@ -13952,14 +14681,20 @@ var update_default = defineCommand({
13952
14681
  }
13953
14682
  if (rebaseResult2.exitCode !== 0) {
13954
14683
  warn("Rebase encountered conflicts. Resolve them manually, then run:");
13955
- info(` ${import_picocolors19.default.bold("git rebase --continue")}`, "");
14684
+ info(` ${import_picocolors20.default.bold("git rebase --continue")}`, "");
13956
14685
  } else {
13957
- success(`Rebased ${import_picocolors19.default.bold(newBranchName)} onto ${import_picocolors19.default.bold(syncSource.ref)}.`);
14686
+ success(`Rebased ${import_picocolors20.default.bold(newBranchName)} onto ${import_picocolors20.default.bold(syncSource.ref)}.`);
13958
14687
  }
13959
- info(`All your changes are preserved. Run ${import_picocolors19.default.bold("contrib submit")} when ready to create a new PR.`, "");
14688
+ info(`All your changes are preserved. Run ${import_picocolors20.default.bold("contrib submit")} when ready to create a new PR.`, "");
13960
14689
  return;
13961
14690
  }
13962
14691
  warn("Discarding local changes...");
14692
+ } else {
14693
+ const proceed = await confirmPrompt(`Switch to ${baseBranch} and delete stale branch ${currentBranch}?`);
14694
+ if (!proceed) {
14695
+ info("No changes made. You are still on your current branch.");
14696
+ return;
14697
+ }
13963
14698
  }
13964
14699
  await fetchRemote(syncSource.remote);
13965
14700
  await resetHard("HEAD");
@@ -13969,30 +14704,30 @@ var update_default = defineCommand({
13969
14704
  process.exit(1);
13970
14705
  }
13971
14706
  await updateLocalBranch(baseBranch, syncSource.ref);
13972
- success(`Synced ${import_picocolors19.default.bold(baseBranch)} with ${import_picocolors19.default.bold(syncSource.ref)}.`);
13973
- info(`Deleting stale branch ${import_picocolors19.default.bold(currentBranch)}...`);
14707
+ success(`Synced ${import_picocolors20.default.bold(baseBranch)} with ${import_picocolors20.default.bold(syncSource.ref)}.`);
14708
+ info(`Deleting stale branch ${import_picocolors20.default.bold(currentBranch)}...`);
13974
14709
  await forceDeleteBranch(currentBranch);
13975
- success(`Deleted ${import_picocolors19.default.bold(currentBranch)}.`);
13976
- info(`Run ${import_picocolors19.default.bold("contrib start")} to begin a new feature branch.`, "");
14710
+ success(`Deleted ${import_picocolors20.default.bold(currentBranch)}.`);
14711
+ info(`Run ${import_picocolors20.default.bold("contrib start")} to begin a new feature branch.`, "");
13977
14712
  return;
13978
14713
  }
13979
- info(`Updating ${import_picocolors19.default.bold(currentBranch)} with latest ${import_picocolors19.default.bold(baseBranch)}...`);
14714
+ info(`Updating ${import_picocolors20.default.bold(currentBranch)} with latest ${import_picocolors20.default.bold(baseBranch)}...`);
13980
14715
  await fetchRemote(syncSource.remote);
13981
14716
  if (!await refExists(syncSource.ref)) {
13982
- error(`Remote ref ${import_picocolors19.default.bold(syncSource.ref)} does not exist.`);
14717
+ error(`Remote ref ${import_picocolors20.default.bold(syncSource.ref)} does not exist.`);
13983
14718
  error("Run `git fetch --all` and verify your remote configuration.");
13984
14719
  process.exit(1);
13985
14720
  }
13986
14721
  await updateLocalBranch(baseBranch, syncSource.ref);
13987
14722
  const rebaseStrategy = await determineRebaseStrategy(currentBranch, syncSource.ref);
13988
14723
  if (rebaseStrategy.strategy === "onto" && rebaseStrategy.ontoOldBase) {
13989
- info(import_picocolors19.default.dim(`Using --onto rebase (branch was based on a different ref)`));
14724
+ info(import_picocolors20.default.dim(`Using --onto rebase (branch was based on a different ref)`));
13990
14725
  }
13991
14726
  const rebaseResult = rebaseStrategy.strategy === "onto" && rebaseStrategy.ontoOldBase ? await rebaseOnto(syncSource.ref, rebaseStrategy.ontoOldBase) : await rebase(syncSource.ref);
13992
14727
  if (rebaseResult.exitCode !== 0) {
13993
14728
  warn("Rebase hit conflicts. Resolve them manually.");
13994
14729
  console.log();
13995
- if (!args["no-ai"]) {
14730
+ if (isAIEnabled(config, args["no-ai"])) {
13996
14731
  const copilotError = await checkCopilotAvailable();
13997
14732
  if (!copilotError) {
13998
14733
  info("Fetching AI conflict resolution suggestions...");
@@ -14010,15 +14745,17 @@ ${content.slice(0, 2000)}
14010
14745
  } catch {}
14011
14746
  }
14012
14747
  if (conflictDiff) {
14013
- const spinner = createSpinner("Analyzing conflicts with AI...");
14748
+ const spinner = createSpinner("Analyzing conflicts with AI...", {
14749
+ tips: LOADING_TIPS
14750
+ });
14014
14751
  const suggestion = await suggestConflictResolution(conflictDiff, args.model);
14015
14752
  if (suggestion) {
14016
14753
  spinner.success("AI conflict guidance ready.");
14017
14754
  console.log(`
14018
- ${import_picocolors19.default.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
14019
- console.log(import_picocolors19.default.dim("─".repeat(60)));
14755
+ ${import_picocolors20.default.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
14756
+ console.log(import_picocolors20.default.dim("─".repeat(60)));
14020
14757
  console.log(suggestion);
14021
- console.log(import_picocolors19.default.dim("─".repeat(60)));
14758
+ console.log(import_picocolors20.default.dim("─".repeat(60)));
14022
14759
  console.log();
14023
14760
  } else {
14024
14761
  spinner.fail("AI could not analyze the conflicts.");
@@ -14026,20 +14763,21 @@ ${import_picocolors19.default.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance
14026
14763
  }
14027
14764
  }
14028
14765
  }
14029
- console.log(import_picocolors19.default.bold("To resolve:"));
14766
+ console.log(import_picocolors20.default.bold("To resolve:"));
14030
14767
  console.log(` 1. Fix conflicts in the affected files`);
14031
- console.log(` 2. ${import_picocolors19.default.cyan("git add <resolved-files>")}`);
14032
- console.log(` 3. ${import_picocolors19.default.cyan("git rebase --continue")}`);
14768
+ console.log(` 2. ${import_picocolors20.default.cyan("git add <resolved-files>")}`);
14769
+ console.log(` 3. ${import_picocolors20.default.cyan("git rebase --continue")}`);
14033
14770
  console.log();
14034
- console.log(` Or abort: ${import_picocolors19.default.cyan("git rebase --abort")}`);
14771
+ console.log(` Or abort: ${import_picocolors20.default.cyan("git rebase --abort")}`);
14035
14772
  process.exit(1);
14036
14773
  }
14037
- success(`${import_picocolors19.default.bold(currentBranch)} has been rebased onto latest ${import_picocolors19.default.bold(baseBranch)}`);
14774
+ success(`${import_picocolors20.default.bold(currentBranch)} has been rebased onto latest ${import_picocolors20.default.bold(baseBranch)}`);
14038
14775
  }
14039
14776
  });
14040
14777
 
14041
14778
  // src/commands/validate.ts
14042
- var import_picocolors20 = __toESM(require_picocolors(), 1);
14779
+ import { readFileSync as readFileSync5 } from "node:fs";
14780
+ var import_picocolors21 = __toESM(require_picocolors(), 1);
14043
14781
  var validate_default = defineCommand({
14044
14782
  meta: {
14045
14783
  name: "validate",
@@ -14049,28 +14787,37 @@ var validate_default = defineCommand({
14049
14787
  message: {
14050
14788
  type: "positional",
14051
14789
  description: "The commit message to validate",
14052
- required: true
14790
+ required: false
14791
+ },
14792
+ file: {
14793
+ type: "string",
14794
+ description: "Path to a commit message file; only the first line is validated"
14053
14795
  }
14054
14796
  },
14055
14797
  async run({ args }) {
14056
14798
  const config = readConfig();
14057
14799
  if (!config) {
14058
- error("No .contributerc.json found. Run `contrib setup` first.");
14800
+ error("No repo config found. Run `contrib setup` first.");
14059
14801
  process.exit(1);
14060
14802
  }
14803
+ projectHeading("validate", "✅");
14061
14804
  const convention = config.commitConvention;
14062
14805
  if (convention === "none") {
14063
14806
  info('Commit convention is set to "none". All messages are accepted.');
14064
14807
  process.exit(0);
14065
14808
  }
14066
- const message = args.message;
14809
+ const message = args.file ? readFileSync5(args.file, "utf-8").split(/\r?\n/, 1)[0] ?? "" : args.message;
14810
+ if (!message) {
14811
+ error("No commit message provided. Pass a message or use --file <path>.");
14812
+ process.exit(1);
14813
+ }
14067
14814
  if (validateCommitMessage(message, convention)) {
14068
14815
  success(`Valid ${CONVENTION_LABELS[convention]} message.`);
14069
14816
  process.exit(0);
14070
14817
  }
14071
14818
  const errors = getValidationError(convention);
14072
14819
  for (const line of errors) {
14073
- console.error(import_picocolors20.default.red(` ✗ ${line}`));
14820
+ console.error(import_picocolors21.default.red(` ✗ ${line}`));
14074
14821
  }
14075
14822
  process.exit(1);
14076
14823
  }
@@ -15369,8 +16116,8 @@ var figlet = (() => {
15369
16116
  }
15370
16117
  };
15371
16118
  me2.fonts = function(callback) {
15372
- return new Promise(function(resolve2, reject) {
15373
- resolve2(fontList);
16119
+ return new Promise(function(resolve3, reject) {
16120
+ resolve3(fontList);
15374
16121
  if (callback) {
15375
16122
  callback(null, fontList);
15376
16123
  }
@@ -15392,12 +16139,12 @@ var nodeFiglet = figlet;
15392
16139
  nodeFiglet.defaults({ fontPath });
15393
16140
  nodeFiglet.loadFont = function(name, callback) {
15394
16141
  const actualFontName = getFontName(name);
15395
- return new Promise((resolve2, reject) => {
16142
+ return new Promise((resolve3, reject) => {
15396
16143
  if (nodeFiglet.figFonts[actualFontName]) {
15397
16144
  if (callback) {
15398
16145
  callback(null, nodeFiglet.figFonts[actualFontName].options);
15399
16146
  }
15400
- resolve2(nodeFiglet.figFonts[actualFontName].options);
16147
+ resolve3(nodeFiglet.figFonts[actualFontName].options);
15401
16148
  return;
15402
16149
  }
15403
16150
  fs2.readFile(path2.join(nodeFiglet.defaults().fontPath, actualFontName + ".flf"), { encoding: "utf-8" }, (err, fontData) => {
@@ -15414,7 +16161,7 @@ nodeFiglet.loadFont = function(name, callback) {
15414
16161
  if (callback) {
15415
16162
  callback(null, font);
15416
16163
  }
15417
- resolve2(font);
16164
+ resolve3(font);
15418
16165
  } catch (error2) {
15419
16166
  const typedError = error2 instanceof Error ? error2 : new Error(String(error2));
15420
16167
  if (callback) {
@@ -15436,7 +16183,7 @@ nodeFiglet.loadFontSync = function(font) {
15436
16183
  return nodeFiglet.parseFont(actualFontName, fontData);
15437
16184
  };
15438
16185
  nodeFiglet.fonts = function(next) {
15439
- return new Promise((resolve2, reject) => {
16186
+ return new Promise((resolve3, reject) => {
15440
16187
  const fontList2 = [];
15441
16188
  fs2.readdir(nodeFiglet.defaults().fontPath, (err, files) => {
15442
16189
  if (err) {
@@ -15450,7 +16197,7 @@ nodeFiglet.fonts = function(next) {
15450
16197
  }
15451
16198
  });
15452
16199
  next && next(null, fontList2);
15453
- resolve2(fontList2);
16200
+ resolve3(fontList2);
15454
16201
  });
15455
16202
  });
15456
16203
  };
@@ -15465,7 +16212,37 @@ nodeFiglet.fontsSync = function() {
15465
16212
  };
15466
16213
 
15467
16214
  // src/ui/banner.ts
15468
- var import_picocolors21 = __toESM(require_picocolors(), 1);
16215
+ var import_picocolors22 = __toESM(require_picocolors(), 1);
16216
+
16217
+ // src/data/announcements.json
16218
+ var announcements_default = [
16219
+ {
16220
+ id: "legacy-config-migration",
16221
+ kind: "notice",
16222
+ title: "Legacy Config Detected",
16223
+ message: "Run cn setup to migrate this clone to local Git config, then delete .contributerc.json.",
16224
+ when: "legacy-config-present"
16225
+ }
16226
+ ];
16227
+
16228
+ // src/utils/announcements.ts
16229
+ var DEFINITIONS = announcements_default;
16230
+ function getActiveAnnouncements(cwd = process.cwd()) {
16231
+ return DEFINITIONS.filter((announcement) => shouldShowAnnouncement(announcement, cwd)).map(({ id, kind, title, message }) => ({ id, kind, title, message }));
16232
+ }
16233
+ function shouldShowAnnouncement(announcement, cwd) {
16234
+ if (!announcement.when) {
16235
+ return true;
16236
+ }
16237
+ switch (announcement.when) {
16238
+ case "legacy-config-present":
16239
+ return hasLegacyConfig(cwd);
16240
+ default:
16241
+ return false;
16242
+ }
16243
+ }
16244
+
16245
+ // src/ui/banner.ts
15469
16246
  var LOGO_BIG;
15470
16247
  try {
15471
16248
  LOGO_BIG = nodeFiglet.textSync(`Contribute
@@ -15487,19 +16264,194 @@ function getAuthor() {
15487
16264
  }
15488
16265
  function showBanner(variant = "small") {
15489
16266
  const logo = variant === "big" ? LOGO_BIG : LOGO_SMALL;
15490
- console.log(import_picocolors21.default.cyan(`
16267
+ console.log(import_picocolors22.default.cyan(`
15491
16268
  ${logo}`));
15492
- console.log(` ${import_picocolors21.default.dim(`v${getVersion()}`)} ${import_picocolors21.default.dim("—")} ${import_picocolors21.default.dim(`Built by ${getAuthor()}`)}`);
16269
+ console.log(` ${import_picocolors22.default.dim(`v${getVersion()}`)} ${import_picocolors22.default.dim("—")} ${import_picocolors22.default.dim(`Built by ${getAuthor()}`)}`);
16270
+ const announcements = getActiveAnnouncements();
16271
+ if (announcements.length > 0) {
16272
+ console.log();
16273
+ renderAnnouncements(announcements);
16274
+ }
15493
16275
  if (variant === "big") {
16276
+ const panelLines = [
16277
+ {
16278
+ label: import_picocolors22.default.bold(import_picocolors22.default.cyan("Getting Started")),
16279
+ rawLabel: "Getting Started",
16280
+ value: "",
16281
+ rawValue: ""
16282
+ },
16283
+ {
16284
+ label: import_picocolors22.default.cyan("cn setup"),
16285
+ rawLabel: "cn setup",
16286
+ value: import_picocolors22.default.dim("configure workflow, remotes, and defaults"),
16287
+ rawValue: "configure workflow, remotes, and defaults"
16288
+ },
16289
+ {
16290
+ label: import_picocolors22.default.cyan("cn doctor"),
16291
+ rawLabel: "cn doctor",
16292
+ value: import_picocolors22.default.dim("verify your environment before doing any work"),
16293
+ rawValue: "verify your environment before doing any work"
16294
+ },
16295
+ {
16296
+ label: import_picocolors22.default.cyan("cn start"),
16297
+ rawLabel: "cn start",
16298
+ value: import_picocolors22.default.dim("create a branch and begin the next task"),
16299
+ rawValue: "create a branch and begin the next task"
16300
+ },
16301
+ {
16302
+ label: "",
16303
+ rawLabel: "",
16304
+ value: "",
16305
+ rawValue: ""
16306
+ },
16307
+ {
16308
+ label: import_picocolors22.default.bold(import_picocolors22.default.cyan("Workflow")),
16309
+ rawLabel: "Workflow",
16310
+ value: "",
16311
+ rawValue: ""
16312
+ },
16313
+ {
16314
+ label: import_picocolors22.default.dim("cn setup → cn commit → cn update → cn submit"),
16315
+ rawLabel: "cn setup → cn commit → cn update → cn submit",
16316
+ value: "",
16317
+ rawValue: ""
16318
+ }
16319
+ ];
16320
+ const terminalWidth = process.stdout.columns ?? 80;
16321
+ const maxContentWidth = Math.max(36, terminalWidth - 8);
16322
+ const unclampedLabelWidth = panelLines.reduce((max, line) => Math.max(max, line.rawLabel.length), 0);
16323
+ const labelWidth = Math.min(unclampedLabelWidth, 18);
16324
+ const valueWidth = Math.max(14, maxContentWidth - labelWidth - 2);
16325
+ const rows = panelLines.map((line) => {
16326
+ const rawLabel = line.rawValue ? truncateText3(line.rawLabel, labelWidth) : line.rawLabel;
16327
+ const rawValue = line.rawValue ? truncateText3(line.rawValue, valueWidth) : "";
16328
+ return {
16329
+ ...line,
16330
+ rawLabel,
16331
+ rawValue
16332
+ };
16333
+ });
16334
+ const contentWidth = Math.min(maxContentWidth, rows.reduce((max, line) => {
16335
+ const lineLength = line.rawValue ? labelWidth + 2 + line.rawValue.length : line.rawLabel.length;
16336
+ return Math.max(max, lineLength);
16337
+ }, 0));
16338
+ console.log();
16339
+ console.log(` ${import_picocolors22.default.dim(`┌${"─".repeat(contentWidth + 2)}┐`)}`);
16340
+ for (const line of rows) {
16341
+ if (!line.rawLabel && !line.rawValue) {
16342
+ console.log(` ${import_picocolors22.default.dim("│")} ${" ".repeat(contentWidth)} ${import_picocolors22.default.dim("│")}`);
16343
+ continue;
16344
+ }
16345
+ const left = line.rawValue ? `${line.label}${" ".repeat(Math.max(0, labelWidth - line.rawLabel.length + 2))}` : line.label;
16346
+ const value = line.rawValue ? import_picocolors22.default.dim(line.rawValue) : "";
16347
+ const rawLength = line.rawValue ? labelWidth + 2 + line.rawValue.length : line.rawLabel.length;
16348
+ const trailing = " ".repeat(Math.max(0, contentWidth - rawLength));
16349
+ console.log(` ${import_picocolors22.default.dim("│")} ${left}${value}${trailing} ${import_picocolors22.default.dim("│")}`);
16350
+ }
16351
+ console.log(` ${import_picocolors22.default.dim(`└${"─".repeat(contentWidth + 2)}┘`)}`);
15494
16352
  console.log();
15495
- console.log(` ${import_picocolors21.default.yellow("Star")} ${import_picocolors21.default.cyan("https://github.com/warengonzaga/contribute-now")}`);
15496
- console.log(` ${import_picocolors21.default.green("Contribute")} ${import_picocolors21.default.cyan("https://github.com/warengonzaga/contribute-now/blob/main/CONTRIBUTING.md")}`);
15497
- console.log(` ${import_picocolors21.default.magenta("Sponsor")} ${import_picocolors21.default.cyan("https://warengonzaga.com/sponsor")}`);
16353
+ console.log(` ${import_picocolors22.default.dim("Star or contribute:")} ${import_picocolors22.default.dim(linkify("gh.waren.build/contribute-now", "https://gh.waren.build/contribute-now"))}`);
16354
+ console.log(` ${import_picocolors22.default.dim("Sponsor:")} ${import_picocolors22.default.dim(linkify("warengonzaga.com/sponsor", "https://warengonzaga.com/sponsor"))}`);
15498
16355
  }
15499
16356
  console.log();
15500
16357
  }
16358
+ function truncateText3(text, maxWidth) {
16359
+ if (text.length <= maxWidth) {
16360
+ return text;
16361
+ }
16362
+ if (maxWidth <= 1) {
16363
+ return text.slice(0, maxWidth);
16364
+ }
16365
+ return `${text.slice(0, maxWidth - 1)}…`;
16366
+ }
16367
+ function linkify(label, url) {
16368
+ return `\x1B]8;;${url}\x07${label}\x1B]8;;\x07`;
16369
+ }
16370
+ function renderAnnouncements(announcements) {
16371
+ for (const announcement of announcements) {
16372
+ renderAnnouncementBanner(announcement);
16373
+ }
16374
+ }
16375
+ function renderAnnouncementBanner(announcement) {
16376
+ const terminalWidth = process.stdout.columns ?? 80;
16377
+ const contentWidth = Math.max(36, Math.min(terminalWidth - 8, 92));
16378
+ const tone = getAnnouncementTone(announcement.kind);
16379
+ const title = `${tone.emoji} ${announcement.title}`;
16380
+ const messageLines = wrapText(announcement.message, contentWidth);
16381
+ const lines = [title, ...messageLines];
16382
+ const rawWidth = Math.max(...lines.map((line) => line.length));
16383
+ console.log(` ${tone.border(`┌${"─".repeat(rawWidth + 2)}┐`)}`);
16384
+ for (const line of lines) {
16385
+ const trailing = " ".repeat(Math.max(0, rawWidth - line.length));
16386
+ const content = line === title ? tone.title(line) : import_picocolors22.default.dim(line);
16387
+ console.log(` ${tone.border("│")} ${content}${trailing} ${tone.border("│")}`);
16388
+ }
16389
+ console.log(` ${tone.border(`└${"─".repeat(rawWidth + 2)}┘`)}`);
16390
+ }
16391
+ function wrapText(text, maxWidth) {
16392
+ if (text.length <= maxWidth) {
16393
+ return [text];
16394
+ }
16395
+ const words = text.split(/\s+/);
16396
+ const lines = [];
16397
+ let current = "";
16398
+ for (const word of words) {
16399
+ const candidate = current ? `${current} ${word}` : word;
16400
+ if (candidate.length <= maxWidth) {
16401
+ current = candidate;
16402
+ continue;
16403
+ }
16404
+ if (current) {
16405
+ lines.push(current);
16406
+ current = word;
16407
+ } else {
16408
+ lines.push(word.slice(0, maxWidth));
16409
+ current = word.slice(maxWidth);
16410
+ }
16411
+ }
16412
+ if (current) {
16413
+ lines.push(current);
16414
+ }
16415
+ return lines;
16416
+ }
16417
+ function getAnnouncementTone(kind) {
16418
+ switch (kind) {
16419
+ case "info":
16420
+ return {
16421
+ emoji: "ℹ",
16422
+ border: import_picocolors22.default.blue,
16423
+ title: (value) => import_picocolors22.default.bold(import_picocolors22.default.blue(value))
16424
+ };
16425
+ case "warning":
16426
+ return {
16427
+ emoji: "\uD83D\uDEA8",
16428
+ border: import_picocolors22.default.red,
16429
+ title: (value) => import_picocolors22.default.bold(import_picocolors22.default.red(value))
16430
+ };
16431
+ default:
16432
+ return {
16433
+ emoji: "⚠",
16434
+ border: import_picocolors22.default.yellow,
16435
+ title: (value) => import_picocolors22.default.bold(import_picocolors22.default.yellow(value))
16436
+ };
16437
+ }
16438
+ }
15501
16439
 
15502
16440
  // src/index.ts
16441
+ function normalizeCliArgs(argv2) {
16442
+ return argv2.map((arg, index) => {
16443
+ const previous = argv2[index - 1];
16444
+ const isSubmitCommand = previous === "submit" || argv2.includes("submit");
16445
+ if (!isSubmitCommand) {
16446
+ return arg;
16447
+ }
16448
+ if (arg === "-pr" || arg === "--pr") {
16449
+ return "--pullrequest";
16450
+ }
16451
+ return arg;
16452
+ });
16453
+ }
16454
+ process.argv = normalizeCliArgs(process.argv);
15503
16455
  var isVersion = process.argv.includes("--version") || process.argv.includes("-v");
15504
16456
  if (!isVersion) {
15505
16457
  const subCommands = [