contribute-now 0.6.2-dev.12fb962 → 0.6.2-dev.1320af4

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 +1764 -914
  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
@@ -10168,11 +10491,11 @@ function suppressSubprocessWarnings() {
10168
10491
  process.env.NODE_NO_WARNINGS = "1";
10169
10492
  }
10170
10493
  function withTimeout(promise, ms) {
10171
- return new Promise((resolve2, reject) => {
10494
+ return new Promise((resolve3, reject) => {
10172
10495
  const timer = setTimeout(() => reject(new Error(`Copilot request timed out after ${ms / 1000}s`)), ms);
10173
10496
  promise.then((val) => {
10174
10497
  clearTimeout(timer);
10175
- resolve2(val);
10498
+ resolve3(val);
10176
10499
  }, (err) => {
10177
10500
  clearTimeout(timer);
10178
10501
  reject(err);
@@ -10183,10 +10506,84 @@ var COPILOT_TIMEOUT_MS = 30000;
10183
10506
  var COPILOT_LONG_TIMEOUT_MS = 90000;
10184
10507
  var BATCH_CONFIG = {
10185
10508
  LARGE_CHANGESET_THRESHOLD: 15,
10509
+ DIRECT_BATCH_THRESHOLD: 40,
10186
10510
  COMPACT_PER_FILE_CHARS: 300,
10187
10511
  MAX_COMPACT_PAYLOAD: 1e4,
10188
- FALLBACK_BATCH_SIZE: 15
10512
+ FALLBACK_BATCH_SIZE: 8
10189
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
+ }
10190
10587
  function parseDiffByFile(rawDiff) {
10191
10588
  const sections = new Map;
10192
10589
  const headerPattern = /^diff --git a\/(.+?) b\/(.+?)$/gm;
@@ -10254,7 +10651,7 @@ ${truncated}
10254
10651
  return result.length > maxTotalChars ? `${result.slice(0, maxTotalChars - 15)}
10255
10652
  ...(truncated)` : result;
10256
10653
  }
10257
- async function checkCopilotAvailable() {
10654
+ async function checkCopilotAvailable2() {
10258
10655
  try {
10259
10656
  const client = await getManagedClient();
10260
10657
  try {
@@ -10358,13 +10755,14 @@ function sanitizeGeneratedCommitMessage(message) {
10358
10755
  async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit", context) {
10359
10756
  try {
10360
10757
  const isLarge = stagedFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
10758
+ const hasMissingDiffCoverage = hasIncompleteDiffCoverage(stagedFiles, diff);
10361
10759
  const multiFileHint = stagedFiles.length > 1 ? `
10362
10760
 
10363
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.` : "";
10364
10762
  const squashHint = context === "squash-merge" ? `
10365
10763
 
10366
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.` : "";
10367
- const diffContent = isLarge ? createCompactDiff(stagedFiles, diff) : diff.slice(0, 4000);
10765
+ const diffContent = isLarge || hasMissingDiffCoverage ? createCompactDiff(stagedFiles, diff) : diff.slice(0, 4000);
10368
10766
  const userMessage = `Generate a commit message for these staged changes:
10369
10767
 
10370
10768
  Files (${stagedFiles.length}): ${stagedFiles.join(", ")}
@@ -10453,9 +10851,15 @@ function normalizeCommitGroups(changedFiles, groups) {
10453
10851
  unassignedFiles
10454
10852
  };
10455
10853
  }
10456
- async function generateCommitGroups(files, diffs, model, convention = "clean-commit") {
10854
+ async function generateCommitGroups(files, diffs, model, convention = "clean-commit", onProgress) {
10457
10855
  const isLarge = files.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
10458
- 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);
10459
10863
  const largeHint = isLarge ? `
10460
10864
 
10461
10865
  NOTE: This is a large changeset (${files.length} files). Compact diffs are provided for every file. Focus on creating well-organized logical groups.` : "";
@@ -10467,10 +10871,22 @@ ${files.join(`
10467
10871
 
10468
10872
  Diffs:
10469
10873
  ${diffContent}${largeHint}`;
10470
- 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
+ }
10471
10885
  if (!result) {
10472
- if (isLarge)
10473
- 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
+ }
10474
10890
  throw new Error("AI returned an empty response");
10475
10891
  }
10476
10892
  const cleaned = extractJson(result);
@@ -10478,14 +10894,18 @@ ${diffContent}${largeHint}`;
10478
10894
  try {
10479
10895
  parsed = JSON.parse(cleaned);
10480
10896
  } catch {
10481
- if (isLarge)
10482
- 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
+ }
10483
10901
  throw new Error(`AI response is not valid JSON. Raw start: "${result.slice(0, 120)}..."`);
10484
10902
  }
10485
10903
  const groups = parsed;
10486
10904
  if (!Array.isArray(groups) || groups.length === 0) {
10487
- if (isLarge)
10488
- 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
+ }
10489
10909
  throw new Error("AI response was not a valid JSON array of commit groups");
10490
10910
  }
10491
10911
  for (const group of groups) {
@@ -10498,17 +10918,18 @@ ${diffContent}${largeHint}`;
10498
10918
  message: sanitizeGeneratedCommitMessage(group.message)
10499
10919
  }));
10500
10920
  }
10501
- async function generateCommitGroupsInBatches(files, diffs, model, convention = "clean-commit") {
10921
+ async function generateCommitGroupsInBatches(files, diffs, model, convention = "clean-commit", onProgress) {
10502
10922
  const batchSize = BATCH_CONFIG.FALLBACK_BATCH_SIZE;
10503
10923
  const allGroups = [];
10504
10924
  const diffSections = parseDiffByFile(diffs);
10925
+ const totalBatches = Math.ceil(files.length / batchSize);
10505
10926
  for (let i2 = 0;i2 < files.length; i2 += batchSize) {
10506
10927
  const batchFiles = files.slice(i2, i2 + batchSize);
10507
10928
  const batchDiff = batchFiles.map((f3) => diffSections.get(f3) ?? "").filter(Boolean).join(`
10508
10929
  `);
10509
- 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);
10510
10931
  const batchNum = Math.floor(i2 / batchSize) + 1;
10511
- const totalBatches = Math.ceil(files.length / batchSize);
10932
+ onProgress?.(`Grouping batch ${batchNum}/${totalBatches} (${batchFiles.length} files)...`);
10512
10933
  const userMessage = `Group these changed files into logical atomic commits:
10513
10934
 
10514
10935
  Files:
@@ -10545,10 +10966,7 @@ NOTE: Processing batch ${batchNum}/${totalBatches} of a large changeset. Group o
10545
10966
  const groupedFiles = new Set(allGroups.flatMap((g3) => g3.files));
10546
10967
  const ungrouped = files.filter((f3) => !groupedFiles.has(f3));
10547
10968
  if (ungrouped.length > 0) {
10548
- allGroups.push({
10549
- files: ungrouped,
10550
- message: `chore: update ${ungrouped.length} remaining file${ungrouped.length !== 1 ? "s" : ""}`
10551
- });
10969
+ allGroups.push(...createRecoveryCommitGroups(ungrouped, convention));
10552
10970
  }
10553
10971
  if (allGroups.length === 0) {
10554
10972
  throw new Error("AI could not group any files even with batch processing");
@@ -10601,56 +11019,299 @@ ${diffContent}`;
10601
11019
  }
10602
11020
  }
10603
11021
 
10604
- // src/utils/gh.ts
10605
- import { execFile as execFileCb2 } from "node:child_process";
10606
- function run2(args) {
10607
- return new Promise((resolve2) => {
10608
- execFileCb2("gh", args, (error2, stdout2, stderr) => {
10609
- resolve2({
10610
- exitCode: error2 ? error2.code === "ENOENT" ? 127 : error2.status ?? 1 : 0,
10611
- stdout: stdout2 ?? "",
10612
- stderr: stderr ?? ""
10613
- });
10614
- });
10615
- });
10616
- }
10617
- async function checkGhInstalled() {
10618
- try {
10619
- const { exitCode } = await run2(["--version"]);
10620
- return exitCode === 0;
10621
- } catch {
10622
- return false;
10623
- }
10624
- }
10625
- async function checkGhAuth() {
10626
- try {
10627
- const { exitCode } = await run2(["auth", "status"]);
10628
- return exitCode === 0;
10629
- } catch {
10630
- 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 [];
10631
11030
  }
10632
- }
10633
- var SAFE_SLUG = /^[\w.-]+$/;
10634
- async function checkRepoPermissions(owner, repo) {
10635
- if (!SAFE_SLUG.test(owner) || !SAFE_SLUG.test(repo))
10636
- return null;
10637
- const { exitCode, stdout: stdout2 } = await run2(["api", `repos/${owner}/${repo}`, "--jq", ".permissions"]);
10638
- if (exitCode !== 0)
10639
- return null;
10640
- try {
10641
- return JSON.parse(stdout2.trim());
10642
- } catch {
10643
- 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];
10644
11036
  }
11037
+ const secondary = truncateText2(normalizedTip, Math.max(MIN_LINE_WIDTH, maxWidth - 2));
11038
+ return [primary, secondary];
10645
11039
  }
10646
- async function isRepoFork() {
10647
- const { exitCode, stdout: stdout2 } = await run2(["repo", "view", "--json", "isFork", "-q", ".isFork"]);
10648
- if (exitCode !== 0)
10649
- return null;
10650
- const val = stdout2.trim();
10651
- if (val === "true")
10652
- return true;
10653
- if (val === "false")
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")
11313
+ return true;
11314
+ if (val === "false")
10654
11315
  return false;
10655
11316
  return null;
10656
11317
  }
@@ -10739,53 +11400,6 @@ async function getMergedPRForBranch(headBranch) {
10739
11400
  }
10740
11401
  }
10741
11402
 
10742
- // src/utils/spinner.ts
10743
- var import_picocolors6 = __toESM(require_picocolors(), 1);
10744
- var FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
10745
- function createSpinner(text) {
10746
- let frameIdx = 0;
10747
- let currentText = text;
10748
- let stopped = false;
10749
- const clearLine = () => {
10750
- process.stderr.write("\r\x1B[K");
10751
- };
10752
- const render = () => {
10753
- if (stopped)
10754
- return;
10755
- const frame = import_picocolors6.default.cyan(FRAMES[frameIdx % FRAMES.length]);
10756
- clearLine();
10757
- process.stderr.write(`${frame} ${currentText}`);
10758
- frameIdx++;
10759
- };
10760
- const timer = setInterval(render, 80);
10761
- render();
10762
- const stop = () => {
10763
- if (stopped)
10764
- return;
10765
- stopped = true;
10766
- clearInterval(timer);
10767
- clearLine();
10768
- };
10769
- return {
10770
- update(newText) {
10771
- currentText = newText;
10772
- },
10773
- success(msg) {
10774
- stop();
10775
- process.stderr.write(`${import_picocolors6.default.green("✔")} ${msg}
10776
- `);
10777
- },
10778
- fail(msg) {
10779
- stop();
10780
- process.stderr.write(`${import_picocolors6.default.red("✖")} ${msg}
10781
- `);
10782
- },
10783
- stop() {
10784
- stop();
10785
- }
10786
- };
10787
- }
10788
-
10789
11403
  // src/commands/clean.ts
10790
11404
  async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
10791
11405
  if (!config)
@@ -10798,44 +11412,22 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
10798
11412
  warn("You have uncommitted changes in your working tree.");
10799
11413
  }
10800
11414
  if (localWork.unpushedCommits > 0) {
10801
- 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.`);
10802
11416
  }
10803
11417
  const SAVE_NEW_BRANCH = "Save changes to a new branch";
10804
11418
  const DISCARD = "Discard all changes and clean up";
10805
11419
  const CANCEL = "Skip this branch";
10806
- 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]);
10807
11421
  if (action === CANCEL)
10808
11422
  return "skipped";
10809
11423
  if (action === SAVE_NEW_BRANCH) {
10810
11424
  if (!config)
10811
11425
  return "skipped";
10812
- info(import_picocolors7.default.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
10813
- const description = await inputPrompt("What are you going to work on?");
10814
- let newBranchName = description;
10815
- if (looksLikeNaturalLanguage(description)) {
10816
- const spinner = createSpinner("Generating branch name suggestion...");
10817
- const suggested = await suggestBranchName(description);
10818
- if (suggested) {
10819
- spinner.success("Branch name suggestion ready.");
10820
- console.log(`
10821
- ${import_picocolors7.default.dim("AI suggestion:")} ${import_picocolors7.default.bold(import_picocolors7.default.cyan(suggested))}`);
10822
- const accepted = await confirmPrompt(`Use ${import_picocolors7.default.bold(suggested)} as your branch name?`);
10823
- newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
10824
- } else {
10825
- spinner.fail("AI did not return a suggestion.");
10826
- newBranchName = await inputPrompt("Enter branch name", description);
10827
- }
10828
- }
10829
- if (!hasPrefix(newBranchName, config.branchPrefixes)) {
10830
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors7.default.bold(newBranchName)}:`, config.branchPrefixes);
10831
- newBranchName = formatBranchName(prefix, newBranchName);
10832
- }
10833
- if (!isValidBranchName(newBranchName)) {
10834
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
10835
- return "skipped";
10836
- }
10837
- if (await branchExists(newBranchName)) {
10838
- 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) {
10839
11431
  return "skipped";
10840
11432
  }
10841
11433
  const renameResult = await renameBranch(currentBranch, newBranchName);
@@ -10843,7 +11435,7 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
10843
11435
  error(`Failed to rename branch: ${renameResult.stderr}`);
10844
11436
  return "skipped";
10845
11437
  }
10846
- 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)}`);
10847
11439
  const syncSource2 = getSyncSource(config);
10848
11440
  await fetchRemote(syncSource2.remote);
10849
11441
  const savedUpstreamRef = await getUpstreamRef();
@@ -10851,10 +11443,10 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
10851
11443
  if (rebaseResult.exitCode !== 0) {
10852
11444
  await rebaseAbort();
10853
11445
  warn("Rebase had conflicts — aborted to keep the repo in a clean state.");
10854
- info(`Your work is saved on ${import_picocolors7.default.bold(newBranchName)}. After cleanup, rebase manually:`, "");
10855
- 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}`)}`, "");
10856
11448
  } else {
10857
- 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)}.`);
10858
11450
  }
10859
11451
  const coResult2 = await checkoutBranch(baseBranch);
10860
11452
  if (coResult2.exitCode !== 0) {
@@ -10862,12 +11454,12 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
10862
11454
  return "saved";
10863
11455
  }
10864
11456
  await updateLocalBranch(baseBranch, syncSource2.ref);
10865
- 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)}.`);
10866
11458
  return "saved";
10867
11459
  }
10868
11460
  }
10869
11461
  const syncSource = getSyncSource(config);
10870
- info(`Switching to ${import_picocolors7.default.bold(baseBranch)} and syncing...`);
11462
+ info(`Switching to ${import_picocolors8.default.bold(baseBranch)} and syncing...`);
10871
11463
  await fetchRemote(syncSource.remote);
10872
11464
  await resetHard("HEAD");
10873
11465
  const coResult = await checkoutBranch(baseBranch);
@@ -10876,7 +11468,7 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
10876
11468
  return "skipped";
10877
11469
  }
10878
11470
  await updateLocalBranch(baseBranch, syncSource.ref);
10879
- 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)}.`);
10880
11472
  return "switched";
10881
11473
  }
10882
11474
  var clean_default = defineCommand({
@@ -10900,13 +11492,13 @@ var clean_default = defineCommand({
10900
11492
  await assertCleanGitState("cleaning");
10901
11493
  const config = readConfig();
10902
11494
  if (!config) {
10903
- error("No .contributerc.json found. Run `contrib setup` first.");
11495
+ error("No repo config found. Run `contrib setup` first.");
10904
11496
  process.exit(1);
10905
11497
  }
10906
11498
  const { origin } = config;
10907
11499
  const baseBranch = getBaseBranch(config);
10908
11500
  let currentBranch = await getCurrentBranch();
10909
- heading("\uD83E\uDDF9 contrib clean");
11501
+ projectHeading("clean", "\uD83E\uDDF9");
10910
11502
  info(`Pruning ${origin} remote refs...`);
10911
11503
  const pruneResult = await pruneRemote(origin);
10912
11504
  if (pruneResult.exitCode === 0) {
@@ -10926,21 +11518,21 @@ var clean_default = defineCommand({
10926
11518
  if (ghInstalled && ghAuthed) {
10927
11519
  const mergedPR = await getMergedPRForBranch(currentBranch);
10928
11520
  if (mergedPR) {
10929
- warn(`PR #${mergedPR.number} (${import_picocolors7.default.bold(mergedPR.title)}) has already been merged.`);
10930
- 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)}`, "");
10931
11523
  goneCandidates.push(currentBranch);
10932
11524
  }
10933
11525
  }
10934
11526
  }
10935
11527
  if (mergedCandidates.length > 0) {
10936
11528
  console.log(`
