gityo 1.0.16 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +0 -1
  2. package/dist/index.js +634 -103
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -104,6 +104,5 @@ Styles control how commit messages, PR titles, and PR bodies are written. Choose
104
104
 
105
105
  - Run `gityo config styles` to view commit, PR title, and PR body styles.
106
106
  - Add `.gityo.json` to a repository for project-specific settings.
107
- - Add `.gityo.md` for project-specific commit instructions.
108
107
  - Run `gityo config` to see config locations.
109
108
  - Use the config schema for available settings and `--help` on each command for its options.
package/dist/index.js CHANGED
@@ -1710,7 +1710,7 @@ function useColor() {
1710
1710
  if (process$1.env.FORCE_COLOR || process$1.env.CLICOLOR_FORCE !== void 0) return true;
1711
1711
  }
1712
1712
  new Command();
1713
- var version$4 = "1.0.16";
1713
+ var version$4 = "1.1.0";
1714
1714
  const keybindingLookup = new Set(["emacs", "vim"]);
1715
1715
  function isKeybinding(value) {
1716
1716
  return keybindingLookup.has(value);
@@ -24031,6 +24031,15 @@ async function getGit(cwd = process.cwd()) {
24031
24031
  liveGit: createLiveGit(root)
24032
24032
  };
24033
24033
  }
