contribute-now 0.1.2 → 0.2.0-dev.70284d0
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 +162 -133
- package/dist/index.js +536 -210
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { defineCommand as
|
|
4
|
+
import { defineCommand as defineCommand11, runMain } from "citty";
|
|
5
5
|
|
|
6
6
|
// src/commands/clean.ts
|
|
7
7
|
import { defineCommand } from "citty";
|
|
@@ -44,12 +44,14 @@ function isGitignored(cwd = process.cwd()) {
|
|
|
44
44
|
}
|
|
45
45
|
function getDefaultConfig() {
|
|
46
46
|
return {
|
|
47
|
+
workflow: "clean-flow",
|
|
47
48
|
role: "contributor",
|
|
48
49
|
mainBranch: "main",
|
|
49
50
|
devBranch: "dev",
|
|
50
51
|
upstream: "upstream",
|
|
51
52
|
origin: "origin",
|
|
52
|
-
branchPrefixes: ["feature", "fix", "docs", "chore", "test", "refactor"]
|
|
53
|
+
branchPrefixes: ["feature", "fix", "docs", "chore", "test", "refactor"],
|
|
54
|
+
commitConvention: "clean-commit"
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -167,8 +169,12 @@ async function createBranch(branch, from) {
|
|
|
167
169
|
async function resetHard(ref) {
|
|
168
170
|
return run(["reset", "--hard", ref]);
|
|
169
171
|
}
|
|
170
|
-
async function
|
|
171
|
-
|
|
172
|
+
async function updateLocalBranch(branch, target) {
|
|
173
|
+
const current = await getCurrentBranch();
|
|
174
|
+
if (current === branch) {
|
|
175
|
+
return resetHard(target);
|
|
176
|
+
}
|
|
177
|
+
return run(["branch", "-f", branch, target]);
|
|
172
178
|
}
|
|
173
179
|
async function pushSetUpstream(remote, branch) {
|
|
174
180
|
return run(["push", "-u", remote, branch]);
|
|
@@ -236,6 +242,9 @@ async function getLog(base, head) {
|
|
|
236
242
|
return stdout.trim().split(`
|
|
237
243
|
`).filter(Boolean);
|
|
238
244
|
}
|
|
245
|
+
async function pullBranch(remote, branch) {
|
|
246
|
+
return run(["pull", remote, branch]);
|
|
247
|
+
}
|
|
239
248
|
|
|
240
249
|
// src/utils/logger.ts
|
|
241
250
|
import { LogEngine, LogMode } from "@wgtechlabs/log-engine";
|
|
@@ -265,6 +274,53 @@ function heading(msg) {
|
|
|
265
274
|
${pc2.bold(msg)}`);
|
|
266
275
|
}
|
|
267
276
|
|
|
277
|
+
// src/utils/workflow.ts
|
|
278
|
+
var WORKFLOW_DESCRIPTIONS = {
|
|
279
|
+
"clean-flow": "Clean Flow — main + dev, squash features into dev, merge dev into main",
|
|
280
|
+
"github-flow": "GitHub Flow — main + feature branches, squash/merge into main",
|
|
281
|
+
"git-flow": "Git Flow — main + develop + release + hotfix branches"
|
|
282
|
+
};
|
|
283
|
+
function getBaseBranch(config) {
|
|
284
|
+
switch (config.workflow) {
|
|
285
|
+
case "clean-flow":
|
|
286
|
+
case "git-flow":
|
|
287
|
+
return config.devBranch ?? "dev";
|
|
288
|
+
case "github-flow":
|
|
289
|
+
return config.mainBranch;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function hasDevBranch(workflow) {
|
|
293
|
+
return workflow === "clean-flow" || workflow === "git-flow";
|
|
294
|
+
}
|
|
295
|
+
function getSyncSource(config) {
|
|
296
|
+
const { workflow, role, mainBranch, origin, upstream } = config;
|
|
297
|
+
const devBranch = config.devBranch ?? "dev";
|
|
298
|
+
switch (workflow) {
|
|
299
|
+
case "clean-flow":
|
|
300
|
+
if (role === "contributor") {
|
|
301
|
+
return { remote: upstream, ref: `${upstream}/${devBranch}`, strategy: "pull" };
|
|
302
|
+
}
|
|
303
|
+
return { remote: origin, ref: `${origin}/${devBranch}`, strategy: "pull" };
|
|
304
|
+
case "github-flow":
|
|
305
|
+
if (role === "contributor") {
|
|
306
|
+
return { remote: upstream, ref: `${upstream}/${mainBranch}`, strategy: "pull" };
|
|
307
|
+
}
|
|
308
|
+
return { remote: origin, ref: `${origin}/${mainBranch}`, strategy: "pull" };
|
|
309
|
+
case "git-flow":
|
|
310
|
+
if (role === "contributor") {
|
|
311
|
+
return { remote: upstream, ref: `${upstream}/${devBranch}`, strategy: "pull" };
|
|
312
|
+
}
|
|
313
|
+
return { remote: origin, ref: `${origin}/${devBranch}`, strategy: "pull" };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function getProtectedBranches(config) {
|
|
317
|
+
const branches = [config.mainBranch];
|
|
318
|
+
if (hasDevBranch(config.workflow) && config.devBranch) {
|
|
319
|
+
branches.push(config.devBranch);
|
|
320
|
+
}
|
|
321
|
+
return branches;
|
|
322
|
+
}
|
|
323
|
+
|
|
268
324
|
// src/commands/clean.ts
|
|
269
325
|
var clean_default = defineCommand({
|
|
270
326
|
meta: {
|
|
@@ -289,12 +345,13 @@ var clean_default = defineCommand({
|
|
|
289
345
|
error("No .contributerc.json found. Run `contrib setup` first.");
|
|
290
346
|
process.exit(1);
|
|
291
347
|
}
|
|
292
|
-
const {
|
|
348
|
+
const { origin } = config;
|
|
349
|
+
const baseBranch = getBaseBranch(config);
|
|
293
350
|
const currentBranch = await getCurrentBranch();
|
|
294
351
|
heading("\uD83E\uDDF9 contrib clean");
|
|
295
|
-
const mergedBranches = await getMergedBranches(
|
|
296
|
-
const
|
|
297
|
-
const candidates = mergedBranches.filter((b) => !
|
|
352
|
+
const mergedBranches = await getMergedBranches(baseBranch);
|
|
353
|
+
const protectedBranches = new Set([...getProtectedBranches(config), currentBranch ?? ""]);
|
|
354
|
+
const candidates = mergedBranches.filter((b) => !protectedBranches.has(b));
|
|
298
355
|
if (candidates.length === 0) {
|
|
299
356
|
info("No merged branches to clean up.");
|
|
300
357
|
} else {
|
|
@@ -332,10 +389,86 @@ ${pc3.bold("Branches to delete:")}`);
|
|
|
332
389
|
import { defineCommand as defineCommand2 } from "citty";
|
|
333
390
|
import pc4 from "picocolors";
|
|
334
391
|
|
|
392
|
+
// src/utils/convention.ts
|
|
393
|
+
var CLEAN_COMMIT_PATTERN = /^(📦|🔧|🗑\uFE0F?|🔒|⚙\uFE0F?|☕|🧪|📖|🚀) (new|update|remove|security|setup|chore|test|docs|release)(!?)( \([a-zA-Z0-9][a-zA-Z0-9-]*\))?: .{1,72}$/u;
|
|
394
|
+
var CONVENTIONAL_COMMIT_PATTERN = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(!?)(\([a-zA-Z0-9][a-zA-Z0-9._-]*\))?: .{1,72}$/;
|
|
395
|
+
var CONVENTION_LABELS = {
|
|
396
|
+
conventional: "Conventional Commits",
|
|
397
|
+
"clean-commit": "Clean Commit (by WGTech Labs)",
|
|
398
|
+
none: "No convention"
|
|
399
|
+
};
|
|
400
|
+
var CONVENTION_DESCRIPTIONS = {
|
|
401
|
+
conventional: "Conventional Commits — feat: | fix: | docs: | chore: etc. (conventionalcommits.org)",
|
|
402
|
+
"clean-commit": "Clean Commit — \uD83D\uDCE6 new: | \uD83D\uDD27 update: | \uD83D\uDDD1️ remove: etc. (by WGTech Labs)",
|
|
403
|
+
none: "No commit convention enforcement"
|
|
404
|
+
};
|
|
405
|
+
var CONVENTION_FORMAT_HINTS = {
|
|
406
|
+
conventional: [
|
|
407
|
+
"Format: <type>[!][(<scope>)]: <description>",
|
|
408
|
+
"Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert",
|
|
409
|
+
"Examples: feat: add login page | fix(auth): resolve token expiry | docs: update README"
|
|
410
|
+
],
|
|
411
|
+
"clean-commit": [
|
|
412
|
+
"Format: <emoji> <type>[!][(<scope>)]: <description>",
|
|
413
|
+
"Types: \uD83D\uDCE6 new | \uD83D\uDD27 update | \uD83D\uDDD1️ remove | \uD83D\uDD12 security | ⚙️ setup | ☕ chore | \uD83E\uDDEA test | \uD83D\uDCD6 docs | \uD83D\uDE80 release",
|
|
414
|
+
"Examples: \uD83D\uDCE6 new: user auth | \uD83D\uDD27 update (api): improve errors | ⚙️ setup (ci): add workflow"
|
|
415
|
+
]
|
|
416
|
+
};
|
|
417
|
+
function validateCommitMessage(message, convention) {
|
|
418
|
+
if (convention === "none")
|
|
419
|
+
return true;
|
|
420
|
+
if (convention === "clean-commit")
|
|
421
|
+
return CLEAN_COMMIT_PATTERN.test(message);
|
|
422
|
+
if (convention === "conventional")
|
|
423
|
+
return CONVENTIONAL_COMMIT_PATTERN.test(message);
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
function getValidationError(convention) {
|
|
427
|
+
if (convention === "none")
|
|
428
|
+
return [];
|
|
429
|
+
return [
|
|
430
|
+
`Commit message does not follow ${CONVENTION_LABELS[convention]} format.`,
|
|
431
|
+
...CONVENTION_FORMAT_HINTS[convention]
|
|
432
|
+
];
|
|
433
|
+
}
|
|
434
|
+
|
|
335
435
|
// src/utils/copilot.ts
|
|
336
436
|
import { CopilotClient } from "@github/copilot-sdk";
|
|
337
|
-
var
|
|
338
|
-
<
|
|
437
|
+
var CONVENTIONAL_COMMIT_SYSTEM_PROMPT = `You are a git commit message generator. Generate a Conventional Commit message following this exact format:
|
|
438
|
+
<type>[!][(<scope>)]: <description>
|
|
439
|
+
|
|
440
|
+
Types:
|
|
441
|
+
feat – a new feature
|
|
442
|
+
fix – a bug fix
|
|
443
|
+
docs – documentation only changes
|
|
444
|
+
style – changes that do not affect code meaning (whitespace, formatting)
|
|
445
|
+
refactor – code change that neither fixes a bug nor adds a feature
|
|
446
|
+
perf – performance improvement
|
|
447
|
+
test – adding or correcting tests
|
|
448
|
+
build – changes to the build system or external dependencies
|
|
449
|
+
ci – changes to CI configuration files and scripts
|
|
450
|
+
chore – other changes that don't modify src or test files
|
|
451
|
+
revert – reverts a previous commit
|
|
452
|
+
|
|
453
|
+
Rules:
|
|
454
|
+
- Breaking change (!) only for: feat, fix, refactor, perf
|
|
455
|
+
- Description: concise, imperative mood, max 72 chars, lowercase start
|
|
456
|
+
- Scope: optional, camelCase or kebab-case component name
|
|
457
|
+
- Return ONLY the commit message line, nothing else
|
|
458
|
+
|
|
459
|
+
Examples:
|
|
460
|
+
feat: add user authentication system
|
|
461
|
+
fix(auth): resolve token expiry issue
|
|
462
|
+
docs: update contributing guidelines
|
|
463
|
+
feat!: redesign authentication API`;
|
|
464
|
+
var CLEAN_COMMIT_SYSTEM_PROMPT = `You are a git commit message generator. Generate a Clean Commit message following this EXACT format:
|
|
465
|
+
<emoji> <type>[!][ (<scope>)]: <description>
|
|
466
|
+
|
|
467
|
+
CRITICAL spacing rules (must follow exactly):
|
|
468
|
+
- There MUST be a space between the emoji and the type
|
|
469
|
+
- If a scope is used, there MUST be a space before the opening parenthesis
|
|
470
|
+
- There MUST be a colon and a space after the type or scope before the description
|
|
471
|
+
- Pattern: EMOJI SPACE TYPE SPACE OPENPAREN SCOPE CLOSEPAREN COLON SPACE DESCRIPTION
|
|
339
472
|
|
|
340
473
|
Emoji and type table:
|
|
341
474
|
\uD83D\uDCE6 new – new features, files, or capabilities
|
|
@@ -350,15 +483,21 @@ Emoji and type table:
|
|
|
350
483
|
|
|
351
484
|
Rules:
|
|
352
485
|
- Breaking change (!) only for: new, update, remove, security
|
|
353
|
-
- Description: concise, imperative mood, max 72 chars
|
|
486
|
+
- Description: concise, imperative mood, max 72 chars, lowercase start
|
|
354
487
|
- Scope: optional, camelCase or kebab-case component name
|
|
355
488
|
- Return ONLY the commit message line, nothing else
|
|
356
489
|
|
|
357
|
-
|
|
358
|
-
\uD83D\uDCE6 new: user authentication system
|
|
490
|
+
Correct examples:
|
|
491
|
+
\uD83D\uDCE6 new: add user authentication system
|
|
359
492
|
\uD83D\uDD27 update (api): improve error handling
|
|
360
493
|
⚙️ setup (ci): configure github actions workflow
|
|
361
|
-
\uD83D\uDCE6 new!:
|
|
494
|
+
\uD83D\uDCE6 new!: redesign authentication system
|
|
495
|
+
\uD83D\uDDD1️ remove (deps): drop unused lodash dependency
|
|
496
|
+
|
|
497
|
+
WRONG (never do this):
|
|
498
|
+
⚙️setup(ci): ... ← missing spaces
|
|
499
|
+
\uD83D\uDCE6new: ... ← missing space after emoji
|
|
500
|
+
\uD83D\uDD27 update(api): ... ← missing space before scope`;
|
|
362
501
|
var BRANCH_NAME_SYSTEM_PROMPT = `You are a git branch name generator. Convert natural language descriptions into proper git branch names.
|
|
363
502
|
|
|
364
503
|
Format: <prefix>/<kebab-case-name>
|
|
@@ -392,8 +531,21 @@ Rules:
|
|
|
392
531
|
- Suggest the most likely correct resolution strategy
|
|
393
532
|
- Never auto-resolve — provide guidance only
|
|
394
533
|
- Be concise and actionable`;
|
|
534
|
+
function suppressSubprocessWarnings() {
|
|
535
|
+
const prev = process.env.NODE_NO_WARNINGS;
|
|
536
|
+
process.env.NODE_NO_WARNINGS = "1";
|
|
537
|
+
return prev;
|
|
538
|
+
}
|
|
539
|
+
function restoreWarnings(prev) {
|
|
540
|
+
if (prev === undefined) {
|
|
541
|
+
delete process.env.NODE_NO_WARNINGS;
|
|
542
|
+
} else {
|
|
543
|
+
process.env.NODE_NO_WARNINGS = prev;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
395
546
|
async function checkCopilotAvailable() {
|
|
396
547
|
let client = null;
|
|
548
|
+
const prev = suppressSubprocessWarnings();
|
|
397
549
|
try {
|
|
398
550
|
client = new CopilotClient;
|
|
399
551
|
await client.start();
|
|
@@ -416,6 +568,7 @@ async function checkCopilotAvailable() {
|
|
|
416
568
|
}
|
|
417
569
|
return `Copilot health check failed: ${msg}`;
|
|
418
570
|
} finally {
|
|
571
|
+
restoreWarnings(prev);
|
|
419
572
|
try {
|
|
420
573
|
await client.stop();
|
|
421
574
|
} catch {}
|
|
@@ -423,17 +576,18 @@ async function checkCopilotAvailable() {
|
|
|
423
576
|
return null;
|
|
424
577
|
}
|
|
425
578
|
async function callCopilot(systemMessage, userMessage, model) {
|
|
579
|
+
const prev = suppressSubprocessWarnings();
|
|
426
580
|
const client = new CopilotClient;
|
|
427
581
|
await client.start();
|
|
428
582
|
try {
|
|
429
583
|
const sessionConfig = {
|
|
430
|
-
systemMessage: { content: systemMessage }
|
|
584
|
+
systemMessage: { mode: "replace", content: systemMessage }
|
|
431
585
|
};
|
|
432
586
|
if (model)
|
|
433
587
|
sessionConfig.model = model;
|
|
434
588
|
const session = await client.createSession(sessionConfig);
|
|
435
589
|
try {
|
|
436
|
-
const response = await session.sendAndWait({
|
|
590
|
+
const response = await session.sendAndWait({ prompt: userMessage });
|
|
437
591
|
if (!response?.data?.content)
|
|
438
592
|
return null;
|
|
439
593
|
return response.data.content;
|
|
@@ -441,10 +595,16 @@ async function callCopilot(systemMessage, userMessage, model) {
|
|
|
441
595
|
await session.destroy();
|
|
442
596
|
}
|
|
443
597
|
} finally {
|
|
598
|
+
restoreWarnings(prev);
|
|
444
599
|
await client.stop();
|
|
445
600
|
}
|
|
446
601
|
}
|
|
447
|
-
|
|
602
|
+
function getCommitSystemPrompt(convention) {
|
|
603
|
+
if (convention === "conventional")
|
|
604
|
+
return CONVENTIONAL_COMMIT_SYSTEM_PROMPT;
|
|
605
|
+
return CLEAN_COMMIT_SYSTEM_PROMPT;
|
|
606
|
+
}
|
|
607
|
+
async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit") {
|
|
448
608
|
try {
|
|
449
609
|
const userMessage = `Generate a commit message for these staged changes:
|
|
450
610
|
|
|
@@ -452,7 +612,7 @@ Files: ${stagedFiles.join(", ")}
|
|
|
452
612
|
|
|
453
613
|
Diff:
|
|
454
614
|
${diff.slice(0, 4000)}`;
|
|
455
|
-
const result = await callCopilot(
|
|
615
|
+
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
|
|
456
616
|
return result?.trim() ?? null;
|
|
457
617
|
} catch {
|
|
458
618
|
return null;
|
|
@@ -498,14 +658,10 @@ ${conflictDiff.slice(0, 4000)}`;
|
|
|
498
658
|
}
|
|
499
659
|
|
|
500
660
|
// src/commands/commit.ts
|
|
501
|
-
var CLEAN_COMMIT_PATTERN = /^(📦|🔧|🗑\uFE0F?|🔒|⚙\uFE0F?|☕|🧪|📖|🚀) (new|update|remove|security|setup|chore|test|docs|release)(!?)( \([a-zA-Z0-9][a-zA-Z0-9-]*\))?: .{1,72}$/u;
|
|
502
|
-
function validateCleanCommit(msg) {
|
|
503
|
-
return CLEAN_COMMIT_PATTERN.test(msg);
|
|
504
|
-
}
|
|
505
661
|
var commit_default = defineCommand2({
|
|
506
662
|
meta: {
|
|
507
663
|
name: "commit",
|
|
508
|
-
description: "Stage changes and create a
|
|
664
|
+
description: "Stage changes and create a commit message (AI-powered)"
|
|
509
665
|
},
|
|
510
666
|
args: {
|
|
511
667
|
model: {
|
|
@@ -556,7 +712,7 @@ ${pc4.bold("Changed files:")}`);
|
|
|
556
712
|
} else {
|
|
557
713
|
info("Generating commit message with AI...");
|
|
558
714
|
const diff = await getStagedDiff();
|
|
559
|
-
commitMessage = await generateCommitMessage(diff, stagedFiles, args.model);
|
|
715
|
+
commitMessage = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
|
|
560
716
|
if (commitMessage) {
|
|
561
717
|
console.log(`
|
|
562
718
|
${pc4.dim("AI suggestion:")} ${pc4.bold(pc4.cyan(commitMessage))}`);
|
|
@@ -580,7 +736,7 @@ ${pc4.bold("Changed files:")}`);
|
|
|
580
736
|
} else if (action === "Regenerate") {
|
|
581
737
|
info("Regenerating...");
|
|
582
738
|
const diff = await getStagedDiff();
|
|
583
|
-
const regen = await generateCommitMessage(diff, stagedFiles, args.model);
|
|
739
|
+
const regen = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
|
|
584
740
|
if (regen) {
|
|
585
741
|
console.log(`
|
|
586
742
|
${pc4.dim("AI suggestion:")} ${pc4.bold(pc4.cyan(regen))}`);
|
|
@@ -594,19 +750,25 @@ ${pc4.bold("Changed files:")}`);
|
|
|
594
750
|
finalMessage = await inputPrompt("Enter commit message");
|
|
595
751
|
}
|
|
596
752
|
} else {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
753
|
+
const convention2 = config.commitConvention;
|
|
754
|
+
if (convention2 !== "none") {
|
|
755
|
+
console.log();
|
|
756
|
+
for (const hint of CONVENTION_FORMAT_HINTS[convention2]) {
|
|
757
|
+
console.log(pc4.dim(hint));
|
|
758
|
+
}
|
|
759
|
+
console.log();
|
|
760
|
+
}
|
|
601
761
|
finalMessage = await inputPrompt("Enter commit message");
|
|
602
762
|
}
|
|
603
763
|
if (!finalMessage) {
|
|
604
764
|
error("No commit message provided.");
|
|
605
765
|
process.exit(1);
|
|
606
766
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
767
|
+
const convention = config.commitConvention;
|
|
768
|
+
if (!validateCommitMessage(finalMessage, convention)) {
|
|
769
|
+
for (const line of getValidationError(convention)) {
|
|
770
|
+
warn(line);
|
|
771
|
+
}
|
|
610
772
|
const proceed = await confirmPrompt("Commit anyway?");
|
|
611
773
|
if (!proceed)
|
|
612
774
|
process.exit(1);
|
|
@@ -620,9 +782,117 @@ ${pc4.bold("Changed files:")}`);
|
|
|
620
782
|
}
|
|
621
783
|
});
|
|
622
784
|
|
|
623
|
-
// src/commands/
|
|
785
|
+
// src/commands/hook.ts
|
|
786
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
787
|
+
import { join as join2 } from "node:path";
|
|
624
788
|
import { defineCommand as defineCommand3 } from "citty";
|
|
625
789
|
import pc5 from "picocolors";
|
|
790
|
+
var HOOK_MARKER = "# managed by contribute-now";
|
|
791
|
+
function getHooksDir(cwd = process.cwd()) {
|
|
792
|
+
return join2(cwd, ".git", "hooks");
|
|
793
|
+
}
|
|
794
|
+
function getHookPath(cwd = process.cwd()) {
|
|
795
|
+
return join2(getHooksDir(cwd), "commit-msg");
|
|
796
|
+
}
|
|
797
|
+
function generateHookScript() {
|
|
798
|
+
return `#!/bin/sh
|
|
799
|
+
${HOOK_MARKER}
|
|
800
|
+
# Validates commit messages against your configured convention.
|
|
801
|
+
# Install: contrib hook install
|
|
802
|
+
# Uninstall: contrib hook uninstall
|
|
803
|
+
|
|
804
|
+
commit_msg_file="$1"
|
|
805
|
+
commit_msg=$(head -1 "$commit_msg_file")
|
|
806
|
+
|
|
807
|
+
# Skip merge commits and fixup/squash commits
|
|
808
|
+
case "$commit_msg" in
|
|
809
|
+
Merge\\ *|fixup!*|squash!*|amend!*) exit 0 ;;
|
|
810
|
+
esac
|
|
811
|
+
|
|
812
|
+
# Validate using contrib CLI
|
|
813
|
+
npx contrib validate "$commit_msg"
|
|
814
|
+
`;
|
|
815
|
+
}
|
|
816
|
+
var hook_default = defineCommand3({
|
|
817
|
+
meta: {
|
|
818
|
+
name: "hook",
|
|
819
|
+
description: "Install or uninstall the commit-msg git hook"
|
|
820
|
+
},
|
|
821
|
+
args: {
|
|
822
|
+
action: {
|
|
823
|
+
type: "positional",
|
|
824
|
+
description: "Action to perform: install or uninstall",
|
|
825
|
+
required: true
|
|
826
|
+
}
|
|
827
|
+
},
|
|
828
|
+
async run({ args }) {
|
|
829
|
+
if (!await isGitRepo()) {
|
|
830
|
+
error("Not inside a git repository.");
|
|
831
|
+
process.exit(1);
|
|
832
|
+
}
|
|
833
|
+
const action = args.action;
|
|
834
|
+
if (action !== "install" && action !== "uninstall") {
|
|
835
|
+
error(`Unknown action "${action}". Use "install" or "uninstall".`);
|
|
836
|
+
process.exit(1);
|
|
837
|
+
}
|
|
838
|
+
if (action === "install") {
|
|
839
|
+
await installHook();
|
|
840
|
+
} else {
|
|
841
|
+
await uninstallHook();
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
async function installHook() {
|
|
846
|
+
heading("\uD83E\uDE9D hook install");
|
|
847
|
+
const config = readConfig();
|
|
848
|
+
if (!config) {
|
|
849
|
+
error("No .contributerc.json found. Run `contrib setup` first.");
|
|
850
|
+
process.exit(1);
|
|
851
|
+
}
|
|
852
|
+
if (config.commitConvention === "none") {
|
|
853
|
+
warn('Commit convention is set to "none". No hook to install.');
|
|
854
|
+
info("Change your convention with `contrib setup` first.");
|
|
855
|
+
process.exit(0);
|
|
856
|
+
}
|
|
857
|
+
const hookPath = getHookPath();
|
|
858
|
+
const hooksDir = getHooksDir();
|
|
859
|
+
if (existsSync2(hookPath)) {
|
|
860
|
+
const existing = readFileSync2(hookPath, "utf-8");
|
|
861
|
+
if (!existing.includes(HOOK_MARKER)) {
|
|
862
|
+
error("A commit-msg hook already exists and was not installed by contribute-now.");
|
|
863
|
+
warn(`Path: ${hookPath}`);
|
|
864
|
+
warn("Remove it manually or back it up before installing.");
|
|
865
|
+
process.exit(1);
|
|
866
|
+
}
|
|
867
|
+
info("Updating existing contribute-now hook...");
|
|
868
|
+
}
|
|
869
|
+
if (!existsSync2(hooksDir)) {
|
|
870
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
871
|
+
}
|
|
872
|
+
writeFileSync2(hookPath, generateHookScript(), { mode: 493 });
|
|
873
|
+
success(`commit-msg hook installed.`);
|
|
874
|
+
info(`Convention: ${pc5.bold(CONVENTION_LABELS[config.commitConvention])}`);
|
|
875
|
+
info(`Path: ${pc5.dim(hookPath)}`);
|
|
876
|
+
}
|
|
877
|
+
async function uninstallHook() {
|
|
878
|
+
heading("\uD83E\uDE9D hook uninstall");
|
|
879
|
+
const hookPath = getHookPath();
|
|
880
|
+
if (!existsSync2(hookPath)) {
|
|
881
|
+
info("No commit-msg hook found. Nothing to uninstall.");
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
const content = readFileSync2(hookPath, "utf-8");
|
|
885
|
+
if (!content.includes(HOOK_MARKER)) {
|
|
886
|
+
error("The commit-msg hook was not installed by contribute-now. Leaving it untouched.");
|
|
887
|
+
process.exit(1);
|
|
888
|
+
}
|
|
889
|
+
rmSync(hookPath);
|
|
890
|
+
success("commit-msg hook removed.");
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// src/commands/setup.ts
|
|
894
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
895
|
+
import pc6 from "picocolors";
|
|
626
896
|
|
|
627
897
|
// src/utils/gh.ts
|
|
628
898
|
import { execFile as execFileCb2 } from "node:child_process";
|
|
@@ -735,7 +1005,7 @@ async function getRepoInfoFromRemote(remote = "origin") {
|
|
|
735
1005
|
}
|
|
736
1006
|
|
|
737
1007
|
// src/commands/setup.ts
|
|
738
|
-
var setup_default =
|
|
1008
|
+
var setup_default = defineCommand4({
|
|
739
1009
|
meta: {
|
|
740
1010
|
name: "setup",
|
|
741
1011
|
description: "Initialize contribute-now config for this repo (.contributerc.json)"
|
|
@@ -746,6 +1016,27 @@ var setup_default = defineCommand3({
|
|
|
746
1016
|
process.exit(1);
|
|
747
1017
|
}
|
|
748
1018
|
heading("\uD83D\uDD27 contribute-now setup");
|
|
1019
|
+
const workflowChoice = await selectPrompt("Which git workflow does this project use?", [
|
|
1020
|
+
"Clean Flow — main + dev, squash features into dev, merge dev into main (recommended)",
|
|
1021
|
+
"GitHub Flow — main + feature branches, squash/merge into main",
|
|
1022
|
+
"Git Flow — main + develop + release + hotfix branches"
|
|
1023
|
+
]);
|
|
1024
|
+
let workflow = "clean-flow";
|
|
1025
|
+
if (workflowChoice.startsWith("GitHub"))
|
|
1026
|
+
workflow = "github-flow";
|
|
1027
|
+
else if (workflowChoice.startsWith("Git Flow"))
|
|
1028
|
+
workflow = "git-flow";
|
|
1029
|
+
info(`Workflow: ${pc6.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
|
|
1030
|
+
const conventionChoice = await selectPrompt("Which commit convention should this project use?", [
|
|
1031
|
+
`${CONVENTION_DESCRIPTIONS["clean-commit"]} (recommended)`,
|
|
1032
|
+
CONVENTION_DESCRIPTIONS.conventional,
|
|
1033
|
+
CONVENTION_DESCRIPTIONS.none
|
|
1034
|
+
]);
|
|
1035
|
+
let commitConvention = "clean-commit";
|
|
1036
|
+
if (conventionChoice.includes("Conventional Commits"))
|
|
1037
|
+
commitConvention = "conventional";
|
|
1038
|
+
else if (conventionChoice.includes("No commit"))
|
|
1039
|
+
commitConvention = "none";
|
|
749
1040
|
const remotes = await getRemotes();
|
|
750
1041
|
if (remotes.length === 0) {
|
|
751
1042
|
error("No git remotes found. Add a remote first (e.g., git remote add origin <url>).");
|
|
@@ -788,8 +1079,8 @@ var setup_default = defineCommand3({
|
|
|
788
1079
|
detectedRole = roleChoice;
|
|
789
1080
|
detectionSource = "user selection";
|
|
790
1081
|
} else {
|
|
791
|
-
info(`Detected role: ${
|
|
792
|
-
const confirmed = await confirmPrompt(`Role detected as ${
|
|
1082
|
+
info(`Detected role: ${pc6.bold(detectedRole)} (via ${detectionSource})`);
|
|
1083
|
+
const confirmed = await confirmPrompt(`Role detected as ${pc6.bold(detectedRole)}. Is this correct?`);
|
|
793
1084
|
if (!confirmed) {
|
|
794
1085
|
const roleChoice = await selectPrompt("Select your role:", ["maintainer", "contributor"]);
|
|
795
1086
|
detectedRole = roleChoice;
|
|
@@ -797,7 +1088,11 @@ var setup_default = defineCommand3({
|
|
|
797
1088
|
}
|
|
798
1089
|
const defaultConfig = getDefaultConfig();
|
|
799
1090
|
const mainBranch = await inputPrompt("Main branch name", defaultConfig.mainBranch);
|
|
800
|
-
|
|
1091
|
+
let devBranch;
|
|
1092
|
+
if (hasDevBranch(workflow)) {
|
|
1093
|
+
const defaultDev = workflow === "git-flow" ? "develop" : "dev";
|
|
1094
|
+
devBranch = await inputPrompt("Dev/develop branch name", defaultDev);
|
|
1095
|
+
}
|
|
801
1096
|
const originRemote = await inputPrompt("Origin remote name", defaultConfig.origin);
|
|
802
1097
|
let upstreamRemote = defaultConfig.upstream;
|
|
803
1098
|
if (detectedRole === "contributor") {
|
|
@@ -814,12 +1109,14 @@ var setup_default = defineCommand3({
|
|
|
814
1109
|
}
|
|
815
1110
|
}
|
|
816
1111
|
const config = {
|
|
1112
|
+
workflow,
|
|
817
1113
|
role: detectedRole,
|
|
818
1114
|
mainBranch,
|
|
819
|
-
devBranch,
|
|
1115
|
+
...devBranch ? { devBranch } : {},
|
|
820
1116
|
upstream: upstreamRemote,
|
|
821
1117
|
origin: originRemote,
|
|
822
|
-
branchPrefixes: defaultConfig.branchPrefixes
|
|
1118
|
+
branchPrefixes: defaultConfig.branchPrefixes,
|
|
1119
|
+
commitConvention
|
|
823
1120
|
};
|
|
824
1121
|
writeConfig(config);
|
|
825
1122
|
success(`✅ Config written to .contributerc.json`);
|
|
@@ -828,15 +1125,21 @@ var setup_default = defineCommand3({
|
|
|
828
1125
|
warn(' echo ".contributerc.json" >> .gitignore');
|
|
829
1126
|
}
|
|
830
1127
|
console.log();
|
|
831
|
-
info(`
|
|
832
|
-
info(`
|
|
833
|
-
info(`
|
|
1128
|
+
info(`Workflow: ${pc6.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
|
|
1129
|
+
info(`Convention: ${pc6.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
|
|
1130
|
+
info(`Role: ${pc6.bold(config.role)}`);
|
|
1131
|
+
if (config.devBranch) {
|
|
1132
|
+
info(`Main: ${pc6.bold(config.mainBranch)} | Dev: ${pc6.bold(config.devBranch)}`);
|
|
1133
|
+
} else {
|
|
1134
|
+
info(`Main: ${pc6.bold(config.mainBranch)}`);
|
|
1135
|
+
}
|
|
1136
|
+
info(`Origin: ${pc6.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc6.bold(config.upstream)}` : ""}`);
|
|
834
1137
|
}
|
|
835
1138
|
});
|
|
836
1139
|
|
|
837
1140
|
// src/commands/start.ts
|
|
838
|
-
import { defineCommand as
|
|
839
|
-
import
|
|
1141
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
1142
|
+
import pc7 from "picocolors";
|
|
840
1143
|
|
|
841
1144
|
// src/utils/branch.ts
|
|
842
1145
|
var DEFAULT_PREFIXES = ["feature", "fix", "docs", "chore", "test", "refactor"];
|
|
@@ -852,10 +1155,10 @@ function looksLikeNaturalLanguage(input) {
|
|
|
852
1155
|
}
|
|
853
1156
|
|
|
854
1157
|
// src/commands/start.ts
|
|
855
|
-
var start_default =
|
|
1158
|
+
var start_default = defineCommand5({
|
|
856
1159
|
meta: {
|
|
857
1160
|
name: "start",
|
|
858
|
-
description: "Create a new feature branch from the latest
|
|
1161
|
+
description: "Create a new feature branch from the latest base branch"
|
|
859
1162
|
},
|
|
860
1163
|
args: {
|
|
861
1164
|
name: {
|
|
@@ -887,7 +1190,9 @@ var start_default = defineCommand4({
|
|
|
887
1190
|
error("You have uncommitted changes. Please commit or stash them before creating a branch.");
|
|
888
1191
|
process.exit(1);
|
|
889
1192
|
}
|
|
890
|
-
const {
|
|
1193
|
+
const { branchPrefixes } = config;
|
|
1194
|
+
const baseBranch = getBaseBranch(config);
|
|
1195
|
+
const syncSource = getSyncSource(config);
|
|
891
1196
|
let branchName = args.name;
|
|
892
1197
|
heading("\uD83C\uDF3F contrib start");
|
|
893
1198
|
const useAI = !args["no-ai"] && looksLikeNaturalLanguage(branchName);
|
|
@@ -896,8 +1201,8 @@ var start_default = defineCommand4({
|
|
|
896
1201
|
const suggested = await suggestBranchName(branchName, args.model);
|
|
897
1202
|
if (suggested) {
|
|
898
1203
|
console.log(`
|
|
899
|
-
${
|
|
900
|
-
const accepted = await confirmPrompt(`Use ${
|
|
1204
|
+
${pc7.dim("AI suggestion:")} ${pc7.bold(pc7.cyan(suggested))}`);
|
|
1205
|
+
const accepted = await confirmPrompt(`Use ${pc7.bold(suggested)} as your branch name?`);
|
|
901
1206
|
if (accepted) {
|
|
902
1207
|
branchName = suggested;
|
|
903
1208
|
} else {
|
|
@@ -906,31 +1211,29 @@ var start_default = defineCommand4({
|
|
|
906
1211
|
}
|
|
907
1212
|
}
|
|
908
1213
|
if (!hasPrefix(branchName, branchPrefixes)) {
|
|
909
|
-
const prefix = await selectPrompt(`Choose a branch type for ${
|
|
1214
|
+
const prefix = await selectPrompt(`Choose a branch type for ${pc7.bold(branchName)}:`, branchPrefixes);
|
|
910
1215
|
branchName = formatBranchName(prefix, branchName);
|
|
911
1216
|
}
|
|
912
|
-
info(`Creating branch: ${
|
|
913
|
-
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
const
|
|
917
|
-
if (resetResult.exitCode !== 0) {}
|
|
918
|
-
const result = await createBranch(branchName, devBranch);
|
|
1217
|
+
info(`Creating branch: ${pc7.bold(branchName)}`);
|
|
1218
|
+
await fetchRemote(syncSource.remote);
|
|
1219
|
+
const updateResult = await updateLocalBranch(baseBranch, syncSource.ref);
|
|
1220
|
+
if (updateResult.exitCode !== 0) {}
|
|
1221
|
+
const result = await createBranch(branchName, baseBranch);
|
|
919
1222
|
if (result.exitCode !== 0) {
|
|
920
1223
|
error(`Failed to create branch: ${result.stderr}`);
|
|
921
1224
|
process.exit(1);
|
|
922
1225
|
}
|
|
923
|
-
success(`✅ Created ${
|
|
1226
|
+
success(`✅ Created ${pc7.bold(branchName)} from latest ${pc7.bold(baseBranch)}`);
|
|
924
1227
|
}
|
|
925
1228
|
});
|
|
926
1229
|
|
|
927
1230
|
// src/commands/status.ts
|
|
928
|
-
import { defineCommand as
|
|
929
|
-
import
|
|
930
|
-
var status_default =
|
|
1231
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
1232
|
+
import pc8 from "picocolors";
|
|
1233
|
+
var status_default = defineCommand6({
|
|
931
1234
|
meta: {
|
|
932
1235
|
name: "status",
|
|
933
|
-
description: "Show sync status of
|
|
1236
|
+
description: "Show sync status of branches"
|
|
934
1237
|
},
|
|
935
1238
|
async run() {
|
|
936
1239
|
if (!await isGitRepo()) {
|
|
@@ -943,60 +1246,57 @@ var status_default = defineCommand5({
|
|
|
943
1246
|
process.exit(1);
|
|
944
1247
|
}
|
|
945
1248
|
heading("\uD83D\uDCCA contribute-now status");
|
|
1249
|
+
console.log(` ${pc8.dim("Workflow:")} ${pc8.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
|
|
1250
|
+
console.log(` ${pc8.dim("Role:")} ${pc8.bold(config.role)}`);
|
|
1251
|
+
console.log();
|
|
946
1252
|
await fetchAll();
|
|
947
1253
|
const currentBranch = await getCurrentBranch();
|
|
948
|
-
const { mainBranch,
|
|
1254
|
+
const { mainBranch, origin, upstream, workflow } = config;
|
|
1255
|
+
const baseBranch = getBaseBranch(config);
|
|
949
1256
|
const isContributor = config.role === "contributor";
|
|
950
1257
|
const dirty = await hasUncommittedChanges();
|
|
951
1258
|
if (dirty) {
|
|
952
|
-
console.log(` ${
|
|
1259
|
+
console.log(` ${pc8.yellow("⚠")} ${pc8.yellow("Uncommitted changes in working tree")}`);
|
|
953
1260
|
console.log();
|
|
954
1261
|
}
|
|
955
1262
|
const mainRemote = `${origin}/${mainBranch}`;
|
|
956
1263
|
const mainDiv = await getDivergence(mainBranch, mainRemote);
|
|
957
1264
|
const mainStatus = formatStatus(mainBranch, mainRemote, mainDiv.ahead, mainDiv.behind);
|
|
958
1265
|
console.log(mainStatus);
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
const branchDiv = await getDivergence(currentBranch, devBranch);
|
|
970
|
-
const branchLine = formatStatus(currentBranch, devBranch, branchDiv.ahead, branchDiv.behind);
|
|
971
|
-
console.log(branchLine + pc7.dim(` (current ${pc7.green("*")})`));
|
|
1266
|
+
if (hasDevBranch(workflow) && config.devBranch) {
|
|
1267
|
+
const devRemoteRef = isContributor ? `${upstream}/${config.devBranch}` : `${origin}/${config.devBranch}`;
|
|
1268
|
+
const devDiv = await getDivergence(config.devBranch, devRemoteRef);
|
|
1269
|
+
const devLine = formatStatus(config.devBranch, devRemoteRef, devDiv.ahead, devDiv.behind);
|
|
1270
|
+
console.log(devLine);
|
|
1271
|
+
}
|
|
1272
|
+
if (currentBranch && currentBranch !== mainBranch && currentBranch !== config.devBranch) {
|
|
1273
|
+
const branchDiv = await getDivergence(currentBranch, baseBranch);
|
|
1274
|
+
const branchLine = formatStatus(currentBranch, baseBranch, branchDiv.ahead, branchDiv.behind);
|
|
1275
|
+
console.log(branchLine + pc8.dim(` (current ${pc8.green("*")})`));
|
|
972
1276
|
} else if (currentBranch) {
|
|
973
|
-
|
|
974
|
-
console.log(pc7.dim(` (on ${pc7.bold(mainBranch)} branch)`));
|
|
975
|
-
} else if (currentBranch === devBranch) {
|
|
976
|
-
console.log(pc7.dim(` (on ${pc7.bold(devBranch)} branch)`));
|
|
977
|
-
}
|
|
1277
|
+
console.log(pc8.dim(` (on ${pc8.bold(currentBranch)} branch)`));
|
|
978
1278
|
}
|
|
979
1279
|
console.log();
|
|
980
1280
|
}
|
|
981
1281
|
});
|
|
982
1282
|
function formatStatus(branch, base, ahead, behind) {
|
|
983
|
-
const label =
|
|
1283
|
+
const label = pc8.bold(branch.padEnd(20));
|
|
984
1284
|
if (ahead === 0 && behind === 0) {
|
|
985
|
-
return ` ${
|
|
1285
|
+
return ` ${pc8.green("✓")} ${label} ${pc8.dim(`in sync with ${base}`)}`;
|
|
986
1286
|
}
|
|
987
1287
|
if (ahead > 0 && behind === 0) {
|
|
988
|
-
return ` ${
|
|
1288
|
+
return ` ${pc8.yellow("↑")} ${label} ${pc8.yellow(`${ahead} commit${ahead !== 1 ? "s" : ""} ahead of ${base}`)}`;
|
|
989
1289
|
}
|
|
990
1290
|
if (behind > 0 && ahead === 0) {
|
|
991
|
-
return ` ${
|
|
1291
|
+
return ` ${pc8.red("↓")} ${label} ${pc8.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
|
|
992
1292
|
}
|
|
993
|
-
return ` ${
|
|
1293
|
+
return ` ${pc8.red("⚡")} ${label} ${pc8.yellow(`${ahead} ahead`)}${pc8.dim(", ")}${pc8.red(`${behind} behind`)} ${pc8.dim(base)}`;
|
|
994
1294
|
}
|
|
995
1295
|
|
|
996
1296
|
// src/commands/submit.ts
|
|
997
|
-
import { defineCommand as
|
|
998
|
-
import
|
|
999
|
-
var submit_default =
|
|
1297
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
1298
|
+
import pc9 from "picocolors";
|
|
1299
|
+
var submit_default = defineCommand7({
|
|
1000
1300
|
meta: {
|
|
1001
1301
|
name: "submit",
|
|
1002
1302
|
description: "Push current branch and create a pull request"
|
|
@@ -1027,18 +1327,20 @@ var submit_default = defineCommand6({
|
|
|
1027
1327
|
error("No .contributerc.json found. Run `contrib setup` first.");
|
|
1028
1328
|
process.exit(1);
|
|
1029
1329
|
}
|
|
1030
|
-
const {
|
|
1330
|
+
const { origin } = config;
|
|
1331
|
+
const baseBranch = getBaseBranch(config);
|
|
1332
|
+
const protectedBranches = getProtectedBranches(config);
|
|
1031
1333
|
const currentBranch = await getCurrentBranch();
|
|
1032
1334
|
if (!currentBranch) {
|
|
1033
1335
|
error("Could not determine current branch.");
|
|
1034
1336
|
process.exit(1);
|
|
1035
1337
|
}
|
|
1036
|
-
if (currentBranch
|
|
1037
|
-
error(`Cannot submit ${
|
|
1338
|
+
if (protectedBranches.includes(currentBranch)) {
|
|
1339
|
+
error(`Cannot submit ${protectedBranches.map((b) => pc9.bold(b)).join(" or ")} as a PR. Switch to your feature branch.`);
|
|
1038
1340
|
process.exit(1);
|
|
1039
1341
|
}
|
|
1040
1342
|
heading("\uD83D\uDE80 contrib submit");
|
|
1041
|
-
info(`Pushing ${
|
|
1343
|
+
info(`Pushing ${pc9.bold(currentBranch)} to ${origin}...`);
|
|
1042
1344
|
const pushResult = await pushSetUpstream(origin, currentBranch);
|
|
1043
1345
|
if (pushResult.exitCode !== 0) {
|
|
1044
1346
|
error(`Failed to push: ${pushResult.stderr}`);
|
|
@@ -1049,10 +1351,10 @@ var submit_default = defineCommand6({
|
|
|
1049
1351
|
if (!ghInstalled || !ghAuthed) {
|
|
1050
1352
|
const repoInfo = await getRepoInfoFromRemote(origin);
|
|
1051
1353
|
if (repoInfo) {
|
|
1052
|
-
const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/compare/${
|
|
1354
|
+
const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/compare/${baseBranch}...${currentBranch}?expand=1`;
|
|
1053
1355
|
console.log();
|
|
1054
1356
|
info("Create your PR manually:");
|
|
1055
|
-
console.log(` ${
|
|
1357
|
+
console.log(` ${pc9.cyan(prUrl)}`);
|
|
1056
1358
|
} else {
|
|
1057
1359
|
info("gh CLI not available. Create your PR manually on GitHub.");
|
|
1058
1360
|
}
|
|
@@ -1064,17 +1366,17 @@ var submit_default = defineCommand6({
|
|
|
1064
1366
|
const copilotError = await checkCopilotAvailable();
|
|
1065
1367
|
if (!copilotError) {
|
|
1066
1368
|
info("Generating AI PR description...");
|
|
1067
|
-
const commits = await getLog(
|
|
1068
|
-
const diff = await getLogDiff(
|
|
1369
|
+
const commits = await getLog(baseBranch, "HEAD");
|
|
1370
|
+
const diff = await getLogDiff(baseBranch, "HEAD");
|
|
1069
1371
|
const result = await generatePRDescription(commits, diff, args.model);
|
|
1070
1372
|
if (result) {
|
|
1071
1373
|
prTitle = result.title;
|
|
1072
1374
|
prBody = result.body;
|
|
1073
1375
|
console.log(`
|
|
1074
|
-
${
|
|
1376
|
+
${pc9.dim("AI title:")} ${pc9.bold(pc9.cyan(prTitle))}`);
|
|
1075
1377
|
console.log(`
|
|
1076
|
-
${
|
|
1077
|
-
console.log(
|
|
1378
|
+
${pc9.dim("AI body preview:")}`);
|
|
1379
|
+
console.log(pc9.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
|
|
1078
1380
|
} else {
|
|
1079
1381
|
warn("AI did not return a PR description.");
|
|
1080
1382
|
}
|
|
@@ -1095,7 +1397,7 @@ ${pc8.dim("AI body preview:")}`);
|
|
|
1095
1397
|
prTitle = await inputPrompt("PR title");
|
|
1096
1398
|
prBody = await inputPrompt("PR body (markdown)");
|
|
1097
1399
|
} else {
|
|
1098
|
-
const fillResult = await createPRFill(
|
|
1400
|
+
const fillResult = await createPRFill(baseBranch, args.draft);
|
|
1099
1401
|
if (fillResult.exitCode !== 0) {
|
|
1100
1402
|
error(`Failed to create PR: ${fillResult.stderr}`);
|
|
1101
1403
|
process.exit(1);
|
|
@@ -1109,7 +1411,7 @@ ${pc8.dim("AI body preview:")}`);
|
|
|
1109
1411
|
prTitle = await inputPrompt("PR title");
|
|
1110
1412
|
prBody = await inputPrompt("PR body (markdown)");
|
|
1111
1413
|
} else {
|
|
1112
|
-
const fillResult = await createPRFill(
|
|
1414
|
+
const fillResult = await createPRFill(baseBranch, args.draft);
|
|
1113
1415
|
if (fillResult.exitCode !== 0) {
|
|
1114
1416
|
error(`Failed to create PR: ${fillResult.stderr}`);
|
|
1115
1417
|
process.exit(1);
|
|
@@ -1123,7 +1425,7 @@ ${pc8.dim("AI body preview:")}`);
|
|
|
1123
1425
|
process.exit(1);
|
|
1124
1426
|
}
|
|
1125
1427
|
const prResult = await createPR({
|
|
1126
|
-
base:
|
|
1428
|
+
base: baseBranch,
|
|
1127
1429
|
title: prTitle,
|
|
1128
1430
|
body: prBody ?? "",
|
|
1129
1431
|
draft: args.draft
|
|
@@ -1137,12 +1439,12 @@ ${pc8.dim("AI body preview:")}`);
|
|
|
1137
1439
|
});
|
|
1138
1440
|
|
|
1139
1441
|
// src/commands/sync.ts
|
|
1140
|
-
import { defineCommand as
|
|
1141
|
-
import
|
|
1142
|
-
var sync_default =
|
|
1442
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
1443
|
+
import pc10 from "picocolors";
|
|
1444
|
+
var sync_default = defineCommand8({
|
|
1143
1445
|
meta: {
|
|
1144
1446
|
name: "sync",
|
|
1145
|
-
description: "
|
|
1447
|
+
description: "Sync your local branches with the remote"
|
|
1146
1448
|
},
|
|
1147
1449
|
args: {
|
|
1148
1450
|
yes: {
|
|
@@ -1162,86 +1464,70 @@ var sync_default = defineCommand7({
|
|
|
1162
1464
|
error("No .contributerc.json found. Run `contrib setup` first.");
|
|
1163
1465
|
process.exit(1);
|
|
1164
1466
|
}
|
|
1165
|
-
const {
|
|
1467
|
+
const { workflow, role, origin } = config;
|
|
1166
1468
|
if (await hasUncommittedChanges()) {
|
|
1167
1469
|
error("You have uncommitted changes. Please commit or stash them before syncing.");
|
|
1168
1470
|
process.exit(1);
|
|
1169
1471
|
}
|
|
1170
|
-
heading(`\uD83D\uDD04 contrib sync (${role})`);
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
const ok = await confirmPrompt(`This will reset ${pc9.bold(devBranch)} to match ${pc9.bold(`${origin}/${mainBranch}`)}.`);
|
|
1186
|
-
if (!ok)
|
|
1187
|
-
process.exit(0);
|
|
1188
|
-
}
|
|
1189
|
-
const coResult = await checkoutBranch(devBranch);
|
|
1190
|
-
if (coResult.exitCode !== 0) {
|
|
1191
|
-
error(`Failed to checkout ${devBranch}: ${coResult.stderr}`);
|
|
1192
|
-
process.exit(1);
|
|
1193
|
-
}
|
|
1194
|
-
const resetResult = await resetHard(`${origin}/${mainBranch}`);
|
|
1195
|
-
if (resetResult.exitCode !== 0) {
|
|
1196
|
-
error(`Failed to reset: ${resetResult.stderr}`);
|
|
1197
|
-
process.exit(1);
|
|
1198
|
-
}
|
|
1199
|
-
const pushResult = await pushForceWithLease(origin, devBranch);
|
|
1200
|
-
if (pushResult.exitCode !== 0) {
|
|
1201
|
-
error(`Failed to push: ${pushResult.stderr}`);
|
|
1202
|
-
process.exit(1);
|
|
1203
|
-
}
|
|
1204
|
-
success(`✅ ${devBranch} has been reset to match ${origin}/${mainBranch} and pushed.`);
|
|
1472
|
+
heading(`\uD83D\uDD04 contrib sync (${workflow}, ${role})`);
|
|
1473
|
+
const baseBranch = getBaseBranch(config);
|
|
1474
|
+
const syncSource = getSyncSource(config);
|
|
1475
|
+
info(`Fetching ${syncSource.remote}...`);
|
|
1476
|
+
const fetchResult = await fetchRemote(syncSource.remote);
|
|
1477
|
+
if (fetchResult.exitCode !== 0) {
|
|
1478
|
+
error(`Failed to fetch ${syncSource.remote}: ${fetchResult.stderr}`);
|
|
1479
|
+
process.exit(1);
|
|
1480
|
+
}
|
|
1481
|
+
if (role === "contributor" && syncSource.remote !== origin) {
|
|
1482
|
+
await fetchRemote(origin);
|
|
1483
|
+
}
|
|
1484
|
+
const div = await getDivergence(baseBranch, syncSource.ref);
|
|
1485
|
+
if (div.ahead > 0 || div.behind > 0) {
|
|
1486
|
+
info(`${pc10.bold(baseBranch)} is ${pc10.yellow(`${div.ahead} ahead`)} and ${pc10.red(`${div.behind} behind`)} ${syncSource.ref}`);
|
|
1205
1487
|
} else {
|
|
1206
|
-
info(
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1488
|
+
info(`${pc10.bold(baseBranch)} is already in sync with ${syncSource.ref}`);
|
|
1489
|
+
}
|
|
1490
|
+
if (!args.yes) {
|
|
1491
|
+
const ok = await confirmPrompt(`This will pull ${pc10.bold(syncSource.ref)} into local ${pc10.bold(baseBranch)}.`);
|
|
1492
|
+
if (!ok)
|
|
1493
|
+
process.exit(0);
|
|
1494
|
+
}
|
|
1495
|
+
const coResult = await checkoutBranch(baseBranch);
|
|
1496
|
+
if (coResult.exitCode !== 0) {
|
|
1497
|
+
error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
|
|
1498
|
+
process.exit(1);
|
|
1499
|
+
}
|
|
1500
|
+
const pullResult = await pullBranch(syncSource.remote, baseBranch);
|
|
1501
|
+
if (pullResult.exitCode !== 0) {
|
|
1502
|
+
error(`Failed to pull: ${pullResult.stderr}`);
|
|
1503
|
+
process.exit(1);
|
|
1504
|
+
}
|
|
1505
|
+
success(`✅ ${baseBranch} is now in sync with ${syncSource.ref}`);
|
|
1506
|
+
if (hasDevBranch(workflow) && role === "maintainer") {
|
|
1507
|
+
const mainDiv = await getDivergence(config.mainBranch, `${origin}/${config.mainBranch}`);
|
|
1508
|
+
if (mainDiv.behind > 0) {
|
|
1509
|
+
info(`Also syncing ${pc10.bold(config.mainBranch)}...`);
|
|
1510
|
+
const mainCoResult = await checkoutBranch(config.mainBranch);
|
|
1511
|
+
if (mainCoResult.exitCode === 0) {
|
|
1512
|
+
const mainPullResult = await pullBranch(origin, config.mainBranch);
|
|
1513
|
+
if (mainPullResult.exitCode === 0) {
|
|
1514
|
+
success(`✅ ${config.mainBranch} is now in sync with ${origin}/${config.mainBranch}`);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
await checkoutBranch(baseBranch);
|
|
1231
1518
|
}
|
|
1232
|
-
success(`✅ ${devBranch} has been reset to match ${upstream}/${devBranch} and pushed.`);
|
|
1233
1519
|
}
|
|
1234
1520
|
}
|
|
1235
1521
|
});
|
|
1236
1522
|
|
|
1237
1523
|
// src/commands/update.ts
|
|
1238
|
-
import { readFileSync as
|
|
1239
|
-
import { defineCommand as
|
|
1240
|
-
import
|
|
1241
|
-
var update_default =
|
|
1524
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1525
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
1526
|
+
import pc11 from "picocolors";
|
|
1527
|
+
var update_default = defineCommand9({
|
|
1242
1528
|
meta: {
|
|
1243
1529
|
name: "update",
|
|
1244
|
-
description: "Rebase current branch onto latest
|
|
1530
|
+
description: "Rebase current branch onto the latest base branch"
|
|
1245
1531
|
},
|
|
1246
1532
|
args: {
|
|
1247
1533
|
model: {
|
|
@@ -1264,14 +1550,16 @@ var update_default = defineCommand8({
|
|
|
1264
1550
|
error("No .contributerc.json found. Run `contrib setup` first.");
|
|
1265
1551
|
process.exit(1);
|
|
1266
1552
|
}
|
|
1267
|
-
const
|
|
1553
|
+
const baseBranch = getBaseBranch(config);
|
|
1554
|
+
const protectedBranches = getProtectedBranches(config);
|
|
1555
|
+
const syncSource = getSyncSource(config);
|
|
1268
1556
|
const currentBranch = await getCurrentBranch();
|
|
1269
1557
|
if (!currentBranch) {
|
|
1270
1558
|
error("Could not determine current branch.");
|
|
1271
1559
|
process.exit(1);
|
|
1272
1560
|
}
|
|
1273
|
-
if (currentBranch
|
|
1274
|
-
error(`Use \`contrib sync\` to update ${
|
|
1561
|
+
if (protectedBranches.includes(currentBranch)) {
|
|
1562
|
+
error(`Use \`contrib sync\` to update ${protectedBranches.map((b) => pc11.bold(b)).join(" or ")} branches.`);
|
|
1275
1563
|
process.exit(1);
|
|
1276
1564
|
}
|
|
1277
1565
|
if (await hasUncommittedChanges()) {
|
|
@@ -1279,12 +1567,10 @@ var update_default = defineCommand8({
|
|
|
1279
1567
|
process.exit(1);
|
|
1280
1568
|
}
|
|
1281
1569
|
heading("\uD83D\uDD03 contrib update");
|
|
1282
|
-
info(`Updating ${
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
await
|
|
1286
|
-
await resetHard(remoteDevRef);
|
|
1287
|
-
const rebaseResult = await rebase(devBranch);
|
|
1570
|
+
info(`Updating ${pc11.bold(currentBranch)} with latest ${pc11.bold(baseBranch)}...`);
|
|
1571
|
+
await fetchRemote(syncSource.remote);
|
|
1572
|
+
await updateLocalBranch(baseBranch, syncSource.ref);
|
|
1573
|
+
const rebaseResult = await rebase(baseBranch);
|
|
1288
1574
|
if (rebaseResult.exitCode !== 0) {
|
|
1289
1575
|
warn("Rebase hit conflicts. Resolve them manually.");
|
|
1290
1576
|
console.log();
|
|
@@ -1296,7 +1582,7 @@ var update_default = defineCommand8({
|
|
|
1296
1582
|
let conflictDiff = "";
|
|
1297
1583
|
for (const file of conflictFiles.slice(0, 3)) {
|
|
1298
1584
|
try {
|
|
1299
|
-
const content =
|
|
1585
|
+
const content = readFileSync3(file, "utf-8");
|
|
1300
1586
|
if (content.includes("<<<<<<<")) {
|
|
1301
1587
|
conflictDiff += `
|
|
1302
1588
|
--- ${file} ---
|
|
@@ -1309,34 +1595,73 @@ ${content.slice(0, 2000)}
|
|
|
1309
1595
|
const suggestion = await suggestConflictResolution(conflictDiff, args.model);
|
|
1310
1596
|
if (suggestion) {
|
|
1311
1597
|
console.log(`
|
|
1312
|
-
${
|
|
1313
|
-
console.log(
|
|
1598
|
+
${pc11.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
|
|
1599
|
+
console.log(pc11.dim("─".repeat(60)));
|
|
1314
1600
|
console.log(suggestion);
|
|
1315
|
-
console.log(
|
|
1601
|
+
console.log(pc11.dim("─".repeat(60)));
|
|
1316
1602
|
console.log();
|
|
1317
1603
|
}
|
|
1318
1604
|
}
|
|
1319
1605
|
}
|
|
1320
1606
|
}
|
|
1321
|
-
console.log(
|
|
1607
|
+
console.log(pc11.bold("To resolve:"));
|
|
1322
1608
|
console.log(` 1. Fix conflicts in the affected files`);
|
|
1323
|
-
console.log(` 2. ${
|
|
1324
|
-
console.log(` 3. ${
|
|
1609
|
+
console.log(` 2. ${pc11.cyan("git add <resolved-files>")}`);
|
|
1610
|
+
console.log(` 3. ${pc11.cyan("git rebase --continue")}`);
|
|
1325
1611
|
console.log();
|
|
1326
|
-
console.log(` Or abort: ${
|
|
1612
|
+
console.log(` Or abort: ${pc11.cyan("git rebase --abort")}`);
|
|
1327
1613
|
process.exit(1);
|
|
1328
1614
|
}
|
|
1329
|
-
success(`✅ ${
|
|
1615
|
+
success(`✅ ${pc11.bold(currentBranch)} has been rebased onto latest ${pc11.bold(baseBranch)}`);
|
|
1616
|
+
}
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
// src/commands/validate.ts
|
|
1620
|
+
import { defineCommand as defineCommand10 } from "citty";
|
|
1621
|
+
import pc12 from "picocolors";
|
|
1622
|
+
var validate_default = defineCommand10({
|
|
1623
|
+
meta: {
|
|
1624
|
+
name: "validate",
|
|
1625
|
+
description: "Validate a commit message against the configured convention"
|
|
1626
|
+
},
|
|
1627
|
+
args: {
|
|
1628
|
+
message: {
|
|
1629
|
+
type: "positional",
|
|
1630
|
+
description: "The commit message to validate",
|
|
1631
|
+
required: true
|
|
1632
|
+
}
|
|
1633
|
+
},
|
|
1634
|
+
async run({ args }) {
|
|
1635
|
+
const config = readConfig();
|
|
1636
|
+
if (!config) {
|
|
1637
|
+
error("No .contributerc.json found. Run `contrib setup` first.");
|
|
1638
|
+
process.exit(1);
|
|
1639
|
+
}
|
|
1640
|
+
const convention = config.commitConvention;
|
|
1641
|
+
if (convention === "none") {
|
|
1642
|
+
info('Commit convention is set to "none". All messages are accepted.');
|
|
1643
|
+
process.exit(0);
|
|
1644
|
+
}
|
|
1645
|
+
const message = args.message;
|
|
1646
|
+
if (validateCommitMessage(message, convention)) {
|
|
1647
|
+
success(`Valid ${CONVENTION_LABELS[convention]} message.`);
|
|
1648
|
+
process.exit(0);
|
|
1649
|
+
}
|
|
1650
|
+
const errors = getValidationError(convention);
|
|
1651
|
+
for (const line of errors) {
|
|
1652
|
+
console.error(pc12.red(` ✗ ${line}`));
|
|
1653
|
+
}
|
|
1654
|
+
process.exit(1);
|
|
1330
1655
|
}
|
|
1331
1656
|
});
|
|
1332
1657
|
|
|
1333
1658
|
// src/ui/banner.ts
|
|
1334
1659
|
import figlet from "figlet";
|
|
1335
|
-
import
|
|
1660
|
+
import pc13 from "picocolors";
|
|
1336
1661
|
// package.json
|
|
1337
1662
|
var package_default = {
|
|
1338
1663
|
name: "contribute-now",
|
|
1339
|
-
version: "0.
|
|
1664
|
+
version: "0.2.0-dev.70284d0",
|
|
1340
1665
|
description: "Git workflow CLI for squash-merge two-branch models. Keeps dev in sync with main after squash merges.",
|
|
1341
1666
|
type: "module",
|
|
1342
1667
|
bin: {
|
|
@@ -1348,12 +1673,12 @@ var package_default = {
|
|
|
1348
1673
|
],
|
|
1349
1674
|
scripts: {
|
|
1350
1675
|
build: "bun build src/index.ts --outfile dist/index.js --target node --packages external",
|
|
1676
|
+
cli: "bun run src/index.ts --",
|
|
1351
1677
|
dev: "bun src/index.ts",
|
|
1352
1678
|
test: "bun test",
|
|
1353
1679
|
lint: "biome check .",
|
|
1354
1680
|
"lint:fix": "biome check --write .",
|
|
1355
1681
|
format: "biome format --write .",
|
|
1356
|
-
prepare: "husky || true",
|
|
1357
1682
|
"www:dev": "bun run --cwd www dev",
|
|
1358
1683
|
"www:build": "bun run --cwd www build",
|
|
1359
1684
|
"www:preview": "bun run --cwd www preview"
|
|
@@ -1390,7 +1715,6 @@ var package_default = {
|
|
|
1390
1715
|
"@biomejs/biome": "^2.4.4",
|
|
1391
1716
|
"@types/bun": "latest",
|
|
1392
1717
|
"@types/figlet": "^1.7.0",
|
|
1393
|
-
husky: "^9.1.7",
|
|
1394
1718
|
typescript: "^5.7.0"
|
|
1395
1719
|
}
|
|
1396
1720
|
};
|
|
@@ -1398,9 +1722,10 @@ var package_default = {
|
|
|
1398
1722
|
// src/ui/banner.ts
|
|
1399
1723
|
var LOGO;
|
|
1400
1724
|
try {
|
|
1401
|
-
LOGO = figlet.textSync(
|
|
1725
|
+
LOGO = figlet.textSync(`Contribute
|
|
1726
|
+
Now`, { font: "ANSI Shadow" });
|
|
1402
1727
|
} catch {
|
|
1403
|
-
LOGO = "
|
|
1728
|
+
LOGO = "Contribute Now";
|
|
1404
1729
|
}
|
|
1405
1730
|
function getVersion() {
|
|
1406
1731
|
return package_default.version ?? "unknown";
|
|
@@ -1408,16 +1733,15 @@ function getVersion() {
|
|
|
1408
1733
|
function getAuthor() {
|
|
1409
1734
|
return typeof package_default.author === "string" ? package_default.author : "unknown";
|
|
1410
1735
|
}
|
|
1411
|
-
function showBanner(
|
|
1412
|
-
console.log(
|
|
1736
|
+
function showBanner(showLinks = false) {
|
|
1737
|
+
console.log(pc13.cyan(`
|
|
1413
1738
|
${LOGO}`));
|
|
1414
|
-
console.log(` ${
|
|
1415
|
-
if (
|
|
1416
|
-
console.log(` ${pc11.dim(package_default.description)}`);
|
|
1739
|
+
console.log(` ${pc13.dim(`v${getVersion()}`)} ${pc13.dim("—")} ${pc13.dim(`Built by ${getAuthor()}`)}`);
|
|
1740
|
+
if (showLinks) {
|
|
1417
1741
|
console.log();
|
|
1418
|
-
console.log(` ${
|
|
1419
|
-
console.log(` ${
|
|
1420
|
-
console.log(` ${
|
|
1742
|
+
console.log(` ${pc13.yellow("Star")} ${pc13.cyan("https://github.com/warengonzaga/contribute-now")}`);
|
|
1743
|
+
console.log(` ${pc13.green("Contribute")} ${pc13.cyan("https://github.com/warengonzaga/contribute-now/blob/main/CONTRIBUTING.md")}`);
|
|
1744
|
+
console.log(` ${pc13.magenta("Sponsor")} ${pc13.cyan("https://warengonzaga.com/sponsor")}`);
|
|
1421
1745
|
}
|
|
1422
1746
|
console.log();
|
|
1423
1747
|
}
|
|
@@ -1425,11 +1749,11 @@ ${LOGO}`));
|
|
|
1425
1749
|
// src/index.ts
|
|
1426
1750
|
var isHelp = process.argv.includes("--help") || process.argv.includes("-h");
|
|
1427
1751
|
showBanner(isHelp);
|
|
1428
|
-
var main =
|
|
1752
|
+
var main = defineCommand11({
|
|
1429
1753
|
meta: {
|
|
1430
1754
|
name: "contrib",
|
|
1431
1755
|
version: getVersion(),
|
|
1432
|
-
description: "Git workflow CLI
|
|
1756
|
+
description: "Git workflow CLI that guides contributors through clean branching, commits, and PRs."
|
|
1433
1757
|
},
|
|
1434
1758
|
args: {
|
|
1435
1759
|
version: {
|
|
@@ -1446,7 +1770,9 @@ var main = defineCommand9({
|
|
|
1446
1770
|
update: update_default,
|
|
1447
1771
|
submit: submit_default,
|
|
1448
1772
|
clean: clean_default,
|
|
1449
|
-
status: status_default
|
|
1773
|
+
status: status_default,
|
|
1774
|
+
hook: hook_default,
|
|
1775
|
+
validate: validate_default
|
|
1450
1776
|
},
|
|
1451
1777
|
run({ args }) {
|
|
1452
1778
|
if (args.version) {
|