10937
- ${import_picocolors7.default.bold("Merged branches to delete:")}`);
11529
+ ${import_picocolors8.default.bold("Merged branches to delete:")}`);
10938
11530
  for (const b2 of mergedCandidates) {
10939
- const marker = b2 === currentBranch ? import_picocolors7.default.yellow(" (current)") : "";
10940
- 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}`);
10941
11533
  }
10942
11534
  console.log();
10943
- 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" : ""}?`);
10944
11536
  if (ok) {
10945
11537
  for (const branch of mergedCandidates) {
10946
11538
  if (branch === currentBranch) {
@@ -10957,7 +11549,7 @@ ${import_picocolors7.default.bold("Merged branches to delete:")}`);
10957
11549
  }
10958
11550
  const result = await deleteBranch(branch);
10959
11551
  if (result.exitCode === 0) {
10960
- success(` Deleted ${import_picocolors7.default.bold(branch)}`);
11552
+ success(` Deleted ${import_picocolors8.default.bold(branch)}`);
10961
11553
  } else {
10962
11554
  warn(` Failed to delete ${branch}: ${result.stderr.trim()}`);
10963
11555
  }
@@ -10968,13 +11560,13 @@ ${import_picocolors7.default.bold("Merged branches to delete:")}`);
10968
11560
  }
10969
11561
  if (goneCandidates.length > 0) {
10970
11562
  console.log(`
10971
- ${import_picocolors7.default.bold("Stale branches (remote deleted, likely squash-merged):")}`);
11563
+ ${import_picocolors8.default.bold("Stale branches (remote deleted, likely squash-merged):")}`);
10972
11564
  for (const b2 of goneCandidates) {
10973
- const marker = b2 === currentBranch ? import_picocolors7.default.yellow(" (current)") : "";
10974
- 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}`);
10975
11567
  }
10976
11568
  console.log();
10977
- 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" : ""}?`);
10978
11570
  if (ok) {
10979
11571
  for (const branch of goneCandidates) {
10980
11572
  if (branch === currentBranch) {
@@ -10991,7 +11583,7 @@ ${import_picocolors7.default.bold("Stale branches (remote deleted, likely squash
10991
11583
  }
10992
11584
  const result = await forceDeleteBranch(branch);
10993
11585
  if (result.exitCode === 0) {
10994
- success(` Deleted ${import_picocolors7.default.bold(branch)}`);
11586
+ success(` Deleted ${import_picocolors8.default.bold(branch)}`);
10995
11587
  } else {
10996
11588
  warn(` Failed to delete ${branch}: ${result.stderr.trim()}`);
10997
11589
  }
@@ -11006,13 +11598,13 @@ ${import_picocolors7.default.bold("Stale branches (remote deleted, likely squash
11006
11598
  const finalBranch = await getCurrentBranch();
11007
11599
  if (finalBranch && protectedBranches.has(finalBranch)) {
11008
11600
  console.log();
11009
- 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.`);
11010
11602
  }
11011
11603
  }
11012
11604
  });
11013
11605
 
11014
11606
  // src/commands/commit.ts
11015
- var import_picocolors8 = __toESM(require_picocolors(), 1);
11607
+ var import_picocolors9 = __toESM(require_picocolors(), 1);
11016
11608
 
11017
11609
  // src/utils/convention.ts
11018
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;
@@ -11066,6 +11658,9 @@ function getValidationError(convention) {
11066
11658
  }
11067
11659
 
11068
11660
  // src/commands/commit.ts
11661
+ function isEmptyGroupCommitResult(detail) {
11662
+ return /no changes added to commit|nothing to commit/i.test(detail);
11663
+ }
11069
11664
  var commit_default = defineCommand({
11070
11665
  meta: {
11071
11666
  name: "commit",
@@ -11095,11 +11690,16 @@ var commit_default = defineCommand({
11095
11690
  await assertCleanGitState("committing");
11096
11691
  const config = readConfig();
11097
11692
  if (!config) {
11098
- error("No .contributerc.json found. Run `contrib setup` first.");
11693
+ error("No repo config found. Run `contrib setup` first.");
11099
11694
  process.exit(1);
11100
11695
  }
11101
- heading("\uD83D\uDCBE contrib commit");
11696
+ projectHeading("commit", "\uD83D\uDCBE");
11697
+ const aiEnabled = isAIEnabled(config, args["no-ai"]);
11102
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
+ }
11103
11703
  await runGroupCommit(args.model, config);
11104
11704
  return;
11105
11705
  }
@@ -11111,9 +11711,9 @@ var commit_default = defineCommand({
11111
11711
  process.exit(1);
11112
11712
  }
11113
11713
  console.log(`
11114
- ${import_picocolors8.default.bold("Changed files:")}`);
11714
+ ${import_picocolors9.default.bold("Changed files:")}`);
11115
11715
  for (const f3 of changedFiles) {
11116
- console.log(` ${import_picocolors8.default.dim("•")} ${f3}`);
11716
+ console.log(` ${import_picocolors9.default.dim("•")} ${f3}`);
11117
11717
  }
11118
11718
  const stageAction = await selectPrompt("No staged changes. How would you like to stage?", [
11119
11719
  "Stage all changes",
@@ -11155,8 +11755,8 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11155
11755
  const dirs = new Set(stagedFiles.map((f3) => f3.split("/")[0]));
11156
11756
  if (dirs.size > 1) {
11157
11757
  console.log();
11158
- 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.`);
11159
- 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."));
11160
11760
  const choice = await selectPrompt("How would you like to proceed?", [
11161
11761
  "Continue as single commit",
11162
11762
  "Switch to group mode (AI splits into atomic commits)",
@@ -11172,20 +11772,22 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11172
11772
  }
11173
11773
  }
11174
11774
  let commitMessage = null;
11175
- const useAI = !args["no-ai"];
11775
+ const useAI = aiEnabled;
11176
11776
  if (useAI) {
11177
- const [copilotError, diff] = await Promise.all([checkCopilotAvailable(), getStagedDiff()]);
11777
+ const [copilotError, diff] = await Promise.all([checkCopilotAvailable2(), getStagedDiff()]);
11178
11778
  if (copilotError) {
11179
11779
  warn(`AI unavailable: ${copilotError}`);
11180
11780
  warn("Falling back to manual commit message entry.");
11181
11781
  } else {
11182
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...";
11183
- const spinner = createSpinner(spinnerMsg);
11783
+ const spinner = createSpinner(spinnerMsg, {
11784
+ tips: LOADING_TIPS
11785
+ });
11184
11786
  commitMessage = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
11185
11787
  if (commitMessage) {
11186
11788
  spinner.success("AI commit message generated.");
11187
11789
  console.log(`
11188
- ${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))}`);
11189
11791
  } else {
11190
11792
  spinner.fail("AI did not return a commit message.");
11191
11793
  warn("Falling back to manual entry.");
@@ -11205,13 +11807,15 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11205
11807
  } else if (action === "Edit this message") {
11206
11808
  finalMessage = await inputPrompt("Edit commit message", commitMessage);
11207
11809
  } else if (action === "Regenerate") {
11208
- const spinner = createSpinner("Regenerating commit message...");
11810
+ const spinner = createSpinner("Regenerating commit message...", {
11811
+ tips: LOADING_TIPS
11812
+ });
11209
11813
  const diff = await getStagedDiff();
11210
11814
  const regen = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
11211
11815
  if (regen) {
11212
11816
  spinner.success("Commit message regenerated.");
11213
11817
  console.log(`
11214
- ${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))}`);
11215
11819
  const ok = await confirmPrompt("Use this message?");
11216
11820
  finalMessage = ok ? regen : await inputPrompt("Enter commit message manually");
11217
11821
  } else {
@@ -11226,7 +11830,7 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11226
11830
  if (convention2 !== "none") {
11227
11831
  console.log();
11228
11832
  for (const hint of CONVENTION_FORMAT_HINTS[convention2]) {
11229
- console.log(import_picocolors8.default.dim(hint));
11833
+ console.log(import_picocolors9.default.dim(hint));
11230
11834
  }
11231
11835
  console.log();
11232
11836
  }
@@ -11250,21 +11854,12 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11250
11854
  error(`Failed to commit: ${result.stderr}`);
11251
11855
  process.exit(1);
11252
11856
  }
11253
- success(`Committed: ${import_picocolors8.default.bold(finalMessage)}`);
11857
+ success(`Committed: ${import_picocolors9.default.bold(finalMessage)}`);
11254
11858
  }
11255
11859
  });
11256
- function getFallbackGroupMessage(convention) {
11257
- if (convention === "conventional") {
11258
- return "chore: commit remaining changes";
11259
- }
11260
- if (convention === "clean-commit") {
11261
- return "☕ chore: commit remaining changes";
11262
- }
11263
- return "commit remaining changes";
11264
- }
11265
11860
  async function runGroupCommit(model, config) {
11266
11861
  const [copilotError, changedFiles] = await Promise.all([
11267
- checkCopilotAvailable(),
11862
+ checkCopilotAvailable2(),
11268
11863
  getChangedFiles()
11269
11864
  ]);
11270
11865
  if (copilotError) {
@@ -11276,11 +11871,13 @@ async function runGroupCommit(model, config) {
11276
11871
  process.exit(1);
11277
11872
  }
11278
11873
  console.log(`
11279
- ${import_picocolors8.default.bold("Changed files:")}`);
11874
+ ${import_picocolors9.default.bold("Changed files:")}`);
11280
11875
  for (const f3 of changedFiles) {
11281
- console.log(` ${import_picocolors8.default.dim("•")} ${f3}`);
11876
+ console.log(` ${import_picocolors9.default.dim("•")} ${f3}`);
11282
11877
  }
11283
- 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
+ });
11284
11881
  const diffs = await getFullDiffForFiles(changedFiles);
11285
11882
  if (!diffs.trim()) {
11286
11883
  spinner.stop();
@@ -11288,7 +11885,7 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11288
11885
  }
11289
11886
  let groups;
11290
11887
  try {
11291
- groups = await generateCommitGroups(changedFiles, diffs, model, config.commitConvention);
11888
+ groups = await generateCommitGroups(changedFiles, diffs, model, config.commitConvention, (message) => spinner.update(message));
11292
11889
  spinner.success(`AI generated ${groups.length} commit group(s).`);
11293
11890
  } catch (err) {
11294
11891
  const reason = err instanceof Error ? err.message : String(err);
@@ -11308,14 +11905,10 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11308
11905
  }
11309
11906
  let validGroups = normalized.groups;
11310
11907
  if (normalized.unassignedFiles.length > 0) {
11311
- warn(`AI left ${normalized.unassignedFiles.length} file(s) ungrouped: ${normalized.unassignedFiles.join(", ")}. Creating a fallback group.`);
11312
- const fallbackMessage = await regenerateGroupMessage(normalized.unassignedFiles, diffs, model, config.commitConvention) ?? getFallbackGroupMessage(config.commitConvention);
11908
+ warn(`AI left ${normalized.unassignedFiles.length} file(s) ungrouped: ${normalized.unassignedFiles.join(", ")}. Auto-resolving recovery groups.`);
11313
11909
  validGroups = [
11314
11910
  ...validGroups,
11315
- {
11316
- files: normalized.unassignedFiles,
11317
- message: fallbackMessage
11318
- }
11911
+ ...createRecoveryCommitGroups(normalized.unassignedFiles, config.commitConvention)
11319
11912
  ];
11320
11913
  }
11321
11914
  if (validGroups.length === 0) {
@@ -11326,13 +11919,13 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11326
11919
  let commitAll = false;
11327
11920
  while (!proceedToCommit) {
11328
11921
  console.log(`
11329
- ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit group(s):`)}
11922
+ ${import_picocolors9.default.bold(`AI suggested ${validGroups.length} commit group(s):`)}
11330
11923
  `);
11331
11924
  for (let i2 = 0;i2 < validGroups.length; i2++) {
11332
11925
  const g3 = validGroups[i2];
11333
- 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)}`);
11334
11927
  for (const f3 of g3.files) {
11335
- console.log(` ${import_picocolors8.default.dim("•")} ${f3}`);
11928
+ console.log(` ${import_picocolors9.default.dim("•")} ${f3}`);
11336
11929
  }
11337
11930
  console.log();
11338
11931
  }
@@ -11347,7 +11940,9 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11347
11940
  process.exit(0);
11348
11941
  }
11349
11942
  if (summaryAction === "Regenerate all messages") {
11350
- const regenSpinner = createSpinner("Regenerating all commit messages...");
11943
+ const regenSpinner = createSpinner("Regenerating all commit messages...", {
11944
+ tips: LOADING_TIPS
11945
+ });
11351
11946
  try {
11352
11947
  validGroups = await regenerateAllGroupMessages(validGroups, diffs, model, config.commitConvention);
11353
11948
  regenSpinner.success("All commit messages regenerated.");
@@ -11378,24 +11973,34 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11378
11973
  error(`Failed to stage group ${i2 + 1}: ${stageResult.stderr}`);
11379
11974
  continue;
11380
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
+ }
11381
11982
  const commitResult = await commitWithMessage(group.message);
11382
11983
  if (commitResult.exitCode !== 0) {
11383
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
+ }
11384
11989
  error(`Failed to commit group ${i2 + 1}: ${detail}`);
11385
11990
  await unstageFiles(stageableFiles);
11386
11991
  continue;
11387
11992
  }
11388
11993
  committed++;
11389
- success(`Committed group ${i2 + 1}: ${import_picocolors8.default.bold(group.message)}`);
11994
+ success(`Committed group ${i2 + 1}: ${import_picocolors9.default.bold(group.message)}`);
11390
11995
  }