24034
+ async function getStagedFiles(git) {
24035
+ return splitNull(await git.raw([
24036
+ "diff",
24037
+ "--cached",
24038
+ "--name-only",
24039
+ "--diff-filter=ACDMRTUXB",
24040
+ "-z"
24041
+ ])).sort((left, right) => left.localeCompare(right));
24042
+ }
24034
24043
  async function getChangedFiles(git) {
24035
24044
  const [unstaged, staged, untracked] = await Promise.all([
24036
24045
  git.raw([
@@ -24039,13 +24048,7 @@ async function getChangedFiles(git) {
24039
24048
  "--diff-filter=ACDMRTUXB",
24040
24049
  "-z"
24041
24050
  ]),
24042
- git.raw([
24043
- "diff",
24044
- "--cached",
24045
- "--name-only",
24046
- "--diff-filter=ACDMRTUXB",
24047
- "-z"
24048
- ]),
24051
+ getStagedFiles(git),
24049
24052
  git.raw([
24050
24053
  "ls-files",
24051
24054
  "--others",
@@ -24054,12 +24057,12 @@ async function getChangedFiles(git) {
24054
24057
  ])
24055
24058
  ]);
24056
24059
  return Array.from(new Set([
24057
- ...splitNull(staged),
24060
+ ...staged,
24058
24061
  ...splitNull(unstaged),
24059
24062
  ...splitNull(untracked)
24060
24063
  ])).sort((left, right) => left.localeCompare(right));
24061
24064
  }
24062
- async function getCommitDiff(git, contextLines = 3) {
24065
+ async function getCommitDiff(git, scope, contextLines = 3) {
24063
24066
  const unified = [`-U${contextLines}`];
24064
24067
  const staged = await git.raw([
24065
24068
  "diff",
@@ -24067,12 +24070,12 @@ async function getCommitDiff(git, contextLines = 3) {
24067
24070
  "--no-ext-diff",
24068
24071
  ...unified
24069
24072
  ]);
24070
- if (staged.trim().length > 0) return {
24071
- diff: staged,
24072
- hasStaged: true
24073
- };
24073
+ if (scope === "staged") return staged;
24074
+ if (scope !== "all") throw new Error(`Unknown commit scope '${scope}'.`);
24075
+ const hasStaged = staged.trim().length > 0;
24074
24076
  const [unstaged, untracked] = await Promise.all([git.raw([
24075
24077
  "diff",
24078
+ ...hasStaged ? ["HEAD"] : [],
24076
24079
  "--no-ext-diff",
24077
24080
  ...unified
24078
24081
  ]), git.raw([
@@ -24081,17 +24084,14 @@ async function getCommitDiff(git, contextLines = 3) {
24081
24084
  "--exclude-standard",
24082
24085
  "-z"
24083
24086
  ])]);
24084
- return {
24085
- diff: [unstaged, ...await Promise.all(splitNull(untracked).map((file$4) => git.raw([
24086
- "diff",
24087
- "--no-index",
24088
- ...unified,
24089
- "--",
24090
- "/dev/null",
24091
- file$4
24092
- ])))].filter((part) => part.length > 0).join("\n"),
24093
- hasStaged: false
24094
- };
24087
+ return [unstaged, ...await Promise.all(splitNull(untracked).map((file$4) => git.raw([
24088
+ "diff",
24089
+ "--no-index",
24090
+ ...unified,
24091
+ "--",
24092
+ "/dev/null",
24093
+ file$4
24094
+ ])))].filter((part) => part.length > 0).join("\n");
24095
24095
  }
24096
24096
  function splitNull(output$3) {
24097
24097
  return output$3.split("\0").filter(Boolean);
@@ -24106,17 +24106,16 @@ function createLiveGit(baseDir) {
24106
24106
  }
24107
24107
  const DEFAULT_MAX_DIFF_TOKENS = 24e3;
24108
24108
  const DEFAULT_PER_FILE_CAP = 400;
24109
- const CHARS_PER_TOKEN = 4;
24109
+ const CHARS_PER_TOKEN$2 = 4;
24110
24110
  const MINIMIZED_CONTEXT_LINES = 1;
24111
24111
  const FILE_SECTION_PREFIX = "diff --git ";
24112
24112
  const HUNK_HEADER_PREFIX = "@@ ";
24113
24113
  const NOISE_FILE_PATTERNS = [/\.(lock|min\.(?:js|mjs|cjs|css)|map)$/i, /-lock\.[^/]+$/i];
24114
24114
  function estimateTokens(text$1) {
24115
- return Math.ceil(text$1.length / CHARS_PER_TOKEN);
24115
+ return Math.ceil(text$1.length / CHARS_PER_TOKEN$2);
24116
24116
  }
24117
24117
  async function minimizeDiff(git, options) {
24118
- const { diff } = await getCommitDiff(git, MINIMIZED_CONTEXT_LINES);
24119
- const sections = splitFileSections(diff).map(parseSection);
24118
+ const sections = splitFileSections(await getCommitDiff(git, options.scope, MINIMIZED_CONTEXT_LINES)).map(parseSection);
24120
24119
  const kept = [];
24121
24120
  const excludedPaths = /* @__PURE__ */ new Set();
24122
24121
  for (const section of sections) if (isNoiseFile(section.path)) excludedPaths.add(section.path);
@@ -24215,7 +24214,7 @@ function splitOversizedSection(section, maxTokens) {
24215
24214
  }), maxTokens);
24216
24215
  }
24217
24216
  function hardSplit(text$1, maxTokens) {
24218
- const maxChars = maxTokens * CHARS_PER_TOKEN;
24217
+ const maxChars = maxTokens * CHARS_PER_TOKEN$2;
24219
24218
  const chunks = [];
24220
24219
  let current = "";
24221
24220
  for (const line of text$1.split("\n")) {
@@ -36987,11 +36986,11 @@ var postToApi$7 = async ({ url: url$4, headers = {}, body, successfulResponseHan
36987
36986
  });
36988
36987
  }
36989
36988
  };
36990
- function tool$3(tool2) {
36989
+ function tool(tool2) {
36991
36990
  return tool2;
36992
36991
  }
36993
36992
  function createProviderDefinedToolFactory$4({ id, inputSchema: inputSchema$1 }) {
36994
- return ({ execute, outputSchema: outputSchema$2, needsApproval, toModelOutput, onInputStart, onInputDelta, onInputAvailable,...args }) => tool$3({
36993
+ return ({ execute, outputSchema: outputSchema$2, needsApproval, toModelOutput, onInputStart, onInputDelta, onInputAvailable,...args }) => tool({
36995
36994
  type: "provider",
36996
36995
  isProviderExecuted: false,
36997
36996
  id,
@@ -37007,7 +37006,7 @@ function createProviderDefinedToolFactory$4({ id, inputSchema: inputSchema$1 })
37007
37006
  });
37008
37007
  }
37009
37008
  function createProviderDefinedToolFactoryWithOutputSchema$3({ id, inputSchema: inputSchema$1, outputSchema: outputSchema$2 }) {
37010
- return ({ execute, needsApproval, toModelOutput, onInputStart, onInputDelta, onInputAvailable,...args }) => tool$3({
37009
+ return ({ execute, needsApproval, toModelOutput, onInputStart, onInputDelta, onInputAvailable,...args }) => tool({
37011
37010
  type: "provider",
37012
37011
  isProviderExecuted: false,
37013
37012
  id,
@@ -37023,7 +37022,7 @@ function createProviderDefinedToolFactoryWithOutputSchema$3({ id, inputSchema: i
37023
37022
  });
37024
37023
  }
37025
37024
  function createProviderExecutedToolFactory$2({ id, inputSchema: inputSchema$1, outputSchema: outputSchema$2, supportsDeferredResults }) {
37026
- return ({ onInputStart, onInputDelta, onInputAvailable,...args }) => tool$3({
37025
+ return ({ onInputStart, onInputDelta, onInputAvailable,...args }) => tool({
37027
37026
  type: "provider",
37028
37027
  isProviderExecuted: true,
37029
37028
  id,
@@ -48321,7 +48320,7 @@ var init_transport = __esmMin((() => {
48321
48320
  init_parseUrl();
48322
48321
  init_toEndpointV1$1();
48323
48322
  }));
48324
- var parseBoolean, expectBoolean, expectNumber, MAX_FLOAT, expectFloat32, expectLong, expectInt, expectInt32, expectShort, expectByte, expectSizedInt, castInt, expectNonNull, expectObject, expectString, expectUnion, strictParseDouble, strictParseFloat, strictParseFloat32, NUMBER_REGEX, parseNumber, limitedParseDouble, handleFloat, limitedParseFloat, limitedParseFloat32, parseFloatString, strictParseLong, strictParseInt, strictParseInt32, strictParseShort, strictParseByte, stackTraceWarning, logger$1;
48323
+ var parseBoolean, expectBoolean, expectNumber, MAX_FLOAT, expectFloat32, expectLong, expectInt, expectInt32, expectShort, expectByte, expectSizedInt, castInt, expectNonNull, expectObject, expectString, expectUnion, strictParseDouble, strictParseFloat, strictParseFloat32, NUMBER_REGEX, parseNumber$1, limitedParseDouble, handleFloat, limitedParseFloat, limitedParseFloat32, parseFloatString, strictParseLong, strictParseInt, strictParseInt32, strictParseShort, strictParseByte, stackTraceWarning, logger$1;
48325
48324
  var init_parse_utils = __esmMin((() => {
48326
48325
  init_transport();
48327
48326
  parseBoolean = (value) => {
@@ -48427,16 +48426,16 @@ var init_parse_utils = __esmMin((() => {
48427
48426
  return asObject;
48428
48427
  };
48429
48428
  strictParseDouble = (value) => {
48430
- if (typeof value == "string") return expectNumber(parseNumber(value));
48429
+ if (typeof value == "string") return expectNumber(parseNumber$1(value));
48431
48430
  return expectNumber(value);
48432
48431
  };
48433
48432
  strictParseFloat = strictParseDouble;
48434
48433
  strictParseFloat32 = (value) => {
48435
- if (typeof value == "string") return expectFloat32(parseNumber(value));
48434
+ if (typeof value == "string") return expectFloat32(parseNumber$1(value));
48436
48435
  return expectFloat32(value);
48437
48436
  };
48438
48437
  NUMBER_REGEX = /(-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)|(-?Infinity)|(NaN)/g;
48439
- parseNumber = (value) => {
48438
+ parseNumber$1 = (value) => {
48440
48439
  const matches = value.match(NUMBER_REGEX);
48441
48440
  if (matches === null || matches[0].length !== value.length) throw new TypeError(`Expected real number, got implicit NaN`);
48442
48441
  return parseFloat(value);
@@ -48460,20 +48459,20 @@ var init_parse_utils = __esmMin((() => {
48460
48459
  }
48461
48460
  };
48462
48461
  strictParseLong = (value) => {
48463
- if (typeof value === "string") return expectLong(parseNumber(value));
48462
+ if (typeof value === "string") return expectLong(parseNumber$1(value));
48464
48463
  return expectLong(value);
48465
48464
  };
48466
48465
  strictParseInt = strictParseLong;
48467
48466
  strictParseInt32 = (value) => {
48468
- if (typeof value === "string") return expectInt32(parseNumber(value));
48467
+ if (typeof value === "string") return expectInt32(parseNumber$1(value));
48469
48468
  return expectInt32(value);
48470
48469
  };
48471
48470
  strictParseShort = (value) => {
48472
- if (typeof value === "string") return expectShort(parseNumber(value));
48471
+ if (typeof value === "string") return expectShort(parseNumber$1(value));
48473
48472
  return expectShort(value);
48474
48473
  };
48475
48474
  strictParseByte = (value) => {
48476
- if (typeof value === "string") return expectByte(parseNumber(value));
48475
+ if (typeof value === "string") return expectByte(parseNumber$1(value));
48477
48476
  return expectByte(value);
48478
48477
  };
48479
48478
  stackTraceWarning = (message) => {
@@ -48495,7 +48494,7 @@ function dateToUtcString(date$10) {
48495
48494
  const secondsString = secondsInt < 10 ? `0${secondsInt}` : `${secondsInt}`;
48496
48495
  return `${DAYS[dayOfWeek]}, ${dayOfMonthString} ${MONTHS[month]} ${year$1} ${hoursString}:${minutesString}:${secondsString} GMT`;
48497
48496
  }
48498
- var DAYS, MONTHS, RFC3339, parseRfc3339DateTime, RFC3339_WITH_OFFSET$1, parseRfc3339DateTimeWithOffset, IMF_FIXDATE$1, RFC_850_DATE$1, ASC_TIME$1, parseRfc7231DateTime, parseEpochTimestamp, buildDate, parseTwoDigitYear, FIFTY_YEARS_IN_MILLIS, adjustRfc850Year, parseMonthByShortName, DAYS_IN_MONTH, validateDayOfMonth, isLeapYear, parseDateValue, parseMilliseconds, parseOffsetToMilliseconds, stripLeadingZeroes;
48497
+ var DAYS, MONTHS, RFC3339, parseRfc3339DateTime, RFC3339_WITH_OFFSET$1, parseRfc3339DateTimeWithOffset, IMF_FIXDATE$1, RFC_850_DATE$1, ASC_TIME$1, parseRfc7231DateTime, parseEpochTimestamp, buildDate, parseTwoDigitYear, FIFTY_YEARS_IN_MILLIS, adjustRfc850Year, parseMonthByShortName, DAYS_IN_MONTH, validateDayOfMonth, isLeapYear, parseDateValue, parseMilliseconds$1, parseOffsetToMilliseconds, stripLeadingZeroes;
48499
48498
  var init_date_utils = __esmMin((() => {
48500
48499
  init_parse_utils();
48501
48500
  DAYS = [
@@ -48608,7 +48607,7 @@ var init_date_utils = __esmMin((() => {
48608
48607
  buildDate = (year$1, month, day, time$7) => {
48609
48608
  const adjustedMonth = month - 1;
48610
48609
  validateDayOfMonth(year$1, adjustedMonth, day);
48611
- return new Date(Date.UTC(year$1, adjustedMonth, day, parseDateValue(time$7.hours, "hour", 0, 23), parseDateValue(time$7.minutes, "minute", 0, 59), parseDateValue(time$7.seconds, "seconds", 0, 60), parseMilliseconds(time$7.fractionalMilliseconds)));
48610
+ return new Date(Date.UTC(year$1, adjustedMonth, day, parseDateValue(time$7.hours, "hour", 0, 23), parseDateValue(time$7.minutes, "minute", 0, 59), parseDateValue(time$7.seconds, "seconds", 0, 60), parseMilliseconds$1(time$7.fractionalMilliseconds)));
48612
48611
  };
48613
48612
  parseTwoDigitYear = (value) => {
48614
48613
  const thisYear = (/* @__PURE__ */ new Date()).getUTCFullYear();
@@ -48653,7 +48652,7 @@ var init_date_utils = __esmMin((() => {
48653
48652
  if (dateVal < lower || dateVal > upper) throw new TypeError(`${type$1} must be between ${lower} and ${upper}, inclusive`);
48654
48653
  return dateVal;
48655
48654
  };
48656
- parseMilliseconds = (value) => {
48655
+ parseMilliseconds$1 = (value) => {
48657
48656
  if (value === null || value === void 0) return 0;
48658
48657
  return strictParseFloat32("0." + value) * 1e3;
48659
48658
  };
@@ -150845,7 +150844,7 @@ function jsonrepair(text$1) {
150845
150844
  throwUnexpectedCharacter();
150846
150845
  function parseValue() {
150847
150846
  parseWhitespaceAndSkipComments();
150848
- const processed$1 = parseObject() || parseArray() || parseString() || parseNumber$1() || parseKeywords() || parseUnquotedString(false) || parseRegex();
150847
+ const processed$1 = parseObject() || parseArray() || parseString() || parseNumber$2() || parseKeywords() || parseUnquotedString(false) || parseRegex();
150849
150848
  parseWhitespaceAndSkipComments();
150850
150849
  return processed$1;
150851
150850
  }
@@ -151139,7 +151138,7 @@ function jsonrepair(text$1) {
151139
151138
  }
151140
151139
  return processed$1;
151141
151140
  }
151142
- function parseNumber$1() {
151141
+ function parseNumber$2() {
151143
151142
  const start = i$5;
151144
151143
  let num = "";
151145
151144
  let invalid = false;
@@ -171147,7 +171146,7 @@ function hasWebSearch(client) {
171147
171146
  return typeof client.webSearch === "function";
171148
171147
  }
171149
171148
  function webSearch(options = {}) {
171150
- return tool$3({
171149
+ return tool({
171151
171150
  description: "Search the web for current information about any topic. Use this when you need up-to-date information, recent news, current statistics, or real-time data that may not be in your training data.",
171152
171151
  inputSchema: webSearchInputSchema,
171153
171152
  execute: async (input2, { abortSignal }) => {
@@ -171203,7 +171202,7 @@ function webSearch(options = {}) {
171203
171202
  }
171204
171203
  var webFetchInputSchema = external_exports$2.object({ url: external_exports$2.string().url("Must be a valid URL").describe("The URL to fetch content from") });
171205
171204
  function webFetch(options = {}) {
171206
- return tool$3({
171205
+ return tool({
171207
171206
  description: "Fetch and read content from a specific web URL. Use this to retrieve the full text content of web pages, articles, documentation, or any publicly accessible web content for analysis or summarization.",
171208
171207
  inputSchema: webFetchInputSchema,
171209
171208
  execute: async (input2, context) => {
@@ -203416,7 +203415,7 @@ var require_dist$2 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
203416
203415
  safeParseJSON: () => safeParseJSON$2,
203417
203416
  safeValidateTypes: () => safeValidateTypes$2,
203418
203417
  standardSchemaValidator: () => standardSchemaValidator,
203419
- tool: () => tool$2,
203418
+ tool: () => tool$3,
203420
203419
  validateTypes: () => validateTypes$2,
203421
203420
  validator: () => validator,
203422
203421
  validatorSymbol: () => validatorSymbol,
@@ -203918,7 +203917,7 @@ var require_dist$2 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
203918
203917
  });
203919
203918
  }
203920
203919
  };
203921
- function tool$2(tool2) {
203920
+ function tool$3(tool2) {
203922
203921
  return tool2;
203923
203922
  }
203924
203923
  function dynamicTool$2(tool2) {
@@ -203928,7 +203927,7 @@ var require_dist$2 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
203928
203927
  };
203929
203928
  }
203930
203929
  function createProviderDefinedToolFactory({ id, name: name$18, inputSchema: inputSchema$1 }) {
203931
- return ({ execute, outputSchema: outputSchema$2, toModelOutput, onInputStart, onInputDelta, onInputAvailable,...args }) => tool$2({
203930
+ return ({ execute, outputSchema: outputSchema$2, toModelOutput, onInputStart, onInputDelta, onInputAvailable,...args }) => tool$3({
203932
203931
  type: "provider-defined",
203933
203932
  id,
203934
203933
  name: name$18,
@@ -203943,7 +203942,7 @@ var require_dist$2 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
203943
203942
  });
203944
203943
  }
203945
203944
  function createProviderDefinedToolFactoryWithOutputSchema({ id, name: name$18, inputSchema: inputSchema$1, outputSchema: outputSchema$2 }) {
203946
- return ({ execute, toModelOutput, onInputStart, onInputDelta, onInputAvailable,...args }) => tool$2({
203945
+ return ({ execute, toModelOutput, onInputStart, onInputDelta, onInputAvailable,...args }) => tool$3({
203947
203946
  type: "provider-defined",
203948
203947
  id,
203949
203948
  name: name$18,
@@ -206837,11 +206836,11 @@ var postToApi$1 = async ({ url: url$4, headers = {}, body, successfulResponseHan
206837
206836
  });
206838
206837
  }
206839
206838
  };
206840
- function tool$1(tool2) {
206839
+ function tool$2(tool2) {
206841
206840
  return tool2;
206842
206841
  }
206843
206842
  function createProviderToolFactory$1({ id, inputSchema: inputSchema$1 }) {
206844
- return ({ execute, outputSchema: outputSchema$2, needsApproval, toModelOutput, onInputStart, onInputDelta, onInputAvailable,...args }) => tool$1({
206843
+ return ({ execute, outputSchema: outputSchema$2, needsApproval, toModelOutput, onInputStart, onInputDelta, onInputAvailable,...args }) => tool$2({
206845
206844
  type: "provider",
206846
206845
  id,
206847
206846
  args,
@@ -206856,7 +206855,7 @@ function createProviderToolFactory$1({ id, inputSchema: inputSchema$1 }) {
206856
206855
  });
206857
206856
  }
206858
206857
  function createProviderToolFactoryWithOutputSchema$1({ id, inputSchema: inputSchema$1, outputSchema: outputSchema$2, supportsDeferredResults }) {
206859
- return ({ execute, needsApproval, toModelOutput, onInputStart, onInputDelta, onInputAvailable,...args }) => tool$1({
206858
+ return ({ execute, needsApproval, toModelOutput, onInputStart, onInputDelta, onInputAvailable,...args }) => tool$2({
206860
206859
  type: "provider",
206861
206860
  id,
206862
206861
  args,
@@ -228815,6 +228814,11 @@ const configSchema = object$3({
228815
228814
  prBodyStyle: string$5().min(1),
228816
228815
  prTitleStyles: record$3(string$5().min(1), instructionSchema),
228817
228816
  prBodyStyles: record$3(string$5().min(1), instructionSchema),
228817
+ prMergeMethod: _enum$4([
228818
+ "merge",
228819
+ "rebase",
228820
+ "squash"
228821
+ ]),
228818
228822
  maxDiffTokens: number$4().int().min(1e3),
228819
228823
  perFileCap: number$4().int().min(50),
228820
228824
  autoAcceptCommitMessage: boolean$4(),
@@ -228832,6 +228836,7 @@ function resolveConfig(input$3) {
228832
228836
  prBodyStyle: parsed.prBodyStyle,
228833
228837
  prTitleStyles: parsed.prTitleStyles,
228834
228838
  prBodyStyles: parsed.prBodyStyles,
228839
+ prMergeMethod: parsed.prMergeMethod ?? "merge",
228835
228840
  maxDiffTokens: parsed.maxDiffTokens,
228836
228841
  perFileCap: parsed.perFileCap,
228837
228842
  autoAcceptCommitMessage: parsed.autoAcceptCommitMessage ?? false,
@@ -228842,15 +228847,26 @@ function resolveConfig(input$3) {
228842
228847
  const PROJECT_CONFIG_FILE_NAME = ".gityo.json";
228843
228848
  const GLOBAL_CONFIG_FILE_PATH = path.join(os.homedir(), ".config", "gityo.json");
228844
228849
  async function loadConfig(cwd = process.cwd()) {
228845
- const [globalConfig$3, projectConfig, projectInstructions] = await Promise.all([
228846
- readGlobalConfig(),
228847
- readProjectConfig(cwd),
228848
- readInstructionsFile(path.join(cwd, ".gityo.md"))
228849
- ]);
228850
+ const [globalConfig$3, projectConfig] = await Promise.all([readGlobalConfig(), readProjectConfig(cwd)]);
228850
228851
  return resolveConfig({
228851
228852
  ...globalConfig$3 ?? {},
228852
228853
  ...projectConfig ?? {},
228853
- commitInstructions: projectInstructions ?? projectConfig?.commitInstructions ?? globalConfig$3?.commitInstructions
228854
+ models: {
228855
+ ...globalConfig$3?.models,
228856
+ ...projectConfig?.models
228857
+ },
228858
+ commitStyles: {
228859
+ ...globalConfig$3?.commitStyles,
228860
+ ...projectConfig?.commitStyles
228861
+ },
228862
+ prTitleStyles: {
228863
+ ...globalConfig$3?.prTitleStyles,
228864
+ ...projectConfig?.prTitleStyles
228865
+ },
228866
+ prBodyStyles: {
228867
+ ...globalConfig$3?.prBodyStyles,
228868
+ ...projectConfig?.prBodyStyles
228869
+ }
228854
228870
  });
228855
228871
  }
228856
228872
  async function readProjectConfig(cwd = process.cwd()) {
@@ -228877,17 +228893,6 @@ async function readConfigFile(filePath) {
228877
228893
  if (!result.success) throw new Error(`Invalid config in ${filePath}: ${result.error.message}`);
228878
228894
  return result.data;
228879
228895
  }
228880
- async function readInstructionsFile(filePath) {
228881
- let contents;
228882
- try {
228883
- contents = await readFile(filePath, "utf8");
228884
- } catch (error$125) {
228885
- if (isMissingFileError(error$125)) return;
228886
- throw error$125;
228887
- }
228888
- const value = contents.trim();
228889
- return value.length > 0 ? value : void 0;
228890
- }
228891
228896
  function isMissingFileError(error$125) {
228892
228897
  return error$125 instanceof Error && "code" in error$125 && typeof error$125.code === "string" && error$125.code === "ENOENT";
228893
228898
  }
@@ -228950,10 +228955,10 @@ async function runWithLoading(label, task) {
228950
228955
  readline.clearLine(process.stdout, 0);
228951
228956
  }
228952
228957
  }
228953
- const MAP_CONCURRENCY = 3;
228954
- const PROMPT_RESERVE_TOKENS = 800;
228955
- const MIN_CHUNK_BUDGET_TOKENS = 1e3;
228956
- const MAX_TOC_LINES = 500;
228958
+ const MAP_CONCURRENCY$1 = 3;
228959
+ const PROMPT_RESERVE_TOKENS$2 = 800;
228960
+ const MIN_CHUNK_BUDGET_TOKENS$1 = 1e3;
228961
+ const MAX_TOC_LINES$1 = 500;
228957
228962
  async function mainController(options = {}) {
228958
228963
  const { git, liveGit } = await getGit();
228959
228964
  const config$5 = await loadConfig((await git.revparse(["--show-toplevel"])).trim());
@@ -228970,9 +228975,9 @@ async function mainController(options = {}) {
228970
228975
  const forceExecPostCommand = options.post || options.yolo;
228971
228976
  let finalCommitMessage = options.input?.trim() ?? "";
228972
228977
  if (typeof options.input === "string" && finalCommitMessage.length === 0) throw new Error("Provided commit message input cannot be empty.");
228973
- const files = await getChangedFiles(git);
228978
+ const { diffScope, files } = await getCommitFiles(git, options.scope ?? "staged-or-changes");
228974
228979
  if (files.length === 0) return console.log("No changed files found.");
228975
- const { diff, hasStaged } = await getCommitDiff(git);
228980
+ const diff = await getCommitDiff(git, diffScope);
228976
228981
  if (finalCommitMessage.length > 0) {
228977
228982
  console.log(source_default.yellow("✓ Using direct commit message"));
228978
228983
  console.log(source_default.cyan.dim(finalCommitMessage));
@@ -228981,6 +228986,7 @@ async function mainController(options = {}) {
228981
228986
  if (finalCommitMessage.length === 0) while (true) {
228982
228987
  finalCommitMessage = (await runWithLoading("Generating commit message", () => generateMessage({
228983
228988
  git,
228989
+ scope: diffScope,
228984
228990
  languageModel,
228985
228991
  style,
228986
228992
  instructions,
@@ -228997,7 +229003,7 @@ async function mainController(options = {}) {
228997
229003
  }
228998
229004
  if (finalCommitMessage.length === 0) throw new Error("Commit message cannot be empty.");
228999
229005
  if (forceLLMGenerate) console.log(source_default.green("✓ Committing changes"));
229000
- if (!hasStaged) await git.add(["-A"]);
229006
+ if (diffScope === "all") await git.add(["-A"]);
229001
229007
  console.log(files.join("\n"));
229002
229008
  console.log("");
229003
229009
  await liveGit.commit(finalCommitMessage);
@@ -229014,19 +229020,35 @@ async function mainController(options = {}) {
229014
229020
  await liveGit.push();
229015
229021
  if (config$5.postCommand === "push-and-pull") await liveGit.pull();
229016
229022
  }
229023
+ async function getCommitFiles(git, commitScope) {
229024
+ const stagedFiles = await getStagedFiles(git);
229025
+ let diffScope;
229026
+ if (commitScope === "everything") diffScope = "all";
229027
+ else if (commitScope === "staged-only") {
229028
+ if (stagedFiles.length === 0) throw new Error("Nothing is staged.");
229029
+ diffScope = "staged";
229030
+ } else if (commitScope === "staged-or-changes") diffScope = stagedFiles.length > 0 ? "staged" : "all";
229031
+ else throw new Error(`Unknown commit scope '${commitScope}'.`);
229032
+ const files = diffScope === "staged" ? stagedFiles : await getChangedFiles(git);
229033
+ return {
229034
+ diffScope,
229035
+ files
229036
+ };
229037
+ }
229017
229038
  async function generateMessage(options) {
229018
229039
  const { git, languageModel, style, instructions, diff } = options;
229019
229040
  if (estimateTokens(diff) <= options.maxDiffTokens) return generateCommitMessage(languageModel, style, instructions, diff);
229020
229041
  const { toc, body } = await minimizeDiff(git, {
229042
+ scope: options.scope,
229021
229043
  perFileCap: options.perFileCap,
229022
229044
  allFiles: options.files
229023
229045
  });
229024
229046
  if (estimateTokens(toc) + estimateTokens(body) <= options.maxDiffTokens) return generateCommitMessage(languageModel, style, instructions, `${toc}\n\n${body}`);
229025
229047
  let effectiveToc = toc;
229026
- let chunkBudget = options.maxDiffTokens - estimateTokens(effectiveToc) - PROMPT_RESERVE_TOKENS;
229027
- if (chunkBudget < MIN_CHUNK_BUDGET_TOKENS) {
229028
- effectiveToc = toc.split("\n").slice(0, MAX_TOC_LINES).join("\n");
229029
- chunkBudget = Math.max(options.maxDiffTokens - estimateTokens(effectiveToc) - PROMPT_RESERVE_TOKENS, MIN_CHUNK_BUDGET_TOKENS);
229048
+ let chunkBudget = options.maxDiffTokens - estimateTokens(effectiveToc) - PROMPT_RESERVE_TOKENS$2;
229049
+ if (chunkBudget < MIN_CHUNK_BUDGET_TOKENS$1) {
229050
+ effectiveToc = toc.split("\n").slice(0, MAX_TOC_LINES$1).join("\n");
229051
+ chunkBudget = Math.max(options.maxDiffTokens - estimateTokens(effectiveToc) - PROMPT_RESERVE_TOKENS$2, MIN_CHUNK_BUDGET_TOKENS$1);
229030
229052
  }
229031
229053
  const chunks = splitDiffIntoChunks(body, chunkBudget);
229032
229054
  const summaries = new Array(chunks.length);
@@ -229037,7 +229059,7 @@ async function generateMessage(options) {
229037
229059
  summaries[index$2] = await summarizeChanges(languageModel, effectiveToc, chunks[index$2]);
229038
229060
  }
229039
229061
  }
229040
- await Promise.all(Array.from({ length: Math.min(MAP_CONCURRENCY, chunks.length) }, worker));
229062
+ await Promise.all(Array.from({ length: Math.min(MAP_CONCURRENCY$1, chunks.length) }, worker));
229041
229063
  return generateCommitMessageFromSummaries(languageModel, style, instructions, effectiveToc, summaries);
229042
229064
  }
229043
229065
  const CONFIG_SCHEMA_URL = "https://github.com/NazmusSayad/gityo/raw/refs/heads/schema/schema.json";
@@ -229153,12 +229175,12 @@ async function createPullRequest$1(input$3) {
229153
229175
  input$3.head
229154
229176
  ]);
229155
229177
  }
229156
- async function mergePullRequest(number$7) {
229178
+ async function mergePullRequest(number$7, method) {
229157
229179
  await execInherit("gh", [
229158
229180
  "pr",
229159
229181
  "merge",
229160
229182
  String(number$7),
229161
- "--merge"
229183
+ `--${method}`
229162
229184
  ]);
229163
229185
  }
229164
229186
  async function openPullRequest(number$7) {
@@ -236143,7 +236165,7 @@ const renderPullRequest = createRenderer({
236143
236165
  async function getCurrentBranch() {
236144
236166
  return (await exec("git", ["branch", "--show-current"])).trim();
236145
236167
  }
236146
- async function getRepoRoot() {
236168
+ async function getRepoRoot$1() {
236147
236169
  return (await exec("git", ["rev-parse", "--show-toplevel"])).trim();
236148
236170
  }
236149
236171
  async function resolvePrBranches(baseArg, headArg) {
@@ -236158,7 +236180,7 @@ async function resolvePrBranches(baseArg, headArg) {
236158
236180
  };
236159
236181
  }
236160
236182
  async function loadPullRequestContext(options = {}) {
236161
- const config$5 = await loadConfig(await getRepoRoot());
236183
+ const config$5 = await loadConfig(await getRepoRoot$1());
236162
236184
  const languageModel = resolveLanguageModel(resolveModelConfig(config$5.models, options.modelKey ?? "default"));
236163
236185
  const titleStyleKey = options.titleStyle ?? config$5.prTitleStyle ?? "default";
236164
236186
  const titleStyle = config$5.prTitleStyles?.[titleStyleKey] ?? PR_TITLE_STYLES[titleStyleKey];
@@ -236247,9 +236269,11 @@ function buildCompareContext(compare, maxDiffTokens) {
236247
236269
  async function handleUncommittedChanges(headBranch, options = {}) {
236248
236270
  if (await getCurrentBranch() !== headBranch) return;
236249
236271
  const { git } = await getGit();
236250
- const files = await getChangedFiles(git);
236272
+ const scope = options.scope ?? "staged-or-changes";
236273
+ const { diffScope, files } = await getCommitFiles(git, scope);
236251
236274
  if (files.length === 0) return;
236252
- console.log(source_default.yellow(`• You have ${files.length} uncommitted local change(s):\n${files.join("\n")}`));
236275
+ const label = diffScope === "staged" ? "staged" : "uncommitted";
236276
+ console.log(source_default.yellow(`• You have ${files.length} ${label} local change(s):\n${files.join("\n")}`));
236253
236277
  console.log("");
236254
236278
  if (!options.yolo) {
236255
236279
  if (!await dist_default({
@@ -236257,14 +236281,15 @@ async function handleUncommittedChanges(headBranch, options = {}) {
236257
236281
  default: true,
236258
236282
  theme: selectionTheme
236259
236283
  })) {
236260
- console.log(source_default.dim("Ignoring uncommitted local changes."));
236284
+ console.log(source_default.dim(`Ignoring ${label} local changes.`));
236261
236285
  console.log("");
236262
236286
  return;
236263
236287
  }
236264
236288
  }
236265
236289
  await mainController({
236266
236290
  yolo: options.yolo,
236267
- push: true
236291
+ push: true,
236292
+ scope
236268
236293
  });
236269
236294
  console.log("");
236270
236295
  }
@@ -236276,7 +236301,10 @@ async function createPullRequestController(baseArg, headArg, options = {}) {
236276
236301
  });
236277
236302
  const systemPrompt = buildPullRequestSystemPrompt(context);
236278
236303
  const branches = await resolvePrBranches(baseArg, headArg);
236279
- await handleUncommittedChanges(branches.head, { yolo: options.yolo ?? false });
236304
+ await handleUncommittedChanges(branches.head, {
236305
+ yolo: options.yolo ?? false,
236306
+ scope: options.all ? "everything" : "staged-or-changes"
236307
+ });
236280
236308
  const pullRequest = await createPullRequest({
236281
236309
  base: branches.base,
236282
236310
  head: branches.head,
@@ -236300,7 +236328,10 @@ async function mergePullRequestController(baseArg, headArg, options = {}) {
236300
236328
  });
236301
236329
  const systemPrompt = buildPullRequestSystemPrompt(context);
236302
236330
  const branches = await resolvePrBranches(baseArg, headArg);
236303
- await handleUncommittedChanges(branches.head, { yolo: options.yolo ?? false });
236331
+ await handleUncommittedChanges(branches.head, {
236332
+ yolo: options.yolo ?? false,
236333
+ scope: options.all ? "everything" : "staged-or-changes"
236334
+ });
236304
236335
  let pullRequest = await findPullRequest(branches.base, branches.head);
236305
236336
  if (pullRequest) console.log(pullRequest.url);
236306
236337
  else pullRequest = await createPullRequest({
@@ -236321,7 +236352,479 @@ async function mergePullRequestController(baseArg, headArg, options = {}) {
236321
236352
  console.log("Pull request merge cancelled.");
236322
236353
  return;
236323
236354
  }
236324
- await mergePullRequest(pullRequest.number);
236355
+ await mergePullRequest(pullRequest.number, options.mergeMethod ?? context.config.prMergeMethod);
236356
+ }
236357
+ const toZeroIfInfinity = (value) => Number.isFinite(value) ? value : 0;
236358
+ function parseNumber(milliseconds) {
236359
+ return {
236360
+ days: Math.trunc(milliseconds / 864e5),
236361
+ hours: Math.trunc(milliseconds / 36e5 % 24),
236362
+ minutes: Math.trunc(milliseconds / 6e4 % 60),
236363
+ seconds: Math.trunc(milliseconds / 1e3 % 60),
236364
+ milliseconds: Math.trunc(milliseconds % 1e3),
236365
+ microseconds: Math.trunc(toZeroIfInfinity(milliseconds * 1e3) % 1e3),
236366
+ nanoseconds: Math.trunc(toZeroIfInfinity(milliseconds * 1e6) % 1e3)
236367
+ };
236368
+ }
236369
+ function parseBigint(milliseconds) {
236370
+ return {
236371
+ days: milliseconds / 86400000n,
236372
+ hours: milliseconds / 3600000n % 24n,
236373
+ minutes: milliseconds / 60000n % 60n,
236374
+ seconds: milliseconds / 1000n % 60n,
236375
+ milliseconds: milliseconds % 1000n,
236376
+ microseconds: 0n,
236377
+ nanoseconds: 0n
236378
+ };
236379
+ }
236380
+ function parseMilliseconds(milliseconds) {
236381
+ switch (typeof milliseconds) {
236382
+ case "number":
236383
+ if (Number.isFinite(milliseconds)) return parseNumber(milliseconds);
236384
+ break;
236385
+ case "bigint": return parseBigint(milliseconds);
236386
+ }
236387
+ throw new TypeError("Expected a finite number or bigint");
236388
+ }
236389
+ const isZero = (value) => value === 0 || value === 0n;
236390
+ const pluralize = (word, count) => count === 1 || count === 1n ? word : `${word}s`;
236391
+ const SECOND_ROUNDING_EPSILON = 1e-7;
236392
+ const ONE_DAY_IN_MILLISECONDS = 24n * 60n * 60n * 1000n;
236393
+ function prettyMilliseconds(milliseconds, options) {
236394
+ const isBigInt = typeof milliseconds === "bigint";
236395
+ if (!isBigInt && !Number.isFinite(milliseconds)) throw new TypeError("Expected a finite number or bigint");
236396
+ options = { ...options };
236397
+ const sign$2 = milliseconds < 0 ? "-" : "";
236398
+ milliseconds = milliseconds < 0 ? -milliseconds : milliseconds;
236399
+ if (options.colonNotation) {
236400
+ options.compact = false;
236401
+ options.formatSubMilliseconds = false;
236402
+ options.separateMilliseconds = false;
236403
+ options.verbose = false;
236404
+ }
236405
+ if (options.compact) {
236406
+ options.unitCount = 1;
236407
+ options.secondsDecimalDigits = 0;
236408
+ options.millisecondsDecimalDigits = 0;
236409
+ }
236410
+ let result = [];
236411
+ const floorDecimals = (value, decimalDigits) => {
236412
+ const flooredInterimValue = Math.floor(value * 10 ** decimalDigits + SECOND_ROUNDING_EPSILON);
236413
+ return (Math.round(flooredInterimValue) / 10 ** decimalDigits).toFixed(decimalDigits);
236414
+ };
236415
+ const add = (value, long, short, valueString) => {
236416
+ if ((result.length === 0 || !options.colonNotation) && isZero(value) && !(options.colonNotation && short === "m")) return;
236417
+ valueString ?? (valueString = String(value));
236418
+ if (options.colonNotation) {
236419
+ const wholeDigits = valueString.includes(".") ? valueString.split(".")[0].length : valueString.length;
236420
+ const minLength = result.length > 0 ? 2 : 1;
236421
+ valueString = "0".repeat(Math.max(0, minLength - wholeDigits)) + valueString;
236422
+ } else valueString += options.verbose ? " " + pluralize(long, value) : short;
236423
+ result.push(valueString);
236424
+ };
236425
+ const parsed = parseMilliseconds(milliseconds);
236426
+ const days = BigInt(parsed.days);
236427
+ if (options.hideYearAndDays) add(BigInt(days) * 24n + BigInt(parsed.hours), "hour", "h");
236428
+ else {
236429
+ if (options.hideYear) add(days, "day", "d");
236430
+ else {
236431
+ add(days / 365n, "year", "y");
236432
+ add(days % 365n, "day", "d");
236433
+ }
236434
+ add(Number(parsed.hours), "hour", "h");
236435
+ }
236436
+ add(Number(parsed.minutes), "minute", "m");
236437
+ if (!options.hideSeconds) if (options.separateMilliseconds || options.formatSubMilliseconds || !options.colonNotation && milliseconds < 1e3 && !options.subSecondsAsDecimals) {
236438
+ const seconds = Number(parsed.seconds);
236439
+ const milliseconds$1 = Number(parsed.milliseconds);
236440
+ const microseconds = Number(parsed.microseconds);
236441
+ const nanoseconds = Number(parsed.nanoseconds);
236442
+ add(seconds, "second", "s");
236443
+ if (options.formatSubMilliseconds) {
236444
+ add(milliseconds$1, "millisecond", "ms");
236445
+ add(microseconds, "microsecond", "µs");
236446
+ add(nanoseconds, "nanosecond", "ns");
236447
+ } else {
236448
+ const millisecondsAndBelow = milliseconds$1 + microseconds / 1e3 + nanoseconds / 1e6;
236449
+ const millisecondsDecimalDigits = typeof options.millisecondsDecimalDigits === "number" ? options.millisecondsDecimalDigits : 0;
236450
+ const roundedMilliseconds = millisecondsAndBelow >= 1 ? Math.round(millisecondsAndBelow) : Math.ceil(millisecondsAndBelow);
236451
+ const maximumMilliseconds = 1e3 - 10 ** -millisecondsDecimalDigits;
236452
+ const millisecondsString = millisecondsDecimalDigits ? Math.min(millisecondsAndBelow, maximumMilliseconds).toFixed(millisecondsDecimalDigits) : Math.min(roundedMilliseconds, maximumMilliseconds);
236453
+ add(Number.parseFloat(millisecondsString), "millisecond", "ms", millisecondsString);
236454
+ }
236455
+ } else {
236456
+ const secondsFixed = floorDecimals((isBigInt ? Number(milliseconds % ONE_DAY_IN_MILLISECONDS) : milliseconds) / 1e3 % 60, typeof options.secondsDecimalDigits === "number" ? options.secondsDecimalDigits : 1);
236457
+ const secondsString = options.keepDecimalsOnWholeSeconds ? secondsFixed : secondsFixed.replace(/\.0+$/, "");
236458
+ add(Number.parseFloat(secondsString), "second", "s", secondsString);
236459
+ }
236460
+ if (result.length === 0) return sign$2 + "0" + (options.verbose ? " milliseconds" : "ms");
236461
+ const separator = options.colonNotation ? ":" : " ";
236462
+ if (typeof options.unitCount === "number") result = result.slice(0, Math.max(options.unitCount, 1));
236463
+ return sign$2 + result.join(separator);
236464
+ }
236465
+ async function listReleases(limit) {
236466
+ const output$3 = await exec("gh", [
236467
+ "release",
236468
+ "list",
236469
+ "--exclude-drafts",
236470
+ "--limit",
236471
+ String(limit),
236472
+ "--json",
236473
+ "tagName,publishedAt"
236474
+ ]);
236475
+ return JSON.parse(output$3).sort((a, b$2) => Date.parse(b$2.publishedAt) - Date.parse(a.publishedAt));
236476
+ }
236477
+ const COMMIT_JQ = "{hash: .sha, date: .commit.author.date, authorName: .commit.author.name, authorEmail: .commit.author.email, message: .commit.message}";
236478
+ async function getBranchHeadSha(branch) {
236479
+ return (await exec("gh", [
236480
+ "api",
236481
+ `repos/{owner}/{repo}/commits/${encodeURIComponent(branch)}`,
236482
+ "--jq",
236483
+ ".sha"
236484
+ ])).trim();
236485
+ }
236486
+ async function listReleaseCommits(fromTag, toSha) {
236487
+ const commits = (await exec("gh", [
236488
+ "api",
236489
+ "--paginate",
236490
+ ...fromTag === null ? [
236491
+ `repos/{owner}/{repo}/commits?sha=${toSha}&per_page=100`,
236492
+ "--jq",
236493
+ `.[] | ${COMMIT_JQ}`
236494
+ ] : [
236495
+ `repos/{owner}/{repo}/compare/${encodeURIComponent(fromTag)}...${toSha}?per_page=100`,
236496
+ "--jq",
236497
+ `.commits[] | ${COMMIT_JQ}`
236498
+ ]
236499
+ ])).split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
236500
+ return fromTag === null ? commits : commits.reverse();
236501
+ }
236502
+ async function releaseExists(tag) {
236503
+ try {
236504
+ await exec("gh", [
236505
+ "release",
236506
+ "view",
236507
+ tag,
236508
+ "--json",
236509
+ "tagName"
236510
+ ]);
236511
+ return true;
236512
+ } catch {
236513
+ return false;
236514
+ }
236515
+ }
236516
+ async function deleteRelease(tag) {
236517
+ await execInherit("gh", [
236518
+ "release",
236519
+ "delete",
236520
+ tag,
236521
+ "--yes"
236522
+ ]);
236523
+ await execInherit("gh", [
236524
+ "api",
236525
+ "--method",
236526
+ "DELETE",
236527
+ `repos/{owner}/{repo}/git/refs/tags/${encodeURIComponent(tag)}`
236528
+ ]);
236529
+ }
236530
+ async function createRelease(input$3) {
236531
+ await execInherit("gh", [
236532
+ "release",
236533
+ "create",
236534
+ input$3.tag,
236535
+ "--target",
236536
+ input$3.target,
236537
+ "--notes",
236538
+ input$3.notes
236539
+ ]);
236540
+ }
236541
+ async function getRepoRoot() {
236542
+ return (await exec("git", ["rev-parse", "--show-toplevel"])).trim();
236543
+ }
236544
+ async function fetchBranchAndTags(branch) {
236545
+ await exec("git", [
236546
+ "fetch",
236547
+ "origin",
236548
+ branch,
236549
+ "--tags",
236550
+ "--force"
236551
+ ]);
236552
+ }
236553
+ async function assertLocalCommitsMatch(fromTag, toSha, githubHashes) {
236554
+ for (const ref of fromTag === null ? [toSha] : [fromTag, toSha]) try {
236555
+ await exec("git", [
236556
+ "cat-file",
236557
+ "-e",
236558
+ `${ref}^{commit}`
236559
+ ]);
236560
+ } catch {
236561
+ throw new Error(`'${ref}' from GitHub is not in your local repository. Fetch it and try again.`);
236562
+ }
236563
+ const range$2 = fromTag === null ? toSha : `${fromTag}..${toSha}`;
236564
+ const localHashes = (await exec("git", ["rev-list", range$2])).split("\n").filter((line) => line.length > 0);
236565
+ const githubSet = new Set(githubHashes);
236566
+ if (!(localHashes.length === githubHashes.length && localHashes.every((hash$5) => githubSet.has(hash$5)))) throw new Error(`Local commits do not match GitHub for ${range$2}: ${localHashes.length} local, ${githubHashes.length} on GitHub.`);
236567
+ }
236568
+ var prompt_default = "You generate concise, well-structured, easy-to-read release notes from git commit history.\n\n**Audience**: end users, product managers, and non-technical stakeholders — not engineers.\n\n## What to include:\n\n- Include every change a user could notice, however small — new features, fixes, improvements, changed behavior. When in doubt, include it: your job is to translate a change into user terms, not to omit it.\n- Drop a change only when it is purely internal with zero user impact (refactors, renames, formatting, internal tooling, dependency/version bumps). Drop it silently — never mention it, not even as a generic \"internal improvements\" note.\n\n## How to write:\n\n- Describe WHAT changed and WHY it matters, not HOW it is implemented.\n- Write for someone who doesn't know the product is built with code. Use everyday language, framing each change as what the user can now do, see, or no longer worry about.\n- Avoid all technical jargon, library/framework names, and implementation-specific terminology.\n\n## Never expose:\n\n- Internal details: source code, file names or paths, class/function/variable names, API routes, database tables, config keys, dependency or package names, or any other internal identifier. Restate the change in user terms instead — omit it only if it has no user impact at all.\n- Sensitive information: credentials, secrets, tokens, internal URLs, private endpoints, or environment-specific values.\n\n## Output format:\n\n- Group related changes under `##` headings (e.g. \"New Features\", \"Bug Fixes\", \"Improvements\", \"Breaking Changes\"), with each change as a bullet point.\n- Put breaking changes in their own section and explain how users should adapt.\n- Output ONLY the release note content — no preamble, commentary, \"Here is...\", or top-level \"Release Note\" title.\n- Separate sections with `##` headings, never `---`.\n- If and ONLY if every change is purely internal with zero user impact, output exactly `_No notable changes in this release._` and nothing else — never explaining or listing what was dropped. If even one change is user-noticeable, write the note normally.\n";
236569
+ const MAP_CONCURRENCY = 3;
236570
+ async function summarizeChunks(languageModel, instructions, chunks) {
236571
+ const summaries = new Array(chunks.length);
236572
+ let nextChunk = 0;
236573
+ async function worker() {
236574
+ while (nextChunk < chunks.length) {
236575
+ const index$2 = nextChunk++;
236576
+ summaries[index$2] = (await generateText({
236577
+ model: languageModel,
236578
+ instructions,
236579
+ messages: [{
236580
+ role: "user",
236581
+ content: chunks[index$2]
236582
+ }]
236583
+ })).text.trim();
236584
+ }
236585
+ }
236586
+ await Promise.all(Array.from({ length: Math.min(MAP_CONCURRENCY, chunks.length) }, worker));
236587
+ return summaries.map((summary, index$2) => `Part ${index$2 + 1}:\n${summary}`).join("\n\n");
236588
+ }
236589
+ const CHARS_PER_TOKEN$1 = 4;
236590
+ const MIN_TOOL_OUTPUT_TOKENS = 200;
236591
+ const PROMPT_RESERVE_TOKENS$1 = 800;
236592
+ const MIN_CHUNK_BUDGET_TOKENS = 1e3;
236593
+ const MAX_TOC_LINES = 500;
236594
+ const BUDGET_EXHAUSTED = "Size limit reached. Write the release notes with the information you already have.";
236595
+ const NOISE_FILE_EXCLUDES = [
236596
+ ":(top,exclude)*.lock",
236597
+ ":(top,exclude)*-lock.*",
236598
+ ":(top,exclude)*.min.js",
236599
+ ":(top,exclude)*.min.mjs",
236600
+ ":(top,exclude)*.min.cjs",
236601
+ ":(top,exclude)*.min.css",
236602
+ ":(top,exclude)*.map"
236603
+ ];
236604
+ const diffSummaryPrompt = `You summarize parts of a large git diff for a release notes generator.
236605
+ Describe WHAT changed and any effect a user could notice.
236606
+ Be factual and terse. Do NOT write release notes. Maximum 120 words.`;
236607
+ function parseLsTree(output$3) {
236608
+ return output$3.split("\n").filter((line) => line.length > 0).map((line) => {
236609
+ const tabIdx = line.indexOf(" ");
236610
+ return tabIdx >= 0 ? line.slice(tabIdx + 1) : "";
236611
+ });
236612
+ }
236613
+ function createReleaseTools(languageModel, limits) {
236614
+ let remainingTokens = limits.maxTokens;
236615
+ function spend(text$1) {
236616
+ const maxChars = remainingTokens * CHARS_PER_TOKEN$1;
236617
+ const fitted = text$1.length <= maxChars ? text$1 : `${text$1.slice(0, maxChars)}\n[... truncated to fit the size limit]`;
236618
+ remainingTokens -= estimateTokens(fitted);
236619
+ return fitted;
236620
+ }
236621
+ async function readCommitDiff(ref) {
236622
+ const fullDiff = await exec("git", [
236623
+ "show",
236624
+ "--no-color",
236625
+ "--pretty=format:",
236626
+ ref
236627
+ ]);
236628
+ if (estimateTokens(fullDiff) <= remainingTokens) return fullDiff;
236629
+ const tocLines = (await exec("git", [
236630
+ "show",
236631
+ "--no-color",
236632
+ "--pretty=format:",
236633
+ "--numstat",
236634
+ ref
236635
+ ])).trim().split("\n");
236636
+ const toc = tocLines.length <= MAX_TOC_LINES ? tocLines.join("\n") : [...tocLines.slice(0, MAX_TOC_LINES), `[... ${tocLines.length - MAX_TOC_LINES} more files]`].join("\n");
236637
+ const header = `This diff is too large to show in full.\n\nChanged files (additions, deletions, path):\n${toc}`;
236638
+ const minimizedDiff = capFileSections(await exec("git", [
236639
+ "show",
236640
+ "--no-color",
236641
+ "--pretty=format:",
236642
+ "--unified=1",
236643
+ ref,
236644
+ "--",
236645
+ ":/",
236646
+ ...NOISE_FILE_EXCLUDES
236647
+ ]), limits.perFileCap);
236648
+ const minimized = `${header}\n\nDiff with less context, generated files excluded, and long files truncated:\n${minimizedDiff}`;
236649
+ if (estimateTokens(minimized) <= remainingTokens) return minimized;
236650
+ return `${header}\n\nSummaries of the diff:\n${await summarizeChunks(languageModel, diffSummaryPrompt, splitDiffIntoChunks(minimizedDiff, Math.max(limits.maxTokens - estimateTokens(toc) - PROMPT_RESERVE_TOKENS$1, MIN_CHUNK_BUDGET_TOKENS)).map((chunk$1) => `All changed files:\n${toc}\n\nChanges (part of a larger diff):\n${chunk$1}`))}`;
236651
+ }
236652
+ return {
236653
+ check_diff: tool({
236654
+ description: "Get the full diff of a specific commit. Returns the patch showing all changes (additions, deletions, modifications) introduced by the commit.",
236655
+ inputSchema: object$3({ commithash: string$5().describe("The commit hash, tag, branch, or any other git ref resolvable by git rev-parse.") }),
236656
+ execute: async ({ commithash }) => {
236657
+ if (remainingTokens < MIN_TOOL_OUTPUT_TOKENS) return { error: BUDGET_EXHAUSTED };
236658
+ return spend(await readCommitDiff(commithash));
236659
+ }
236660
+ }),
236661
+ browse_code: tool({
236662
+ description: "Browse the code at a specific commit. If the path points to a file, returns its content. If the path points to a folder, returns the list of files and sub-folders it contains. Pass an empty string for the path to browse the repository root.",
236663
+ inputSchema: object$3({
236664
+ commithash: string$5().describe("The commit hash, tag, branch, or any other git ref resolvable by git rev-parse."),
236665
+ path: string$5().describe("Path to a file or folder within the repository, relative to the repo root. Use forward slashes. Pass an empty string for the root.")
236666
+ }),
236667
+ execute: async ({ commithash, path: path$4 }) => {
236668
+ if (remainingTokens < MIN_TOOL_OUTPUT_TOKENS) return { error: BUDGET_EXHAUSTED };
236669
+ const normalized = path$4.replace(/^\/+|\/+$/g, "");
236670
+ if (normalized === "") return {
236671
+ type: "folder",
236672
+ items: parseLsTree(spend(await exec("git", ["ls-tree", commithash])))
236673
+ };
236674
+ const ref = `${commithash}:${normalized}`;
236675
+ let objectType$2;
236676
+ try {
236677
+ objectType$2 = (await exec("git", [
236678
+ "cat-file",
236679
+ "-t",
236680
+ ref
236681
+ ])).trim();
236682
+ } catch {
236683
+ return { error: "doesn't exists" };
236684
+ }
236685
+ if (objectType$2 === "tree") return {
236686
+ type: "folder",
236687
+ items: parseLsTree(spend(await exec("git", ["ls-tree", ref])))
236688
+ };
236689
+ if (objectType$2 === "blob") return {
236690
+ type: "file",
236691
+ content: spend(await exec("git", ["show", ref]))
236692
+ };
236693
+ return { error: "doesn't exists" };
236694
+ }
236695
+ })
236696
+ };
236697
+ }
236698
+ function capFileSections(diff, perFileCap) {
236699
+ return ("\n" + diff.trimStart()).split("\ndiff --git ").slice(1).map((section) => {
236700
+ const lines = `diff --git ${section}`.split("\n");
236701
+ const bodyLines = lines.length - 1;
236702
+ if (bodyLines <= perFileCap) return lines.join("\n");
236703
+ return [...lines.slice(0, perFileCap + 1), `[... ${bodyLines - perFileCap} more lines truncated]`].join("\n");
236704
+ }).join("\n");
236705
+ }
236706
+ const EMPTY_RELEASE_NOTES = "_No notable changes in this release._";
236707
+ const PROMPT_RESERVE_TOKENS = 800;
236708
+ const CHARS_PER_TOKEN = 4;
236709
+ const commitsSummaryPrompt = `You summarize part of a git commit history for a release notes generator.
236710
+ List every change a user could notice, each with the commit hash(es) it comes from.
236711
+ Skip purely internal changes. Be factual and terse. Do NOT write release notes. Maximum 200 words.`;
236712
+ async function generateReleaseNotes(languageModel, commits, limits) {
236713
+ const commitsContext = await buildCommitsContext(languageModel, commits, limits.maxTokens);
236714
+ return (await generateText({
236715
+ model: languageModel,
236716
+ instructions: prompt_default,
236717
+ tools: createReleaseTools(languageModel, limits),
236718
+ stopWhen: isStepCount(1e3),
236719
+ messages: [{
236720
+ role: "user",
236721
+ content: [{
236722
+ type: "text",
236723
+ text: "Here are the commits related to the release:"
236724
+ }, {
236725
+ type: "text",
236726
+ text: commitsContext
236727
+ }]
236728
+ }]
236729
+ })).text.trim();
236730
+ }
236731
+ async function buildCommitsContext(languageModel, commits, maxTokens) {
236732
+ const fullList = commits.map((commit) => formatCommit(commit, commit.message)).join("\n\n");
236733
+ if (estimateTokens(fullList) <= maxTokens) return fullList;
236734
+ const subjectList = commits.map((commit) => formatCommit(commit, commit.message.split("\n")[0])).join("\n\n");
236735
+ if (estimateTokens(subjectList) <= maxTokens) return `Only commit subjects are shown because the full messages are too large. Use check_diff for details.\n\n${subjectList}`;
236736
+ const chunkBudget = Math.max(maxTokens - PROMPT_RESERVE_TOKENS, 1e3);
236737
+ const chunks = [];
236738
+ let current = "";
236739
+ for (const commit of commits) {
236740
+ const entry = formatCommit(commit, commit.message).slice(0, chunkBudget * CHARS_PER_TOKEN);
236741
+ if (current.length > 0 && estimateTokens(`${current}\n\n${entry}`) > chunkBudget) {
236742
+ chunks.push(current);
236743
+ current = "";
236744
+ }
236745
+ current = current.length === 0 ? entry : `${current}\n\n${entry}`;
236746
+ }
236747
+ if (current.length > 0) chunks.push(current);
236748
+ const summaries = await summarizeChunks(languageModel, commitsSummaryPrompt, chunks);
236749
+ return `There are ${commits.length} commits, too many to show in full, so here are summaries of them. Use check_diff for details.\n\n${summaries}`;
236750
+ }
236751
+ function formatCommit(commit, message) {
236752
+ return [
236753
+ `- Commithash: ${commit.hash}`,
236754
+ ` - > ${message.replaceAll("\n", "\n > ")}`,
236755
+ ` - Timestamp: ${commit.date}`,
236756
+ ` - Author: ${commit.authorName} <${commit.authorEmail}>`
236757
+ ].join("\n");
236758
+ }
236759
+ const renderReleaseNotes = createRenderer({
236760
+ width: 80,
236761
+ listIndent: 2
236762
+ });
236763
+ async function releaseController(tagArg, options = {}) {
236764
+ if (options.yolo && tagArg === void 0) throw new Error("A release tag is required with --yolo.");
236765
+ const config$5 = await loadConfig(await getRepoRoot());
236766
+ const languageModel = resolveLanguageModel(resolveModelConfig(config$5.models, options.model ?? "default"));
236767
+ const releases = await listReleases(100);
236768
+ let tag = tagArg?.trim() ?? "";
236769
+ if (tagArg === void 0) {
236770
+ printRecentReleases(releases.slice(0, 5).reverse());
236771
+ tag = (await dist_default$1({
236772
+ message: "Release tag",
236773
+ theme: selectionTheme,
236774
+ validate: (value) => value.trim().length > 0 || "Release tag cannot be empty."
236775
+ })).trim();
236776
+ }
236777
+ if (tag.length === 0) throw new Error("Release tag cannot be empty.");
236778
+ const exists$2 = await releaseExists(tag);
236779
+ if (exists$2 && !options.force) {
236780
+ if (!await dist_default({
236781
+ message: source_default.yellow(`Release ${tag} already exists. Recreate it?`),
236782
+ default: false,
236783
+ theme: selectionTheme
236784
+ })) {
236785
+ console.log("Release creation cancelled.");
236786
+ return;
236787
+ }
236788
+ }
236789
+ const branch = await getDefaultBranch();
236790
+ if (branch.length === 0) throw new Error("Could not determine the default branch.");
236791
+ const tagIndex = releases.findIndex((release) => release.tagName === tag);
236792
+ const previousTag = (tagIndex === -1 ? releases[0] : releases[tagIndex + 1])?.tagName ?? null;
236793
+ const headSha = await getBranchHeadSha(branch);
236794
+ const commits = await runWithLoading("Loading commits from GitHub", () => listReleaseCommits(previousTag, headSha));
236795
+ await runWithLoading("Fetching commits", () => fetchBranchAndTags(branch));
236796
+ await assertLocalCommitsMatch(previousTag, headSha, commits.map((commit) => commit.hash));
236797
+ console.log(source_default.dim(`${commits.length} commit(s) since ${previousTag ?? "the first commit"}`));
236798
+ let notes = "";
236799
+ while (true) {
236800
+ notes = commits.length === 0 ? EMPTY_RELEASE_NOTES : await runWithLoading("Generating release notes", () => generateReleaseNotes(languageModel, commits, {
236801
+ maxTokens: config$5.maxDiffTokens ?? DEFAULT_MAX_DIFF_TOKENS,
236802
+ perFileCap: config$5.perFileCap ?? DEFAULT_PER_FILE_CAP
236803
+ }));
236804
+ console.log(renderReleaseNotes(notes).trim());
236805
+ console.log("");
236806
+ if (options.yolo || await acceptGenerated(`Create release ${source_default.cyan.bold(tag)} on ${source_default.yellow.bold(branch)}`, "generate new release notes")) break;
236807
+ }
236808
+ if (notes.length === 0) throw new Error("The selected model returned empty release notes.");
236809
+ if (exists$2) await deleteRelease(tag);
236810
+ await createRelease({
236811
+ tag,
236812
+ target: headSha,
236813
+ notes
236814
+ });
236815
+ }
236816
+ function printRecentReleases(releases) {
236817
+ if (releases.length === 0) return;
236818
+ const longestTag = Math.max(...releases.map((release) => release.tagName.length));
236819
+ console.log(source_default.blue.bold("Recent:"));
236820
+ for (const release of releases) {
236821
+ const age = prettyMilliseconds(Date.now() - Date.parse(release.publishedAt), {
236822
+ verbose: true,
236823
+ unitCount: 1
236824
+ });
236825
+ console.log(source_default.cyan(release.tagName.padEnd(longestTag)), source_default.dim(`${age} ago`));
236826
+ }
236827
+ console.log("");
236325
236828
  }
236326
236829
  async function showStylesController() {
236327
236830
  const config$5 = await loadConfig();
@@ -236358,26 +236861,54 @@ function handleError(fn$1) {
236358
236861
  }
236359
236862
  });
236360
236863
  }
236361
- const app = new Command().name("gityo").description("Stage changes, generate a commit message, create a commit, and run a post-commit git command.").option("-i, --input <input>", "Use the provided input as the commit message.").option("-s, --style <style>", "Commit message style key to use (defaults to \"default\").").option("-m, --model <model>", "Model key from config to use (defaults to \"default\").").option("-g, --generate", "Generate a commit message and commit without asking.").option("-p, --post", "Run the post-commit git command without asking.").option("-y, --yolo", "Skip all questions, and generate message, commit, run post command. [Will fail if no model available]").version(`v${version$4}`, "-v, --version", "Show the current version.").action((options) => {
236864
+ const app = new Command().enablePositionalOptions().name("gityo").description("Stage changes, generate a commit message, create a commit, and run a post-commit git command.").option("-i, --input <input>", "Use the provided input as the commit message.").option("-s, --style <style>", "Commit message style key to use (defaults to \"default\").").option("--model <model>", "Model key from config to use (defaults to \"default\").").option("-g, --generate", "Generate a commit message and commit without asking.").option("-p, --post", "Run the post-commit git command without asking.").option("-S, --staged", "Commit only staged files. Fails if nothing is staged.").option("-A, --all", "Commit all changes, not just staged files.").option("-y, --yolo", "Skip all questions, and generate message, commit, run post command. [Will fail if no model available]").version(`v${version$4}`, "-v, --version", "Show the current version.").action((options) => {
236362
236865
  if (options.generate && options.input) {
236363
236866
  console.error("Cannot use --generate and --input together.");
236364
236867
  process.exit(1);
236365
236868
  }
236366
- handleError(() => mainController(options));
236869
+ if (options.staged && options.all) {
236870
+ console.error("Cannot use --staged and --all together.");
236871
+ process.exit(1);
236872
+ }
236873
+ handleError(() => mainController({
236874
+ ...options,
236875
+ scope: options.all ? "everything" : options.staged ? "staged-only" : "staged-or-changes"
236876
+ }));
236367
236877
  });
236368
- app.command("commit").description("Stage changes, generate a commit message, create a commit, and run a post-commit git command.").option("-i, --input <input>", "Use the provided input as the commit message.").option("-s, --style <style>", "Commit message style key to use (defaults to \"default\").").option("-m, --model <model>", "Model key from config to use (defaults to \"default\").").option("-g, --generate", "Generate a commit message and commit without asking.").option("-p, --post", "Run the post-commit git command without asking.").option("-y, --yolo", "Skip all questions, and generate message, commit, run post command. [Will fail if no model available]").action((options) => {
236878
+ app.command("commit").description("Stage changes, generate a commit message, create a commit, and run a post-commit git command.").option("-i, --input <input>", "Use the provided input as the commit message.").option("-s, --style <style>", "Commit message style key to use (defaults to \"default\").").option("--model <model>", "Model key from config to use (defaults to \"default\").").option("-g, --generate", "Generate a commit message and commit without asking.").option("-p, --post", "Run the post-commit git command without asking.").option("-S, --staged", "Commit only staged files. Fails if nothing is staged.").option("-A, --all", "Commit all changes, not just staged files.").option("-y, --yolo", "Skip all questions, and generate message, commit, run post command. [Will fail if no model available]").action((options) => {
236369
236879
  if (options.generate && options.input) {
236370
236880
  console.error("Cannot use --generate and --input together.");
236371
236881
  process.exit(1);
236372
236882
  }
236373
- handleError(() => mainController(options));
236883
+ if (options.staged && options.all) {
236884
+ console.error("Cannot use --staged and --all together.");
236885
+ process.exit(1);
236886
+ }
236887
+ handleError(() => mainController({
236888
+ ...options,
236889
+ scope: options.all ? "everything" : options.staged ? "staged-only" : "staged-or-changes"
236890
+ }));
236374
236891
  });
236375
236892
  const pr = app.command("pr").description("Create and merge GitHub pull requests.");
236376
- pr.command("create").description("Create a pull request with an AI-generated title and body.").argument("[base-branch]", "Base branch to merge into (defaults to the repository default branch).").argument("[head-branch]", "Head branch with the changes (defaults to the current branch).").option("-m, --model <model>", "Model key from config to use (defaults to \"default\").").option("-t, --title-style <style>", "Title style key to use (defaults to \"default\").").option("-b, --body-style <style>", "Body style key to use (defaults to \"default\").").option("-y, --yolo", "Create the pull request without asking for confirmation.").option("-w, --web", "Open the pull request in a browser.").action((baseBranch, headBranch, options) => {
236893
+ pr.command("create").description("Create a pull request with an AI-generated title and body.").argument("[base-branch]", "Base branch to merge into (defaults to the repository default branch).").argument("[head-branch]", "Head branch with the changes (defaults to the current branch).").option("--model <model>", "Model key from config to use (defaults to \"default\").").option("-t, --title-style <style>", "Title style key to use (defaults to \"default\").").option("-b, --body-style <style>", "Body style key to use (defaults to \"default\").").option("-y, --yolo", "Create the pull request without asking for confirmation.").option("-A, --all", "Commit all local changes, not just staged files, before continuing.").option("-w, --web", "Open the pull request in a browser.").action((baseBranch, headBranch, options) => {
236377
236894
  handleError(() => createPullRequestController(baseBranch, headBranch, options));
236378
236895
  });
236379
- pr.command("merge").description("Create the pull request if needed, then merge it.").argument("[base-branch]", "Base branch to merge into (defaults to the repository default branch).").argument("[head-branch]", "Head branch with the changes (defaults to the current branch).").option("-m, --model <model>", "Model key from config to use (defaults to \"default\").").option("-t, --title-style <style>", "Title style key to use (defaults to \"default\").").option("-b, --body-style <style>", "Body style key to use (defaults to \"default\").").option("-y, --yolo", "Merge the pull request without asking for confirmation.").action((baseBranch, headBranch, options) => {
236380
- handleError(() => mergePullRequestController(baseBranch, headBranch, options));
236896
+ pr.command("merge").description("Create the pull request if needed, then merge it.").argument("[base-branch]", "Base branch to merge into (defaults to the repository default branch).").argument("[head-branch]", "Head branch with the changes (defaults to the current branch).").option("--model <model>", "Model key from config to use (defaults to \"default\").").option("-t, --title-style <style>", "Title style key to use (defaults to \"default\").").option("-b, --body-style <style>", "Body style key to use (defaults to \"default\").").option("-y, --yolo", "Merge the pull request without asking for confirmation.").option("-A, --all", "Commit all local changes, not just staged files, before continuing.").option("-m, --merge", "Merge the commits with the base branch.").option("-r, --rebase", "Rebase the commits onto the base branch.").option("-s, --squash", "Squash the commits into one commit.").action((baseBranch, headBranch, options) => {
236897
+ if ([
236898
+ options.merge,
236899
+ options.rebase,
236900
+ options.squash
236901
+ ].filter(Boolean).length > 1) {
236902
+ console.error("Use only one of --merge, --rebase, or --squash.");
236903
+ process.exit(1);
236904
+ }
236905
+ handleError(() => mergePullRequestController(baseBranch, headBranch, {
236906
+ ...options,
236907
+ mergeMethod: options.merge ? "merge" : options.rebase ? "rebase" : options.squash ? "squash" : void 0
236908
+ }));
236909
+ });
236910
+ app.command("release").description("Create a GitHub release with AI-generated release notes.").argument("[tag]", "Release tag to create (asks when omitted).").option("--model <model>", "Model key from config to use (defaults to \"default\").").option("-y, --yolo", "Create the release without asking for confirmation (requires a tag).").option("-f, --force", "Recreate the release without asking if it already exists.").action((tag, options) => {
236911
+ handleError(() => releaseController(tag, options));
236381
236912
  });
236382
236913
  app.command("config").description("Show where to manage your gityo configuration.").action(() => {
236383
236914
  handleError(showConfigController);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gityo",
3
- "version": "1.0.16",
3
+ "version": "1.1.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/NazmusSayad/gityo.git"