contribute-now 0.4.0-dev.d24b735 → 0.4.0-dev.e93c162
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 +55 -2
- package/dist/index.js +160 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ bun install -g contribute-now
|
|
|
64
64
|
## Prerequisites
|
|
65
65
|
|
|
66
66
|
- **[Git](https://git-scm.com/)** — required
|
|
67
|
-
- **[GitHub CLI](https://cli.github.com)** (`gh`) —
|
|
67
|
+
- **[GitHub CLI](https://cli.github.com)** (`gh`) — recommended; required for PR creation, role detection, and merge status checks
|
|
68
68
|
- **[GitHub Copilot](https://github.com/features/copilot)** — optional; enables AI features
|
|
69
69
|
|
|
70
70
|
---
|
|
@@ -129,10 +129,13 @@ Stage your changes and create a validated, AI-generated commit message matching
|
|
|
129
129
|
contrib commit # AI-generated message
|
|
130
130
|
contrib commit --no-ai # manual entry, still validated
|
|
131
131
|
contrib commit --model gpt-4.1 # specific AI model
|
|
132
|
+
contrib commit --group # AI groups changes into atomic commits
|
|
132
133
|
```
|
|
133
134
|
|
|
134
135
|
After the AI generates a message, you can **accept**, **edit**, **regenerate**, or **write manually**. Messages are always validated against your convention — with a soft warning if they don't match (you can still commit).
|
|
135
136
|
|
|
137
|
+
**Group commit mode** (`--group`): AI analyzes all staged and unstaged changes, groups related files into logical atomic commits, and generates a commit message for each group. Great for splitting a large set of changes into clean, reviewable commits.
|
|
138
|
+
|
|
136
139
|
---
|
|
137
140
|
|
|
138
141
|
### `contrib update`
|
|
@@ -180,6 +183,55 @@ contrib status
|
|
|
180
183
|
|
|
181
184
|
---
|
|
182
185
|
|
|
186
|
+
### `contrib doctor`
|
|
187
|
+
|
|
188
|
+
Diagnose the contribute-now CLI environment and configuration. Checks tools, dependencies, config, git state, fork setup, workflow, and environment.
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
contrib doctor # pretty-printed report
|
|
192
|
+
contrib doctor --json # machine-readable JSON output
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Checks include:
|
|
196
|
+
- CLI version and runtime (Bun/Node)
|
|
197
|
+
- git and GitHub CLI availability and authentication
|
|
198
|
+
- `.contributerc.json` validity and `.gitignore` status
|
|
199
|
+
- Git repo state (uncommitted changes, lock files, shallow clone)
|
|
200
|
+
- Fork and remote configuration
|
|
201
|
+
- Workflow and branch setup
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
### `contrib log`
|
|
206
|
+
|
|
207
|
+
Show a colorized, workflow-aware commit log with graph visualization.
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
contrib log # last 20 commits with graph
|
|
211
|
+
contrib log -n 50 # last 50 commits
|
|
212
|
+
contrib log --all # all branches
|
|
213
|
+
contrib log --no-graph # flat view without graph lines
|
|
214
|
+
contrib log -b feature/x # log for a specific branch
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Protected branches (main, dev) are highlighted, and the current branch is color-coded for quick orientation.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
### `contrib branch`
|
|
222
|
+
|
|
223
|
+
List branches with workflow-aware labels and tracking status.
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
contrib branch # local branches
|
|
227
|
+
contrib branch --all # local + remote branches
|
|
228
|
+
contrib branch --remote # remote branches only
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Branches are annotated with workflow labels (e.g., base, dev, feature) and tracking info (upstream, gone, no remote).
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
183
235
|
### `contrib hook`
|
|
184
236
|
|
|
185
237
|
Install or uninstall a `commit-msg` git hook that validates every commit against your configured convention — no Husky or lint-staged needed.
|
|
@@ -210,8 +262,9 @@ contrib validate "added stuff" # exit 1
|
|
|
210
262
|
All AI features are powered by **GitHub Copilot** via `@github/copilot-sdk` and are entirely **optional** — every command has a manual fallback.
|
|
211
263
|
|
|
212
264
|
| Command | AI Feature | Fallback |
|
|
213
|
-
|
|
265
|
+
|---------|------------|----------|
|
|
214
266
|
| `commit` | Generate commit message from staged diff | Type manually |
|
|
267
|
+
| `commit --group` | Group related changes into atomic commits | Manual staging + commit |
|
|
215
268
|
| `start` | Suggest branch name from natural language | Prefix picker + manual |
|
|
216
269
|
| `update` | Conflict resolution guidance | Standard git instructions |
|
|
217
270
|
| `submit` | Generate PR title and body | `gh pr create --fill` or manual |
|
package/dist/index.js
CHANGED
|
@@ -970,6 +970,79 @@ function withTimeout(promise, ms) {
|
|
|
970
970
|
}
|
|
971
971
|
var COPILOT_TIMEOUT_MS = 30000;
|
|
972
972
|
var COPILOT_LONG_TIMEOUT_MS = 90000;
|
|
973
|
+
var BATCH_CONFIG = {
|
|
974
|
+
LARGE_CHANGESET_THRESHOLD: 15,
|
|
975
|
+
COMPACT_PER_FILE_CHARS: 300,
|
|
976
|
+
MAX_COMPACT_PAYLOAD: 1e4,
|
|
977
|
+
FALLBACK_BATCH_SIZE: 15
|
|
978
|
+
};
|
|
979
|
+
function parseDiffByFile(rawDiff) {
|
|
980
|
+
const sections = new Map;
|
|
981
|
+
const headerPattern = /^diff --git a\/(.+?) b\/(.+?)$/gm;
|
|
982
|
+
const positions = [];
|
|
983
|
+
for (let match = headerPattern.exec(rawDiff);match !== null; match = headerPattern.exec(rawDiff)) {
|
|
984
|
+
const aFile = match[1];
|
|
985
|
+
const bFile = match[2] ?? aFile;
|
|
986
|
+
positions.push({ aFile, bFile, start: match.index });
|
|
987
|
+
}
|
|
988
|
+
for (let i = 0;i < positions.length; i++) {
|
|
989
|
+
const { aFile, bFile, start } = positions[i];
|
|
990
|
+
const end = i + 1 < positions.length ? positions[i + 1].start : rawDiff.length;
|
|
991
|
+
const section = rawDiff.slice(start, end);
|
|
992
|
+
sections.set(aFile, section);
|
|
993
|
+
if (bFile && bFile !== aFile) {
|
|
994
|
+
sections.set(bFile, section);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return sections;
|
|
998
|
+
}
|
|
999
|
+
function extractDiffStats(diffSection) {
|
|
1000
|
+
let added = 0;
|
|
1001
|
+
let removed = 0;
|
|
1002
|
+
for (const line of diffSection.split(`
|
|
1003
|
+
`)) {
|
|
1004
|
+
if (line.startsWith("+") && !line.startsWith("+++"))
|
|
1005
|
+
added++;
|
|
1006
|
+
if (line.startsWith("-") && !line.startsWith("---"))
|
|
1007
|
+
removed++;
|
|
1008
|
+
}
|
|
1009
|
+
return { added, removed };
|
|
1010
|
+
}
|
|
1011
|
+
function createCompactDiff(files, rawDiff, maxTotalChars = BATCH_CONFIG.MAX_COMPACT_PAYLOAD) {
|
|
1012
|
+
if (files.length === 0)
|
|
1013
|
+
return "";
|
|
1014
|
+
const diffSections = parseDiffByFile(rawDiff);
|
|
1015
|
+
const perFileBudget = Math.min(BATCH_CONFIG.COMPACT_PER_FILE_CHARS, Math.floor(maxTotalChars / files.length));
|
|
1016
|
+
const parts = [];
|
|
1017
|
+
for (const file of files) {
|
|
1018
|
+
const section = diffSections.get(file);
|
|
1019
|
+
if (section) {
|
|
1020
|
+
const stats = extractDiffStats(section);
|
|
1021
|
+
const header = `[${file}] (+${stats.added}/-${stats.removed})`;
|
|
1022
|
+
if (section.length <= perFileBudget) {
|
|
1023
|
+
parts.push(`${header}
|
|
1024
|
+
${section}`);
|
|
1025
|
+
} else {
|
|
1026
|
+
const availableForBody = perFileBudget - header.length - 20;
|
|
1027
|
+
if (availableForBody <= 0) {
|
|
1028
|
+
parts.push(header);
|
|
1029
|
+
} else {
|
|
1030
|
+
const truncated = section.slice(0, availableForBody);
|
|
1031
|
+
parts.push(`${header}
|
|
1032
|
+
${truncated}
|
|
1033
|
+
...(truncated)`);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
} else {
|
|
1037
|
+
parts.push(`[${file}] (new/binary file — no diff available)`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
const result = parts.join(`
|
|
1041
|
+
|
|
1042
|
+
`);
|
|
1043
|
+
return result.length > maxTotalChars ? `${result.slice(0, maxTotalChars - 15)}
|
|
1044
|
+
...(truncated)` : result;
|
|
1045
|
+
}
|
|
973
1046
|
async function checkCopilotAvailable() {
|
|
974
1047
|
try {
|
|
975
1048
|
const client = await getManagedClient();
|
|
@@ -1070,16 +1143,18 @@ function extractJson(raw) {
|
|
|
1070
1143
|
}
|
|
1071
1144
|
async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit") {
|
|
1072
1145
|
try {
|
|
1146
|
+
const isLarge = stagedFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
|
|
1073
1147
|
const multiFileHint = stagedFiles.length > 1 ? `
|
|
1074
1148
|
|
|
1075
1149
|
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.` : "";
|
|
1150
|
+
const diffContent = isLarge ? createCompactDiff(stagedFiles, diff) : diff.slice(0, 4000);
|
|
1076
1151
|
const userMessage = `Generate a commit message for these staged changes:
|
|
1077
1152
|
|
|
1078
|
-
Files: ${stagedFiles.join(", ")}
|
|
1153
|
+
Files (${stagedFiles.length}): ${stagedFiles.join(", ")}
|
|
1079
1154
|
|
|
1080
1155
|
Diff:
|
|
1081
|
-
${
|
|
1082
|
-
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
|
|
1156
|
+
${diffContent}${multiFileHint}`;
|
|
1157
|
+
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model, isLarge ? COPILOT_LONG_TIMEOUT_MS : COPILOT_TIMEOUT_MS);
|
|
1083
1158
|
return result?.trim() ?? null;
|
|
1084
1159
|
} catch {
|
|
1085
1160
|
return null;
|
|
@@ -1128,16 +1203,23 @@ ${conflictDiff.slice(0, 4000)}`;
|
|
|
1128
1203
|
}
|
|
1129
1204
|
}
|
|
1130
1205
|
async function generateCommitGroups(files, diffs, model, convention = "clean-commit") {
|
|
1206
|
+
const isLarge = files.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
|
|
1207
|
+
const diffContent = isLarge ? createCompactDiff(files, diffs) : diffs.slice(0, 6000);
|
|
1208
|
+
const largeHint = isLarge ? `
|
|
1209
|
+
|
|
1210
|
+
NOTE: This is a large changeset (${files.length} files). Compact diffs are provided for every file. Focus on creating well-organized logical groups.` : "";
|
|
1131
1211
|
const userMessage = `Group these changed files into logical atomic commits:
|
|
1132
1212
|
|
|
1133
1213
|
Files:
|
|
1134
1214
|
${files.join(`
|
|
1135
1215
|
`)}
|
|
1136
1216
|
|
|
1137
|
-
Diffs
|
|
1138
|
-
${
|
|
1217
|
+
Diffs:
|
|
1218
|
+
${diffContent}${largeHint}`;
|
|
1139
1219
|
const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
|
|
1140
1220
|
if (!result) {
|
|
1221
|
+
if (isLarge)
|
|
1222
|
+
return generateCommitGroupsInBatches(files, diffs, model, convention);
|
|
1141
1223
|
throw new Error("AI returned an empty response");
|
|
1142
1224
|
}
|
|
1143
1225
|
const cleaned = extractJson(result);
|
|
@@ -1145,10 +1227,14 @@ ${diffs.slice(0, 6000)}`;
|
|
|
1145
1227
|
try {
|
|
1146
1228
|
parsed = JSON.parse(cleaned);
|
|
1147
1229
|
} catch {
|
|
1230
|
+
if (isLarge)
|
|
1231
|
+
return generateCommitGroupsInBatches(files, diffs, model, convention);
|
|
1148
1232
|
throw new Error(`AI response is not valid JSON. Raw start: "${result.slice(0, 120)}..."`);
|
|
1149
1233
|
}
|
|
1150
1234
|
const groups = parsed;
|
|
1151
1235
|
if (!Array.isArray(groups) || groups.length === 0) {
|
|
1236
|
+
if (isLarge)
|
|
1237
|
+
return generateCommitGroupsInBatches(files, diffs, model, convention);
|
|
1152
1238
|
throw new Error("AI response was not a valid JSON array of commit groups");
|
|
1153
1239
|
}
|
|
1154
1240
|
for (const group of groups) {
|
|
@@ -1158,7 +1244,63 @@ ${diffs.slice(0, 6000)}`;
|
|
|
1158
1244
|
}
|
|
1159
1245
|
return groups;
|
|
1160
1246
|
}
|
|
1247
|
+
async function generateCommitGroupsInBatches(files, diffs, model, convention = "clean-commit") {
|
|
1248
|
+
const batchSize = BATCH_CONFIG.FALLBACK_BATCH_SIZE;
|
|
1249
|
+
const allGroups = [];
|
|
1250
|
+
const diffSections = parseDiffByFile(diffs);
|
|
1251
|
+
for (let i = 0;i < files.length; i += batchSize) {
|
|
1252
|
+
const batchFiles = files.slice(i, i + batchSize);
|
|
1253
|
+
const batchDiff = batchFiles.map((f) => diffSections.get(f) ?? "").filter(Boolean).join(`
|
|
1254
|
+
`);
|
|
1255
|
+
const batchDiffContent = batchFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD ? createCompactDiff(batchFiles, batchDiff) : batchDiff.slice(0, 6000);
|
|
1256
|
+
const batchNum = Math.floor(i / batchSize) + 1;
|
|
1257
|
+
const totalBatches = Math.ceil(files.length / batchSize);
|
|
1258
|
+
const userMessage = `Group these changed files into logical atomic commits:
|
|
1259
|
+
|
|
1260
|
+
Files:
|
|
1261
|
+
${batchFiles.join(`
|
|
1262
|
+
`)}
|
|
1263
|
+
|
|
1264
|
+
Diffs:
|
|
1265
|
+
${batchDiffContent}
|
|
1266
|
+
|
|
1267
|
+
NOTE: Processing batch ${batchNum}/${totalBatches} of a large changeset. Group only the files listed above.`;
|
|
1268
|
+
try {
|
|
1269
|
+
const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
|
|
1270
|
+
if (!result)
|
|
1271
|
+
continue;
|
|
1272
|
+
const cleaned = extractJson(result);
|
|
1273
|
+
const parsed = JSON.parse(cleaned);
|
|
1274
|
+
if (Array.isArray(parsed)) {
|
|
1275
|
+
for (const group of parsed) {
|
|
1276
|
+
if (Array.isArray(group.files) && typeof group.message === "string") {
|
|
1277
|
+
const batchFileSet = new Set(batchFiles);
|
|
1278
|
+
const filteredFiles = group.files.filter((f) => batchFileSet.has(f));
|
|
1279
|
+
if (filteredFiles.length > 0) {
|
|
1280
|
+
allGroups.push({ ...group, files: filteredFiles });
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
} catch {}
|
|
1286
|
+
}
|
|
1287
|
+
const groupedFiles = new Set(allGroups.flatMap((g) => g.files));
|
|
1288
|
+
const ungrouped = files.filter((f) => !groupedFiles.has(f));
|
|
1289
|
+
if (ungrouped.length > 0) {
|
|
1290
|
+
allGroups.push({
|
|
1291
|
+
files: ungrouped,
|
|
1292
|
+
message: `chore: update ${ungrouped.length} remaining file${ungrouped.length !== 1 ? "s" : ""}`
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
if (allGroups.length === 0) {
|
|
1296
|
+
throw new Error("AI could not group any files even with batch processing");
|
|
1297
|
+
}
|
|
1298
|
+
return allGroups;
|
|
1299
|
+
}
|
|
1161
1300
|
async function regenerateAllGroupMessages(groups, diffs, model, convention = "clean-commit") {
|
|
1301
|
+
const totalFiles = groups.reduce((sum, g) => sum + g.files.length, 0);
|
|
1302
|
+
const isLarge = totalFiles >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
|
|
1303
|
+
const diffContent = isLarge ? createCompactDiff(groups.flatMap((g) => g.files), diffs) : diffs.slice(0, 6000);
|
|
1162
1304
|
const groupSummary = groups.map((g, i) => `Group ${i + 1}: [${g.files.join(", ")}]`).join(`
|
|
1163
1305
|
`);
|
|
1164
1306
|
const userMessage = `Regenerate ONLY the commit messages for these pre-defined file groups. Do NOT change the file groupings.
|
|
@@ -1166,8 +1308,8 @@ async function regenerateAllGroupMessages(groups, diffs, model, convention = "cl
|
|
|
1166
1308
|
Groups:
|
|
1167
1309
|
${groupSummary}
|
|
1168
1310
|
|
|
1169
|
-
Diffs
|
|
1170
|
-
${
|
|
1311
|
+
Diffs:
|
|
1312
|
+
${diffContent}`;
|
|
1171
1313
|
const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
|
|
1172
1314
|
if (!result)
|
|
1173
1315
|
return groups;
|
|
@@ -1186,12 +1328,14 @@ ${diffs.slice(0, 6000)}`;
|
|
|
1186
1328
|
}
|
|
1187
1329
|
async function regenerateGroupMessage(files, diffs, model, convention = "clean-commit") {
|
|
1188
1330
|
try {
|
|
1331
|
+
const isLarge = files.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
|
|
1332
|
+
const diffContent = isLarge ? createCompactDiff(files, diffs) : diffs.slice(0, 4000);
|
|
1189
1333
|
const userMessage = `Generate a single commit message for these files:
|
|
1190
1334
|
|
|
1191
1335
|
Files: ${files.join(", ")}
|
|
1192
1336
|
|
|
1193
1337
|
Diff:
|
|
1194
|
-
${
|
|
1338
|
+
${diffContent}`;
|
|
1195
1339
|
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
|
|
1196
1340
|
return result?.trim() ?? null;
|
|
1197
1341
|
} catch {
|
|
@@ -1749,7 +1893,8 @@ ${pc6.bold("Changed files:")}`);
|
|
|
1749
1893
|
warn(`AI unavailable: ${copilotError}`);
|
|
1750
1894
|
warn("Falling back to manual commit message entry.");
|
|
1751
1895
|
} else {
|
|
1752
|
-
const
|
|
1896
|
+
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...";
|
|
1897
|
+
const spinner = createSpinner(spinnerMsg);
|
|
1753
1898
|
commitMessage = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
|
|
1754
1899
|
if (commitMessage) {
|
|
1755
1900
|
spinner.success("AI commit message generated.");
|
|
@@ -1840,7 +1985,7 @@ ${pc6.bold("Changed files:")}`);
|
|
|
1840
1985
|
for (const f of changedFiles) {
|
|
1841
1986
|
console.log(` ${pc6.dim("•")} ${f}`);
|
|
1842
1987
|
}
|
|
1843
|
-
const spinner = createSpinner(`Asking AI to group ${changedFiles.length} file(s) into logical commits...`);
|
|
1988
|
+
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...`);
|
|
1844
1989
|
const diffs = await getFullDiffForFiles(changedFiles);
|
|
1845
1990
|
if (!diffs.trim()) {
|
|
1846
1991
|
spinner.stop();
|
|
@@ -2018,8 +2163,8 @@ import pc7 from "picocolors";
|
|
|
2018
2163
|
// package.json
|
|
2019
2164
|
var package_default = {
|
|
2020
2165
|
name: "contribute-now",
|
|
2021
|
-
version: "0.4.0-dev.
|
|
2022
|
-
description: "
|
|
2166
|
+
version: "0.4.0-dev.e93c162",
|
|
2167
|
+
description: "Developer CLI that automates git workflows — branching, syncing, committing, and PRs — with multi-workflow and commit convention support.",
|
|
2023
2168
|
type: "module",
|
|
2024
2169
|
bin: {
|
|
2025
2170
|
contrib: "dist/index.js",
|
|
@@ -2705,6 +2850,9 @@ async function shouldContinueSetupWithExistingConfig(options) {
|
|
|
2705
2850
|
onWarn("Found .contributerc.json but it appears invalid.");
|
|
2706
2851
|
const shouldContinue = await confirm2("Continue setup and overwrite invalid config?");
|
|
2707
2852
|
if (!shouldContinue) {
|
|
2853
|
+
if (ensureIgnored()) {
|
|
2854
|
+
onInfo("Added .contributerc.json to .gitignore to avoid committing personal config.");
|
|
2855
|
+
}
|
|
2708
2856
|
onInfo("Keeping existing file. Run setup again when ready to repair it.");
|
|
2709
2857
|
return false;
|
|
2710
2858
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "contribute-now",
|
|
3
|
-
"version": "0.4.0-dev.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.0-dev.e93c162",
|
|
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": {
|
|
7
7
|
"contrib": "dist/index.js",
|