11391
11996
  } else {
11392
11997
  for (let i2 = 0;i2 < validGroups.length; i2++) {
11393
11998
  const group = validGroups[i2];
11394
- console.log(import_picocolors8.default.bold(`
11999
+ console.log(import_picocolors9.default.bold(`
11395
12000
  ── Group ${i2 + 1}/${validGroups.length} ──`));
11396
- console.log(` ${import_picocolors8.default.cyan(group.message)}`);
12001
+ console.log(` ${import_picocolors9.default.cyan(group.message)}`);
11397
12002
  for (const f3 of group.files) {
11398
- console.log(` ${import_picocolors8.default.dim("•")} ${f3}`);
12003
+ console.log(` ${import_picocolors9.default.dim("•")} ${f3}`);
11399
12004
  }
11400
12005
  let message = group.message;
11401
12006
  let actionDone = false;
@@ -11412,12 +12017,14 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11412
12017
  continue;
11413
12018
  }
11414
12019
  if (action === "Regenerate message") {
11415
- const regenSpinner = createSpinner("Regenerating commit message for this group...");
12020
+ const regenSpinner = createSpinner("Regenerating commit message for this group...", {
12021
+ tips: LOADING_TIPS
12022
+ });
11416
12023
  const newMsg = await regenerateGroupMessage(group.files, diffs, model, config.commitConvention);
11417
12024
  if (newMsg) {
11418
12025
  message = newMsg;
11419
12026
  group.message = newMsg;
11420
- regenSpinner.success(`New message: ${import_picocolors8.default.bold(message)}`);
12027
+ regenSpinner.success(`New message: ${import_picocolors9.default.bold(message)}`);
11421
12028
  } else {
11422
12029
  regenSpinner.fail("AI could not generate a new message. Keeping current one.");
11423
12030
  }
@@ -11459,16 +12066,28 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11459
12066
  actionDone = true;
11460
12067
  continue;
11461
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
+ }
11462
12076
  const commitResult = await commitWithMessage(message);
11463
12077
  if (commitResult.exitCode !== 0) {
11464
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
+ }
11465
12084
  error(`Failed to commit group ${i2 + 1}: ${detail}`);
11466
12085
  await unstageFiles(stageableFiles);
11467
12086
  actionDone = true;
11468
12087
  continue;
11469
12088
  }
11470
12089
  committed++;
11471
- success(`Committed group ${i2 + 1}: ${import_picocolors8.default.bold(message)}`);
12090
+ success(`Committed group ${i2 + 1}: ${import_picocolors9.default.bold(message)}`);
11472
12091
  actionDone = true;
11473
12092
  }
11474
12093
  }
@@ -11484,11 +12103,11 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11484
12103
 
11485
12104
  // src/commands/doctor.ts
11486
12105
  import { execFile as execFileCb3 } from "node:child_process";
11487
- var import_picocolors9 = __toESM(require_picocolors(), 1);
12106
+ var import_picocolors10 = __toESM(require_picocolors(), 1);
11488
12107
  // package.json
11489
12108
  var package_default = {
11490
12109
  name: "contribute-now",
11491
- version: "0.6.2-dev.12fb962",
12110
+ version: "0.6.2-dev.1320af4",
11492
12111
  description: "Developer CLI that automates git workflows — branching, syncing, committing, and PRs — with multi-workflow and commit convention support.",
11493
12112
  type: "module",
11494
12113
  bin: {
@@ -11579,16 +12198,16 @@ async function getRepoInfoFromRemote(remote = "origin") {
11579
12198
  }
11580
12199
 
11581
12200
  // src/commands/doctor.ts
11582
- var PASS = ` ${import_picocolors9.default.green("✔")} `;
11583
- var FAIL = ` ${import_picocolors9.default.red("✗")} `;
11584
- 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("⚠")} `;
11585
12204
  function printReport(report) {
11586
12205
  for (const section of report.sections) {
11587
12206
  console.log(`
11588
- ${import_picocolors9.default.bold(import_picocolors9.default.underline(section.title))}`);
12207
+ ${import_picocolors10.default.bold(import_picocolors10.default.underline(section.title))}`);
11589
12208
  for (const check of section.checks) {
11590
12209
  const prefix = check.ok ? check.warning ? WARN : PASS : FAIL;
11591
- 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;
11592
12211
  console.log(`${prefix}${text}`);
11593
12212
  }
11594
12213
  }
@@ -11606,9 +12225,9 @@ function toJson(report) {
11606
12225
  })), null, 2);
11607
12226
  }
11608
12227
  function runCmd(cmd, args) {
11609
- return new Promise((resolve2) => {
12228
+ return new Promise((resolve3) => {
11610
12229
  execFileCb3(cmd, args, (error2, stdout2) => {
11611
- resolve2({
12230
+ resolve3({
11612
12231
  ok: !error2,
11613
12232
  stdout: (stdout2 ?? "").trim()
11614
12233
  });
@@ -11670,18 +12289,32 @@ async function configSection() {
11670
12289
  const exists = configExists();
11671
12290
  if (!exists) {
11672
12291
  checks.push({
11673
- label: ".contributerc.json not found",
12292
+ label: "Repo config not found",
11674
12293
  ok: false,
11675
- detail: "run `contrib setup` to create it"
12294
+ detail: "run `contrib setup` to create local config for this clone"
11676
12295
  });
11677
12296
  return { title: "Config", checks };
11678
12297
  }
11679
12298
  const config = readConfig();
11680
12299
  if (!config) {
11681
- checks.push({ label: ".contributerc.json found but invalid", ok: false });
12300
+ checks.push({ label: "Repo config found but invalid", ok: false });
11682
12301
  return { title: "Config", checks };
11683
12302
  }
11684
- 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
+ }
11685
12318
  const desc = WORKFLOW_DESCRIPTIONS[config.workflow] ?? config.workflow;
11686
12319
  checks.push({
11687
12320
  label: `Workflow: ${config.workflow}`,
@@ -11696,13 +12329,21 @@ async function configSection() {
11696
12329
  ok: !!config.devBranch
11697
12330
  });
11698
12331
  }
11699
- const ignored = isGitignored();
11700
- checks.push({
11701
- label: ignored ? ".contributerc.json in .gitignore" : ".contributerc.json NOT in .gitignore",
11702
- ok: true,
11703
- warning: !ignored,
11704
- detail: ignored ? undefined : "consider adding it to .gitignore"
11705
- });
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
+ }
11706
12347
  return { title: "Config", checks };
11707
12348
  }
11708
12349
  async function gitSection() {
@@ -11851,20 +12492,20 @@ var doctor_default = defineCommand({
11851
12492
  console.log(toJson(report));
11852
12493
  return;
11853
12494
  }
11854
- heading("\uD83E\uDE7A contribute-now doctor");
12495
+ projectHeading("doctor", "\uD83E\uDE7A");
11855
12496
  printReport(report);
11856
12497
  const total = report.sections.flatMap((s2) => s2.checks);
11857
12498
  const failures = total.filter((c3) => !c3.ok);
11858
12499
  const warnings = total.filter((c3) => c3.ok && c3.warning);
11859
12500
  if (failures.length === 0 && warnings.length === 0) {
11860
- 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.
11861
12502
  `);
11862
12503
  } else {
11863
12504
  if (failures.length > 0) {
11864
- 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.`)}`);
11865
12506
  }
11866
12507
  if (warnings.length > 0) {
11867
- 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" : ""}.`)}`);
11868
12509
  }
11869
12510
  console.log();
11870
12511
  }
@@ -11872,9 +12513,9 @@ var doctor_default = defineCommand({
11872
12513
  });
11873
12514
 
11874
12515
  // src/commands/hook.ts
11875
- 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";
11876
12517
  import { join as join4 } from "node:path";
11877
- var import_picocolors10 = __toESM(require_picocolors(), 1);
12518
+ var import_picocolors11 = __toESM(require_picocolors(), 1);
11878
12519
  var HOOK_MARKER = "# managed by contribute-now";
11879
12520
  function getHooksDir(cwd = process.cwd()) {
11880
12521
  return join4(cwd, ".git", "hooks");
@@ -11942,10 +12583,10 @@ var hook_default = defineCommand({
11942
12583
  }
11943
12584
  });
11944
12585
  async function installHook() {
11945
- heading("\uD83E\uDE9D hook install");
12586
+ projectHeading("hook install", "\uD83E\uDE9D");
11946
12587
  const config = readConfig();
11947
12588
  if (!config) {
11948
- error("No .contributerc.json found. Run `contrib setup` first.");
12589
+ error("No repo config found. Run `contrib setup` first.");
11949
12590
  process.exit(1);
11950
12591
  }
11951
12592
  if (config.commitConvention === "none") {
@@ -11966,16 +12607,16 @@ async function installHook() {
11966
12607
  info("Updating existing contribute-now hook...");
11967
12608
  }
11968
12609
  if (!existsSync4(hooksDir)) {
11969
- mkdirSync2(hooksDir, { recursive: true });
12610
+ mkdirSync3(hooksDir, { recursive: true });
11970
12611
  }
11971
12612
  writeFileSync3(hookPath, generateHookScript(), { mode: 493 });
11972
12613
  success(`commit-msg hook installed.`);
11973
- info(`Convention: ${import_picocolors10.default.bold(CONVENTION_LABELS[config.commitConvention])}`, "");
11974
- 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)}`, "");
11975
12616
  warn("Note: hooks can be bypassed with `git commit --no-verify`.");
11976
12617
  }
11977
12618
  async function uninstallHook() {
11978
- heading("\uD83E\uDE9D hook uninstall");
12619
+ projectHeading("hook uninstall", "\uD83E\uDE9D");
11979
12620
  const hookPath = getHookPath();
11980
12621
  if (!existsSync4(hookPath)) {
11981
12622
  info("No commit-msg hook found. Nothing to uninstall.");
@@ -11991,7 +12632,10 @@ async function uninstallHook() {
11991
12632
  }
11992
12633
 
11993
12634
  // src/commands/log.ts
11994
- 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
+ }
11995
12639
  var log_default = defineCommand({
11996
12640
  meta: {
11997
12641
  name: "log",
@@ -12012,7 +12656,13 @@ var log_default = defineCommand({
12012
12656
  remote: {
12013
12657
  type: "boolean",
12014
12658
  alias: "r",
12015
- 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",
12016
12666
  default: false
12017
12667
  },
12018
12668
  full: {
@@ -12039,12 +12689,15 @@ var log_default = defineCommand({
12039
12689
  process.exit(1);
12040
12690
  }
12041
12691
  const config = readConfig();
12042
- 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;
12043
12694
  const showGraph = args.graph;
12044
12695
  const targetBranch = args.branch;
12045
- let mode = "local";
12696
+ let mode = "overview";
12046
12697
  if (args.all)
12047
12698
  mode = "all";
12699
+ else if (args.local)
12700
+ mode = "local";
12048
12701
  else if (args.remote)
12049
12702
  mode = "remote";
12050
12703
  else if (args.full || targetBranch)
@@ -12060,33 +12713,74 @@ var log_default = defineCommand({
12060
12713
  compareRef = fallback;
12061
12714
  usingFallback = true;
12062
12715
  }
12063
- }
12064
- heading("\uD83D\uDCDC commit log");
12065
- printModeHeader(mode, currentBranch, compareRef, usingFallback);
12066
- if (mode === "local" || mode === "remote") {
12067
- 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) {
12068
12753
  console.log();
12069
- console.log(import_picocolors11.default.yellow(" ⚠ Could not determine a comparison branch."));
12070
- console.log(import_picocolors11.default.dim(" No upstream tracking set and no remote base branch found."));
12071
- 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."));
12072
12756
  console.log();
12073
- printGuidance();
12074
12757
  return;
12075
12758
  }
12076
- 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
+ });
12077
12767
  if (!hasCommits) {
12078
- printGuidance();
12079
12768
  return;
12080
12769
  }
12081
12770
  } else {
12082
- 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
+ });
12083
12779
  if (!hasCommits) {
12084
- printGuidance();
12085
12780
  return;
12086
12781
  }
12087
12782
  }
12088
- printFooter(mode, count, targetBranch);
12089
- printGuidance();
12783
+ printFooter(mode, localCount, overviewRemoteCount, targetBranch);
12090
12784
  }
12091
12785
  });
12092
12786
  async function resolveBaseBranchRef(config) {
@@ -12108,38 +12802,49 @@ async function resolveBaseBranchRef(config) {
12108
12802
  }
12109
12803
  return null;
12110
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
+ }
12111
12812
  function printModeHeader(mode, currentBranch, compareRef, usingFallback = false) {
12112
12813
  const branch = currentBranch ?? "HEAD";
12113
- const fallbackNote = usingFallback ? import_picocolors11.default.yellow(" (no upstream — comparing against base branch)") : "";
12114
- console.log();
12814
+ const fallbackNote = usingFallback ? import_picocolors12.default.yellow(" (no upstream — comparing against base branch)") : "";
12115
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;
12116
12822
  case "local":
12117
- 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);
12118
12824
  if (compareRef) {
12119
- 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")}`));
12120
12826
  }
12121
12827
  break;
12122
12828
  case "remote":
12123
- 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);
12124
12830
  if (compareRef) {
12125
- 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)}`));
12126
12832
  }
12127
12833
  break;
12128
12834
  case "full":
12129
- 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)}`));
12130
12836
  break;
12131
12837
  case "all":
12132
- 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`));
12133
12839
  break;
12134
12840
  }
12135
12841
  }
12136
12842
  async function renderScopedLog(options) {
12137
- const { mode, count, upstream, showGraph, protectedBranches, currentBranch } = options;
12843
+ const { count, upstream, showGraph, protectedBranches, currentBranch } = options;
12138
12844
  if (showGraph) {
12139
- const graphFn = mode === "local" ? getLocalCommitsGraph : getRemoteOnlyCommitsGraph;
12140
- const lines = await graphFn({ count, upstream });
12845
+ const lines = await getLocalCommitsGraph({ count, upstream });
12141
12846
  if (lines.length === 0) {
12142
- printEmptyState(mode);
12847
+ printEmptyState("local");
12143
12848
  return false;
12144
12849
  }
12145
12850
  console.log();
@@ -12147,15 +12852,14 @@ async function renderScopedLog(options) {
12147
12852
  console.log(` ${colorizeGraphLine(line, protectedBranches, currentBranch)}`);
12148
12853
  }
12149
12854
  } else {
12150
- const entryFn = mode === "local" ? getLocalCommitsEntries : getRemoteOnlyCommitsEntries;
12151
- const entries = await entryFn({ count, upstream });
12855
+ const entries = await getLocalCommitsEntries({ count, upstream });
12152
12856
  if (entries.length === 0) {
12153
- printEmptyState(mode);
12857
+ printEmptyState("local");
12154
12858
  return false;
12155
12859
  }
12156
12860
  console.log();
12157
12861
  for (const entry of entries) {
12158
- const hashStr = import_picocolors11.default.yellow(entry.hash);
12862
+ const hashStr = import_picocolors12.default.yellow(entry.hash);
12159
12863
  const refsStr = entry.refs ? ` ${colorizeRefs(entry.refs, protectedBranches, currentBranch)}` : "";
12160
12864
  const subjectStr = colorizeSubject(entry.subject);
12161
12865
  console.log(` ${hashStr}${refsStr} ${subjectStr}`);
@@ -12163,12 +12867,58 @@ async function renderScopedLog(options) {
12163
12867
  }
12164
12868
  return true;
12165
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
+ }
12166
12916
  function printEmptyState(mode) {
12167
12917
  console.log();
12168
12918
  if (mode === "local") {
12169
- 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!"));
12170
12920
  } else {
12171
- 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!"));
12172
12922
  }
12173
12923
  console.log();
12174
12924
  }
@@ -12177,7 +12927,7 @@ async function renderFullLog(options) {
12177
12927
  if (showGraph) {
12178
12928
  const lines = await getLogGraph({ count, all, branch: targetBranch });
12179
12929
  if (lines.length === 0) {
12180
- console.log(import_picocolors11.default.dim(" No commits found."));
12930
+ console.log(import_picocolors12.default.dim(" No commits found."));
12181
12931
  console.log();
12182
12932
  return false;
12183
12933
  }
@@ -12188,13 +12938,13 @@ async function renderFullLog(options) {
12188
12938
  } else {
12189
12939
  const entries = await getLogEntries({ count, all, branch: targetBranch });
12190
12940
  if (entries.length === 0) {
12191
- console.log(import_picocolors11.default.dim(" No commits found."));
12941
+ console.log(import_picocolors12.default.dim(" No commits found."));
12192
12942
  console.log();
12193
12943
  return false;
12194
12944
  }
12195
12945
  console.log();
12196
12946
  for (const entry of entries) {
12197
- const hashStr = import_picocolors11.default.yellow(entry.hash);
12947
+ const hashStr = import_picocolors12.default.yellow(entry.hash);
12198
12948
  const refsStr = entry.refs ? ` ${colorizeRefs(entry.refs, protectedBranches, currentBranch)}` : "";
12199
12949
  const subjectStr = colorizeSubject(entry.subject);
12200
12950
  console.log(` ${hashStr}${refsStr} ${subjectStr}`);
@@ -12202,46 +12952,37 @@ async function renderFullLog(options) {
12202
12952
  }
12203
12953
  return true;
12204
12954
  }
12205
- function printFooter(mode, count, targetBranch) {
12955
+ function printFooter(mode, count, overviewRemoteCount, targetBranch) {
12206
12956
  console.log();
12207
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;
12208
12961
  case "local":
12209
- 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`));
12210
12963
  break;
12211
12964
  case "remote":
12212
- 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`));
12213
12966
  break;
12214
12967
  case "full":
12215
- 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})` : ""}`));
12216
12969
  break;
12217
12970
  case "all":
12218
- 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)`));
12219
12972
  break;
12220
12973
  }
12221
12974
  }
12222
- function printGuidance() {
12223
- console.log();
12224
- console.log(import_picocolors11.default.dim(" ─── quick guide ───"));
12225
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log")} local unpushed commits (default)`));
12226
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log --remote")} commits on remote not yet pulled`));
12227
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log --full")} full history for the current branch`));
12228
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log --all")} commits across all branches`));
12229
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log -n 50")} change the commit limit (default: 20)`));
12230
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log -b dev")} view log for a specific branch`));
12231
- console.log(import_picocolors11.default.dim(` ${import_picocolors11.default.bold("contrib log --no-graph")} flat list without graph lines`));
12232
- console.log();
12233
- }
12234
12975
  function colorizeGraphLine(line, protectedBranches, currentBranch) {
12235
12976
  const match = line.match(/^([|/\\*\s_.-]*)([a-f0-9]{7,12})(\s+\(([^)]+)\))?\s*(.*)/);
12236
12977
  if (!match) {
12237
- return import_picocolors11.default.cyan(line);
12978
+ return import_picocolors12.default.cyan(line);
12238
12979
  }
12239
12980
  const [, graphPart = "", hash, , refs, subject = ""] = match;
12240
12981
  const parts = [];
12241
12982
  if (graphPart) {
12242
12983
  parts.push(colorizeGraphChars(graphPart));
12243
12984
  }
