contribute-now 0.8.0-dev.7db6dea → 0.8.0-pr.e913c8d
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.
- package/README.md +16 -5
- package/dist/cli.js +483 -167
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -88,7 +88,8 @@ cn setup # short alias — even shorter than git!
|
|
|
88
88
|
|
|
89
89
|
- **[Git](https://git-scm.com/)** — required
|
|
90
90
|
- **[GitHub CLI](https://cli.github.com)** (`gh`) — recommended; required for PR creation, role detection, and merge status checks
|
|
91
|
-
- **[GitHub Copilot](https://github.com/features/copilot)** — optional;
|
|
91
|
+
- **[GitHub Copilot](https://github.com/features/copilot)** — optional; one of the supported AI providers
|
|
92
|
+
- **[Ollama Cloud](https://ollama.com)** or **[OpenRouter](https://openrouter.ai)** API key — optional; alternative AI providers
|
|
92
93
|
|
|
93
94
|
---
|
|
94
95
|
|
|
@@ -106,7 +107,7 @@ Steps:
|
|
|
106
107
|
1. Choose **workflow mode** — Clean Flow, GitHub Flow, or Git Flow
|
|
107
108
|
2. Choose **commit convention** — Clean Commit, Conventional Commits, or None
|
|
108
109
|
3. Choose whether **AI features** should be enabled for this repo
|
|
109
|
-
4. If using **Ollama Cloud**, pick from the available models returned by your
|
|
110
|
+
4. If using **Ollama Cloud** or **OpenRouter**, enter your API key; pick from the available models returned by your key, or enter one manually
|
|
110
111
|
5. Detect remotes and auto-detect your **role** (maintainer or contributor)
|
|
111
112
|
6. Confirm branch and remote names
|
|
112
113
|
7. Write `.git/contribute-now/config.json` (or update `.contributerc.json` if that legacy file is still the active source)
|
|
@@ -127,7 +128,7 @@ cn config --json
|
|
|
127
128
|
cn config --edit
|
|
128
129
|
```
|
|
129
130
|
|
|
130
|
-
Use `--edit` to update workflow settings, branch names, commit convention, AI provider details,
|
|
131
|
+
Use `--edit` to update workflow settings, branch names, commit convention, AI provider details, stored API keys (Ollama Cloud or OpenRouter), and to choose from the currently available models for the selected provider.
|
|
131
132
|
|
|
132
133
|
---
|
|
133
134
|
|
|
@@ -306,7 +307,7 @@ cn validate "added stuff" # exit 1
|
|
|
306
307
|
|
|
307
308
|
## AI Features
|
|
308
309
|
|
|
309
|
-
All AI features are
|
|
310
|
+
All AI features are **optional** — every command has a manual fallback. Three providers are supported: **GitHub Copilot**, **Ollama Cloud**, and **OpenRouter**.
|
|
310
311
|
|
|
311
312
|
| Command | AI Feature | Fallback |
|
|
312
313
|
|---------|------------|----------|
|
|
@@ -316,7 +317,17 @@ All AI features are powered by **GitHub Copilot** via `@github/copilot-sdk` and
|
|
|
316
317
|
| `update` | Conflict resolution guidance | Standard git instructions |
|
|
317
318
|
| `submit` | Generate PR title and body | `gh pr create --fill` or manual |
|
|
318
319
|
|
|
319
|
-
Pass `--no-ai` to any command to skip AI entirely. Use `--model <name>` to select a specific
|
|
320
|
+
Pass `--no-ai` to any command to skip AI entirely. Use `--model <name>` to select a specific model (e.g., `gpt-4.1`, `claude-sonnet-4`).
|
|
321
|
+
|
|
322
|
+
### AI Providers
|
|
323
|
+
|
|
324
|
+
| Provider | Auth | How it works |
|
|
325
|
+
|----------|------|--------------|
|
|
326
|
+
| **GitHub Copilot** *(default)* | `gh auth login` | Uses your existing GitHub/Copilot auth via the `@github/copilot-sdk` |
|
|
327
|
+
| **Ollama Cloud** | API key (stored in local secrets) | OpenAI-compatible API; model list fetched from your key on setup |
|
|
328
|
+
| **OpenRouter** | API key (stored in local secrets) | Unified API that routes to many model providers (OpenAI, Anthropic, Google, etc.) |
|
|
329
|
+
|
|
330
|
+
Select your provider during `cn setup` or change it later with `cn config --edit`. API keys for Ollama Cloud and OpenRouter are stored as plain JSON in `~/.contribute-now/secrets/store.json` with file permissions restricted to the current user (mode 0600) — never in the plain config file.
|
|
320
331
|
|
|
321
332
|
---
|
|
322
333
|
|
package/dist/cli.js
CHANGED
|
@@ -7206,7 +7206,7 @@ function configExists(cwd = process.cwd()) {
|
|
|
7206
7206
|
var VALID_WORKFLOWS = ["clean-flow", "github-flow", "git-flow"];
|
|
7207
7207
|
var VALID_ROLES = ["maintainer", "contributor"];
|
|
7208
7208
|
var VALID_CONVENTIONS = ["conventional", "clean-commit", "none"];
|
|
7209
|
-
var VALID_AI_PROVIDERS = ["copilot", "ollama-cloud"];
|
|
7209
|
+
var VALID_AI_PROVIDERS = ["copilot", "ollama-cloud", "openrouter"];
|
|
7210
7210
|
function isAIEnabled(config, cliNoAI = false) {
|
|
7211
7211
|
return config.aiEnabled !== false && !cliNoAI;
|
|
7212
7212
|
}
|
|
@@ -10624,6 +10624,7 @@ import { join as join5, resolve as resolve4 } from "path";
|
|
|
10624
10624
|
var CONTRIBUTE_NOW_SECRETS_DIRNAME = ".contribute-now";
|
|
10625
10625
|
var CONTRIBUTE_NOW_SECRETS_STORE_DIRNAME = "secrets";
|
|
10626
10626
|
var OLLAMA_CLOUD_API_KEY = "ollama.cloud.apiKey";
|
|
10627
|
+
var OPENROUTER_API_KEY = "openrouter.apiKey";
|
|
10627
10628
|
function getSecretsStorePath(baseDir = homedir()) {
|
|
10628
10629
|
return resolve4(baseDir, CONTRIBUTE_NOW_SECRETS_DIRNAME, CONTRIBUTE_NOW_SECRETS_STORE_DIRNAME);
|
|
10629
10630
|
}
|
|
@@ -10689,6 +10690,36 @@ async function deleteOllamaCloudApiKey(baseDir = homedir()) {
|
|
|
10689
10690
|
writeSecretsStore(nextStore, baseDir);
|
|
10690
10691
|
return true;
|
|
10691
10692
|
}
|
|
10693
|
+
async function hasOpenRouterApiKey(baseDir = homedir()) {
|
|
10694
|
+
return typeof readSecretsStore(baseDir)?.[OPENROUTER_API_KEY] === "string";
|
|
10695
|
+
}
|
|
10696
|
+
async function getOpenRouterApiKey(baseDir = homedir()) {
|
|
10697
|
+
return readSecretsStore(baseDir)?.[OPENROUTER_API_KEY] ?? null;
|
|
10698
|
+
}
|
|
10699
|
+
async function setOpenRouterApiKey(value, baseDir = homedir()) {
|
|
10700
|
+
const existingStore = readSecretsStore(baseDir) ?? {};
|
|
10701
|
+
writeSecretsStore({
|
|
10702
|
+
...existingStore,
|
|
10703
|
+
[OPENROUTER_API_KEY]: value
|
|
10704
|
+
}, baseDir);
|
|
10705
|
+
}
|
|
10706
|
+
async function deleteOpenRouterApiKey(baseDir = homedir()) {
|
|
10707
|
+
const existingStore = readSecretsStore(baseDir);
|
|
10708
|
+
if (!existingStore || !(OPENROUTER_API_KEY in existingStore)) {
|
|
10709
|
+
return false;
|
|
10710
|
+
}
|
|
10711
|
+
const nextStore = { ...existingStore };
|
|
10712
|
+
delete nextStore[OPENROUTER_API_KEY];
|
|
10713
|
+
if (Object.keys(nextStore).length === 0) {
|
|
10714
|
+
try {
|
|
10715
|
+
rmSync(getSecretsFilePath(baseDir), { force: true });
|
|
10716
|
+
rmSync(getSecretsStorePath(baseDir), { recursive: true, force: true });
|
|
10717
|
+
} catch {}
|
|
10718
|
+
return true;
|
|
10719
|
+
}
|
|
10720
|
+
writeSecretsStore(nextStore, baseDir);
|
|
10721
|
+
return true;
|
|
10722
|
+
}
|
|
10692
10723
|
|
|
10693
10724
|
// src/utils/copilot.ts
|
|
10694
10725
|
var CONVENTIONAL_COMMIT_SYSTEM_PROMPT = `Git commit message generator. Format: <type>[!][(<scope>)]: <description>
|
|
@@ -10756,6 +10787,8 @@ Rules: title concise present tense, describes the PR theme not individual commit
|
|
|
10756
10787
|
var CONFLICT_RESOLUTION_SYSTEM_PROMPT = `Git merge conflict advisor. Explain each side, suggest resolution strategy. Never auto-resolve \u2014 guidance only. Be concise and actionable.`;
|
|
10757
10788
|
var DEFAULT_OLLAMA_CLOUD_MODEL = "gpt-oss:120b";
|
|
10758
10789
|
var DEFAULT_OLLAMA_CLOUD_HOST = "https://ollama.com/v1";
|
|
10790
|
+
var DEFAULT_OPENROUTER_MODEL = "openai/gpt-4o-mini";
|
|
10791
|
+
var DEFAULT_OPENROUTER_HOST = "https://openrouter.ai/api/v1";
|
|
10759
10792
|
function prioritizeOllamaCloudModels(models, preferredModel = DEFAULT_OLLAMA_CLOUD_MODEL) {
|
|
10760
10793
|
const uniqueModels = [...new Set(models.map((model) => model.trim()).filter(Boolean))];
|
|
10761
10794
|
const sortedModels = [...uniqueModels].sort((left, right) => left.localeCompare(right));
|
|
@@ -10763,7 +10796,9 @@ function prioritizeOllamaCloudModels(models, preferredModel = DEFAULT_OLLAMA_CLO
|
|
|
10763
10796
|
}
|
|
10764
10797
|
function extractOllamaCloudModelIds(payload) {
|
|
10765
10798
|
const records = typeof payload === "object" && payload !== null ? Array.isArray(payload.data) ? payload.data : Array.isArray(payload.models) ? payload.models : [] : [];
|
|
10766
|
-
return [
|
|
10799
|
+
return [
|
|
10800
|
+
...new Set(records.map(getOllamaCloudModelId).filter((id) => id !== null))
|
|
10801
|
+
].sort((left, right) => left.localeCompare(right));
|
|
10767
10802
|
}
|
|
10768
10803
|
function getOllamaCloudModelId(record) {
|
|
10769
10804
|
if (typeof record !== "object" || record === null) {
|
|
@@ -10792,6 +10827,40 @@ function normalizeOllamaCloudHost(host) {
|
|
|
10792
10827
|
const trimmed = (host?.trim() || DEFAULT_OLLAMA_CLOUD_HOST).replace(/\/+$/, "");
|
|
10793
10828
|
return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
|
|
10794
10829
|
}
|
|
10830
|
+
function extractOpenRouterModelIds(payload) {
|
|
10831
|
+
const records = typeof payload === "object" && payload !== null ? Array.isArray(payload.data) ? payload.data : [] : [];
|
|
10832
|
+
return [
|
|
10833
|
+
...new Set(records.map(getOpenRouterModelId).filter((id) => id !== null))
|
|
10834
|
+
].sort((left, right) => left.localeCompare(right));
|
|
10835
|
+
}
|
|
10836
|
+
function getOpenRouterModelId(record) {
|
|
10837
|
+
if (typeof record !== "object" || record === null) {
|
|
10838
|
+
return null;
|
|
10839
|
+
}
|
|
10840
|
+
const candidate = typeof record.id === "string" ? record.id : null;
|
|
10841
|
+
const normalized = candidate?.trim();
|
|
10842
|
+
return normalized ? normalized : null;
|
|
10843
|
+
}
|
|
10844
|
+
async function fetchOpenRouterModels(apiKey) {
|
|
10845
|
+
const response = await fetch(`${DEFAULT_OPENROUTER_HOST}/models`, {
|
|
10846
|
+
headers: {
|
|
10847
|
+
Accept: "application/json",
|
|
10848
|
+
Authorization: `Bearer ${apiKey}`
|
|
10849
|
+
}
|
|
10850
|
+
});
|
|
10851
|
+
if (!response.ok) {
|
|
10852
|
+
if (response.status === 401 || response.status === 403) {
|
|
10853
|
+
throw new Error("OpenRouter authentication failed");
|
|
10854
|
+
}
|
|
10855
|
+
throw new Error(`OpenRouter model lookup failed (${response.status} ${response.statusText})`);
|
|
10856
|
+
}
|
|
10857
|
+
return extractOpenRouterModelIds(await response.json());
|
|
10858
|
+
}
|
|
10859
|
+
function prioritizeOpenRouterModels(models, preferredModel = DEFAULT_OPENROUTER_MODEL) {
|
|
10860
|
+
const uniqueModels = [...new Set(models.map((model) => model.trim()).filter(Boolean))];
|
|
10861
|
+
const sortedModels = [...uniqueModels].sort((left, right) => left.localeCompare(right));
|
|
10862
|
+
return sortedModels.includes(preferredModel) ? [preferredModel, ...sortedModels.filter((model) => model !== preferredModel)] : sortedModels;
|
|
10863
|
+
}
|
|
10795
10864
|
function resolveAIConfig(config) {
|
|
10796
10865
|
const resolvedConfig = config ?? readConfig();
|
|
10797
10866
|
const provider = resolvedConfig?.aiProvider ?? "copilot";
|
|
@@ -10803,6 +10872,14 @@ function resolveAIConfig(config) {
|
|
|
10803
10872
|
host: DEFAULT_OLLAMA_CLOUD_HOST
|
|
10804
10873
|
};
|
|
10805
10874
|
}
|
|
10875
|
+
if (provider === "openrouter") {
|
|
10876
|
+
return {
|
|
10877
|
+
provider,
|
|
10878
|
+
providerLabel: "OpenRouter",
|
|
10879
|
+
model: resolvedConfig?.aiModel?.trim() || DEFAULT_OPENROUTER_MODEL,
|
|
10880
|
+
host: DEFAULT_OPENROUTER_HOST
|
|
10881
|
+
};
|
|
10882
|
+
}
|
|
10806
10883
|
return {
|
|
10807
10884
|
provider: "copilot",
|
|
10808
10885
|
providerLabel: "GitHub Copilot"
|
|
@@ -10996,6 +11073,28 @@ async function checkCopilotAvailable2() {
|
|
|
10996
11073
|
return `Could not reach Ollama Cloud API: ${msg}`;
|
|
10997
11074
|
}
|
|
10998
11075
|
}
|
|
11076
|
+
if (aiConfig.provider === "openrouter") {
|
|
11077
|
+
if (!await hasOpenRouterApiKey()) {
|
|
11078
|
+
return "OpenRouter API key not found. Run `cn setup` to save it.";
|
|
11079
|
+
}
|
|
11080
|
+
try {
|
|
11081
|
+
const apiKey = await getOpenRouterApiKey();
|
|
11082
|
+
if (!apiKey) {
|
|
11083
|
+
return "OpenRouter API key not found. Run `cn setup` to save it.";
|
|
11084
|
+
}
|
|
11085
|
+
await fetchOpenRouterModels(apiKey);
|
|
11086
|
+
return null;
|
|
11087
|
+
} catch (err) {
|
|
11088
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11089
|
+
if (msg === "OpenRouter authentication failed") {
|
|
11090
|
+
return "OpenRouter authentication failed. Update your saved API key with `cn setup`.";
|
|
11091
|
+
}
|
|
11092
|
+
if (msg.startsWith("OpenRouter model lookup failed")) {
|
|
11093
|
+
return msg.replace("model lookup", "health check");
|
|
11094
|
+
}
|
|
11095
|
+
return `Could not reach OpenRouter API: ${msg}`;
|
|
11096
|
+
}
|
|
11097
|
+
}
|
|
10999
11098
|
try {
|
|
11000
11099
|
const client = await getManagedClient();
|
|
11001
11100
|
try {
|
|
@@ -11097,11 +11196,54 @@ async function callOllamaCloud(systemMessage, userMessage, model, timeoutMs = CO
|
|
|
11097
11196
|
clearTimeout(timer);
|
|
11098
11197
|
}
|
|
11099
11198
|
}
|
|
11199
|
+
async function callOpenRouter(systemMessage, userMessage, model, timeoutMs = COPILOT_TIMEOUT_MS) {
|
|
11200
|
+
const aiConfig = resolveAIConfig();
|
|
11201
|
+
const apiKey = await getOpenRouterApiKey();
|
|
11202
|
+
if (!apiKey) {
|
|
11203
|
+
throw new Error("OpenRouter API key is not configured");
|
|
11204
|
+
}
|
|
11205
|
+
const controller = new AbortController;
|
|
11206
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
11207
|
+
try {
|
|
11208
|
+
const response = await fetch(`${DEFAULT_OPENROUTER_HOST}/chat/completions`, {
|
|
11209
|
+
method: "POST",
|
|
11210
|
+
headers: {
|
|
11211
|
+
Authorization: `Bearer ${apiKey}`,
|
|
11212
|
+
"Content-Type": "application/json",
|
|
11213
|
+
"HTTP-Referer": "https://github.com/warengonzaga/contribute-now",
|
|
11214
|
+
"X-Title": "contribute-now"
|
|
11215
|
+
},
|
|
11216
|
+
body: JSON.stringify({
|
|
11217
|
+
model: model?.trim() || aiConfig.model || DEFAULT_OPENROUTER_MODEL,
|
|
11218
|
+
messages: [
|
|
11219
|
+
{ role: "system", content: systemMessage },
|
|
11220
|
+
{ role: "user", content: userMessage }
|
|
11221
|
+
],
|
|
11222
|
+
stream: false
|
|
11223
|
+
}),
|
|
11224
|
+
signal: controller.signal
|
|
11225
|
+
});
|
|
11226
|
+
if (!response.ok) {
|
|
11227
|
+
const body = await response.text();
|
|
11228
|
+
if (response.status === 401 || response.status === 403) {
|
|
11229
|
+
throw new Error("OpenRouter authentication failed");
|
|
11230
|
+
}
|
|
11231
|
+
throw new Error(`OpenRouter request failed (${response.status} ${response.statusText}): ${body.slice(0, 200)}`);
|
|
11232
|
+
}
|
|
11233
|
+
const data = await response.json();
|
|
11234
|
+
return data.choices?.[0]?.message?.content?.trim() || null;
|
|
11235
|
+
} finally {
|
|
11236
|
+
clearTimeout(timer);
|
|
11237
|
+
}
|
|
11238
|
+
}
|
|
11100
11239
|
async function callAI(systemMessage, userMessage, model, timeoutMs = COPILOT_TIMEOUT_MS) {
|
|
11101
11240
|
const aiConfig = resolveAIConfig();
|
|
11102
11241
|
if (aiConfig.provider === "ollama-cloud") {
|
|
11103
11242
|
return callOllamaCloud(systemMessage, userMessage, model, timeoutMs);
|
|
11104
11243
|
}
|
|
11244
|
+
if (aiConfig.provider === "openrouter") {
|
|
11245
|
+
return callOpenRouter(systemMessage, userMessage, model, timeoutMs);
|
|
11246
|
+
}
|
|
11105
11247
|
return callCopilot(systemMessage, userMessage, model, timeoutMs);
|
|
11106
11248
|
}
|
|
11107
11249
|
function getCommitSystemPrompt(convention) {
|
|
@@ -11992,116 +12134,8 @@ ${import_picocolors8.default.bold("Stale branches (remote deleted, likely squash
|
|
|
11992
12134
|
}
|
|
11993
12135
|
});
|
|
11994
12136
|
|
|
11995
|
-
// src/commands/discard.ts
|
|
11996
|
-
var import_picocolors9 = __toESM(require_picocolors(), 1);
|
|
11997
|
-
var discard_default = defineCommand({
|
|
11998
|
-
meta: {
|
|
11999
|
-
name: "discard",
|
|
12000
|
-
description: "Discard the current feature branch and return to the base branch"
|
|
12001
|
-
},
|
|
12002
|
-
args: {
|
|
12003
|
-
force: {
|
|
12004
|
-
type: "boolean",
|
|
12005
|
-
alias: "f",
|
|
12006
|
-
description: "Skip confirmation and discard immediately",
|
|
12007
|
-
default: false
|
|
12008
|
-
}
|
|
12009
|
-
},
|
|
12010
|
-
async run({ args }) {
|
|
12011
|
-
if (!await isGitRepo()) {
|
|
12012
|
-
error("Not inside a git repository.");
|
|
12013
|
-
process.exit(1);
|
|
12014
|
-
}
|
|
12015
|
-
await assertCleanGitState("discarding a branch");
|
|
12016
|
-
const config = readConfig();
|
|
12017
|
-
if (!config) {
|
|
12018
|
-
error("No repo config found. Run `cn setup` first.");
|
|
12019
|
-
process.exit(1);
|
|
12020
|
-
}
|
|
12021
|
-
const currentBranch = await getCurrentBranch();
|
|
12022
|
-
const baseBranch = getBaseBranch(config);
|
|
12023
|
-
await projectHeading("discard", "\uD83D\uDDD1\uFE0F");
|
|
12024
|
-
if (isBranchProtected(currentBranch, config)) {
|
|
12025
|
-
error(`${import_picocolors9.default.bold(currentBranch)} is a protected branch and cannot be discarded.`);
|
|
12026
|
-
info(`Switch to a feature branch first, then run ${import_picocolors9.default.bold("cn discard")}.`);
|
|
12027
|
-
process.exit(1);
|
|
12028
|
-
}
|
|
12029
|
-
if (currentBranch === baseBranch) {
|
|
12030
|
-
info(`You are already on ${import_picocolors9.default.bold(baseBranch)}.`);
|
|
12031
|
-
process.exit(0);
|
|
12032
|
-
}
|
|
12033
|
-
const { origin } = config;
|
|
12034
|
-
const localWork = await hasLocalWork(origin, currentBranch);
|
|
12035
|
-
const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
|
|
12036
|
-
if (hasWork) {
|
|
12037
|
-
if (localWork.uncommitted) {
|
|
12038
|
-
warn("You have uncommitted changes in your working tree.");
|
|
12039
|
-
}
|
|
12040
|
-
if (localWork.unpushedCommits > 0) {
|
|
12041
|
-
warn(`You have ${import_picocolors9.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on this branch.`);
|
|
12042
|
-
}
|
|
12043
|
-
warn("Discarding this branch will permanently lose that work.");
|
|
12044
|
-
const SAVE_FIRST = "Save my changes first (cn save), then discard";
|
|
12045
|
-
const DISCARD_ANYWAY = "Discard anyway \u2014 I do not need this work";
|
|
12046
|
-
const CANCEL = "Keep the branch, take me back";
|
|
12047
|
-
const action = await selectPrompt("This branch has unsaved work. What would you like to do?", [SAVE_FIRST, DISCARD_ANYWAY, CANCEL]);
|
|
12048
|
-
if (action === CANCEL) {
|
|
12049
|
-
info("Discard cancelled. Your branch is untouched.");
|
|
12050
|
-
process.exit(0);
|
|
12051
|
-
}
|
|
12052
|
-
if (action === SAVE_FIRST) {
|
|
12053
|
-
if (!localWork.uncommitted) {
|
|
12054
|
-
info("No uncommitted changes to stash \u2014 unpushed commits will still be lost.");
|
|
12055
|
-
const confirm = await confirmPrompt("Continue discarding the branch?");
|
|
12056
|
-
if (!confirm) {
|
|
12057
|
-
info("Discard cancelled.");
|
|
12058
|
-
process.exit(0);
|
|
12059
|
-
}
|
|
12060
|
-
} else {
|
|
12061
|
-
const stashResult = await stashChanges(`work-in-progress on ${currentBranch}`);
|
|
12062
|
-
if (stashResult.exitCode !== 0) {
|
|
12063
|
-
error(`Failed to save changes: ${stashResult.stderr}`);
|
|
12064
|
-
process.exit(1);
|
|
12065
|
-
}
|
|
12066
|
-
success(`Changes saved. Use ${import_picocolors9.default.bold("cn save --restore")} to bring them back.`);
|
|
12067
|
-
}
|
|
12068
|
-
}
|
|
12069
|
-
} else if (!args.force) {
|
|
12070
|
-
const confirmed = await confirmPrompt(`Discard ${import_picocolors9.default.bold(currentBranch)} and return to ${import_picocolors9.default.bold(baseBranch)}?`);
|
|
12071
|
-
if (!confirmed) {
|
|
12072
|
-
info("Discard cancelled.");
|
|
12073
|
-
process.exit(0);
|
|
12074
|
-
}
|
|
12075
|
-
}
|
|
12076
|
-
const upstreamRef = await getUpstreamRef();
|
|
12077
|
-
let deleteRemote = false;
|
|
12078
|
-
if (upstreamRef) {
|
|
12079
|
-
deleteRemote = await confirmPrompt(`Also delete the remote branch ${import_picocolors9.default.bold(upstreamRef)}?`);
|
|
12080
|
-
}
|
|
12081
|
-
const checkoutResult = await checkoutBranch(baseBranch);
|
|
12082
|
-
if (checkoutResult.exitCode !== 0) {
|
|
12083
|
-
error(`Failed to switch to ${import_picocolors9.default.bold(baseBranch)}: ${checkoutResult.stderr}`);
|
|
12084
|
-
process.exit(1);
|
|
12085
|
-
}
|
|
12086
|
-
const deleteResult = await forceDeleteBranch(currentBranch);
|
|
12087
|
-
if (deleteResult.exitCode !== 0) {
|
|
12088
|
-
error(`Failed to delete branch ${import_picocolors9.default.bold(currentBranch)}: ${deleteResult.stderr}`);
|
|
12089
|
-
process.exit(1);
|
|
12090
|
-
}
|
|
12091
|
-
success(`Discarded ${import_picocolors9.default.bold(currentBranch)} and switched back to ${import_picocolors9.default.bold(baseBranch)}`);
|
|
12092
|
-
if (deleteRemote) {
|
|
12093
|
-
const remoteDeleteResult = await deleteRemoteBranch(origin, currentBranch);
|
|
12094
|
-
if (remoteDeleteResult.exitCode !== 0) {
|
|
12095
|
-
warn(`Could not delete remote branch: ${remoteDeleteResult.stderr.trim()}`);
|
|
12096
|
-
} else {
|
|
12097
|
-
success(`Deleted remote branch ${import_picocolors9.default.bold(`${origin}/${currentBranch}`)}`);
|
|
12098
|
-
}
|
|
12099
|
-
}
|
|
12100
|
-
}
|
|
12101
|
-
});
|
|
12102
|
-
|
|
12103
12137
|
// src/commands/commit.ts
|
|
12104
|
-
var
|
|
12138
|
+
var import_picocolors9 = __toESM(require_picocolors(), 1);
|
|
12105
12139
|
|
|
12106
12140
|
// src/utils/convention.ts
|
|
12107
12141
|
var CLEAN_COMMIT_PATTERN = /^(\uD83D\uDCE6|\uD83D\uDD27|\uD83D\uDDD1\uFE0F?|\uD83D\uDD12|\u2699\uFE0F?|\u2615|\uD83E\uDDEA|\uD83D\uDCD6|\uD83D\uDE80) (new|update|remove|security|setup|chore|test|docs|release)(!?)( \([a-zA-Z0-9][a-zA-Z0-9-]*\))?: .{1,72}$/u;
|
|
@@ -12208,9 +12242,9 @@ var commit_default = defineCommand({
|
|
|
12208
12242
|
process.exit(1);
|
|
12209
12243
|
}
|
|
12210
12244
|
console.log(`
|
|
12211
|
-
${
|
|
12245
|
+
${import_picocolors9.default.bold("Changed files:")}`);
|
|
12212
12246
|
for (const f3 of changedFiles) {
|
|
12213
|
-
console.log(` ${
|
|
12247
|
+
console.log(` ${import_picocolors9.default.dim("\u2022")} ${f3}`);
|
|
12214
12248
|
}
|
|
12215
12249
|
const stageAction = await selectPrompt("No staged changes. How would you like to stage?", [
|
|
12216
12250
|
"Stage all changes",
|
|
@@ -12252,8 +12286,8 @@ ${import_picocolors10.default.bold("Changed files:")}`);
|
|
|
12252
12286
|
const dirs = new Set(stagedFiles.map((f3) => f3.split("/")[0]));
|
|
12253
12287
|
if (dirs.size > 1) {
|
|
12254
12288
|
console.log();
|
|
12255
|
-
warn(`You're staging ${
|
|
12256
|
-
info(
|
|
12289
|
+
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.`);
|
|
12290
|
+
info(import_picocolors9.default.dim("Large commits mixing different topics make history harder to read and bisect. " + "For cleaner history, consider splitting into atomic commits."));
|
|
12257
12291
|
const choice = await selectPrompt("How would you like to proceed?", [
|
|
12258
12292
|
"Continue as single commit",
|
|
12259
12293
|
"Switch to group mode (AI splits into atomic commits)",
|
|
@@ -12284,7 +12318,7 @@ ${import_picocolors10.default.bold("Changed files:")}`);
|
|
|
12284
12318
|
if (commitMessage) {
|
|
12285
12319
|
spinner.success("AI commit message generated.");
|
|
12286
12320
|
console.log(`
|
|
12287
|
-
${
|
|
12321
|
+
${import_picocolors9.default.dim("AI suggestion:")} ${import_picocolors9.default.bold(import_picocolors9.default.cyan(commitMessage))}`);
|
|
12288
12322
|
} else {
|
|
12289
12323
|
spinner.fail("AI did not return a commit message.");
|
|
12290
12324
|
warn("Falling back to manual entry.");
|
|
@@ -12312,7 +12346,7 @@ ${import_picocolors10.default.bold("Changed files:")}`);
|
|
|
12312
12346
|
if (regen) {
|
|
12313
12347
|
spinner.success("Commit message regenerated.");
|
|
12314
12348
|
console.log(`
|
|
12315
|
-
${
|
|
12349
|
+
${import_picocolors9.default.dim("AI suggestion:")} ${import_picocolors9.default.bold(import_picocolors9.default.cyan(regen))}`);
|
|
12316
12350
|
const ok = await confirmPrompt("Use this message?");
|
|
12317
12351
|
finalMessage = ok ? regen : await inputPrompt("Enter commit message manually");
|
|
12318
12352
|
} else {
|
|
@@ -12327,7 +12361,7 @@ ${import_picocolors10.default.bold("Changed files:")}`);
|
|
|
12327
12361
|
if (convention2 !== "none") {
|
|
12328
12362
|
console.log();
|
|
12329
12363
|
for (const hint of CONVENTION_FORMAT_HINTS[convention2]) {
|
|
12330
|
-
console.log(
|
|
12364
|
+
console.log(import_picocolors9.default.dim(hint));
|
|
12331
12365
|
}
|
|
12332
12366
|
console.log();
|
|
12333
12367
|
}
|
|
@@ -12351,7 +12385,7 @@ ${import_picocolors10.default.bold("Changed files:")}`);
|
|
|
12351
12385
|
error(`Failed to commit: ${result.stderr}`);
|
|
12352
12386
|
process.exit(1);
|
|
12353
12387
|
}
|
|
12354
|
-
success(`Committed: ${
|
|
12388
|
+
success(`Committed: ${import_picocolors9.default.bold(finalMessage)}`);
|
|
12355
12389
|
}
|
|
12356
12390
|
});
|
|
12357
12391
|
async function runGroupCommit(model, config) {
|
|
@@ -12368,9 +12402,9 @@ async function runGroupCommit(model, config) {
|
|
|
12368
12402
|
process.exit(1);
|
|
12369
12403
|
}
|
|
12370
12404
|
console.log(`
|
|
12371
|
-
${
|
|
12405
|
+
${import_picocolors9.default.bold("Changed files:")}`);
|
|
12372
12406
|
for (const f3 of changedFiles) {
|
|
12373
|
-
console.log(` ${
|
|
12407
|
+
console.log(` ${import_picocolors9.default.dim("\u2022")} ${f3}`);
|
|
12374
12408
|
}
|
|
12375
12409
|
const spinner = createSpinner(changedFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD ? `Asking AI to group ${changedFiles.length} file(s) into logical commits (using optimized batching)...` : `Asking AI to group ${changedFiles.length} file(s) into logical commits...`, {
|
|
12376
12410
|
tips: LOADING_TIPS
|
|
@@ -12416,13 +12450,13 @@ ${import_picocolors10.default.bold("Changed files:")}`);
|
|
|
12416
12450
|
let commitAll = false;
|
|
12417
12451
|
while (!proceedToCommit) {
|
|
12418
12452
|
console.log(`
|
|
12419
|
-
${
|
|
12453
|
+
${import_picocolors9.default.bold(`AI suggested ${validGroups.length} commit group(s):`)}
|
|
12420
12454
|
`);
|
|
12421
12455
|
for (let i2 = 0;i2 < validGroups.length; i2++) {
|
|
12422
12456
|
const g3 = validGroups[i2];
|
|
12423
|
-
console.log(` ${
|
|
12457
|
+
console.log(` ${import_picocolors9.default.cyan(`Group ${i2 + 1}:`)} ${import_picocolors9.default.bold(g3.message)}`);
|
|
12424
12458
|
for (const f3 of g3.files) {
|
|
12425
|
-
console.log(` ${
|
|
12459
|
+
console.log(` ${import_picocolors9.default.dim("\u2022")} ${f3}`);
|
|
12426
12460
|
}
|
|
12427
12461
|
console.log();
|
|
12428
12462
|
}
|
|
@@ -12488,16 +12522,16 @@ ${import_picocolors10.default.bold(`AI suggested ${validGroups.length} commit gr
|
|
|
12488
12522
|
continue;
|
|
12489
12523
|
}
|
|
12490
12524
|
committed++;
|
|
12491
|
-
success(`Committed group ${i2 + 1}: ${
|
|
12525
|
+
success(`Committed group ${i2 + 1}: ${import_picocolors9.default.bold(group.message)}`);
|
|
12492
12526
|
}
|
|
12493
12527
|
} else {
|
|
12494
12528
|
for (let i2 = 0;i2 < validGroups.length; i2++) {
|
|
12495
12529
|
const group = validGroups[i2];
|
|
12496
|
-
console.log(
|
|
12530
|
+
console.log(import_picocolors9.default.bold(`
|
|
12497
12531
|
\u2500\u2500 Group ${i2 + 1}/${validGroups.length} \u2500\u2500`));
|
|
12498
|
-
console.log(` ${
|
|
12532
|
+
console.log(` ${import_picocolors9.default.cyan(group.message)}`);
|
|
12499
12533
|
for (const f3 of group.files) {
|
|
12500
|
-
console.log(` ${
|
|
12534
|
+
console.log(` ${import_picocolors9.default.dim("\u2022")} ${f3}`);
|
|
12501
12535
|
}
|
|
12502
12536
|
let message = group.message;
|
|
12503
12537
|
let actionDone = false;
|
|
@@ -12521,7 +12555,7 @@ ${import_picocolors10.default.bold(`AI suggested ${validGroups.length} commit gr
|
|
|
12521
12555
|
if (newMsg) {
|
|
12522
12556
|
message = newMsg;
|
|
12523
12557
|
group.message = newMsg;
|
|
12524
|
-
regenSpinner.success(`New message: ${
|
|
12558
|
+
regenSpinner.success(`New message: ${import_picocolors9.default.bold(message)}`);
|
|
12525
12559
|
} else {
|
|
12526
12560
|
regenSpinner.fail("AI could not generate a new message. Keeping current one.");
|
|
12527
12561
|
}
|
|
@@ -12584,7 +12618,7 @@ ${import_picocolors10.default.bold(`AI suggested ${validGroups.length} commit gr
|
|
|
12584
12618
|
continue;
|
|
12585
12619
|
}
|
|
12586
12620
|
committed++;
|
|
12587
|
-
success(`Committed group ${i2 + 1}: ${
|
|
12621
|
+
success(`Committed group ${i2 + 1}: ${import_picocolors9.default.bold(message)}`);
|
|
12588
12622
|
actionDone = true;
|
|
12589
12623
|
}
|
|
12590
12624
|
}
|
|
@@ -12599,7 +12633,7 @@ ${import_picocolors10.default.bold(`AI suggested ${validGroups.length} commit gr
|
|
|
12599
12633
|
}
|
|
12600
12634
|
|
|
12601
12635
|
// src/commands/config.ts
|
|
12602
|
-
var
|
|
12636
|
+
var import_picocolors10 = __toESM(require_picocolors(), 1);
|
|
12603
12637
|
var WORKFLOW_OPTIONS = [
|
|
12604
12638
|
{ value: "clean-flow", label: WORKFLOW_DESCRIPTIONS["clean-flow"] },
|
|
12605
12639
|
{ value: "github-flow", label: WORKFLOW_DESCRIPTIONS["github-flow"] },
|
|
@@ -12616,7 +12650,8 @@ var CONVENTION_OPTIONS = [
|
|
|
12616
12650
|
];
|
|
12617
12651
|
var AI_PROVIDER_OPTIONS = [
|
|
12618
12652
|
{ value: "copilot", label: "GitHub Copilot" },
|
|
12619
|
-
{ value: "ollama-cloud", label: "Ollama Cloud" }
|
|
12653
|
+
{ value: "ollama-cloud", label: "Ollama Cloud" },
|
|
12654
|
+
{ value: "openrouter", label: "OpenRouter" }
|
|
12620
12655
|
];
|
|
12621
12656
|
function parseBranchPrefixesInput(input, fallback) {
|
|
12622
12657
|
const values = input.split(",").map((value) => value.trim()).filter(Boolean);
|
|
@@ -12650,6 +12685,10 @@ function finalizeEditedConfig(current, draft) {
|
|
|
12650
12685
|
next.aiModel = (draft.aiModel?.trim() || DEFAULT_OLLAMA_CLOUD_MODEL).trim();
|
|
12651
12686
|
return next;
|
|
12652
12687
|
}
|
|
12688
|
+
if (next.aiProvider === "openrouter") {
|
|
12689
|
+
next.aiModel = (draft.aiModel?.trim() || DEFAULT_OPENROUTER_MODEL).trim();
|
|
12690
|
+
return next;
|
|
12691
|
+
}
|
|
12653
12692
|
delete next.aiModel;
|
|
12654
12693
|
return next;
|
|
12655
12694
|
}
|
|
@@ -12657,6 +12696,8 @@ function buildConfigSnapshot(config, meta) {
|
|
|
12657
12696
|
const aiConfig = resolveAIConfig(config);
|
|
12658
12697
|
const aiEnabled = isAIEnabled(config);
|
|
12659
12698
|
const usingOllamaCloud = aiEnabled && aiConfig.provider === "ollama-cloud";
|
|
12699
|
+
const usingOpenRouter = aiEnabled && aiConfig.provider === "openrouter";
|
|
12700
|
+
const needsSecretInfo = usingOllamaCloud || usingOpenRouter;
|
|
12660
12701
|
return {
|
|
12661
12702
|
source: meta.source,
|
|
12662
12703
|
location: meta.location,
|
|
@@ -12677,7 +12718,8 @@ function buildConfigSnapshot(config, meta) {
|
|
|
12677
12718
|
providerLabel: aiEnabled ? aiConfig.providerLabel : null,
|
|
12678
12719
|
model: aiEnabled ? aiConfig.model ?? null : null,
|
|
12679
12720
|
ollamaCloudApiKeyPresent: usingOllamaCloud ? meta.hasOllamaCloudApiKey : null,
|
|
12680
|
-
|
|
12721
|
+
openrouterApiKeyPresent: usingOpenRouter ? meta.hasOpenRouterApiKey : null,
|
|
12722
|
+
secretsPath: needsSecretInfo ? meta.secretsPath : null
|
|
12681
12723
|
}
|
|
12682
12724
|
};
|
|
12683
12725
|
}
|
|
@@ -12714,6 +12756,36 @@ async function promptForOllamaCloudModelSelection(apiKey, fallbackModel) {
|
|
|
12714
12756
|
}
|
|
12715
12757
|
return inputPrompt("Ollama Cloud model", fallbackModel);
|
|
12716
12758
|
}
|
|
12759
|
+
async function promptForOpenRouterModelSelection(apiKey, fallbackModel) {
|
|
12760
|
+
if (apiKey) {
|
|
12761
|
+
try {
|
|
12762
|
+
info("Fetching available OpenRouter models...");
|
|
12763
|
+
const models = prioritizeOpenRouterModels(await fetchOpenRouterModels(apiKey));
|
|
12764
|
+
if (models.length > 0) {
|
|
12765
|
+
const manualChoice = "Enter model manually";
|
|
12766
|
+
const choices = models.map((model) => ({
|
|
12767
|
+
value: model,
|
|
12768
|
+
label: model === DEFAULT_OPENROUTER_MODEL ? `${model} (default)` : model
|
|
12769
|
+
}));
|
|
12770
|
+
const selected = await selectPrompt("OpenRouter model", [
|
|
12771
|
+
...choices.map((choice) => choice.label),
|
|
12772
|
+
manualChoice
|
|
12773
|
+
]);
|
|
12774
|
+
if (selected !== manualChoice) {
|
|
12775
|
+
return choices.find((choice) => choice.label === selected)?.value ?? fallbackModel;
|
|
12776
|
+
}
|
|
12777
|
+
} else {
|
|
12778
|
+
warn("OpenRouter returned no available models. Enter the model name manually.");
|
|
12779
|
+
}
|
|
12780
|
+
} catch (err) {
|
|
12781
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12782
|
+
warn(`Could not fetch OpenRouter models: ${message}`);
|
|
12783
|
+
}
|
|
12784
|
+
} else {
|
|
12785
|
+
warn("No OpenRouter API key is available yet, so the model list cannot be fetched.");
|
|
12786
|
+
}
|
|
12787
|
+
return inputPrompt("OpenRouter model", fallbackModel);
|
|
12788
|
+
}
|
|
12717
12789
|
async function selectCurrentValue(message, options, current) {
|
|
12718
12790
|
const choices = options.map((option) => ({
|
|
12719
12791
|
value: option.value,
|
|
@@ -12728,7 +12800,7 @@ async function selectBooleanValue(message, current, trueLabel, falseLabel) {
|
|
|
12728
12800
|
{ value: "false", label: falseLabel }
|
|
12729
12801
|
], current ? "true" : "false").then((value) => value === "true");
|
|
12730
12802
|
}
|
|
12731
|
-
async function promptForConfigEdits(current, hasExistingOllamaApiKey) {
|
|
12803
|
+
async function promptForConfigEdits(current, hasExistingOllamaApiKey, hasExistingOpenRouterApiKey) {
|
|
12732
12804
|
const workflow = await selectCurrentValue("Workflow mode", WORKFLOW_OPTIONS, current.workflow);
|
|
12733
12805
|
const role = await selectCurrentValue("Your role in this clone", ROLE_OPTIONS, current.role);
|
|
12734
12806
|
const mainBranch = await inputPrompt("Main branch name", current.mainBranch);
|
|
@@ -12749,6 +12821,8 @@ async function promptForConfigEdits(current, hasExistingOllamaApiKey) {
|
|
|
12749
12821
|
let aiModel;
|
|
12750
12822
|
let ollamaApiKeyAction = "keep";
|
|
12751
12823
|
let ollamaApiKey;
|
|
12824
|
+
let openrouterApiKeyAction = "keep";
|
|
12825
|
+
let openrouterApiKey;
|
|
12752
12826
|
if (aiEnabled) {
|
|
12753
12827
|
const currentProvider = current.aiProvider ?? "copilot";
|
|
12754
12828
|
aiProvider = await selectCurrentValue("AI provider", AI_PROVIDER_OPTIONS, currentProvider);
|
|
@@ -12780,16 +12854,72 @@ async function promptForConfigEdits(current, hasExistingOllamaApiKey) {
|
|
|
12780
12854
|
}
|
|
12781
12855
|
const modelLookupApiKey = ollamaApiKeyAction === "set" ? ollamaApiKey ?? null : ollamaApiKeyAction === "keep" ? await getOllamaCloudApiKey() : null;
|
|
12782
12856
|
aiModel = await promptForOllamaCloudModelSelection(modelLookupApiKey, current.aiProvider === "ollama-cloud" ? current.aiModel ?? DEFAULT_OLLAMA_CLOUD_MODEL : DEFAULT_OLLAMA_CLOUD_MODEL);
|
|
12783
|
-
|
|
12784
|
-
|
|
12857
|
+
if (hasExistingOpenRouterApiKey) {
|
|
12858
|
+
const shouldDeleteOpenRouterKey = await confirmPrompt("Delete the stored OpenRouter API key from the local secrets store?");
|
|
12859
|
+
if (shouldDeleteOpenRouterKey) {
|
|
12860
|
+
openrouterApiKeyAction = "delete";
|
|
12861
|
+
}
|
|
12862
|
+
}
|
|
12863
|
+
} else if (aiProvider === "openrouter") {
|
|
12864
|
+
if (hasExistingOpenRouterApiKey) {
|
|
12865
|
+
const apiKeyChoice = await selectPrompt("OpenRouter API key", [
|
|
12866
|
+
"Keep existing stored key",
|
|
12867
|
+
"Replace stored key",
|
|
12868
|
+
"Delete stored key"
|
|
12869
|
+
]);
|
|
12870
|
+
if (apiKeyChoice === "Replace stored key") {
|
|
12871
|
+
openrouterApiKey = (await passwordPrompt("Enter the new OpenRouter API key")).trim();
|
|
12872
|
+
if (!openrouterApiKey) {
|
|
12873
|
+
throw new Error("OpenRouter API key cannot be empty when replacing the stored key.");
|
|
12874
|
+
}
|
|
12875
|
+
openrouterApiKeyAction = "set";
|
|
12876
|
+
} else if (apiKeyChoice === "Delete stored key") {
|
|
12877
|
+
openrouterApiKeyAction = "delete";
|
|
12878
|
+
}
|
|
12879
|
+
} else {
|
|
12880
|
+
const addApiKey = await confirmPrompt("No OpenRouter API key is stored. Add one now?");
|
|
12881
|
+
if (addApiKey) {
|
|
12882
|
+
openrouterApiKey = (await passwordPrompt("Enter your OpenRouter API key")).trim();
|
|
12883
|
+
if (!openrouterApiKey) {
|
|
12884
|
+
throw new Error("OpenRouter API key cannot be empty when enabling OpenRouter.");
|
|
12885
|
+
}
|
|
12886
|
+
openrouterApiKeyAction = "set";
|
|
12887
|
+
}
|
|
12888
|
+
}
|
|
12889
|
+
const modelLookupApiKey = openrouterApiKeyAction === "set" ? openrouterApiKey ?? null : openrouterApiKeyAction === "keep" ? await getOpenRouterApiKey() : null;
|
|
12890
|
+
aiModel = await promptForOpenRouterModelSelection(modelLookupApiKey, current.aiProvider === "openrouter" ? current.aiModel ?? DEFAULT_OPENROUTER_MODEL : DEFAULT_OPENROUTER_MODEL);
|
|
12891
|
+
if (hasExistingOllamaApiKey) {
|
|
12892
|
+
const shouldDeleteStoredKey = await confirmPrompt("Delete the stored Ollama Cloud API key from the local secrets store?");
|
|
12893
|
+
if (shouldDeleteStoredKey) {
|
|
12894
|
+
ollamaApiKeyAction = "delete";
|
|
12895
|
+
}
|
|
12896
|
+
}
|
|
12897
|
+
} else {
|
|
12898
|
+
if (hasExistingOllamaApiKey) {
|
|
12899
|
+
const shouldDeleteStoredKey = await confirmPrompt("Delete the stored Ollama Cloud API key from the local secrets store?");
|
|
12900
|
+
if (shouldDeleteStoredKey) {
|
|
12901
|
+
ollamaApiKeyAction = "delete";
|
|
12902
|
+
}
|
|
12903
|
+
}
|
|
12904
|
+
if (hasExistingOpenRouterApiKey) {
|
|
12905
|
+
const shouldDeleteOpenRouterKey = await confirmPrompt("Delete the stored OpenRouter API key from the local secrets store?");
|
|
12906
|
+
if (shouldDeleteOpenRouterKey) {
|
|
12907
|
+
openrouterApiKeyAction = "delete";
|
|
12908
|
+
}
|
|
12909
|
+
}
|
|
12910
|
+
}
|
|
12911
|
+
} else {
|
|
12912
|
+
if (hasExistingOllamaApiKey) {
|
|
12913
|
+
const shouldDeleteStoredKey = await confirmPrompt("AI is disabled. Delete the stored Ollama Cloud API key from the local secrets store?");
|
|
12785
12914
|
if (shouldDeleteStoredKey) {
|
|
12786
12915
|
ollamaApiKeyAction = "delete";
|
|
12787
12916
|
}
|
|
12788
12917
|
}
|
|
12789
|
-
|
|
12790
|
-
|
|
12791
|
-
|
|
12792
|
-
|
|
12918
|
+
if (hasExistingOpenRouterApiKey) {
|
|
12919
|
+
const shouldDeleteOpenRouterKey = await confirmPrompt("AI is disabled. Delete the stored OpenRouter API key from the local secrets store?");
|
|
12920
|
+
if (shouldDeleteOpenRouterKey) {
|
|
12921
|
+
openrouterApiKeyAction = "delete";
|
|
12922
|
+
}
|
|
12793
12923
|
}
|
|
12794
12924
|
}
|
|
12795
12925
|
return {
|
|
@@ -12808,17 +12938,17 @@ async function promptForConfigEdits(current, hasExistingOllamaApiKey) {
|
|
|
12808
12938
|
showTips
|
|
12809
12939
|
}),
|
|
12810
12940
|
ollamaApiKeyAction,
|
|
12811
|
-
ollamaApiKey
|
|
12941
|
+
ollamaApiKey,
|
|
12942
|
+
openrouterApiKeyAction,
|
|
12943
|
+
openrouterApiKey
|
|
12812
12944
|
};
|
|
12813
12945
|
}
|
|
12814
|
-
async function
|
|
12946
|
+
async function applyApiKeyEdits(result) {
|
|
12815
12947
|
if (result.ollamaApiKeyAction === "set" && result.ollamaApiKey) {
|
|
12816
12948
|
await setOllamaCloudApiKey(result.ollamaApiKey);
|
|
12817
12949
|
success("Stored Ollama Cloud API key in the local secrets store.");
|
|
12818
|
-
info(`Secrets path: ${
|
|
12819
|
-
|
|
12820
|
-
}
|
|
12821
|
-
if (result.ollamaApiKeyAction === "delete") {
|
|
12950
|
+
info(`Secrets path: ${import_picocolors10.default.bold(getSecretsStorePath())}`);
|
|
12951
|
+
} else if (result.ollamaApiKeyAction === "delete") {
|
|
12822
12952
|
const deleted = await deleteOllamaCloudApiKey();
|
|
12823
12953
|
if (deleted) {
|
|
12824
12954
|
success("Deleted stored Ollama Cloud API key.");
|
|
@@ -12826,31 +12956,49 @@ async function applyOllamaApiKeyEdit(result) {
|
|
|
12826
12956
|
info("No stored Ollama Cloud API key was found to delete.");
|
|
12827
12957
|
}
|
|
12828
12958
|
}
|
|
12959
|
+
if (result.openrouterApiKeyAction === "set" && result.openrouterApiKey) {
|
|
12960
|
+
await setOpenRouterApiKey(result.openrouterApiKey);
|
|
12961
|
+
success("Stored OpenRouter API key in the local secrets store.");
|
|
12962
|
+
info(`Secrets path: ${import_picocolors10.default.bold(getSecretsStorePath())}`);
|
|
12963
|
+
} else if (result.openrouterApiKeyAction === "delete") {
|
|
12964
|
+
const deleted = await deleteOpenRouterApiKey();
|
|
12965
|
+
if (deleted) {
|
|
12966
|
+
success("Deleted stored OpenRouter API key.");
|
|
12967
|
+
} else {
|
|
12968
|
+
info("No stored OpenRouter API key was found to delete.");
|
|
12969
|
+
}
|
|
12970
|
+
}
|
|
12829
12971
|
}
|
|
12830
12972
|
function printConfigSummary(snapshot) {
|
|
12831
|
-
info(`Config source: ${
|
|
12832
|
-
info(`Config path: ${
|
|
12833
|
-
info(`Workflow: ${
|
|
12834
|
-
info(`Convention: ${
|
|
12835
|
-
info(`Role: ${
|
|
12973
|
+
info(`Config source: ${import_picocolors10.default.bold(snapshot.source)}`);
|
|
12974
|
+
info(`Config path: ${import_picocolors10.default.bold(snapshot.location)}`);
|
|
12975
|
+
info(`Workflow: ${import_picocolors10.default.bold(snapshot.workflowLabel)}`);
|
|
12976
|
+
info(`Convention: ${import_picocolors10.default.bold(snapshot.commitConventionLabel)}`);
|
|
12977
|
+
info(`Role: ${import_picocolors10.default.bold(snapshot.role)}`);
|
|
12836
12978
|
if (snapshot.devBranch) {
|
|
12837
|
-
info(`Main: ${
|
|
12979
|
+
info(`Main: ${import_picocolors10.default.bold(snapshot.mainBranch)} | Dev: ${import_picocolors10.default.bold(snapshot.devBranch)}`);
|
|
12838
12980
|
} else {
|
|
12839
|
-
info(`Main: ${
|
|
12981
|
+
info(`Main: ${import_picocolors10.default.bold(snapshot.mainBranch)}`);
|
|
12840
12982
|
}
|
|
12841
|
-
info(`Origin: ${
|
|
12842
|
-
info(`Branch prefixes: ${
|
|
12843
|
-
info(`Guides: ${
|
|
12844
|
-
info(`AI: ${
|
|
12983
|
+
info(`Origin: ${import_picocolors10.default.bold(snapshot.origin)} | Upstream: ${import_picocolors10.default.bold(snapshot.upstream)}`);
|
|
12984
|
+
info(`Branch prefixes: ${import_picocolors10.default.bold(snapshot.branchPrefixes.join(", "))}`);
|
|
12985
|
+
info(`Guides: ${import_picocolors10.default.bold(snapshot.showTips ? "shown" : "hidden")}`);
|
|
12986
|
+
info(`AI: ${import_picocolors10.default.bold(snapshot.ai.enabled ? "enabled" : "disabled")}`);
|
|
12845
12987
|
if (snapshot.ai.enabled && snapshot.ai.providerLabel) {
|
|
12846
|
-
info(`AI provider: ${
|
|
12988
|
+
info(`AI provider: ${import_picocolors10.default.bold(snapshot.ai.providerLabel)}`);
|
|
12847
12989
|
if (snapshot.ai.model) {
|
|
12848
|
-
info(`AI model: ${
|
|
12990
|
+
info(`AI model: ${import_picocolors10.default.bold(snapshot.ai.model)}`);
|
|
12849
12991
|
}
|
|
12850
12992
|
if (snapshot.ai.provider === "ollama-cloud") {
|
|
12851
|
-
info(`Ollama Cloud API key: ${
|
|
12993
|
+
info(`Ollama Cloud API key: ${import_picocolors10.default.bold(snapshot.ai.ollamaCloudApiKeyPresent ? "stored" : "missing")}`);
|
|
12994
|
+
if (snapshot.ai.secretsPath) {
|
|
12995
|
+
info(`Secrets path: ${import_picocolors10.default.bold(snapshot.ai.secretsPath)}`);
|
|
12996
|
+
}
|
|
12997
|
+
}
|
|
12998
|
+
if (snapshot.ai.provider === "openrouter") {
|
|
12999
|
+
info(`OpenRouter API key: ${import_picocolors10.default.bold(snapshot.ai.openrouterApiKeyPresent ? "stored" : "missing")}`);
|
|
12852
13000
|
if (snapshot.ai.secretsPath) {
|
|
12853
|
-
info(`Secrets path: ${
|
|
13001
|
+
info(`Secrets path: ${import_picocolors10.default.bold(snapshot.ai.secretsPath)}`);
|
|
12854
13002
|
}
|
|
12855
13003
|
}
|
|
12856
13004
|
}
|
|
@@ -12898,14 +13046,15 @@ var config_default = defineCommand({
|
|
|
12898
13046
|
}
|
|
12899
13047
|
if (args.edit) {
|
|
12900
13048
|
try {
|
|
12901
|
-
const editResult = await promptForConfigEdits(config, await hasOllamaCloudApiKey());
|
|
13049
|
+
const editResult = await promptForConfigEdits(config, await hasOllamaCloudApiKey(), await hasOpenRouterApiKey());
|
|
12902
13050
|
writeConfig(editResult.config);
|
|
12903
|
-
await
|
|
13051
|
+
await applyApiKeyEdits(editResult);
|
|
12904
13052
|
success("Updated repo config.");
|
|
12905
13053
|
printConfigSummary(buildConfigSnapshot(editResult.config, {
|
|
12906
13054
|
source,
|
|
12907
13055
|
location: getConfigLocationLabel(),
|
|
12908
13056
|
hasOllamaCloudApiKey: await hasOllamaCloudApiKey(),
|
|
13057
|
+
hasOpenRouterApiKey: await hasOpenRouterApiKey(),
|
|
12909
13058
|
secretsPath: getSecretsStorePath()
|
|
12910
13059
|
}));
|
|
12911
13060
|
return;
|
|
@@ -12918,6 +13067,7 @@ var config_default = defineCommand({
|
|
|
12918
13067
|
source,
|
|
12919
13068
|
location: getConfigLocationLabel(),
|
|
12920
13069
|
hasOllamaCloudApiKey: await hasOllamaCloudApiKey(),
|
|
13070
|
+
hasOpenRouterApiKey: await hasOpenRouterApiKey(),
|
|
12921
13071
|
secretsPath: getSecretsStorePath()
|
|
12922
13072
|
});
|
|
12923
13073
|
if (args.json) {
|
|
@@ -12926,18 +13076,126 @@ var config_default = defineCommand({
|
|
|
12926
13076
|
}
|
|
12927
13077
|
printConfigSummary(snapshot);
|
|
12928
13078
|
console.log();
|
|
12929
|
-
console.log(` ${
|
|
13079
|
+
console.log(` ${import_picocolors10.default.dim("Run `cn config --edit` to update these settings.")}`);
|
|
12930
13080
|
console.log();
|
|
12931
13081
|
}
|
|
12932
13082
|
});
|
|
12933
13083
|
|
|
13084
|
+
// src/commands/discard.ts
|
|
13085
|
+
var import_picocolors11 = __toESM(require_picocolors(), 1);
|
|
13086
|
+
var discard_default = defineCommand({
|
|
13087
|
+
meta: {
|
|
13088
|
+
name: "discard",
|
|
13089
|
+
description: "Discard the current feature branch and return to the base branch"
|
|
13090
|
+
},
|
|
13091
|
+
args: {
|
|
13092
|
+
force: {
|
|
13093
|
+
type: "boolean",
|
|
13094
|
+
alias: "f",
|
|
13095
|
+
description: "Skip confirmation and discard immediately",
|
|
13096
|
+
default: false
|
|
13097
|
+
}
|
|
13098
|
+
},
|
|
13099
|
+
async run({ args }) {
|
|
13100
|
+
if (!await isGitRepo()) {
|
|
13101
|
+
error("Not inside a git repository.");
|
|
13102
|
+
process.exit(1);
|
|
13103
|
+
}
|
|
13104
|
+
await assertCleanGitState("discarding a branch");
|
|
13105
|
+
const config = readConfig();
|
|
13106
|
+
if (!config) {
|
|
13107
|
+
error("No repo config found. Run `cn setup` first.");
|
|
13108
|
+
process.exit(1);
|
|
13109
|
+
}
|
|
13110
|
+
const currentBranch = await getCurrentBranch();
|
|
13111
|
+
const baseBranch = getBaseBranch(config);
|
|
13112
|
+
await projectHeading("discard", "\uD83D\uDDD1\uFE0F");
|
|
13113
|
+
if (isBranchProtected(currentBranch, config)) {
|
|
13114
|
+
error(`${import_picocolors11.default.bold(currentBranch)} is a protected branch and cannot be discarded.`);
|
|
13115
|
+
info(`Switch to a feature branch first, then run ${import_picocolors11.default.bold("cn discard")}.`);
|
|
13116
|
+
process.exit(1);
|
|
13117
|
+
}
|
|
13118
|
+
if (currentBranch === baseBranch) {
|
|
13119
|
+
info(`You are already on ${import_picocolors11.default.bold(baseBranch)}.`);
|
|
13120
|
+
process.exit(0);
|
|
13121
|
+
}
|
|
13122
|
+
const { origin } = config;
|
|
13123
|
+
const localWork = await hasLocalWork(origin, currentBranch);
|
|
13124
|
+
const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
|
|
13125
|
+
if (hasWork) {
|
|
13126
|
+
if (localWork.uncommitted) {
|
|
13127
|
+
warn("You have uncommitted changes in your working tree.");
|
|
13128
|
+
}
|
|
13129
|
+
if (localWork.unpushedCommits > 0) {
|
|
13130
|
+
warn(`You have ${import_picocolors11.default.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on this branch.`);
|
|
13131
|
+
}
|
|
13132
|
+
warn("Discarding this branch will permanently lose that work.");
|
|
13133
|
+
const SAVE_FIRST = "Save my changes first (cn save), then discard";
|
|
13134
|
+
const DISCARD_ANYWAY = "Discard anyway \u2014 I do not need this work";
|
|
13135
|
+
const CANCEL = "Keep the branch, take me back";
|
|
13136
|
+
const action = await selectPrompt("This branch has unsaved work. What would you like to do?", [SAVE_FIRST, DISCARD_ANYWAY, CANCEL]);
|
|
13137
|
+
if (action === CANCEL) {
|
|
13138
|
+
info("Discard cancelled. Your branch is untouched.");
|
|
13139
|
+
process.exit(0);
|
|
13140
|
+
}
|
|
13141
|
+
if (action === SAVE_FIRST) {
|
|
13142
|
+
if (!localWork.uncommitted) {
|
|
13143
|
+
info("No uncommitted changes to stash \u2014 unpushed commits will still be lost.");
|
|
13144
|
+
const confirm = await confirmPrompt("Continue discarding the branch?");
|
|
13145
|
+
if (!confirm) {
|
|
13146
|
+
info("Discard cancelled.");
|
|
13147
|
+
process.exit(0);
|
|
13148
|
+
}
|
|
13149
|
+
} else {
|
|
13150
|
+
const stashResult = await stashChanges(`work-in-progress on ${currentBranch}`);
|
|
13151
|
+
if (stashResult.exitCode !== 0) {
|
|
13152
|
+
error(`Failed to save changes: ${stashResult.stderr}`);
|
|
13153
|
+
process.exit(1);
|
|
13154
|
+
}
|
|
13155
|
+
success(`Changes saved. Use ${import_picocolors11.default.bold("cn save --restore")} to bring them back.`);
|
|
13156
|
+
}
|
|
13157
|
+
}
|
|
13158
|
+
} else if (!args.force) {
|
|
13159
|
+
const confirmed = await confirmPrompt(`Discard ${import_picocolors11.default.bold(currentBranch)} and return to ${import_picocolors11.default.bold(baseBranch)}?`);
|
|
13160
|
+
if (!confirmed) {
|
|
13161
|
+
info("Discard cancelled.");
|
|
13162
|
+
process.exit(0);
|
|
13163
|
+
}
|
|
13164
|
+
}
|
|
13165
|
+
const upstreamRef = await getUpstreamRef();
|
|
13166
|
+
let deleteRemote = false;
|
|
13167
|
+
if (upstreamRef) {
|
|
13168
|
+
deleteRemote = await confirmPrompt(`Also delete the remote branch ${import_picocolors11.default.bold(upstreamRef)}?`);
|
|
13169
|
+
}
|
|
13170
|
+
const checkoutResult = await checkoutBranch(baseBranch);
|
|
13171
|
+
if (checkoutResult.exitCode !== 0) {
|
|
13172
|
+
error(`Failed to switch to ${import_picocolors11.default.bold(baseBranch)}: ${checkoutResult.stderr}`);
|
|
13173
|
+
process.exit(1);
|
|
13174
|
+
}
|
|
13175
|
+
const deleteResult = await forceDeleteBranch(currentBranch);
|
|
13176
|
+
if (deleteResult.exitCode !== 0) {
|
|
13177
|
+
error(`Failed to delete branch ${import_picocolors11.default.bold(currentBranch)}: ${deleteResult.stderr}`);
|
|
13178
|
+
process.exit(1);
|
|
13179
|
+
}
|
|
13180
|
+
success(`Discarded ${import_picocolors11.default.bold(currentBranch)} and switched back to ${import_picocolors11.default.bold(baseBranch)}`);
|
|
13181
|
+
if (deleteRemote) {
|
|
13182
|
+
const remoteDeleteResult = await deleteRemoteBranch(origin, currentBranch);
|
|
13183
|
+
if (remoteDeleteResult.exitCode !== 0) {
|
|
13184
|
+
warn(`Could not delete remote branch: ${remoteDeleteResult.stderr.trim()}`);
|
|
13185
|
+
} else {
|
|
13186
|
+
success(`Deleted remote branch ${import_picocolors11.default.bold(`${origin}/${currentBranch}`)}`);
|
|
13187
|
+
}
|
|
13188
|
+
}
|
|
13189
|
+
}
|
|
13190
|
+
});
|
|
13191
|
+
|
|
12934
13192
|
// src/commands/doctor.ts
|
|
12935
13193
|
import { execFile as execFileCb3 } from "child_process";
|
|
12936
13194
|
var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
12937
13195
|
// package.json
|
|
12938
13196
|
var package_default = {
|
|
12939
13197
|
name: "contribute-now",
|
|
12940
|
-
version: "0.8.0-
|
|
13198
|
+
version: "0.8.0-pr.e913c8d",
|
|
12941
13199
|
description: "Developer CLI that automates git workflows \u2014 branching, syncing, committing, and PRs \u2014 with multi-workflow and commit convention support.",
|
|
12942
13200
|
type: "module",
|
|
12943
13201
|
bin: {
|
|
@@ -13190,6 +13448,15 @@ async function configSection() {
|
|
|
13190
13448
|
detail: hasSecretsStore() ? "stored in the local secrets store" : "run `cn setup` to save it"
|
|
13191
13449
|
});
|
|
13192
13450
|
}
|
|
13451
|
+
if (aiConfig.provider === "openrouter") {
|
|
13452
|
+
const hasApiKey = await hasOpenRouterApiKey();
|
|
13453
|
+
checks.push({
|
|
13454
|
+
label: hasApiKey ? "OpenRouter API key present" : "OpenRouter API key missing",
|
|
13455
|
+
ok: true,
|
|
13456
|
+
warning: !hasApiKey,
|
|
13457
|
+
detail: hasSecretsStore() ? "stored in the local secrets store" : "run `cn setup` to save it"
|
|
13458
|
+
});
|
|
13459
|
+
}
|
|
13193
13460
|
}
|
|
13194
13461
|
if (hasDevBranch(config.workflow)) {
|
|
13195
13462
|
checks.push({
|
|
@@ -14121,6 +14388,32 @@ async function promptForOllamaCloudModel(apiKey, host = DEFAULT_OLLAMA_CLOUD_HOS
|
|
|
14121
14388
|
}
|
|
14122
14389
|
return inputPrompt(`Ollama Cloud model (default: ${DEFAULT_OLLAMA_CLOUD_MODEL} \u2014 press Enter to keep)`, DEFAULT_OLLAMA_CLOUD_MODEL);
|
|
14123
14390
|
}
|
|
14391
|
+
async function promptForOpenRouterModel(apiKey) {
|
|
14392
|
+
try {
|
|
14393
|
+
info("Fetching available OpenRouter models...");
|
|
14394
|
+
const models = prioritizeOpenRouterModels(await fetchOpenRouterModels(apiKey));
|
|
14395
|
+
if (models.length > 0) {
|
|
14396
|
+
const manualChoice = "Enter model manually";
|
|
14397
|
+
const choices = models.map((model) => ({
|
|
14398
|
+
value: model,
|
|
14399
|
+
label: model === DEFAULT_OPENROUTER_MODEL ? `${model} (default)` : model
|
|
14400
|
+
}));
|
|
14401
|
+
const selected = await selectPrompt("Which OpenRouter model should this clone use?", [
|
|
14402
|
+
...choices.map((choice) => choice.label),
|
|
14403
|
+
manualChoice
|
|
14404
|
+
]);
|
|
14405
|
+
if (selected !== manualChoice) {
|
|
14406
|
+
return choices.find((choice) => choice.label === selected)?.value ?? DEFAULT_OPENROUTER_MODEL;
|
|
14407
|
+
}
|
|
14408
|
+
} else {
|
|
14409
|
+
warn("OpenRouter returned no available models. Enter the model name manually.");
|
|
14410
|
+
}
|
|
14411
|
+
} catch (err) {
|
|
14412
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
14413
|
+
warn(`Could not fetch OpenRouter models: ${message}`);
|
|
14414
|
+
}
|
|
14415
|
+
return inputPrompt(`OpenRouter model (default: ${DEFAULT_OPENROUTER_MODEL} \u2014 press Enter to keep)`, DEFAULT_OPENROUTER_MODEL);
|
|
14416
|
+
}
|
|
14124
14417
|
var setup_default = defineCommand({
|
|
14125
14418
|
meta: {
|
|
14126
14419
|
name: "setup",
|
|
@@ -14172,9 +14465,16 @@ var setup_default = defineCommand({
|
|
|
14172
14465
|
if (enableAI) {
|
|
14173
14466
|
const providerChoice = await selectPrompt("Which AI provider should this clone use?", [
|
|
14174
14467
|
"GitHub Copilot \u2014 use your existing GitHub/Copilot auth",
|
|
14175
|
-
"Ollama Cloud \u2014 use an API key stored in the local secrets store"
|
|
14468
|
+
"Ollama Cloud \u2014 use an API key stored in the local secrets store",
|
|
14469
|
+
"OpenRouter \u2014 use an API key stored in the local secrets store"
|
|
14176
14470
|
]);
|
|
14177
|
-
|
|
14471
|
+
if (providerChoice.startsWith("Ollama Cloud")) {
|
|
14472
|
+
aiProvider = "ollama-cloud";
|
|
14473
|
+
} else if (providerChoice.startsWith("OpenRouter")) {
|
|
14474
|
+
aiProvider = "openrouter";
|
|
14475
|
+
} else {
|
|
14476
|
+
aiProvider = "copilot";
|
|
14477
|
+
}
|
|
14178
14478
|
if (aiProvider === "ollama-cloud") {
|
|
14179
14479
|
const apiKey = (await passwordPrompt("Enter your Ollama Cloud API key")).trim();
|
|
14180
14480
|
if (!apiKey) {
|
|
@@ -14191,6 +14491,22 @@ var setup_default = defineCommand({
|
|
|
14191
14491
|
error(`Failed to store Ollama Cloud API key: ${message}`);
|
|
14192
14492
|
process.exit(1);
|
|
14193
14493
|
}
|
|
14494
|
+
} else if (aiProvider === "openrouter") {
|
|
14495
|
+
const apiKey = (await passwordPrompt("Enter your OpenRouter API key")).trim();
|
|
14496
|
+
if (!apiKey) {
|
|
14497
|
+
error("OpenRouter API key is required when OpenRouter is selected.");
|
|
14498
|
+
process.exit(1);
|
|
14499
|
+
}
|
|
14500
|
+
aiModel = await promptForOpenRouterModel(apiKey);
|
|
14501
|
+
try {
|
|
14502
|
+
await setOpenRouterApiKey(apiKey);
|
|
14503
|
+
success("Stored OpenRouter API key in the local secrets store.");
|
|
14504
|
+
info(`Secrets path: ${import_picocolors16.default.bold(getSecretsStorePath())}`);
|
|
14505
|
+
} catch (err) {
|
|
14506
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
14507
|
+
error(`Failed to store OpenRouter API key: ${message}`);
|
|
14508
|
+
process.exit(1);
|
|
14509
|
+
}
|
|
14194
14510
|
}
|
|
14195
14511
|
}
|
|
14196
14512
|
const showTips = await confirmPrompt("Show beginner quick guides and loading tips in command output?");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "contribute-now",
|
|
3
|
-
"version": "0.8.0-
|
|
3
|
+
"version": "0.8.0-pr.e913c8d",
|
|
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": {
|