contribute-now 0.8.0-dev.0a7f557 → 0.8.0-dev.703cd39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +216 -45
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -7291,10 +7291,13 @@ import {
7291
7291
  statSync,
7292
7292
  writeFileSync
7293
7293
  } from "fs";
7294
+ import { homedir } from "os";
7294
7295
  import { dirname, join, resolve } from "path";
7295
7296
  var CONFIG_FILENAME = ".contributerc.json";
7296
7297
  var LOCAL_CONFIG_DIRNAME = "contribute-now";
7297
7298
  var LOCAL_CONFIG_FILENAME = "config.json";
7299
+ var GLOBAL_CONFIG_DIRNAME = ".contribute-now";
7300
+ var GLOBAL_CONFIG_FILENAME = "config.json";
7298
7301
  function findRepoRoot(cwd = process.cwd()) {
7299
7302
  let current = resolve(cwd);
7300
7303
  while (true) {
@@ -7384,13 +7387,44 @@ function parseConfigFile(path) {
7384
7387
  aiHost: _aiHost,
7385
7388
  ...config
7386
7389
  } = parsed;
7387
- return {
7390
+ const normalizedConfig = {
7388
7391
  ...config,
7389
- aiEnabled: parsed.aiEnabled !== false,
7390
7392
  aiProvider: parsed.aiProvider,
7391
7393
  aiModel: parsed.aiModel?.trim() || undefined,
7392
7394
  showTips: parsed.showTips !== false
7393
7395
  };
7396
+ if (typeof parsed.aiEnabled === "boolean") {
7397
+ normalizedConfig.aiEnabled = parsed.aiEnabled;
7398
+ }
7399
+ return normalizedConfig;
7400
+ } catch {
7401
+ return null;
7402
+ }
7403
+ }
7404
+ function parseGlobalConfigFile(path) {
7405
+ try {
7406
+ const raw = readFileSync(path, "utf-8");
7407
+ const parsed = JSON.parse(raw);
7408
+ if (typeof parsed !== "object" || parsed === null) {
7409
+ return null;
7410
+ }
7411
+ if (parsed.aiProvider !== undefined && (typeof parsed.aiProvider !== "string" || !VALID_AI_PROVIDERS.includes(parsed.aiProvider))) {
7412
+ console.error(`Invalid aiProvider "${String(parsed.aiProvider)}" in ${GLOBAL_CONFIG_FILENAME}. Valid: ${VALID_AI_PROVIDERS.join(", ")}`);
7413
+ return null;
7414
+ }
7415
+ if (parsed.aiModel !== undefined && (typeof parsed.aiModel !== "string" || !parsed.aiModel.trim())) {
7416
+ console.error(`Invalid ${GLOBAL_CONFIG_FILENAME}: aiModel must be a non-empty string.`);
7417
+ return null;
7418
+ }
7419
+ if (parsed.aiEnabled !== undefined && typeof parsed.aiEnabled !== "boolean") {
7420
+ console.error(`Invalid ${GLOBAL_CONFIG_FILENAME}: aiEnabled must be a boolean when set.`);
7421
+ return null;
7422
+ }
7423
+ return {
7424
+ aiEnabled: parsed.aiEnabled,
7425
+ aiProvider: parsed.aiProvider,
7426
+ aiModel: parsed.aiModel?.trim() || undefined
7427
+ };
7394
7428
  } catch {
7395
7429
  return null;
7396
7430
  }
@@ -7405,6 +7439,9 @@ function getConfigPath(cwd = process.cwd()) {
7405
7439
  function getLegacyConfigPath(cwd = process.cwd()) {
7406
7440
  return join(findRepoRoot(cwd) ?? cwd, CONFIG_FILENAME);
7407
7441
  }
7442
+ function getGlobalConfigPath(baseDir = homedir()) {
7443
+ return join(baseDir, GLOBAL_CONFIG_DIRNAME, GLOBAL_CONFIG_FILENAME);
7444
+ }
7408
7445
  function getLocalConfigPath(cwd = process.cwd()) {
7409
7446
  const gitDir = resolveGitDir(cwd);
7410
7447
  if (!gitDir) {
@@ -7439,12 +7476,25 @@ function getConfigLocationLabel(cwd = process.cwd()) {
7439
7476
  function configExists(cwd = process.cwd()) {
7440
7477
  return getConfigSource(cwd) !== null;
7441
7478
  }
7479
+ function globalConfigExists(baseDir = homedir()) {
7480
+ return existsSync(getGlobalConfigPath(baseDir));
7481
+ }
7442
7482
  var VALID_WORKFLOWS = ["clean-flow", "github-flow", "git-flow"];
7443
7483
  var VALID_ROLES = ["maintainer", "contributor"];
7444
7484
  var VALID_CONVENTIONS = ["conventional", "clean-commit", "none"];
7445
7485
  var VALID_AI_PROVIDERS = ["copilot", "ollama-cloud", "openrouter"];
7446
- function isAIEnabled(config, cliNoAI = false) {
7447
- return config.aiEnabled !== false && !cliNoAI;
7486
+ function isAIEnabled(config, cliNoAI = false, globalConfig) {
7487
+ if (cliNoAI) {
7488
+ return false;
7489
+ }
7490
+ if (typeof config.aiEnabled === "boolean") {
7491
+ return config.aiEnabled;
7492
+ }
7493
+ const resolvedGlobalConfig = globalConfig === undefined ? readGlobalConfig() : globalConfig;
7494
+ if (typeof resolvedGlobalConfig?.aiEnabled === "boolean") {
7495
+ return resolvedGlobalConfig.aiEnabled;
7496
+ }
7497
+ return true;
7448
7498
  }
7449
7499
  function shouldShowTips(config) {
7450
7500
  return config?.showTips !== false;
@@ -7458,6 +7508,13 @@ function readConfig(cwd = process.cwd()) {
7458
7508
  return null;
7459
7509
  return parseConfigFile(path);
7460
7510
  }
7511
+ function readGlobalConfig(baseDir = homedir()) {
7512
+ const path = getGlobalConfigPath(baseDir);
7513
+ if (!existsSync(path)) {
7514
+ return null;
7515
+ }
7516
+ return parseGlobalConfigFile(path);
7517
+ }
7461
7518
  function writeConfig(config, cwd = process.cwd()) {
7462
7519
  const path = getConfigPath(cwd);
7463
7520
  const { aiHost: _aiHost, ...storedConfig } = config;
@@ -7465,6 +7522,15 @@ function writeConfig(config, cwd = process.cwd()) {
7465
7522
  writeFileSync(path, `${JSON.stringify(storedConfig, null, 2)}
7466
7523
  `, "utf-8");
7467
7524
  }
7525
+ function writeGlobalConfig(config, baseDir = homedir()) {
7526
+ const path = getGlobalConfigPath(baseDir);
7527
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
7528
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}
7529
+ `, {
7530
+ encoding: "utf-8",
7531
+ mode: 384
7532
+ });
7533
+ }
7468
7534
  function isGitignored(cwd = process.cwd()) {
7469
7535
  const gitignorePath = join(cwd, ".gitignore");
7470
7536
  if (!existsSync(gitignorePath))
@@ -10873,19 +10939,19 @@ init_dist();
10873
10939
 
10874
10940
  // src/utils/secrets.ts
10875
10941
  import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync4 } from "fs";
10876
- import { homedir } from "os";
10942
+ import { homedir as homedir2 } from "os";
10877
10943
  import { join as join5, resolve as resolve4 } from "path";
10878
10944
  var CONTRIBUTE_NOW_SECRETS_DIRNAME = ".contribute-now";
10879
10945
  var CONTRIBUTE_NOW_SECRETS_STORE_DIRNAME = "secrets";
10880
10946
  var OLLAMA_CLOUD_API_KEY = "ollama.cloud.apiKey";
10881
10947
  var OPENROUTER_API_KEY = "openrouter.apiKey";
10882
- function getSecretsStorePath(baseDir = homedir()) {
10948
+ function getSecretsStorePath(baseDir = homedir2()) {
10883
10949
  return resolve4(baseDir, CONTRIBUTE_NOW_SECRETS_DIRNAME, CONTRIBUTE_NOW_SECRETS_STORE_DIRNAME);
10884
10950
  }
10885
- function getSecretsFilePath(baseDir = homedir()) {
10951
+ function getSecretsFilePath(baseDir = homedir2()) {
10886
10952
  return join5(getSecretsStorePath(baseDir), "store.json");
10887
10953
  }
10888
- function readSecretsStore(baseDir = homedir()) {
10954
+ function readSecretsStore(baseDir = homedir2()) {
10889
10955
  const filePath = getSecretsFilePath(baseDir);
10890
10956
  if (!existsSync5(filePath)) {
10891
10957
  return null;
@@ -10897,7 +10963,7 @@ function readSecretsStore(baseDir = homedir()) {
10897
10963
  return null;
10898
10964
  }
10899
10965
  }
10900
- function writeSecretsStore(store, baseDir = homedir()) {
10966
+ function writeSecretsStore(store, baseDir = homedir2()) {
10901
10967
  const storePath = getSecretsStorePath(baseDir);
10902
10968
  const filePath = getSecretsFilePath(baseDir);
10903
10969
  mkdirSync4(storePath, { recursive: true, mode: 448 });
@@ -10911,23 +10977,23 @@ function writeSecretsStore(store, baseDir = homedir()) {
10911
10977
  chmodSync(filePath, 384);
10912
10978
  } catch {}
10913
10979
  }
10914
- function hasSecretsStore(baseDir = homedir()) {
10980
+ function hasSecretsStore(baseDir = homedir2()) {
10915
10981
  return existsSync5(getSecretsFilePath(baseDir));
10916
10982
  }
10917
- async function hasOllamaCloudApiKey(baseDir = homedir()) {
10983
+ async function hasOllamaCloudApiKey(baseDir = homedir2()) {
10918
10984
  return typeof readSecretsStore(baseDir)?.[OLLAMA_CLOUD_API_KEY] === "string";
10919
10985
  }
10920
- async function getOllamaCloudApiKey(baseDir = homedir()) {
10986
+ async function getOllamaCloudApiKey(baseDir = homedir2()) {
10921
10987
  return readSecretsStore(baseDir)?.[OLLAMA_CLOUD_API_KEY] ?? null;
10922
10988
  }
10923
- async function setOllamaCloudApiKey(value, baseDir = homedir()) {
10989
+ async function setOllamaCloudApiKey(value, baseDir = homedir2()) {
10924
10990
  const existingStore = readSecretsStore(baseDir) ?? {};
10925
10991
  writeSecretsStore({
10926
10992
  ...existingStore,
10927
10993
  [OLLAMA_CLOUD_API_KEY]: value
10928
10994
  }, baseDir);
10929
10995
  }
10930
- async function deleteOllamaCloudApiKey(baseDir = homedir()) {
10996
+ async function deleteOllamaCloudApiKey(baseDir = homedir2()) {
10931
10997
  const existingStore = readSecretsStore(baseDir);
10932
10998
  if (!existingStore || !(OLLAMA_CLOUD_API_KEY in existingStore)) {
10933
10999
  return false;
@@ -10944,20 +11010,20 @@ async function deleteOllamaCloudApiKey(baseDir = homedir()) {
10944
11010
  writeSecretsStore(nextStore, baseDir);
10945
11011
  return true;
10946
11012
  }
10947
- async function hasOpenRouterApiKey(baseDir = homedir()) {
11013
+ async function hasOpenRouterApiKey(baseDir = homedir2()) {
10948
11014
  return typeof readSecretsStore(baseDir)?.[OPENROUTER_API_KEY] === "string";
10949
11015
  }
10950
- async function getOpenRouterApiKey(baseDir = homedir()) {
11016
+ async function getOpenRouterApiKey(baseDir = homedir2()) {
10951
11017
  return readSecretsStore(baseDir)?.[OPENROUTER_API_KEY] ?? null;
10952
11018
  }
10953
- async function setOpenRouterApiKey(value, baseDir = homedir()) {
11019
+ async function setOpenRouterApiKey(value, baseDir = homedir2()) {
10954
11020
  const existingStore = readSecretsStore(baseDir) ?? {};
10955
11021
  writeSecretsStore({
10956
11022
  ...existingStore,
10957
11023
  [OPENROUTER_API_KEY]: value
10958
11024
  }, baseDir);
10959
11025
  }
10960
- async function deleteOpenRouterApiKey(baseDir = homedir()) {
11026
+ async function deleteOpenRouterApiKey(baseDir = homedir2()) {
10961
11027
  const existingStore = readSecretsStore(baseDir);
10962
11028
  if (!existingStore || !(OPENROUTER_API_KEY in existingStore)) {
10963
11029
  return false;
@@ -11043,6 +11109,31 @@ var DEFAULT_OLLAMA_CLOUD_MODEL = "gpt-oss:120b";
11043
11109
  var DEFAULT_OLLAMA_CLOUD_HOST = "https://ollama.com/v1";
11044
11110
  var DEFAULT_OPENROUTER_MODEL = "openai/gpt-4o-mini";
11045
11111
  var DEFAULT_OPENROUTER_HOST = "https://openrouter.ai/api/v1";
11112
+ function resolveAIConfigFromSources(repoConfig, globalConfig) {
11113
+ const provider = repoConfig?.aiProvider ?? globalConfig?.aiProvider ?? "copilot";
11114
+ const useGlobalModel = !repoConfig?.aiModel && globalConfig?.aiProvider === provider;
11115
+ const globalModel = useGlobalModel ? globalConfig?.aiModel?.trim() : undefined;
11116
+ if (provider === "ollama-cloud") {
11117
+ return {
11118
+ provider,
11119
+ providerLabel: "Ollama Cloud",
11120
+ model: repoConfig?.aiModel?.trim() || globalModel || DEFAULT_OLLAMA_CLOUD_MODEL,
11121
+ host: DEFAULT_OLLAMA_CLOUD_HOST
11122
+ };
11123
+ }
11124
+ if (provider === "openrouter") {
11125
+ return {
11126
+ provider,
11127
+ providerLabel: "OpenRouter",
11128
+ model: repoConfig?.aiModel?.trim() || globalModel || DEFAULT_OPENROUTER_MODEL,
11129
+ host: DEFAULT_OPENROUTER_HOST
11130
+ };
11131
+ }
11132
+ return {
11133
+ provider: "copilot",
11134
+ providerLabel: "GitHub Copilot"
11135
+ };
11136
+ }
11046
11137
  function prioritizeOllamaCloudModels(models, preferredModel = DEFAULT_OLLAMA_CLOUD_MODEL) {
11047
11138
  const uniqueModels = [...new Set(models.map((model) => model.trim()).filter(Boolean))];
11048
11139
  const sortedModels = [...uniqueModels].sort((left, right) => left.localeCompare(right));
@@ -11116,28 +11207,9 @@ function prioritizeOpenRouterModels(models, preferredModel = DEFAULT_OPENROUTER_
11116
11207
  return sortedModels.includes(preferredModel) ? [preferredModel, ...sortedModels.filter((model) => model !== preferredModel)] : sortedModels;
11117
11208
  }
11118
11209
  function resolveAIConfig(config) {
11119
- const resolvedConfig = config ?? readConfig();
11120
- const provider = resolvedConfig?.aiProvider ?? "copilot";
11121
- if (provider === "ollama-cloud") {
11122
- return {
11123
- provider,
11124
- providerLabel: "Ollama Cloud",
11125
- model: resolvedConfig?.aiModel?.trim() || DEFAULT_OLLAMA_CLOUD_MODEL,
11126
- host: DEFAULT_OLLAMA_CLOUD_HOST
11127
- };
11128
- }
11129
- if (provider === "openrouter") {
11130
- return {
11131
- provider,
11132
- providerLabel: "OpenRouter",
11133
- model: resolvedConfig?.aiModel?.trim() || DEFAULT_OPENROUTER_MODEL,
11134
- host: DEFAULT_OPENROUTER_HOST
11135
- };
11136
- }
11137
- return {
11138
- provider: "copilot",
11139
- providerLabel: "GitHub Copilot"
11140
- };
11210
+ const repoConfig = config ?? readConfig();
11211
+ const globalConfig = readGlobalConfig();
11212
+ return resolveAIConfigFromSources(repoConfig, globalConfig);
11141
11213
  }
11142
11214
  function suppressSubprocessWarnings() {
11143
11215
  process.env.NODE_NO_WARNINGS = "1";
@@ -12840,6 +12912,62 @@ function buildConfigSnapshot(config, meta) {
12840
12912
  }
12841
12913
  };
12842
12914
  }
12915
+ function normalizeGlobalConfig(config) {
12916
+ const normalized = {
12917
+ aiEnabled: config.aiEnabled !== false
12918
+ };
12919
+ if (normalized.aiEnabled) {
12920
+ normalized.aiProvider = config.aiProvider ?? "copilot";
12921
+ if (normalized.aiProvider === "ollama-cloud") {
12922
+ normalized.aiModel = (config.aiModel?.trim() || DEFAULT_OLLAMA_CLOUD_MODEL).trim();
12923
+ } else if (normalized.aiProvider === "openrouter") {
12924
+ normalized.aiModel = (config.aiModel?.trim() || DEFAULT_OPENROUTER_MODEL).trim();
12925
+ }
12926
+ }
12927
+ return normalized;
12928
+ }
12929
+ function buildGlobalConfigSnapshot(config) {
12930
+ const normalized = normalizeGlobalConfig(config);
12931
+ const aiProvider = normalized.aiProvider ?? "copilot";
12932
+ const aiEnabled = normalized.aiEnabled !== false;
12933
+ const providerLabel = aiProvider === "ollama-cloud" ? "Ollama Cloud" : aiProvider === "openrouter" ? "OpenRouter" : "GitHub Copilot";
12934
+ return {
12935
+ location: getGlobalConfigPath(),
12936
+ exists: globalConfigExists(),
12937
+ ai: {
12938
+ enabled: aiEnabled,
12939
+ provider: aiProvider,
12940
+ providerLabel,
12941
+ model: aiEnabled ? normalized.aiModel ?? null : null
12942
+ }
12943
+ };
12944
+ }
12945
+ async function promptForGlobalConfigEdits(current) {
12946
+ const normalized = normalizeGlobalConfig(current);
12947
+ const aiEnabled = await selectBooleanValue("Global AI default", normalized.aiEnabled !== false, "Enabled", "Disabled");
12948
+ if (!aiEnabled) {
12949
+ return {
12950
+ config: {
12951
+ aiEnabled: false
12952
+ }
12953
+ };
12954
+ }
12955
+ const currentProvider = normalized.aiProvider ?? "copilot";
12956
+ const aiProvider = await selectCurrentValue("Global AI provider", AI_PROVIDER_OPTIONS, currentProvider);
12957
+ let aiModel;
12958
+ if (aiProvider === "ollama-cloud") {
12959
+ aiModel = await inputPrompt("Global Ollama Cloud model", normalized.aiModel ?? DEFAULT_OLLAMA_CLOUD_MODEL);
12960
+ } else if (aiProvider === "openrouter") {
12961
+ aiModel = await inputPrompt("Global OpenRouter model", normalized.aiModel ?? DEFAULT_OPENROUTER_MODEL);
12962
+ }
12963
+ return {
12964
+ config: {
12965
+ aiEnabled: true,
12966
+ aiProvider,
12967
+ aiModel: aiModel?.trim() || undefined
12968
+ }
12969
+ };
12970
+ }
12843
12971
  function defaultDevBranchForWorkflow(workflow) {
12844
12972
  return workflow === "git-flow" ? "develop" : "dev";
12845
12973
  }
@@ -13120,12 +13248,26 @@ function printConfigSummary(snapshot) {
13120
13248
  }
13121
13249
  }
13122
13250
  }
13251
+ function printGlobalConfigSummary(snapshot) {
13252
+ info(`Global config path: ${import_picocolors10.default.bold(snapshot.location)}`);
13253
+ info(`Global defaults file: ${import_picocolors10.default.bold(snapshot.exists ? "present" : "missing (using built-ins)")}`);
13254
+ info(`AI default: ${import_picocolors10.default.bold(snapshot.ai.enabled ? "enabled" : "disabled")}`);
13255
+ info(`AI provider: ${import_picocolors10.default.bold(snapshot.ai.providerLabel)}`);
13256
+ if (snapshot.ai.model) {
13257
+ info(`AI model: ${import_picocolors10.default.bold(snapshot.ai.model)}`);
13258
+ }
13259
+ }
13123
13260
  var config_default = defineCommand({
13124
13261
  meta: {
13125
13262
  name: "config",
13126
13263
  description: "Inspect or edit the repo config without rerunning setup"
13127
13264
  },
13128
13265
  args: {
13266
+ global: {
13267
+ type: "boolean",
13268
+ description: "Read or edit global defaults instead of repo config",
13269
+ default: false
13270
+ },
13129
13271
  json: {
13130
13272
  type: "boolean",
13131
13273
  description: "Print the active repo config as JSON with metadata",
@@ -13138,15 +13280,44 @@ var config_default = defineCommand({
13138
13280
  }
13139
13281
  },
13140
13282
  async run({ args }) {
13141
- if (!await isGitRepo()) {
13142
- error("Not inside a git repository.");
13143
- process.exit(1);
13144
- }
13145
13283
  if (args.json && args.edit) {
13146
13284
  error("Use either --json or --edit, not both at the same time.");
13147
13285
  process.exit(1);
13148
13286
  }
13149
13287
  await projectHeading("config", "\u2699\uFE0F");
13288
+ if (args.global) {
13289
+ const currentGlobal = readGlobalConfig() ?? {};
13290
+ if (args.edit) {
13291
+ try {
13292
+ const editResult = await promptForGlobalConfigEdits(currentGlobal);
13293
+ writeGlobalConfig(normalizeGlobalConfig(editResult.config));
13294
+ success("Updated global defaults.");
13295
+ const snapshot3 = buildGlobalConfigSnapshot(readGlobalConfig() ?? {});
13296
+ printGlobalConfigSummary(snapshot3);
13297
+ if (args.json) {
13298
+ console.log(JSON.stringify(snapshot3, null, 2));
13299
+ }
13300
+ return;
13301
+ } catch (err) {
13302
+ error(err instanceof Error ? err.message : String(err));
13303
+ process.exit(1);
13304
+ }
13305
+ }
13306
+ const snapshot2 = buildGlobalConfigSnapshot(currentGlobal);
13307
+ if (args.json) {
13308
+ console.log(JSON.stringify(snapshot2, null, 2));
13309
+ return;
13310
+ }
13311
+ printGlobalConfigSummary(snapshot2);
13312
+ console.log();
13313
+ console.log(` ${import_picocolors10.default.dim("Run `cn config --global --edit` to update global defaults.")}`);
13314
+ console.log();
13315
+ return;
13316
+ }
13317
+ if (!await isGitRepo()) {
13318
+ error("Not inside a git repository.");
13319
+ process.exit(1);
13320
+ }
13150
13321
  if (!configExists()) {
13151
13322
  error("No repo config found. Run `cn setup` first.");
13152
13323
  process.exit(1);
@@ -13312,7 +13483,7 @@ var import_picocolors12 = __toESM(require_picocolors(), 1);
13312
13483
  // package.json
13313
13484
  var package_default = {
13314
13485
  name: "contribute-now",
13315
- version: "0.8.0-dev.0a7f557",
13486
+ version: "0.8.0-dev.703cd39",
13316
13487
  description: "Developer CLI that automates git workflows \u2014 branching, syncing, committing, and PRs \u2014 with multi-workflow and commit convention support.",
13317
13488
  type: "module",
13318
13489
  bin: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contribute-now",
3
- "version": "0.8.0-dev.0a7f557",
3
+ "version": "0.8.0-dev.703cd39",
4
4
  "description": "Developer CLI that automates git workflows — branching, syncing, committing, and PRs — with multi-workflow and commit convention support.",
5
5
  "type": "module",
6
6
  "bin": {