12244
- parts.push(import_picocolors11.default.yellow(hash));
12985
+ parts.push(import_picocolors12.default.yellow(hash));
12245
12986
  if (refs) {
12246
12987
  parts.push(` (${colorizeRefs(refs, protectedBranches, currentBranch)})`);
12247
12988
  }
@@ -12252,15 +12993,15 @@ function colorizeGraphChars(graphPart) {
12252
12993
  return graphPart.split("").map((ch) => {
12253
12994
  switch (ch) {
12254
12995
  case "*":
12255
- return import_picocolors11.default.green(ch);
12996
+ return import_picocolors12.default.green(ch);
12256
12997
  case "|":
12257
- return import_picocolors11.default.cyan(ch);
12998
+ return import_picocolors12.default.cyan(ch);
12258
12999
  case "/":
12259
13000
  case "\\":
12260
- return import_picocolors11.default.cyan(ch);
13001
+ return import_picocolors12.default.cyan(ch);
12261
13002
  case "-":
12262
13003
  case "_":
12263
- return import_picocolors11.default.cyan(ch);
13004
+ return import_picocolors12.default.cyan(ch);
12264
13005
  default:
12265
13006
  return ch;
12266
13007
  }
@@ -12272,50 +13013,50 @@ function colorizeRefs(refs, protectedBranches, currentBranch) {
12272
13013
  if (trimmed.startsWith("HEAD ->") || trimmed === "HEAD") {
12273
13014
  const branchName = trimmed.replace("HEAD -> ", "");
12274
13015
  if (trimmed === "HEAD") {
12275
- return import_picocolors11.default.bold(import_picocolors11.default.cyan("HEAD"));
13016
+ return import_picocolors12.default.bold(import_picocolors12.default.cyan("HEAD"));
12276
13017
  }
12277
- 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)}`;
12278
13019
  }
12279
13020
  if (trimmed.startsWith("tag:")) {
12280
- return import_picocolors11.default.bold(import_picocolors11.default.magenta(trimmed));
13021
+ return import_picocolors12.default.bold(import_picocolors12.default.magenta(trimmed));
12281
13022
  }
12282
13023
  return colorizeRefName(trimmed, protectedBranches, currentBranch);
12283
- }).join(import_picocolors11.default.dim(", "));
13024
+ }).join(import_picocolors12.default.dim(", "));
12284
13025
  }
12285
13026
  function colorizeRefName(name, protectedBranches, currentBranch) {
12286
13027
  const isRemote = name.includes("/");
12287
13028
  const localName = isRemote ? name.split("/").slice(1).join("/") : name;
12288
13029
  if (protectedBranches.includes(localName)) {
12289
- 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));
12290
13031
  }
12291
13032
  if (localName === currentBranch) {
12292
- return import_picocolors11.default.bold(import_picocolors11.default.green(name));
13033
+ return import_picocolors12.default.bold(import_picocolors12.default.green(name));
12293
13034
  }
12294
13035
  if (isRemote) {
12295
- return import_picocolors11.default.blue(name);
13036
+ return import_picocolors12.default.blue(name);
12296
13037
  }
12297
- return import_picocolors11.default.green(name);
13038
+ return import_picocolors12.default.green(name);
12298
13039
  }
12299
13040
  function colorizeSubject(subject) {
12300
13041
  const emojiMatch = subject.match(/^((?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F)+\s*)/u);
12301
13042
  if (emojiMatch) {
12302
13043
  const emoji = emojiMatch[1];
12303
13044
  const rest = subject.slice(emoji.length);
12304
- return `${emoji}${import_picocolors11.default.white(rest)}`;
13045
+ return `${emoji}${import_picocolors12.default.white(rest)}`;
12305
13046
  }
12306
13047
  if (subject.startsWith("Merge ")) {
12307
- return import_picocolors11.default.dim(subject);
13048
+ return import_picocolors12.default.dim(subject);
12308
13049
  }
12309
- return import_picocolors11.default.white(subject);
13050
+ return import_picocolors12.default.white(subject);
12310
13051
  }
12311
13052
 
12312
13053
  // src/commands/save.ts
12313
- var import_picocolors12 = __toESM(require_picocolors(), 1);
12314
13054
  import { execFile as execFileCb4 } from "node:child_process";
13055
+ var import_picocolors13 = __toESM(require_picocolors(), 1);
12315
13056
  function gitRun(args) {
12316
- return new Promise((resolve2) => {
13057
+ return new Promise((resolve3) => {
12317
13058
  execFileCb4("git", args, (err, stdout2, stderr) => {
12318
- resolve2({
13059
+ resolve3({
12319
13060
  exitCode: err ? err.code === "ENOENT" ? 127 : err.status ?? 1 : 0,
12320
13061
  stdout: stdout2 ?? "",
12321
13062
  stderr: stderr ?? ""
@@ -12372,7 +13113,7 @@ var save_default = defineCommand({
12372
13113
  }
12373
13114
  });
12374
13115
  async function handleSave(message) {
12375
- heading("\uD83D\uDCBE contrib save");
13116
+ projectHeading("save", "\uD83D\uDCBE");
12376
13117
  const currentBranch = await getCurrentBranch();
12377
13118
  const label = message ?? `work-in-progress on ${currentBranch ?? "unknown"}`;
12378
13119
  const stashMsg = `contrib-save: ${label}`;
@@ -12385,11 +13126,11 @@ async function handleSave(message) {
12385
13126
  info("No uncommitted changes to save.");
12386
13127
  return;
12387
13128
  }
12388
- success(`Saved: ${import_picocolors12.default.dim(label)}`);
12389
- 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.`, "");
12390
13131
  }
12391
13132
  async function handleRestore() {
12392
- heading("\uD83D\uDCBE contrib save --restore");
13133
+ projectHeading("save --restore", "\uD83D\uDCBE");
12393
13134
  const stashes = await getStashList();
12394
13135
  if (stashes.length === 0) {
12395
13136
  info("No saved changes found.");
@@ -12402,7 +13143,7 @@ async function handleRestore() {
12402
13143
  warn("You may have conflicts. Resolve them and run `git stash drop` when done.");
12403
13144
  process.exit(1);
12404
13145
  }
12405
- success(`Restored: ${import_picocolors12.default.dim(stashes[0].message)}`);
13146
+ success(`Restored: ${import_picocolors13.default.dim(stashes[0].message)}`);
12406
13147
  return;
12407
13148
  }
12408
13149
  const choices = stashes.map((s2) => `${s2.index} ${s2.message}`);
@@ -12415,10 +13156,10 @@ async function handleRestore() {
12415
13156
  process.exit(1);
12416
13157
  }
12417
13158
  const match = stashes.find((s2) => String(s2.index) === idx);
12418
- success(`Restored: ${import_picocolors12.default.dim(match?.message ?? "saved changes")}`);
13159
+ success(`Restored: ${import_picocolors13.default.dim(match?.message ?? "saved changes")}`);
12419
13160
  }
12420
13161
  async function handleList() {
12421
- heading("\uD83D\uDCBE contrib save --list");
13162
+ projectHeading("save --list", "\uD83D\uDCBE");
12422
13163
  const stashes = await getStashList();
12423
13164
  if (stashes.length === 0) {
12424
13165
  info("No saved changes.");
@@ -12426,16 +13167,16 @@ async function handleList() {
12426
13167
  }
12427
13168
  console.log();
12428
13169
  for (const s2 of stashes) {
12429
- const idx = import_picocolors12.default.dim(`[${s2.index}]`);
13170
+ const idx = import_picocolors13.default.dim(`[${s2.index}]`);
12430
13171
  const msg = s2.message;
12431
13172
  console.log(` ${idx} ${msg}`);
12432
13173
  }
12433
13174
  console.log();
12434
- info(`Use ${import_picocolors12.default.bold("contrib save --restore")} to bring changes back.`, "");
12435
- 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.`, "");
12436
13177
  }
12437
13178
  async function handleDrop() {
12438
- heading("\uD83D\uDCBE contrib save --drop");
13179
+ projectHeading("save --drop", "\uD83D\uDCBE");
12439
13180
  const stashes = await getStashList();
12440
13181
  if (stashes.length === 0) {
12441
13182
  info("No saved changes to drop.");
@@ -12450,7 +13191,7 @@ async function handleDrop() {
12450
13191
  process.exit(1);
12451
13192
  }
12452
13193
  const match = stashes.find((s2) => String(s2.index) === idx);
12453
- success(`Dropped: ${import_picocolors12.default.dim(match?.message ?? "saved changes")}`);
13194
+ success(`Dropped: ${import_picocolors13.default.dim(match?.message ?? "saved changes")}`);
12454
13195
  }
12455
13196
  async function getStashList() {
12456
13197
  const result = await gitRun(["stash", "list"]);
@@ -12467,38 +13208,23 @@ async function getStashList() {
12467
13208
  }
12468
13209
 
12469
13210
  // src/commands/setup.ts
12470
- var import_picocolors13 = __toESM(require_picocolors(), 1);
13211
+ var import_picocolors14 = __toESM(require_picocolors(), 1);
12471
13212
  async function shouldContinueSetupWithExistingConfig(options) {
12472
- const {
12473
- existingConfig,
12474
- hasConfigFile,
12475
- confirm,
12476
- ensureIgnored,
12477
- onInfo,
12478
- onWarn,
12479
- onSuccess,
12480
- summary
12481
- } = options;
13213
+ const { existingConfig, hasConfigFile, confirm, onInfo, onWarn, onSuccess, summary } = options;
12482
13214
  if (existingConfig) {
12483
- onInfo("Existing .contributerc.json detected:");
13215
+ onInfo("Existing repo config detected:");
12484
13216
  summary(existingConfig);
12485
13217
  const shouldContinue = await confirm("Continue setup and overwrite existing config?");
12486
13218
  if (!shouldContinue) {
12487
- if (ensureIgnored()) {
12488
- onInfo("Added .contributerc.json to .gitignore to avoid committing personal config.");
12489
- }
12490
13219
  onSuccess("Keeping existing setup.");
12491
13220
  return false;
12492
13221
  }
12493
13222
  return true;
12494
13223
  }
12495
13224
  if (hasConfigFile) {
12496
- onWarn("Found .contributerc.json but it appears invalid.");
13225
+ onWarn("Found an existing repo config but it appears invalid.");
12497
13226
  const shouldContinue = await confirm("Continue setup and overwrite invalid config?");
12498
13227
  if (!shouldContinue) {
12499
- if (ensureIgnored()) {
12500
- onInfo("Added .contributerc.json to .gitignore to avoid committing personal config.");
12501
- }
12502
13228
  onInfo("Keeping existing file. Run setup again when ready to repair it.");
12503
13229
  return false;
12504
13230
  }
@@ -12508,20 +13234,19 @@ async function shouldContinueSetupWithExistingConfig(options) {
12508
13234
  var setup_default = defineCommand({
12509
13235
  meta: {
12510
13236
  name: "setup",
12511
- description: "Initialize contribute-now config for this repo (.contributerc.json)"
13237
+ description: "Initialize contribute-now config for this repo using local Git storage"
12512
13238
  },
12513
13239
  async run() {
12514
13240
  if (!await isGitRepo()) {
12515
13241
  error("Not inside a git repository. Run this command from within a git repo.");
12516
13242
  process.exit(1);
12517
13243
  }
12518
- heading("\uD83D\uDD27 contribute-now setup");
13244
+ projectHeading("setup", "\uD83D\uDD27");
12519
13245
  const existingConfig = readConfig();
12520
13246
  const shouldContinue = await shouldContinueSetupWithExistingConfig({
12521
13247
  existingConfig,
12522
13248
  hasConfigFile: configExists(),
12523
13249
  confirm: confirmPrompt,
12524
- ensureIgnored: ensureGitignored,
12525
13250
  onInfo: info,
12526
13251
  onWarn: warn,
12527
13252
  onSuccess: success,
@@ -12540,7 +13265,7 @@ var setup_default = defineCommand({
12540
13265
  workflow = "github-flow";
12541
13266
  else if (workflowChoice.startsWith("Git Flow"))
12542
13267
  workflow = "git-flow";
12543
- info(`Workflow: ${import_picocolors13.default.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
13268
+ info(`Workflow: ${import_picocolors14.default.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
12544
13269
  const conventionChoice = await selectPrompt("Which commit convention should this project use?", [
12545
13270
  `${CONVENTION_DESCRIPTIONS["clean-commit"]} (recommended)`,
12546
13271
  CONVENTION_DESCRIPTIONS.conventional,
@@ -12551,6 +13276,8 @@ var setup_default = defineCommand({
12551
13276
  commitConvention = "conventional";
12552
13277
  else if (conventionChoice.includes("No commit"))
12553
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?");
12554
13281
  const remotes = await getRemotes();
12555
13282
  if (remotes.length === 0) {
12556
13283
  error("No git remotes found. Add a remote first (e.g., git remote add origin <url>).");
@@ -12604,15 +13331,15 @@ var setup_default = defineCommand({
12604
13331
  detectedRole = roleChoice;
12605
13332
  detectionSource = "user selection";
12606
13333
  } else {
12607
- info(`Detected role: ${import_picocolors13.default.bold(detectedRole)} (via ${detectionSource})`);
12608
- 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?`);
12609
13336
  if (!confirmed) {
12610
13337
  const roleChoice = await selectPrompt("Select your role:", ["maintainer", "contributor"]);
12611
13338
  detectedRole = roleChoice;
12612
13339
  }
12613
13340
  }
12614
13341
  const defaultConfig = getDefaultConfig();
12615
- 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."));
12616
13343
  const mainBranchDefault = defaultConfig.mainBranch;
12617
13344
  const mainBranch = await inputPrompt(`Main branch name (default: ${mainBranchDefault} — press Enter to keep)`, mainBranchDefault);
12618
13345
  let devBranch;
@@ -12638,7 +13365,7 @@ var setup_default = defineCommand({
12638
13365
  error("Setup cannot continue without the upstream remote for contributors.");
12639
13366
  process.exit(1);
12640
13367
  }
12641
- success(`Added remote ${import_picocolors13.default.bold(upstreamRemote)} → ${upstreamUrl}`);
13368
+ success(`Added remote ${import_picocolors14.default.bold(upstreamRemote)} → ${upstreamUrl}`);
12642
13369
  } else {
12643
13370
  error("An upstream remote URL is required for contributors.");
12644
13371
  info("Add it manually: git remote add upstream <url>", "");
@@ -12654,54 +13381,58 @@ var setup_default = defineCommand({
12654
13381
  upstream: upstreamRemote,
12655
13382
  origin: originRemote,
12656
13383
  branchPrefixes: defaultConfig.branchPrefixes,
12657
- commitConvention
13384
+ commitConvention,
13385
+ aiEnabled: enableAI,
13386
+ showTips
12658
13387
  };
12659
13388
  writeConfig(config);
12660
- 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.", "");
12661
13391
  const syncRemote = config.role === "contributor" ? config.upstream : config.origin;
12662
- info(`Fetching ${import_picocolors13.default.bold(syncRemote)} to verify branch configuration...`, "");
13392
+ info(`Fetching ${import_picocolors14.default.bold(syncRemote)} to verify branch configuration...`, "");
12663
13393
  await fetchRemote(syncRemote);
12664
13394
  const mainRef = `${syncRemote}/${config.mainBranch}`;
12665
13395
  if (!await refExists(mainRef)) {
12666
- 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.`);
12667
13397
  warn("Config was saved — verify the branch name and re-run setup if needed.");
12668
13398
  }
12669
13399
  if (config.devBranch) {
12670
13400
  const devRef = `${syncRemote}/${config.devBranch}`;
12671
13401
  if (!await refExists(devRef)) {
12672
- 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.`);
12673
13403
  warn("Config was saved — verify the branch name and re-run setup if needed.");
12674
13404
  }
12675
13405
  }
12676
- if (ensureGitignored()) {
12677
- info("Added .contributerc.json to .gitignore to avoid committing personal config.");
12678
- }
12679
13406
  console.log();
12680
- info(`Workflow: ${import_picocolors13.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
12681
- info(`Convention: ${import_picocolors13.default.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
12682
- 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)}`);
12683
13412
  if (config.devBranch) {
12684
- 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)}`);
12685
13414
  } else {
12686
- info(`Main: ${import_picocolors13.default.bold(config.mainBranch)}`);
13415
+ info(`Main: ${import_picocolors14.default.bold(config.mainBranch)}`);
12687
13416
  }
12688
- 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)}` : ""}`);
12689
13418
  }
12690
13419
  });
12691
13420
  function logConfigSummary(config) {
12692
- info(`Workflow: ${import_picocolors13.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
12693
- info(`Convention: ${import_picocolors13.default.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
12694
- 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)}`);
12695
13426
  if (config.devBranch) {
12696
- 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)}`);
12697
13428
  } else {
12698
- info(`Main: ${import_picocolors13.default.bold(config.mainBranch)}`);
13429
+ info(`Main: ${import_picocolors14.default.bold(config.mainBranch)}`);
12699
13430
  }
12700
- 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)}` : ""}`);
12701
13432
  }
12702
13433
 
12703
13434
  // src/commands/start.ts
12704
- var import_picocolors14 = __toESM(require_picocolors(), 1);
13435
+ var import_picocolors15 = __toESM(require_picocolors(), 1);
12705
13436
  var start_default = defineCommand({
12706
13437
  meta: {
12707
13438
  name: "start",
@@ -12731,7 +13462,7 @@ var start_default = defineCommand({
12731
13462
  await assertCleanGitState("starting a new branch");
12732
13463
  const config = readConfig();
12733
13464
  if (!config) {
12734
- error("No .contributerc.json found. Run `contrib setup` first.");
13465
+ error("No repo config found. Run `contrib setup` first.");
12735
13466
  process.exit(1);
12736
13467
  }
12737
13468
  if (await hasUncommittedChanges()) {
@@ -12741,57 +13472,28 @@ var start_default = defineCommand({
12741
13472
  const { branchPrefixes } = config;
12742
13473
  const baseBranch = getBaseBranch(config);
12743
13474
  const syncSource = getSyncSource(config);
12744
- let branchName = args.name;
12745
- 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
+ });
12746
13483
  if (!branchName) {
12747
- branchName = await inputPrompt("What are you going to work on?");
12748
- if (!branchName || branchName.trim().length === 0) {
12749
- error("A branch name or description is required.");
12750
- process.exit(1);
12751
- }
12752
- branchName = branchName.trim();
12753
- }
12754
- const useAI = !args["no-ai"] && looksLikeNaturalLanguage(branchName);
12755
- if (useAI) {
12756
- const spinner = createSpinner("Generating branch name suggestion...");
12757
- const suggested = await suggestBranchName(branchName, args.model);
12758
- if (suggested) {
12759
- spinner.success("Branch name suggestion ready.");
12760
- console.log(`
12761
- ${import_picocolors14.default.dim("AI suggestion:")} ${import_picocolors14.default.bold(import_picocolors14.default.cyan(suggested))}`);
12762
- const accepted = await confirmPrompt(`Use ${import_picocolors14.default.bold(suggested)} as your branch name?`);
12763
- if (accepted) {
12764
- branchName = suggested;
12765
- } else {
12766
- branchName = await inputPrompt("Enter branch name", branchName);
12767
- }
12768
- } else {
12769
- spinner.fail("AI did not return a branch name suggestion.");
12770
- }
12771
- }
12772
- if (!hasPrefix(branchName, branchPrefixes)) {
12773
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors14.default.bold(branchName)}:`, branchPrefixes);
12774
- branchName = formatBranchName(prefix, branchName);
12775
- }
12776
- if (!isValidBranchName(branchName)) {
12777
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
12778
- process.exit(1);
12779
- }
12780
- info(`Creating branch: ${import_picocolors14.default.bold(branchName)}`);
12781
- if (await branchExists(branchName)) {
12782
- error(`Branch ${import_picocolors14.default.bold(branchName)} already exists.`);
12783
- info(` Use ${import_picocolors14.default.bold(`git checkout ${branchName}`)} to switch to it, or choose a different name.`, "");
12784
- process.exit(1);
13484
+ warn("Start cancelled.");
13485
+ process.exit(0);
12785
13486
  }
13487
+ info(`Creating branch: ${import_picocolors15.default.bold(branchName)}`);
12786
13488
  await fetchRemote(syncSource.remote);
12787
13489
  if (!await refExists(syncSource.ref)) {
12788
- 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)}.`);
12789
13491
  }
12790
13492
  const currentBranch = await getCurrentBranch();
12791
13493
  if (currentBranch === baseBranch && await refExists(syncSource.ref)) {
12792
13494
  const ahead = await countCommitsAhead(baseBranch, syncSource.ref);
12793
13495
  if (ahead > 0) {
12794
- 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)}.`);
12795
13497
  info(" Syncing will discard those commits. Consider backing them up first (e.g. create a branch).");
12796
13498
  const proceed = await confirmPrompt("Discard local commits and sync to remote?");
12797
13499
  if (!proceed) {
@@ -12808,10 +13510,10 @@ var start_default = defineCommand({
12808
13510
  error(`Failed to create branch: ${result2.stderr}`);
12809
13511
  process.exit(1);
12810
13512
  }
12811
- 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)}`);
12812
13514
  return;
12813
13515
  }
12814
- error(`Failed to update ${import_picocolors14.default.bold(baseBranch)}: ${updateResult.stderr}`);
13516
+ error(`Failed to update ${import_picocolors15.default.bold(baseBranch)}: ${updateResult.stderr}`);
12815
13517
  info("Make sure your base branch exists locally or the remote ref is available.", "");
12816
13518
  process.exit(1);
12817
13519
  }
@@ -12820,12 +13522,12 @@ var start_default = defineCommand({
12820
13522
  error(`Failed to create branch: ${result.stderr}`);
12821
13523
  process.exit(1);
12822
13524
  }
12823
- 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)}`);
12824
13526
  }
12825
13527
  });
12826
13528
 
12827
13529
  // src/commands/status.ts
12828
- var import_picocolors15 = __toESM(require_picocolors(), 1);
13530
+ var import_picocolors16 = __toESM(require_picocolors(), 1);
12829
13531
  var status_default = defineCommand({
12830
13532
  meta: {
12831
13533
  name: "status",
@@ -12838,12 +13540,12 @@ var status_default = defineCommand({
12838
13540
  }
12839
13541
  const config = readConfig();
12840
13542
  if (!config) {
12841
- error("No .contributerc.json found. Run `contrib setup` first.");
13543
+ error("No repo config found. Run `contrib setup` first.");
12842
13544
  process.exit(1);
12843
13545
  }
12844
- heading("\uD83D\uDCCA contribute-now status");
12845
- console.log(` ${import_picocolors15.default.dim("Workflow:")} ${import_picocolors15.default.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
12846
- 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)}`);
12847
13549
  console.log();
12848
13550
  await fetchAll();
12849
13551
  const currentBranch = await getCurrentBranch();
@@ -12852,7 +13554,7 @@ var status_default = defineCommand({
12852
13554
  const isContributor = config.role === "contributor";
12853
13555
  const [dirty, fileStatus] = await Promise.all([hasUncommittedChanges(), getFileStatus()]);
12854
13556
  if (dirty) {
12855
- 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")}`);
12856
13558
  console.log();
12857
13559
  }
12858
13560
  const mainRemote = `${origin}/${mainBranch}`;
@@ -12871,28 +13573,32 @@ var status_default = defineCommand({
12871
13573
  if (isFeatureBranch) {
12872
13574
  const branchDiv = await getDivergence(currentBranch, baseBranch);
12873
13575
  const branchLine = formatStatus(currentBranch, baseBranch, branchDiv.ahead, branchDiv.behind);
12874
- 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("*")})`));
12875
13577
  branchStatus = await detectBranchStatus(currentBranch, baseBranch);
12876
13578
  if (branchStatus.merged) {
12877
- 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")}`);
12878
13580
  }
12879
13581
  if (branchStatus.stale) {
12880
- 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`)}`);
12881
13583
  }
12882
13584
  } else if (currentBranch) {
12883
- 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)`));
12884
13586
  }
12885
13587
  let branchesAligned = true;
12886
13588
  {
12887
13589
  const alignRefs = [];
12888
13590
  const devRemote = isContributor ? upstream : origin;
13591
+ const devBranch = hasDevBranch(workflow) ? config.devBranch : undefined;
12889
13592
  const hashResults = await Promise.all([
12890
13593
  getCommitHash(mainBranch).then((h2) => ({ name: mainBranch, hash: h2 })),
12891
- getCommitHash(`${origin}/${mainBranch}`).then((h2) => ({ name: `${origin}/${mainBranch}`, hash: h2 })),
12892
- ...hasDevBranch(workflow) && config.devBranch ? [
12893
- getCommitHash(config.devBranch).then((h2) => ({ name: config.devBranch, hash: h2 })),
12894
- getCommitHash(`${devRemote}/${config.devBranch}`).then((h2) => ({
12895
- 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}`,
12896
13602
  hash: h2
12897
13603
  }))
12898
13604
  ] : []
@@ -12906,24 +13612,27 @@ var status_default = defineCommand({
12906
13612
  for (const { name, hash } of alignRefs) {
12907
13613
  if (!groups.has(hash))
12908
13614
  groups.set(hash, []);
12909
- groups.get(hash).push(name);
13615
+ const group = groups.get(hash);
13616
+ if (group) {
13617
+ group.push(name);
13618
+ }
12910
13619
  }
12911
13620
  branchesAligned = groups.size === 1;
12912
13621
  console.log();
12913
- console.log(` ${import_picocolors15.default.bold("\uD83D\uDD17 Branch Alignment")}`);
13622
+ console.log(` ${import_picocolors16.default.bold("\uD83D\uDD17 Branch Alignment")}`);
12914
13623
  for (const [hash, names] of groups) {
12915
13624
  const short = hash.slice(0, 7);
12916
- const nameStr = names.map((n2) => import_picocolors15.default.bold(n2)).join(import_picocolors15.default.dim(" · "));
12917
- 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}`);
12918
13627
  const subject = await getCommitSubject(hash);
12919
13628
  if (subject) {
12920
- console.log(` ${import_picocolors15.default.dim(subject)}`);
13629
+ console.log(` ${import_picocolors16.default.dim(subject)}`);
12921
13630
  }
12922
13631
  }
12923
13632
  if (branchesAligned) {
12924
- 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")}`);
12925
13634
  } else {
12926
- 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")}`);
12927
13636
  }
12928
13637
  }
12929
13638
  }
@@ -12931,74 +13640,50 @@ var status_default = defineCommand({
12931
13640
  if (hasFiles) {
12932
13641
  console.log();
12933
13642
  if (fileStatus.staged.length > 0) {
12934
- console.log(` ${import_picocolors15.default.green("Staged for commit:")}`);
13643
+ console.log(` ${import_picocolors16.default.green("Staged for commit:")}`);
12935
13644
  for (const { file, status } of fileStatus.staged) {
12936
- 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}`);
12937
13646
  }
12938
13647
  }
12939
13648
  if (fileStatus.modified.length > 0) {
12940
- console.log(` ${import_picocolors15.default.yellow("Unstaged changes:")}`);
13649
+ console.log(` ${import_picocolors16.default.yellow("Unstaged changes:")}`);
12941
13650
  for (const { file, status } of fileStatus.modified) {
12942
- 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}`);
12943
13652
  }
12944
13653
  }
12945
13654
  if (fileStatus.untracked.length > 0) {
12946
- console.log(` ${import_picocolors15.default.red("Untracked files:")}`);
13655
+ console.log(` ${import_picocolors16.default.red("Untracked files:")}`);
12947
13656
  for (const file of fileStatus.untracked) {
12948
- console.log(` ${import_picocolors15.default.red("?")} ${file}`);
13657
+ console.log(` ${import_picocolors16.default.red("?")} ${file}`);
12949
13658
  }
12950
13659
  }
12951
13660
  } else if (!dirty) {
12952
- console.log(` ${import_picocolors15.default.green("✓")} ${import_picocolors15.default.dim("Working tree clean")}`);
12953
- }
12954
- const tips = [];
12955
- if (!branchesAligned) {
12956
- tips.push(`Run ${import_picocolors15.default.bold("contrib sync")} to align your local branches with the remote`);
12957
- }
12958
- if (fileStatus.staged.length > 0) {
12959
- tips.push(`Run ${import_picocolors15.default.bold("contrib commit")} to commit staged changes`);
12960
- }
12961
- if (fileStatus.modified.length > 0 || fileStatus.untracked.length > 0) {
12962
- tips.push(`Run ${import_picocolors15.default.bold("contrib commit")} to stage and commit changes`);
12963
- }
12964
- if (isFeatureBranch && branchStatus) {
12965
- if (branchStatus.merged) {
12966
- tips.push(`Run ${import_picocolors15.default.bold("contrib clean")} to delete this merged branch`);
12967
- } else if (branchStatus.stale) {
12968
- 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`);
12969
- } else if (fileStatus.staged.length === 0 && fileStatus.modified.length === 0 && fileStatus.untracked.length === 0) {
12970
- const branchDiv = await getDivergence(currentBranch, `${origin}/${currentBranch}`);
12971
- if (branchDiv.ahead > 0) {
12972
- tips.push(`Run ${import_picocolors15.default.bold("contrib submit")} to push and create/update your PR`);
12973
- }
12974
- }
12975
- }
12976
- if (tips.length > 0) {
12977
- console.log();
12978
- console.log(` ${import_picocolors15.default.dim("\uD83D\uDCA1 Tip:")}`);
12979
- for (const tip of tips) {
12980
- console.log(` ${import_picocolors15.default.dim(tip)}`);
12981
- }
13661
+ console.log(` ${import_picocolors16.default.green("✓")} ${import_picocolors16.default.dim("Working tree clean")}`);
12982
13662
  }
12983
13663
  console.log();
12984
13664
  }
12985
13665
  });
12986
13666
  function formatStatus(branch, base, ahead, behind) {
12987
- const label = import_picocolors15.default.bold(branch.padEnd(20));
13667
+ const label = import_picocolors16.default.bold(branch.padEnd(20));
12988
13668
  if (ahead === 0 && behind === 0) {
12989
- 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}`)}`;
12990
13670
  }
12991
13671
  if (ahead > 0 && behind === 0) {
12992
- 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}`)}`;
12993
13673
  }
12994
13674
  if (behind > 0 && ahead === 0) {
12995
- 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}`)}`;
12996
13676
  }
12997
- 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)}`;
12998
13678
  }
12999
13679
  var STALE_THRESHOLD_DAYS = 14;
13000
13680
  async function detectBranchStatus(branch, baseBranch) {
13001
- 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
+ };
13002
13687
  const div = await getDivergence(branch, baseBranch);
13003
13688
  const hasWork = div.ahead > 0;
13004
13689
  if (hasWork) {
@@ -13040,34 +13725,47 @@ async function detectBranchStatus(branch, baseBranch) {
13040
13725
  }
13041
13726
 
13042
13727
  // src/commands/submit.ts
13043
- var import_picocolors16 = __toESM(require_picocolors(), 1);
13728
+ var import_picocolors17 = __toESM(require_picocolors(), 1);
13044
13729
  async function performSquashMerge(origin, baseBranch, featureBranch, options) {
13045
- info(`Checking out ${import_picocolors16.default.bold(baseBranch)}...`);
13730
+ info(`Checking out ${import_picocolors17.default.bold(baseBranch)}...`);
13046
13731
  const coResult = await checkoutBranch(baseBranch);
13047
13732
  if (coResult.exitCode !== 0) {
13048
13733
  error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
13049
13734
  process.exit(1);
13050
13735
  }
13051
- 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)}...`);
13052
13737
  const mergeResult = await mergeSquash(featureBranch);
13053
13738
  if (mergeResult.exitCode !== 0) {
13054
13739
  error(`Squash merge failed: ${mergeResult.stderr}`);
13055
13740
  process.exit(1);
13056
13741
  }
13057
13742
  let message = options?.defaultMsg;
13058
- if (!message) {
13059
- const copilotError = await checkCopilotAvailable();
13743
+ if (!message && options?.useAI !== false) {
13744
+ const copilotError = await checkCopilotAvailable2();
13060
13745
  if (!copilotError) {
13061
- const spinner = createSpinner("Generating AI commit message for squash merge...");
13062
- const [stagedDiff, stagedFiles] = await Promise.all([getStagedDiff(), getStagedFiles()]);
13063
- const aiMsg = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit", "squash-merge");
13064
- if (aiMsg) {
13065
- message = aiMsg;
13066
- spinner.success("AI commit message generated.");
13067
- console.log(`
13068
- ${import_picocolors16.default.dim("AI suggestion:")} ${import_picocolors16.default.bold(import_picocolors16.default.cyan(message))}`);
13069
- } 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
+ }
13070
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;
13071
13769
  }
13072
13770
  } else {
13073
13771
  warn(`AI unavailable: ${copilotError}`);
@@ -13076,28 +13774,28 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
13076
13774
  let finalMsg = null;
13077
13775
  if (message) {
13078
13776
  while (!finalMsg) {
13079
- const action = await selectPrompt("What would you like to do?", [
13080
- "Accept this message",
13081
- "Edit this message",
13082
- "Regenerate",
13083
- "Write manually"
13084
- ]);
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);
13085
13782
  if (action === "Accept this message") {
13086
13783
  finalMsg = message;
13087
13784
  } else if (action === "Edit this message") {
13088
13785
  finalMsg = await inputPrompt("Edit commit message", message);
13089
13786
  } else if (action === "Regenerate") {
13090
- const spinner = createSpinner("Regenerating commit message...");
13787
+ const spinner = createSpinner("Regenerating commit message...", {
13788
+ tips: LOADING_TIPS
13789
+ });
13091
13790
  const [stagedDiff, stagedFiles] = await Promise.all([getStagedDiff(), getStagedFiles()]);
13092
13791
  const regen = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit", "squash-merge");
13093
13792
  if (regen) {
13094
13793
  message = regen;
13095
13794
  spinner.success("Commit message regenerated.");
13096
13795
  console.log(`
13097
- ${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))}`);
13098
13797
  } else {
13099
13798
  spinner.fail("Regeneration failed.");
13100
- finalMsg = await inputPrompt("Enter commit message");
13101
13799
  }
13102
13800
  } else {
13103
13801
  finalMsg = await inputPrompt("Enter commit message");
@@ -13111,13 +13809,13 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
13111
13809
  error(`Commit failed: ${commitResult.stderr}`);
13112
13810
  process.exit(1);
13113
13811
  }
13114
- info(`Pushing ${import_picocolors16.default.bold(baseBranch)} to ${origin}...`);
13812
+ info(`Pushing ${import_picocolors17.default.bold(baseBranch)} to ${origin}...`);
13115
13813
  const pushResult = await pushBranch(origin, baseBranch);
13116
13814
  if (pushResult.exitCode !== 0) {
13117
13815
  error(`Failed to push ${baseBranch}: ${pushResult.stderr}`);
13118
13816
  process.exit(1);
13119
13817
  }
13120
- info(`Deleting local branch ${import_picocolors16.default.bold(featureBranch)}...`);
13818
+ info(`Deleting local branch ${import_picocolors17.default.bold(featureBranch)}...`);
13121
13819
  const delLocal = await forceDeleteBranch(featureBranch);
13122
13820
  if (delLocal.exitCode !== 0) {
13123
13821
  warn(`Could not delete local branch: ${delLocal.stderr.trim()}`);
@@ -13125,14 +13823,14 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
13125
13823
  const remoteBranchRef = `${origin}/${featureBranch}`;
13126
13824
  const remoteExists = await branchExists(remoteBranchRef);
13127
13825
  if (remoteExists) {
13128
- info(`Deleting remote branch ${import_picocolors16.default.bold(featureBranch)}...`);
13826
+ info(`Deleting remote branch ${import_picocolors17.default.bold(featureBranch)}...`);
13129
13827
  const delRemote = await deleteRemoteBranch(origin, featureBranch);
13130
13828
  if (delRemote.exitCode !== 0) {
13131
13829
  warn(`Could not delete remote branch: ${delRemote.stderr.trim()}`);
13132
13830
  }
13133
13831
  }
13134
- success(`Squash merged ${import_picocolors16.default.bold(featureBranch)} into ${import_picocolors16.default.bold(baseBranch)} and pushed.`);
13135
- 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.`, "");
13136
13834
  }
13137
13835
  var submit_default = defineCommand({
13138
13836
  meta: {
@@ -13145,6 +13843,18 @@ var submit_default = defineCommand({
13145
13843
  description: "Create PR as draft",
13146
13844
  default: false
13147
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
+ },
13148
13858
  "no-ai": {
13149
13859
  type: "boolean",
13150
13860
  description: "Skip AI PR description generation",
@@ -13163,10 +13873,11 @@ var submit_default = defineCommand({
13163
13873
  await assertCleanGitState("submitting");
13164
13874
  const config = readConfig();
13165
13875
  if (!config) {
13166
- error("No .contributerc.json found. Run `contrib setup` first.");
13876
+ error("No repo config found. Run `contrib setup` first.");
13167
13877
  process.exit(1);
13168
13878
  }
13169
13879
  const { origin } = config;
13880
+ const aiEnabled = isAIEnabled(config, args["no-ai"]);
13170
13881
  const baseBranch = getBaseBranch(config);
13171
13882
  const protectedBranches = getProtectedBranches(config);
13172
13883
  const currentBranch = await getCurrentBranch();
@@ -13175,8 +13886,8 @@ var submit_default = defineCommand({
13175
13886
  process.exit(1);
13176
13887
  }
13177
13888
  if (protectedBranches.includes(currentBranch)) {
13178
- heading("\uD83D\uDE80 contrib submit");
13179
- 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.`);
13180
13891
  await fetchAll();
13181
13892
  const remoteRef = `${origin}/${currentBranch}`;
13182
13893
  const localWork = await hasLocalWork(origin, currentBranch);
@@ -13185,11 +13896,11 @@ var submit_default = defineCommand({
13185
13896
  const hasAnything = hasCommits || dirty;
13186
13897
  if (!hasAnything) {
13187
13898
  error("No local changes or commits to move. Switch to a feature branch first.");
13188
- 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.`, "");
13189
13900
  process.exit(1);
13190
13901
  }
13191
13902
  if (hasCommits) {
13192
- 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)}.`);
13193
13904
  }
13194
13905
  if (dirty) {
13195
13906
  info("You also have uncommitted changes in the working tree.");
@@ -13205,58 +13916,35 @@ var submit_default = defineCommand({
13205
13916
  info("No changes made. You are still on your current branch.");
13206
13917
  return;
13207
13918
  }
13208
- info(import_picocolors16.default.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
13209
- const description = await inputPrompt("What are you going to work on?");
13210
- let newBranchName = description;
13211
- if (looksLikeNaturalLanguage(description)) {
13212
- const copilotError = await checkCopilotAvailable();
13213
- if (!copilotError) {
13214
- const spinner = createSpinner("Generating branch name suggestion...");
13215
- const suggested = await suggestBranchName(description, args.model);
13216
- if (suggested) {
13217
- spinner.success("Branch name suggestion ready.");
13218
- console.log(`
13219
- ${import_picocolors16.default.dim("AI suggestion:")} ${import_picocolors16.default.bold(import_picocolors16.default.cyan(suggested))}`);
13220
- const accepted = await confirmPrompt(`Use ${import_picocolors16.default.bold(suggested)} as your branch name?`);
13221
- newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
13222
- } else {
13223
- spinner.fail("AI did not return a suggestion.");
13224
- newBranchName = await inputPrompt("Enter branch name", description);
13225
- }
13226
- }
13227
- }
13228
- if (!hasPrefix(newBranchName, config.branchPrefixes)) {
13229
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors16.default.bold(newBranchName)}:`, config.branchPrefixes);
13230
- newBranchName = formatBranchName(prefix, newBranchName);
13231
- }
13232
- if (!isValidBranchName(newBranchName)) {
13233
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
13234
- process.exit(1);
13235
- }
13236
- if (await branchExists(newBranchName)) {
13237
- error(`Branch ${import_picocolors16.default.bold(newBranchName)} already exists. Choose a different name.`);
13238
- 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;
13239
13927
  }
13240
13928
  const branchResult = await createBranch(newBranchName);
13241
13929
  if (branchResult.exitCode !== 0) {
13242
13930
  error(`Failed to create branch: ${branchResult.stderr}`);
13243
13931
  process.exit(1);
13244
13932
  }
13245
- success(`Created ${import_picocolors16.default.bold(newBranchName)} with your changes.`);
13933
+ success(`Created ${import_picocolors17.default.bold(newBranchName)} with your changes.`);
13246
13934
  await updateLocalBranch(currentBranch, remoteRef);
13247
- 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.`, "");
13248
13936
  console.log();
13249
- success(`You're now on ${import_picocolors16.default.bold(newBranchName)} with all your work intact.`);
13250
- 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.`, "");
13251
13939
  return;
13252
13940
  }
13253
- heading("\uD83D\uDE80 contrib submit");
13941
+ projectHeading("submit", "\uD83D\uDE80");
13254
13942
  const ghInstalled = await checkGhInstalled();
13255
13943
  const ghAuthed = ghInstalled && await checkGhAuth();
13256
13944
  if (ghInstalled && ghAuthed) {
13257
13945
  const mergedPR = await getMergedPRForBranch(currentBranch);
13258
13946
  if (mergedPR) {
13259
- 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.`);
13260
13948
  const localWork = await hasLocalWork(origin, currentBranch);
13261
13949
  const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
13262
13950
  if (hasWork) {
@@ -13264,7 +13952,7 @@ var submit_default = defineCommand({
13264
13952
  warn("You have uncommitted changes in your working tree.");
13265
13953
  }
13266
13954
  if (localWork.unpushedCommits > 0) {
13267
- 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.`);
13268
13956
  }
13269
13957
  const SAVE_NEW_BRANCH = "Save changes to a new branch";
13270
13958
  const DISCARD = "Discard all changes and clean up";
@@ -13275,46 +13963,26 @@ var submit_default = defineCommand({
13275
13963
  return;
13276
13964
  }
13277
13965
  if (action === SAVE_NEW_BRANCH) {
13278
- info(import_picocolors16.default.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
13279
- const description = await inputPrompt("What are you going to work on?");
13280
- let newBranchName = description;
13281
- if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
13282
- const spinner = createSpinner("Generating branch name suggestion...");
13283
- const suggested = await suggestBranchName(description, args.model);
13284
- if (suggested) {
13285
- spinner.success("Branch name suggestion ready.");
13286
- console.log(`
13287
- ${import_picocolors16.default.dim("AI suggestion:")} ${import_picocolors16.default.bold(import_picocolors16.default.cyan(suggested))}`);
13288
- const accepted = await confirmPrompt(`Use ${import_picocolors16.default.bold(suggested)} as your branch name?`);
13289
- newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
13290
- } else {
13291
- spinner.fail("AI did not return a suggestion.");
13292
- newBranchName = await inputPrompt("Enter branch name", description);
13293
- }
13294
- }
13295
- if (!hasPrefix(newBranchName, config.branchPrefixes)) {
13296
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors16.default.bold(newBranchName)}:`, config.branchPrefixes);
13297
- newBranchName = formatBranchName(prefix, newBranchName);
13298
- }
13299
- if (!isValidBranchName(newBranchName)) {
13300
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
13301
- 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;
13302
13974
  }
13303
13975
  const staleUpstream = await getUpstreamRef();
13304
13976
  const staleUpstreamHash = staleUpstream ? await getCommitHash(staleUpstream) : null;
13305
- if (await branchExists(newBranchName)) {
13306
- error(`Branch ${import_picocolors16.default.bold(newBranchName)} already exists. Choose a different name.`);
13307
- process.exit(1);
13308
- }
13309
13977
  const renameResult = await renameBranch(currentBranch, newBranchName);
13310
13978
  if (renameResult.exitCode !== 0) {
13311
13979
  error(`Failed to rename branch: ${renameResult.stderr}`);
13312
13980
  process.exit(1);
13313
13981
  }
13314
- 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)}`);
13315
13983
  await unsetUpstream();
13316
13984
  const syncSource2 = getSyncSource(config);
13317
- 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)}...`);
13318
13986
  await fetchRemote(syncSource2.remote);
13319
13987
  let rebaseResult;
13320
13988
  if (staleUpstreamHash) {
@@ -13325,17 +13993,17 @@ var submit_default = defineCommand({
13325
13993
  }
13326
13994
  if (rebaseResult.exitCode !== 0) {
13327
13995
  warn("Rebase encountered conflicts. Resolve them manually, then run:");
13328
- info(` ${import_picocolors16.default.bold("git rebase --continue")}`, "");
13996
+ info(` ${import_picocolors17.default.bold("git rebase --continue")}`, "");
13329
13997
  } else {
13330
- 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)}.`);
13331
13999
  }
13332
- 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.`, "");
13333
14001
  return;
13334
14002
  }
13335
14003
  warn("Discarding local changes...");
13336
14004
  }
13337
14005
  const syncSource = getSyncSource(config);
13338
- info(`Switching to ${import_picocolors16.default.bold(baseBranch)} and syncing...`);
14006
+ info(`Switching to ${import_picocolors17.default.bold(baseBranch)} and syncing...`);
13339
14007
  await fetchRemote(syncSource.remote);
13340
14008
  await resetHard("HEAD");
13341
14009
  const coResult = await checkoutBranch(baseBranch);
@@ -13344,23 +14012,23 @@ var submit_default = defineCommand({
13344
14012
  process.exit(1);
13345
14013
  }
13346
14014
  await updateLocalBranch(baseBranch, syncSource.ref);
13347
- success(`Synced ${import_picocolors16.default.bold(baseBranch)} with ${import_picocolors16.default.bold(syncSource.ref)}.`);
13348
- 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)}...`);
13349
14017
  const delResult = await forceDeleteBranch(currentBranch);
13350
14018
  if (delResult.exitCode === 0) {
13351
- success(`Deleted ${import_picocolors16.default.bold(currentBranch)}.`);
14019
+ success(`Deleted ${import_picocolors17.default.bold(currentBranch)}.`);
13352
14020
  } else {
13353
14021
  warn(`Could not delete branch: ${delResult.stderr.trim()}`);
13354
14022
  }
13355
14023
  console.log();
13356
- 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.`);
13357
14025
  return;
13358
14026
  }
13359
14027
  }
13360
14028
  if (ghInstalled && ghAuthed) {
13361
14029
  const existingPR = await getPRForBranch(currentBranch);
13362
14030
  if (existingPR) {
13363
- info(`Pushing ${import_picocolors16.default.bold(currentBranch)} to ${origin}...`);
14031
+ info(`Pushing ${import_picocolors17.default.bold(currentBranch)} to ${origin}...`);
13364
14032
  const pushResult2 = await pushSetUpstream(origin, currentBranch);
13365
14033
  if (pushResult2.exitCode !== 0) {
13366
14034
  error(`Failed to push: ${pushResult2.stderr}`);
@@ -13371,8 +14039,8 @@ var submit_default = defineCommand({
13371
14039
  }
13372
14040
  process.exit(1);
13373
14041
  }
13374
- success(`Pushed changes to existing PR #${existingPR.number}: ${import_picocolors16.default.bold(existingPR.title)}`);
13375
- 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)}`);
13376
14044
  return;
13377
14045
  }
13378
14046
  }
@@ -13380,22 +14048,24 @@ var submit_default = defineCommand({
13380
14048
  let prBody = null;
13381
14049
  async function tryGenerateAI() {
13382
14050
  const [copilotError, commits, diff] = await Promise.all([
13383
- checkCopilotAvailable(),
14051
+ checkCopilotAvailable2(),
13384
14052
  getLog(baseBranch, "HEAD"),
13385
14053
  getLogDiff(baseBranch, "HEAD")
13386
14054
  ]);
13387
14055
  if (!copilotError) {
13388
- const spinner = createSpinner("Generating AI PR description...");
14056
+ const spinner = createSpinner("Generating AI PR description...", {
14057
+ tips: LOADING_TIPS
14058
+ });
13389
14059
  const result = await generatePRDescription(commits, diff, args.model, config.commitConvention);
13390
14060
  if (result) {
13391
14061
  prTitle = result.title;
13392
14062
  prBody = result.body;
13393
14063
  spinner.success("PR description generated.");
13394
14064
  console.log(`
13395
- ${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))}`);
13396
14066
  console.log(`
13397
- ${import_picocolors16.default.dim("AI body preview:")}`);
13398
- 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 ? "..." : "")));
13399
14069
  } else {
13400
14070
  spinner.fail("AI did not return a PR description.");
13401
14071
  }
@@ -13408,8 +14078,28 @@ ${import_picocolors16.default.dim("AI body preview:")}`);
13408
14078
  const REGENERATE = "Regenerate AI description";
13409
14079
  let submitAction = "cancel";
13410
14080
  const isMaintainer = config.role === "maintainer";
13411
- if (isMaintainer) {
13412
- 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
+ ]);
13413
14103
  if (maintainerChoice === CANCEL) {
13414
14104
  warn("Submit cancelled.");
13415
14105
  return;
@@ -13417,12 +14107,13 @@ ${import_picocolors16.default.dim("AI body preview:")}`);
13417
14107
  if (maintainerChoice === SQUASH_LOCAL) {
13418
14108
  await performSquashMerge(origin, baseBranch, currentBranch, {
13419
14109
  model: args.model,
13420
- convention: config.commitConvention
14110
+ convention: config.commitConvention,
14111
+ useAI: aiEnabled
13421
14112
  });
13422
14113
  return;
13423
14114
  }
13424
14115
  }
13425
- if (!args["no-ai"]) {
14116
+ if (aiEnabled) {
13426
14117
  await tryGenerateAI();
13427
14118
  }
13428
14119
  let actionResolved = false;
@@ -13461,7 +14152,7 @@ ${import_picocolors16.default.dim("AI body preview:")}`);
13461
14152
  }
13462
14153
  } else {
13463
14154
  const choices = [];
13464
- if (!args["no-ai"])
14155
+ if (aiEnabled)
13465
14156
  choices.push(REGENERATE);
13466
14157
  choices.push("Write title & body manually", "Use gh --fill (auto-fill from commits)", CANCEL);
13467
14158
  const action = await selectPrompt("How would you like to create the PR?", choices);
@@ -13485,7 +14176,7 @@ ${import_picocolors16.default.dim("AI body preview:")}`);
13485
14176
  warn("Submit cancelled.");
13486
14177
  return;
13487
14178
  }
13488
- info(`Pushing ${import_picocolors16.default.bold(currentBranch)} to ${origin}...`);
14179
+ info(`Pushing ${import_picocolors17.default.bold(currentBranch)} to ${origin}...`);
13489
14180
  const pushResult = await pushSetUpstream(origin, currentBranch);
13490
14181
  if (pushResult.exitCode !== 0) {
13491
14182
  error(`Failed to push: ${pushResult.stderr}`);
@@ -13504,7 +14195,7 @@ ${import_picocolors16.default.dim("AI body preview:")}`);
13504
14195
  const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/compare/${baseBranch}...${currentBranch}?expand=1`;
13505
14196
  console.log();
13506
14197
  info("Create your PR manually:", "");
13507
- console.log(` ${import_picocolors16.default.cyan(prUrl)}`);
14198
+ console.log(` ${import_picocolors17.default.cyan(prUrl)}`);
13508
14199
  } else {
13509
14200
  info("gh CLI not available. Create your PR manually on GitHub.", "");
13510
14201
  }
@@ -13538,7 +14229,7 @@ ${import_picocolors16.default.dim("AI body preview:")}`);
13538
14229
  });
13539
14230
 
13540
14231
  // src/commands/switch.ts
13541
- var import_picocolors17 = __toESM(require_picocolors(), 1);
14232
+ var import_picocolors18 = __toESM(require_picocolors(), 1);
13542
14233
  var switch_default = defineCommand({
13543
14234
  meta: {
13544
14235
  name: "switch",
@@ -13559,7 +14250,7 @@ var switch_default = defineCommand({
13559
14250
  const config = readConfig();
13560
14251
  const protectedBranches = config ? getProtectedBranches(config) : ["main", "master"];
13561
14252
  const currentBranch = await getCurrentBranch();
13562
- heading("\uD83D\uDD00 contrib switch");
14253
+ projectHeading("switch", "\uD83D\uDD00");
13563
14254
  let targetBranch = args.name;
13564
14255
  if (!targetBranch) {
13565
14256
  const localBranches = await getLocalBranches();
@@ -13570,11 +14261,11 @@ var switch_default = defineCommand({
13570
14261
  const choices = localBranches.filter((b2) => b2.name !== currentBranch).map((b2) => {
13571
14262
  const labels = [];
13572
14263
  if (protectedBranches.includes(b2.name))
13573
- labels.push(import_picocolors17.default.red("protected"));
14264
+ labels.push(import_picocolors18.default.red("protected"));
13574
14265
  if (b2.upstream)
13575
- labels.push(import_picocolors17.default.dim(`→ ${b2.upstream}`));
14266
+ labels.push(import_picocolors18.default.dim(`→ ${b2.upstream}`));
13576
14267
  if (b2.gone)
13577
- labels.push(import_picocolors17.default.red("remote gone"));
14268
+ labels.push(import_picocolors18.default.red("remote gone"));
13578
14269
  const suffix = labels.length > 0 ? ` ${labels.join(" · ")}` : "";
13579
14270
  return `${b2.name}${suffix}`;
13580
14271
  });
@@ -13586,7 +14277,7 @@ var switch_default = defineCommand({
13586
14277
  targetBranch = selected.split(/\s{2,}/)[0].trim();
13587
14278
  }
13588
14279
  if (targetBranch === currentBranch) {
13589
- info(`Already on ${import_picocolors17.default.bold(targetBranch)}.`);
14280
+ info(`Already on ${import_picocolors18.default.bold(targetBranch)}.`);
13590
14281
  return;
13591
14282
  }
13592
14283
  if (await hasUncommittedChanges()) {
@@ -13605,7 +14296,7 @@ var switch_default = defineCommand({
13605
14296
  const stashMsg = `contrib-save: auto-save from ${currentBranch}`;
13606
14297
  try {
13607
14298
  await exec("git", ["stash", "push", "-m", stashMsg]);
13608
- info(`Saved changes: ${import_picocolors17.default.dim(stashMsg)}`);
14299
+ info(`Saved changes: ${import_picocolors18.default.dim(stashMsg)}`);
13609
14300
  } catch {
13610
14301
  error("Failed to save changes. Please commit or save manually.");
13611
14302
  process.exit(1);
@@ -13621,9 +14312,9 @@ var switch_default = defineCommand({
13621
14312
  }
13622
14313
  process.exit(1);
13623
14314
  }
13624
- success(`Switched to ${import_picocolors17.default.bold(targetBranch)}`);
13625
- info(`Your changes from ${import_picocolors17.default.bold(currentBranch ?? "previous branch")} are saved.`, "");
13626
- 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.`, "");
13627
14318
  return;
13628
14319
  }
13629
14320
  const result = await checkoutBranch(targetBranch);
@@ -13631,12 +14322,12 @@ var switch_default = defineCommand({
13631
14322
  error(`Failed to switch to ${targetBranch}: ${result.stderr}`);
13632
14323
  process.exit(1);
13633
14324
  }
13634
- success(`Switched to ${import_picocolors17.default.bold(targetBranch)}`);
14325
+ success(`Switched to ${import_picocolors18.default.bold(targetBranch)}`);
13635
14326
  }
13636
14327
  });
13637
14328
 
13638
14329
  // src/commands/sync.ts
13639
- var import_picocolors18 = __toESM(require_picocolors(), 1);
14330
+ var import_picocolors19 = __toESM(require_picocolors(), 1);
13640
14331
  var sync_default = defineCommand({
13641
14332
  meta: {
13642
14333
  name: "sync",
@@ -13667,7 +14358,7 @@ var sync_default = defineCommand({
13667
14358
  await assertCleanGitState("syncing");
13668
14359
  const config = readConfig();
13669
14360
  if (!config) {
13670
- error("No .contributerc.json found. Run `contrib setup` first.");
14361
+ error("No repo config found. Run `contrib setup` first.");
13671
14362
  process.exit(1);
13672
14363
  }
13673
14364
  const { workflow, role, origin } = config;
@@ -13675,7 +14366,7 @@ var sync_default = defineCommand({
13675
14366
  error("You have uncommitted changes. Please commit or stash them before syncing.");
13676
14367
  process.exit(1);
13677
14368
  }
13678
- heading(`\uD83D\uDD04 contrib sync (${workflow}, ${role})`);
14369
+ projectHeading(`sync (${workflow}, ${role})`, "\uD83D\uDD04");
13679
14370
  const baseBranch = getBaseBranch(config);
13680
14371
  const syncSource = getSyncSource(config);
13681
14372
  info(`Fetching ${syncSource.remote}...`);
@@ -13688,24 +14379,24 @@ var sync_default = defineCommand({
13688
14379
  await fetchRemote(origin);
13689
14380
  }
13690
14381
  if (!await refExists(syncSource.ref)) {
13691
- 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.`);
13692
14383
  info("This can happen if the branch was renamed or deleted on the remote.", "");
13693
- 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")}.`, "");
13694
14385
  process.exit(1);
13695
14386
  }
13696
14387
  let allowMergeCommit = false;
13697
14388
  const div = await getDivergence(baseBranch, syncSource.ref);
13698
14389
  if (div.ahead > 0 || div.behind > 0) {
13699
- 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}`);
13700
14391
  } else {
13701
- 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}`);
13702
14393
  }
13703
14394
  if (div.ahead > 0) {
13704
14395
  const currentBranch = await getCurrentBranch();
13705
14396
  const protectedBranches = getProtectedBranches(config);
13706
14397
  const isOnProtected = currentBranch && protectedBranches.includes(currentBranch);
13707
14398
  if (isOnProtected) {
13708
- 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.`);
13709
14400
  info("Pulling now could create a merge commit, which breaks clean history.");
13710
14401
  console.log();
13711
14402
  const MOVE_BRANCH = "Move my commits to a new feature branch, then sync";
@@ -13721,44 +14412,21 @@ var sync_default = defineCommand({
13721
14412
  return;
13722
14413
  }
13723
14414
  if (action === MOVE_BRANCH) {
13724
- info(import_picocolors18.default.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
13725
- const description = await inputPrompt("What are you going to work on?");
13726
- let newBranchName = description;
13727
- if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
13728
- const copilotError = await checkCopilotAvailable();
13729
- if (!copilotError) {
13730
- const spinner = createSpinner("Generating branch name suggestion...");
13731
- const suggested = await suggestBranchName(description, args.model);
13732
- if (suggested) {
13733
- spinner.success("Branch name suggestion ready.");
13734
- console.log(`
13735
- ${import_picocolors18.default.dim("AI suggestion:")} ${import_picocolors18.default.bold(import_picocolors18.default.cyan(suggested))}`);
13736
- const accepted = await confirmPrompt(`Use ${import_picocolors18.default.bold(suggested)} as your branch name?`);
13737
- newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
13738
- } else {
13739
- spinner.fail("AI did not return a suggestion.");
13740
- newBranchName = await inputPrompt("Enter branch name", description);
13741
- }
13742
- }
13743
- }
13744
- if (!hasPrefix(newBranchName, config.branchPrefixes)) {
13745
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors18.default.bold(newBranchName)}:`, config.branchPrefixes);
13746
- newBranchName = formatBranchName(prefix, newBranchName);
13747
- }
13748
- if (!isValidBranchName(newBranchName)) {
13749
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
13750
- process.exit(1);
13751
- }
13752
- if (await branchExists(newBranchName)) {
13753
- error(`Branch ${import_picocolors18.default.bold(newBranchName)} already exists. Choose a different name.`);
13754
- 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;
13755
14423
  }
13756
14424
  const branchResult = await createBranch(newBranchName);
13757
14425
  if (branchResult.exitCode !== 0) {
13758
14426
  error(`Failed to create branch: ${branchResult.stderr}`);
13759
14427
  process.exit(1);
13760
14428
  }
13761
- success(`Created ${import_picocolors18.default.bold(newBranchName)} with your commits.`);
14429
+ success(`Created ${import_picocolors19.default.bold(newBranchName)} with your commits.`);
13762
14430
  const coResult2 = await checkoutBranch(baseBranch);
13763
14431
  if (coResult2.exitCode !== 0) {
13764
14432
  error(`Failed to checkout ${baseBranch}: ${coResult2.stderr}`);
@@ -13766,11 +14434,11 @@ var sync_default = defineCommand({
13766
14434
  }
13767
14435
  const remoteRef = syncSource.ref;
13768
14436
  await updateLocalBranch(baseBranch, remoteRef);
13769
- success(`Reset ${import_picocolors18.default.bold(baseBranch)} to ${import_picocolors18.default.bold(remoteRef)}.`);
13770
- 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}`);
13771
14439
  console.log();
13772
- info(`Your commits are safe on ${import_picocolors18.default.bold(newBranchName)}.`, "");
13773
- 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)}.`, "");
13774
14442
  return;
13775
14443
  }
13776
14444
  allowMergeCommit = true;
@@ -13778,7 +14446,7 @@ var sync_default = defineCommand({
13778
14446
  }
13779
14447
  }
13780
14448
  if (!args.yes) {
13781
- 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)}.`);
13782
14450
  if (!ok)
13783
14451
  process.exit(0);
13784
14452
  }
@@ -13792,8 +14460,8 @@ var sync_default = defineCommand({
13792
14460
  if (allowMergeCommit) {
13793
14461
  error(`Pull failed: ${pullResult.stderr.trim()}`);
13794
14462
  } else {
13795
- error(`Fast-forward pull failed. Your local ${import_picocolors18.default.bold(baseBranch)} may have diverged.`);
13796
- 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.`, "");
13797
14465
  }
13798
14466
  process.exit(1);
13799
14467
  }
@@ -13801,7 +14469,7 @@ var sync_default = defineCommand({
13801
14469
  if (hasDevBranch(workflow) && role === "maintainer") {
13802
14470
  const mainDiv = await getDivergence(config.mainBranch, `${origin}/${config.mainBranch}`);
13803
14471
  if (mainDiv.behind > 0) {
13804
- info(`Also syncing ${import_picocolors18.default.bold(config.mainBranch)}...`);
14472
+ info(`Also syncing ${import_picocolors19.default.bold(config.mainBranch)}...`);
13805
14473
  const mainCoResult = await checkoutBranch(config.mainBranch);
13806
14474
  if (mainCoResult.exitCode === 0) {
13807
14475
  const mainPullResult = await pullFastForwardOnly(origin, config.mainBranch);
@@ -13836,23 +14504,26 @@ var sync_default = defineCommand({
13836
14504
  for (const { name, hash } of refs) {
13837
14505
  if (!groups.has(hash))
13838
14506
  groups.set(hash, []);
13839
- groups.get(hash).push(name);
14507
+ const group = groups.get(hash);
14508
+ if (group) {
14509
+ group.push(name);
14510
+ }
13840
14511
  }
13841
14512
  console.log();
13842
- console.log(` ${import_picocolors18.default.bold("\uD83D\uDD17 Branch Alignment")}`);
14513
+ console.log(` ${import_picocolors19.default.bold("\uD83D\uDD17 Branch Alignment")}`);
13843
14514
  for (const [hash, names] of groups) {
13844
14515
  const short = hash.slice(0, 7);
13845
- const nameStr = names.map((n2) => import_picocolors18.default.bold(n2)).join(import_picocolors18.default.dim(" · "));
13846
- 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}`);
13847
14518
  const subject = await getCommitSubject(hash);
13848
14519
  if (subject) {
13849
- console.log(` ${import_picocolors18.default.dim(subject)}`);
14520
+ console.log(` ${import_picocolors19.default.dim(subject)}`);
13850
14521
  }
13851
14522
  }
13852
14523
  if (groups.size === 1) {
13853
- 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")}`);
13854
14525
  } else {
13855
- 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")}`);
13856
14527
  }
13857
14528
  }
13858
14529
  }
@@ -13861,7 +14532,10 @@ var sync_default = defineCommand({
13861
14532
 
13862
14533
  // src/commands/update.ts
13863
14534
  import { readFileSync as readFileSync4 } from "node:fs";
13864
- 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
+ }
13865
14539
  var update_default = defineCommand({
13866
14540
  meta: {
13867
14541
  name: "update",
@@ -13886,7 +14560,7 @@ var update_default = defineCommand({
13886
14560
  await assertCleanGitState("updating");
13887
14561
  const config = readConfig();
13888
14562
  if (!config) {
13889
- error("No .contributerc.json found. Run `contrib setup` first.");
14563
+ error("No repo config found. Run `contrib setup` first.");
13890
14564
  process.exit(1);
13891
14565
  }
13892
14566
  const baseBranch = getBaseBranch(config);
@@ -13898,8 +14572,8 @@ var update_default = defineCommand({
13898
14572
  process.exit(1);
13899
14573
  }
13900
14574
  if (protectedBranches.includes(currentBranch)) {
13901
- heading("\uD83D\uDD03 contrib update");
13902
- 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.`);
13903
14577
  await fetchAll();
13904
14578
  const { origin } = config;
13905
14579
  const remoteRef = `${origin}/${currentBranch}`;
@@ -13908,12 +14582,12 @@ var update_default = defineCommand({
13908
14582
  const hasCommits = localWork.unpushedCommits > 0;
13909
14583
  const hasAnything = hasCommits || dirty;
13910
14584
  if (!hasAnything) {
13911
- info(`No local changes found on ${import_picocolors19.default.bold(currentBranch)}.`);
13912
- 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.`);
13913
14587
  process.exit(1);
13914
14588
  }
13915
14589
  if (hasCommits) {
13916
- 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)}.`);
13917
14591
  }
13918
14592
  if (dirty) {
13919
14593
  info("You also have uncommitted changes in the working tree.");
@@ -13929,111 +14603,73 @@ var update_default = defineCommand({
13929
14603
  info("No changes made. You are still on your current branch.");
13930
14604
  return;
13931
14605
  }
13932
- info(import_picocolors19.default.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
13933
- const description = await inputPrompt("What are you going to work on?");
13934
- let newBranchName = description;
13935
- if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
13936
- const copilotError = await checkCopilotAvailable();
13937
- if (!copilotError) {
13938
- const spinner = createSpinner("Generating branch name suggestion...");
13939
- const suggested = await suggestBranchName(description, args.model);
13940
- if (suggested) {
13941
- spinner.success("Branch name suggestion ready.");
13942
- console.log(`
13943
- ${import_picocolors19.default.dim("AI suggestion:")} ${import_picocolors19.default.bold(import_picocolors19.default.cyan(suggested))}`);
13944
- const accepted = await confirmPrompt(`Use ${import_picocolors19.default.bold(suggested)} as your branch name?`);
13945
- newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
13946
- } else {
13947
- spinner.fail("AI did not return a suggestion.");
13948
- newBranchName = await inputPrompt("Enter branch name", description);
13949
- }
13950
- }
13951
- }
13952
- if (!hasPrefix(newBranchName, config.branchPrefixes)) {
13953
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors19.default.bold(newBranchName)}:`, config.branchPrefixes);
13954
- newBranchName = formatBranchName(prefix, newBranchName);
13955
- }
13956
- if (!isValidBranchName(newBranchName)) {
13957
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
13958
- 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;
13959
14614
  }
13960
14615
  const branchResult = await createBranch(newBranchName);
13961
14616
  if (branchResult.exitCode !== 0) {
13962
14617
  error(`Failed to create branch: ${branchResult.stderr}`);
13963
14618
  process.exit(1);
13964
14619
  }
13965
- success(`Created ${import_picocolors19.default.bold(newBranchName)} with your changes.`);
14620
+ success(`Created ${import_picocolors20.default.bold(newBranchName)} with your changes.`);
13966
14621
  await updateLocalBranch(currentBranch, remoteRef);
13967
- 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.`, "");
13968
14623
  console.log();
13969
- success(`You're now on ${import_picocolors19.default.bold(newBranchName)} with all your work intact.`);
13970
- 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)}.`, "");
13971
14626
  return;
13972
14627
  }
13973
14628
  if (await hasUncommittedChanges()) {
13974
14629
  error("You have uncommitted changes. Please commit or stash them first.");
13975
14630
  process.exit(1);
13976
14631
  }
13977
- heading("\uD83D\uDD03 contrib update");
14632
+ projectHeading("update", "\uD83D\uDD03");
13978
14633
  const mergedPR = await getMergedPRForBranch(currentBranch);
13979
14634
  if (mergedPR) {
13980
- warn(`PR #${mergedPR.number} (${import_picocolors19.default.bold(mergedPR.title)}) has already been merged.`);
13981
- info(`Link: ${import_picocolors19.default.underline(mergedPR.url)}`, "");
13982
- const localWork = await hasLocalWork(syncSource.remote, currentBranch);
13983
- 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);
13984
14640
  if (hasWork) {
13985
- if (localWork.uncommitted) {
14641
+ if (dirty) {
13986
14642
  info("You have uncommitted local changes.");
13987
14643
  }
13988
- if (localWork.unpushedCommits > 0) {
13989
- 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)}.`);
13990
14646
  }
13991
14647
  const SAVE_NEW_BRANCH = "Save changes to a new branch";
13992
14648
  const DISCARD = "Discard all changes and clean up";
13993
14649
  const CANCEL = "Cancel";
13994
- 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]);
13995
14651
  if (action === CANCEL) {
13996
14652
  info("No changes made. You are still on your current branch.");
13997
14653
  return;
13998
14654
  }
13999
14655
  if (action === SAVE_NEW_BRANCH) {
14000
- info(import_picocolors19.default.dim("Tip: Describe what you're going to work on in plain English and we'll generate a branch name."));
14001
- const description = await inputPrompt("What are you going to work on?");
14002
- let newBranchName = description;
14003
- if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
14004
- const spinner = createSpinner("Generating branch name suggestion...");
14005
- const suggested = await suggestBranchName(description, args.model);
14006
- if (suggested) {
14007
- spinner.success("Branch name suggestion ready.");
14008
- console.log(`
14009
- ${import_picocolors19.default.dim("AI suggestion:")} ${import_picocolors19.default.bold(import_picocolors19.default.cyan(suggested))}`);
14010
- const accepted = await confirmPrompt(`Use ${import_picocolors19.default.bold(suggested)} as your branch name?`);
14011
- newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
14012
- } else {
14013
- spinner.fail("AI did not return a suggestion.");
14014
- newBranchName = await inputPrompt("Enter branch name", description);
14015
- }
14016
- }
14017
- if (!hasPrefix(newBranchName, config.branchPrefixes)) {
14018
- const prefix = await selectPrompt(`Choose a branch type for ${import_picocolors19.default.bold(newBranchName)}:`, config.branchPrefixes);
14019
- newBranchName = formatBranchName(prefix, newBranchName);
14020
- }
14021
- if (!isValidBranchName(newBranchName)) {
14022
- error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
14023
- 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;
14024
14664
  }
14025
14665
  const staleUpstream = await getUpstreamRef();
14026
14666
  const staleUpstreamHash = staleUpstream ? await getCommitHash(staleUpstream) : null;
14027
- if (await branchExists(newBranchName)) {
14028
- error(`Branch ${import_picocolors19.default.bold(newBranchName)} already exists. Choose a different name.`);
14029
- process.exit(1);
14030
- }
14031
14667
  const renameResult = await renameBranch(currentBranch, newBranchName);
14032
14668
  if (renameResult.exitCode !== 0) {
14033
14669
  error(`Failed to rename branch: ${renameResult.stderr}`);
14034
14670
  process.exit(1);
14035
14671
  }
14036
- 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)}`);
14037
14673
  await unsetUpstream();
14038
14674
  await fetchRemote(syncSource.remote);
14039
14675
  let rebaseResult2;
@@ -14045,14 +14681,20 @@ var update_default = defineCommand({
14045
14681
  }
14046
14682
  if (rebaseResult2.exitCode !== 0) {
14047
14683
  warn("Rebase encountered conflicts. Resolve them manually, then run:");
14048
- info(` ${import_picocolors19.default.bold("git rebase --continue")}`, "");
14684
+ info(` ${import_picocolors20.default.bold("git rebase --continue")}`, "");
14049
14685
  } else {
14050
- 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)}.`);
14051
14687
  }
14052
- 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.`, "");
14053
14689
  return;
14054
14690
  }
14055
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
+ }
14056
14698
  }
14057
14699
  await fetchRemote(syncSource.remote);
14058
14700
  await resetHard("HEAD");
@@ -14062,30 +14704,30 @@ var update_default = defineCommand({
14062
14704
  process.exit(1);
14063
14705
  }
14064
14706
  await updateLocalBranch(baseBranch, syncSource.ref);
14065
- success(`Synced ${import_picocolors19.default.bold(baseBranch)} with ${import_picocolors19.default.bold(syncSource.ref)}.`);
14066
- 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)}...`);
14067
14709
  await forceDeleteBranch(currentBranch);
14068
- success(`Deleted ${import_picocolors19.default.bold(currentBranch)}.`);
14069
- 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.`, "");
14070
14712
  return;
14071
14713
  }
14072
- 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)}...`);
14073
14715
  await fetchRemote(syncSource.remote);
14074
14716
  if (!await refExists(syncSource.ref)) {
14075
- 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.`);
14076
14718
  error("Run `git fetch --all` and verify your remote configuration.");
14077
14719
  process.exit(1);
14078
14720
  }
14079
14721
  await updateLocalBranch(baseBranch, syncSource.ref);
14080
14722
  const rebaseStrategy = await determineRebaseStrategy(currentBranch, syncSource.ref);
14081
14723
  if (rebaseStrategy.strategy === "onto" && rebaseStrategy.ontoOldBase) {
14082
- 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)`));
14083
14725
  }
14084
14726
  const rebaseResult = rebaseStrategy.strategy === "onto" && rebaseStrategy.ontoOldBase ? await rebaseOnto(syncSource.ref, rebaseStrategy.ontoOldBase) : await rebase(syncSource.ref);
14085
14727
  if (rebaseResult.exitCode !== 0) {
14086
14728
  warn("Rebase hit conflicts. Resolve them manually.");
14087
14729
  console.log();
14088
- if (!args["no-ai"]) {
14730
+ if (isAIEnabled(config, args["no-ai"])) {
14089
14731
  const copilotError = await checkCopilotAvailable();
14090
14732
  if (!copilotError) {
14091
14733
  info("Fetching AI conflict resolution suggestions...");
@@ -14103,15 +14745,17 @@ ${content.slice(0, 2000)}
14103
14745
  } catch {}
14104
14746
  }
14105
14747
  if (conflictDiff) {
14106
- const spinner = createSpinner("Analyzing conflicts with AI...");
14748
+ const spinner = createSpinner("Analyzing conflicts with AI...", {
14749
+ tips: LOADING_TIPS
14750
+ });
14107
14751
  const suggestion = await suggestConflictResolution(conflictDiff, args.model);
14108
14752
  if (suggestion) {
14109
14753
  spinner.success("AI conflict guidance ready.");
14110
14754
  console.log(`
14111
- ${import_picocolors19.default.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
14112
- 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)));
14113
14757
  console.log(suggestion);
14114
- console.log(import_picocolors19.default.dim("─".repeat(60)));
14758
+ console.log(import_picocolors20.default.dim("─".repeat(60)));
14115
14759
  console.log();
14116
14760
  } else {
14117
14761
  spinner.fail("AI could not analyze the conflicts.");
@@ -14119,21 +14763,21 @@ ${import_picocolors19.default.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance
14119
14763
  }
14120
14764
  }
14121
14765
  }
14122
- console.log(import_picocolors19.default.bold("To resolve:"));
14766
+ console.log(import_picocolors20.default.bold("To resolve:"));
14123
14767
  console.log(` 1. Fix conflicts in the affected files`);
14124
- console.log(` 2. ${import_picocolors19.default.cyan("git add <resolved-files>")}`);
14125
- 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")}`);
14126
14770
  console.log();
14127
- console.log(` Or abort: ${import_picocolors19.default.cyan("git rebase --abort")}`);
14771
+ console.log(` Or abort: ${import_picocolors20.default.cyan("git rebase --abort")}`);
14128
14772
  process.exit(1);
14129
14773
  }
14130
- 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)}`);
14131
14775
  }
14132
14776
  });
14133
14777
 
14134
14778
  // src/commands/validate.ts
14135
- var import_picocolors20 = __toESM(require_picocolors(), 1);
14136
14779
  import { readFileSync as readFileSync5 } from "node:fs";
14780
+ var import_picocolors21 = __toESM(require_picocolors(), 1);
14137
14781
  var validate_default = defineCommand({
14138
14782
  meta: {
14139
14783
  name: "validate",
@@ -14153,9 +14797,10 @@ var validate_default = defineCommand({
14153
14797
  async run({ args }) {
14154
14798
  const config = readConfig();
14155
14799
  if (!config) {
14156
- error("No .contributerc.json found. Run `contrib setup` first.");
14800
+ error("No repo config found. Run `contrib setup` first.");
14157
14801
  process.exit(1);
14158
14802
  }
14803
+ projectHeading("validate", "✅");
14159
14804
  const convention = config.commitConvention;
14160
14805
  if (convention === "none") {
14161
14806
  info('Commit convention is set to "none". All messages are accepted.');
@@ -14172,7 +14817,7 @@ var validate_default = defineCommand({
14172
14817
  }
14173
14818
  const errors = getValidationError(convention);
14174
14819
  for (const line of errors) {
14175
- console.error(import_picocolors20.default.red(` ✗ ${line}`));
14820
+ console.error(import_picocolors21.default.red(` ✗ ${line}`));
14176
14821
  }
14177
14822
  process.exit(1);
14178
14823
  }
@@ -15471,8 +16116,8 @@ var figlet = (() => {
15471
16116
  }
15472
16117
  };
15473
16118
  me2.fonts = function(callback) {
15474
- return new Promise(function(resolve2, reject) {
15475
- resolve2(fontList);
16119
+ return new Promise(function(resolve3, reject) {
16120
+ resolve3(fontList);
15476
16121
  if (callback) {
15477
16122
  callback(null, fontList);
15478
16123
  }
@@ -15494,12 +16139,12 @@ var nodeFiglet = figlet;
15494
16139
  nodeFiglet.defaults({ fontPath });
15495
16140
  nodeFiglet.loadFont = function(name, callback) {
15496
16141
  const actualFontName = getFontName(name);
15497
- return new Promise((resolve2, reject) => {
16142
+ return new Promise((resolve3, reject) => {
15498
16143
  if (nodeFiglet.figFonts[actualFontName]) {
15499
16144
  if (callback) {
15500
16145
  callback(null, nodeFiglet.figFonts[actualFontName].options);
15501
16146
  }
15502
- resolve2(nodeFiglet.figFonts[actualFontName].options);
16147
+ resolve3(nodeFiglet.figFonts[actualFontName].options);
15503
16148
  return;
15504
16149
  }
15505
16150
  fs2.readFile(path2.join(nodeFiglet.defaults().fontPath, actualFontName + ".flf"), { encoding: "utf-8" }, (err, fontData) => {
@@ -15516,7 +16161,7 @@ nodeFiglet.loadFont = function(name, callback) {
15516
16161
  if (callback) {
15517
16162
  callback(null, font);
15518
16163
  }
15519
- resolve2(font);
16164
+ resolve3(font);
15520
16165
  } catch (error2) {
15521
16166
  const typedError = error2 instanceof Error ? error2 : new Error(String(error2));
15522
16167
  if (callback) {
@@ -15538,7 +16183,7 @@ nodeFiglet.loadFontSync = function(font) {
15538
16183
  return nodeFiglet.parseFont(actualFontName, fontData);
15539
16184
  };
15540
16185
  nodeFiglet.fonts = function(next) {
15541
- return new Promise((resolve2, reject) => {
16186
+ return new Promise((resolve3, reject) => {
15542
16187
  const fontList2 = [];
15543
16188
  fs2.readdir(nodeFiglet.defaults().fontPath, (err, files) => {
15544
16189
  if (err) {
@@ -15552,7 +16197,7 @@ nodeFiglet.fonts = function(next) {
15552
16197
  }
15553
16198
  });
15554
16199
  next && next(null, fontList2);
15555
- resolve2(fontList2);
16200
+ resolve3(fontList2);
15556
16201
  });
15557
16202
  });
15558
16203
  };
@@ -15567,7 +16212,37 @@ nodeFiglet.fontsSync = function() {
15567
16212
  };
15568
16213
 
15569
16214
  // src/ui/banner.ts
15570
- 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
15571
16246
  var LOGO_BIG;
15572
16247
  try {
15573
16248
  LOGO_BIG = nodeFiglet.textSync(`Contribute
@@ -15589,19 +16264,194 @@ function getAuthor() {
15589
16264
  }
15590
16265
  function showBanner(variant = "small") {
15591
16266
  const logo = variant === "big" ? LOGO_BIG : LOGO_SMALL;
15592
- console.log(import_picocolors21.default.cyan(`
16267
+ console.log(import_picocolors22.default.cyan(`
15593
16268
  ${logo}`));
15594
- 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
+ }
15595
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)}┘`)}`);
15596
16352
  console.log();
15597
- console.log(` ${import_picocolors21.default.yellow("Star")} ${import_picocolors21.default.cyan("https://github.com/warengonzaga/contribute-now")}`);
15598
- console.log(` ${import_picocolors21.default.green("Contribute")} ${import_picocolors21.default.cyan("https://github.com/warengonzaga/contribute-now/blob/main/CONTRIBUTING.md")}`);
15599
- 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"))}`);
15600
16355
  }
15601
16356
  console.log();
15602
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
+ }
15603
16439
 
15604
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);
15605
16455
  var isVersion = process.argv.includes("--version") || process.argv.includes("-v");
15606
16456
  if (!isVersion) {
15607
16457
  const subCommands = [