@denisvieiradev/gitwise-core 0.1.0 → 1.3.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.
- package/dist/index.d.ts +60 -20
- package/dist/index.js +561 -186
- package/dist/index.js.map +1 -1
- package/dist/testing/index.d.ts +3 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/types-BURqSokx.d.ts +36 -0
- package/package.json +2 -2
- package/dist/types-DnMpR1qf.d.ts +0 -29
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ var __export = (target, all) => {
|
|
|
7
7
|
// package.json
|
|
8
8
|
var package_default = {
|
|
9
9
|
name: "@denisvieiradev/gitwise-core",
|
|
10
|
-
version: "
|
|
10
|
+
version: "1.3.0",
|
|
11
11
|
description: "Shared logic for gitwise: non-interactive commit/review/pr/release commands, LLM providers, git/github primitives, prompt templates.",
|
|
12
12
|
type: "module",
|
|
13
13
|
main: "./dist/index.js",
|
|
@@ -60,7 +60,7 @@ var package_default = {
|
|
|
60
60
|
node: ">=22.12.0"
|
|
61
61
|
},
|
|
62
62
|
dependencies: {
|
|
63
|
-
"@anthropic-ai/sdk": "^0.
|
|
63
|
+
"@anthropic-ai/sdk": "^0.127.0"
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
66
|
|
|
@@ -486,10 +486,10 @@ async function stashDropNamed(cwd, stashName) {
|
|
|
486
486
|
async function cleanForced(cwd) {
|
|
487
487
|
await run(["clean", "-fd"], cwd);
|
|
488
488
|
}
|
|
489
|
-
async function showFileAtHead(cwd,
|
|
490
|
-
debug("git command", { args: ["show", `HEAD:${
|
|
489
|
+
async function showFileAtHead(cwd, path7) {
|
|
490
|
+
debug("git command", { args: ["show", `HEAD:${path7}`], cwd });
|
|
491
491
|
try {
|
|
492
|
-
const result = await exec("git", ["show", `HEAD:${
|
|
492
|
+
const result = await exec("git", ["show", `HEAD:${path7}`], {
|
|
493
493
|
cwd,
|
|
494
494
|
timeout: GIT_TIMEOUT_MS,
|
|
495
495
|
maxBuffer: GIT_MAX_BUFFER
|
|
@@ -890,22 +890,16 @@ function defaultIsProcessAlive(pid) {
|
|
|
890
890
|
}
|
|
891
891
|
|
|
892
892
|
// src/providers/claude-code.ts
|
|
893
|
-
import
|
|
893
|
+
import os2 from "os";
|
|
894
|
+
import path3 from "path";
|
|
895
|
+
|
|
896
|
+
// src/providers/cli-subprocess.ts
|
|
897
|
+
import { execSync, spawn } from "child_process";
|
|
894
898
|
import fs from "fs";
|
|
895
899
|
import os from "os";
|
|
896
900
|
import path2 from "path";
|
|
897
|
-
import {
|
|
898
|
-
|
|
899
|
-
var LARGE_PROMPT_THRESHOLD = 1e5;
|
|
900
|
-
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
901
|
-
var COMMON_CLAUDE_PATHS = [
|
|
902
|
-
// Native installs (Homebrew, manual) — preferred over npm
|
|
903
|
-
"/opt/homebrew/bin/claude",
|
|
904
|
-
"/usr/local/bin/claude",
|
|
905
|
-
path2.join(os.homedir(), ".claude", "local", "claude"),
|
|
906
|
-
// npm global installs — fallback
|
|
907
|
-
path2.join(os.homedir(), ".npm-global", "bin", "claude")
|
|
908
|
-
];
|
|
901
|
+
import { StringDecoder } from "string_decoder";
|
|
902
|
+
import { clearTimeout, setTimeout as setTimeout2 } from "timers";
|
|
909
903
|
function isExecutable(filePath) {
|
|
910
904
|
try {
|
|
911
905
|
fs.accessSync(filePath, fs.constants.X_OK);
|
|
@@ -914,207 +908,441 @@ function isExecutable(filePath) {
|
|
|
914
908
|
return false;
|
|
915
909
|
}
|
|
916
910
|
}
|
|
917
|
-
function
|
|
918
|
-
if (customPath)
|
|
919
|
-
|
|
920
|
-
return null;
|
|
921
|
-
}
|
|
922
|
-
for (const candidate of COMMON_CLAUDE_PATHS) {
|
|
911
|
+
function resolveCliBinary(name, commonPaths, customPath) {
|
|
912
|
+
if (customPath) return isExecutable(customPath) ? customPath : null;
|
|
913
|
+
for (const candidate of commonPaths) {
|
|
923
914
|
if (isExecutable(candidate)) return candidate;
|
|
924
915
|
}
|
|
925
916
|
try {
|
|
926
|
-
const found = execSync(
|
|
917
|
+
const found = execSync(`which ${name}`, { stdio: "pipe" }).toString().trim();
|
|
927
918
|
if (found && isExecutable(found)) return found;
|
|
928
919
|
} catch {
|
|
929
920
|
}
|
|
930
921
|
const nvmDir = path2.join(os.homedir(), ".nvm", "versions", "node");
|
|
931
922
|
try {
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
const candidate = path2.join(nvmDir, version2, "bin", "claude");
|
|
923
|
+
for (const version2 of fs.readdirSync(nvmDir)) {
|
|
924
|
+
const candidate = path2.join(nvmDir, version2, "bin", name);
|
|
935
925
|
if (isExecutable(candidate)) return candidate;
|
|
936
926
|
}
|
|
937
927
|
} catch {
|
|
938
928
|
}
|
|
939
929
|
return null;
|
|
940
930
|
}
|
|
941
|
-
var
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
931
|
+
var LARGE_PROMPT_THRESHOLD = 1e5;
|
|
932
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
933
|
+
var KILL_GRACE_MS = 5e3;
|
|
934
|
+
var CliSubprocessProvider = class {
|
|
935
|
+
constructor(spec, models, cliPath) {
|
|
936
|
+
this.spec = spec;
|
|
945
937
|
this.models = models;
|
|
946
|
-
this.
|
|
938
|
+
this.binaryPath = cliPath || spec.resolveBinary() || spec.defaultCommand;
|
|
947
939
|
}
|
|
940
|
+
binaryPath;
|
|
948
941
|
async chat(req) {
|
|
949
|
-
const modelId = this.
|
|
950
|
-
debug(
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
942
|
+
const modelId = this.models[req.tier];
|
|
943
|
+
debug(`Calling ${this.spec.toolName}`, { model: modelId, tier: req.tier, binary: this.binaryPath });
|
|
944
|
+
const prompt = this.spec.foldSystemPrompt ? `${req.systemPrompt}
|
|
945
|
+
|
|
946
|
+
${req.userMessage}` : req.userMessage;
|
|
947
|
+
const large = Buffer.byteLength(prompt, "utf8") > LARGE_PROMPT_THRESHOLD;
|
|
948
|
+
const args = this.spec.buildArgs({ prompt, systemPrompt: req.systemPrompt, modelId, large });
|
|
949
|
+
const stdout = await this.spawnCli(args, large ? prompt : "");
|
|
950
|
+
const parsed = this.spec.parseOutput(stdout);
|
|
954
951
|
return {
|
|
955
|
-
content:
|
|
956
|
-
tokens: {
|
|
957
|
-
|
|
958
|
-
output: result.usage.output_tokens
|
|
959
|
-
}
|
|
952
|
+
content: parsed.content,
|
|
953
|
+
tokens: parsed.tokens ?? { input: 0, output: 0 },
|
|
954
|
+
tokensAvailable: parsed.tokens !== null
|
|
960
955
|
};
|
|
961
956
|
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
"
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
"--model",
|
|
969
|
-
modelId,
|
|
970
|
-
"--output-format",
|
|
971
|
-
"json"
|
|
972
|
-
];
|
|
973
|
-
return args;
|
|
974
|
-
}
|
|
975
|
-
async callViaCli(args) {
|
|
976
|
-
try {
|
|
977
|
-
const { stdout } = await execFileAsync(this.claudeBinaryPath, args, {
|
|
978
|
-
timeout: DEFAULT_TIMEOUT_MS,
|
|
979
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
980
|
-
...{ input: "" }
|
|
957
|
+
spawnCli(args, input) {
|
|
958
|
+
return new Promise((resolve, reject) => {
|
|
959
|
+
const ownGroup = process.platform !== "win32";
|
|
960
|
+
const child = spawn(this.binaryPath, args, {
|
|
961
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
962
|
+
detached: ownGroup
|
|
981
963
|
});
|
|
982
|
-
|
|
983
|
-
} catch (err) {
|
|
984
|
-
const execErr = err;
|
|
985
|
-
if (execErr.stdout) {
|
|
964
|
+
const killTree = (sig) => {
|
|
986
965
|
try {
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
}
|
|
991
|
-
} catch (parseErr) {
|
|
992
|
-
if (parseErr instanceof Error && parseErr.message.startsWith("Claude CLI error:")) {
|
|
993
|
-
throw parseErr;
|
|
994
|
-
}
|
|
966
|
+
if (ownGroup && child.pid !== void 0) process.kill(-child.pid, sig);
|
|
967
|
+
else child.kill(sig);
|
|
968
|
+
} catch {
|
|
995
969
|
}
|
|
970
|
+
};
|
|
971
|
+
const timeoutMs = this.spec.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
972
|
+
let timedOut = false;
|
|
973
|
+
let escalation;
|
|
974
|
+
const timer = setTimeout2(() => {
|
|
975
|
+
timedOut = true;
|
|
976
|
+
killTree("SIGTERM");
|
|
977
|
+
escalation = setTimeout2(() => killTree("SIGKILL"), KILL_GRACE_MS);
|
|
978
|
+
escalation.unref();
|
|
979
|
+
}, timeoutMs);
|
|
980
|
+
const reapOnExit = () => killTree("SIGKILL");
|
|
981
|
+
let interruptedBy;
|
|
982
|
+
const onSignal = (sig) => {
|
|
983
|
+
interruptedBy = sig;
|
|
984
|
+
killTree("SIGKILL");
|
|
985
|
+
reject(new Error(`${this.spec.toolName} was interrupted by ${sig}`));
|
|
986
|
+
cleanup();
|
|
987
|
+
if (process.listenerCount(sig) === 0) process.kill(process.pid, sig);
|
|
988
|
+
};
|
|
989
|
+
const onSigint = () => onSignal("SIGINT");
|
|
990
|
+
const onSigterm = () => onSignal("SIGTERM");
|
|
991
|
+
process.once("exit", reapOnExit);
|
|
992
|
+
process.on("SIGINT", onSigint);
|
|
993
|
+
process.on("SIGTERM", onSigterm);
|
|
994
|
+
function cleanup() {
|
|
995
|
+
clearTimeout(timer);
|
|
996
|
+
clearTimeout(escalation);
|
|
997
|
+
process.off("exit", reapOnExit);
|
|
998
|
+
process.off("SIGINT", onSigint);
|
|
999
|
+
process.off("SIGTERM", onSigterm);
|
|
996
1000
|
}
|
|
997
|
-
const stderr = execErr.stderr?.replace(/Warning: no stdin data.*\n?/g, "").trim();
|
|
998
|
-
if (stderr) {
|
|
999
|
-
throw new Error(`Claude CLI failed: ${stderr}`);
|
|
1000
|
-
}
|
|
1001
|
-
throw this.wrapError(err);
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
async callViaStdin(args, input) {
|
|
1005
|
-
return new Promise((resolve, reject) => {
|
|
1006
|
-
const child = spawn(this.claudeBinaryPath, args, {
|
|
1007
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1008
|
-
timeout: DEFAULT_TIMEOUT_MS
|
|
1009
|
-
});
|
|
1010
1001
|
let stdout = "";
|
|
1011
1002
|
let stderr = "";
|
|
1003
|
+
const outDecoder = new StringDecoder("utf8");
|
|
1004
|
+
const errDecoder = new StringDecoder("utf8");
|
|
1012
1005
|
child.stdout.on("data", (data) => {
|
|
1013
|
-
stdout +=
|
|
1006
|
+
stdout += outDecoder.write(data);
|
|
1014
1007
|
});
|
|
1015
1008
|
child.stderr.on("data", (data) => {
|
|
1016
|
-
stderr +=
|
|
1009
|
+
stderr += errDecoder.write(data);
|
|
1017
1010
|
});
|
|
1018
|
-
child.on("close", (code) => {
|
|
1019
|
-
if (
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
}
|
|
1027
|
-
} catch {
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
const filteredStderr = stderr.replace(/Warning: no stdin data.*\n?/g, "").trim();
|
|
1031
|
-
reject(
|
|
1032
|
-
new Error(
|
|
1033
|
-
`Claude CLI exited with code ${code}${filteredStderr ? `: ${filteredStderr}` : ""}`
|
|
1034
|
-
)
|
|
1035
|
-
);
|
|
1011
|
+
child.on("close", (code, signal) => {
|
|
1012
|
+
if (timedOut) killTree("SIGKILL");
|
|
1013
|
+
cleanup();
|
|
1014
|
+
stdout += outDecoder.end();
|
|
1015
|
+
stderr += errDecoder.end();
|
|
1016
|
+
if (interruptedBy) return;
|
|
1017
|
+
if (timedOut) {
|
|
1018
|
+
reject(new Error(`${this.spec.toolName} timed out after ${Math.round(timeoutMs / 1e3)}s`));
|
|
1036
1019
|
return;
|
|
1037
1020
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
reject(err);
|
|
1021
|
+
if (code === null && signal) {
|
|
1022
|
+
reject(new Error(`${this.spec.toolName} was terminated by signal ${signal}`));
|
|
1023
|
+
return;
|
|
1042
1024
|
}
|
|
1025
|
+
if (code !== 0) {
|
|
1026
|
+
reject(new Error(this.exitErrorMessage(code, stdout, stderr)));
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
resolve(stdout);
|
|
1043
1030
|
});
|
|
1044
1031
|
child.on("error", (err) => {
|
|
1032
|
+
cleanup();
|
|
1045
1033
|
reject(this.wrapError(err));
|
|
1046
1034
|
});
|
|
1035
|
+
child.stdin.on("error", () => void 0);
|
|
1047
1036
|
child.stdin.write(input);
|
|
1048
1037
|
child.stdin.end();
|
|
1049
1038
|
});
|
|
1050
1039
|
}
|
|
1051
|
-
|
|
1040
|
+
exitErrorMessage(code, stdout, stderr) {
|
|
1041
|
+
if (this.spec.formatExitError) return this.spec.formatExitError(code, stdout, stderr);
|
|
1042
|
+
const trimmed = stderr.trim();
|
|
1043
|
+
return `${this.spec.toolName} exited with code ${code}${trimmed ? `: ${trimmed}` : ""}`;
|
|
1044
|
+
}
|
|
1045
|
+
wrapError(err) {
|
|
1046
|
+
const message = err instanceof Error ? err.message : String(err?.message ?? err);
|
|
1047
|
+
const code = err?.code;
|
|
1048
|
+
if (code === "ENOENT" || message.includes("ENOENT")) {
|
|
1049
|
+
return new GitwiseError({
|
|
1050
|
+
code: "PROVIDER_UNAVAILABLE",
|
|
1051
|
+
message: `${this.spec.toolName} not found at "${this.binaryPath}". ${this.spec.installHint}`,
|
|
1052
|
+
exitCode: EXIT_CODES.API_FAILED,
|
|
1053
|
+
cause: err
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
return err instanceof Error ? err : new Error(message);
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
// src/providers/claude-code.ts
|
|
1061
|
+
var COMMON_CLAUDE_PATHS = [
|
|
1062
|
+
// Native installs (Homebrew, manual) — preferred over npm
|
|
1063
|
+
"/opt/homebrew/bin/claude",
|
|
1064
|
+
"/usr/local/bin/claude",
|
|
1065
|
+
path3.join(os2.homedir(), ".claude", "local", "claude"),
|
|
1066
|
+
// npm global installs — fallback
|
|
1067
|
+
path3.join(os2.homedir(), ".npm-global", "bin", "claude")
|
|
1068
|
+
];
|
|
1069
|
+
function resolveClaudeBinary(customPath) {
|
|
1070
|
+
return resolveCliBinary("claude", COMMON_CLAUDE_PATHS, customPath);
|
|
1071
|
+
}
|
|
1072
|
+
var claudeCodeSpec = {
|
|
1073
|
+
toolName: "Claude Code CLI",
|
|
1074
|
+
installHint: "Re-run `gw config` to reconfigure.",
|
|
1075
|
+
defaultCommand: "claude",
|
|
1076
|
+
foldSystemPrompt: false,
|
|
1077
|
+
resolveBinary: resolveClaudeBinary,
|
|
1078
|
+
buildArgs({ prompt, systemPrompt, modelId, large }) {
|
|
1079
|
+
return [
|
|
1080
|
+
"-p",
|
|
1081
|
+
...large ? [] : [prompt],
|
|
1082
|
+
"--system-prompt",
|
|
1083
|
+
systemPrompt,
|
|
1084
|
+
"--model",
|
|
1085
|
+
modelId,
|
|
1086
|
+
"--output-format",
|
|
1087
|
+
"json"
|
|
1088
|
+
];
|
|
1089
|
+
},
|
|
1090
|
+
parseOutput(stdout) {
|
|
1052
1091
|
const parsed = JSON.parse(stdout);
|
|
1053
1092
|
if (parsed.is_error) {
|
|
1054
1093
|
throw new Error(`Claude CLI returned error: ${parsed.result}`);
|
|
1055
1094
|
}
|
|
1056
|
-
const usage = { input_tokens: 0, output_tokens: 0 };
|
|
1057
|
-
if (parsed.usage) {
|
|
1058
|
-
usage.input_tokens = parsed.usage.input_tokens ?? 0;
|
|
1059
|
-
usage.output_tokens = parsed.usage.output_tokens ?? 0;
|
|
1060
|
-
}
|
|
1061
1095
|
return {
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1096
|
+
content: parsed.result ?? "",
|
|
1097
|
+
tokens: {
|
|
1098
|
+
input: parsed.usage?.input_tokens ?? 0,
|
|
1099
|
+
output: parsed.usage?.output_tokens ?? 0
|
|
1100
|
+
}
|
|
1065
1101
|
};
|
|
1102
|
+
},
|
|
1103
|
+
formatExitError(code, stdout, stderr) {
|
|
1104
|
+
if (stdout) {
|
|
1105
|
+
try {
|
|
1106
|
+
const parsed = JSON.parse(stdout);
|
|
1107
|
+
if (parsed.is_error) return `Claude CLI error: ${parsed.result}`;
|
|
1108
|
+
} catch {
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
const filteredStderr = stderr.replace(/Warning: no stdin data.*\n?/g, "").trim();
|
|
1112
|
+
return `Claude CLI exited with code ${code}${filteredStderr ? `: ${filteredStderr}` : ""}`;
|
|
1066
1113
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1114
|
+
};
|
|
1115
|
+
var ClaudeCodeProvider = class extends CliSubprocessProvider {
|
|
1116
|
+
constructor(models, claudeCliPath) {
|
|
1117
|
+
super(claudeCodeSpec, models, claudeCliPath);
|
|
1069
1118
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
// src/providers/codex.ts
|
|
1122
|
+
import os3 from "os";
|
|
1123
|
+
import path4 from "path";
|
|
1124
|
+
var COMMON_CODEX_PATHS = [
|
|
1125
|
+
// Native installs (Homebrew cask, manual, standalone installer) — preferred over npm
|
|
1126
|
+
"/opt/homebrew/bin/codex",
|
|
1127
|
+
"/usr/local/bin/codex",
|
|
1128
|
+
path4.join(os3.homedir(), ".local", "bin", "codex"),
|
|
1129
|
+
// npm global installs (`npm install -g @openai/codex`) — fallback
|
|
1130
|
+
path4.join(os3.homedir(), ".npm-global", "bin", "codex")
|
|
1131
|
+
];
|
|
1132
|
+
function resolveCodexBinary(customPath) {
|
|
1133
|
+
return resolveCliBinary("codex", COMMON_CODEX_PATHS, customPath);
|
|
1134
|
+
}
|
|
1135
|
+
function parseEvents(stdout) {
|
|
1136
|
+
const events = [];
|
|
1137
|
+
for (const line of stdout.split("\n")) {
|
|
1138
|
+
const trimmed = line.trim();
|
|
1139
|
+
if (!trimmed) continue;
|
|
1140
|
+
try {
|
|
1141
|
+
events.push(JSON.parse(trimmed));
|
|
1142
|
+
} catch {
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return events;
|
|
1146
|
+
}
|
|
1147
|
+
var codexSpec = {
|
|
1148
|
+
toolName: "Codex CLI",
|
|
1149
|
+
installHint: "Install it (`npm install -g @openai/codex`) or re-run `gw provider` to choose another provider.",
|
|
1150
|
+
defaultCommand: "codex",
|
|
1151
|
+
foldSystemPrompt: true,
|
|
1152
|
+
// Full agent turns take longer than a single Claude Code completion.
|
|
1153
|
+
timeoutMs: 3e5,
|
|
1154
|
+
resolveBinary: resolveCodexBinary,
|
|
1155
|
+
buildArgs({ prompt, modelId, large }) {
|
|
1156
|
+
return [
|
|
1157
|
+
"exec",
|
|
1158
|
+
"--json",
|
|
1159
|
+
"--ephemeral",
|
|
1160
|
+
"--skip-git-repo-check",
|
|
1161
|
+
"--sandbox",
|
|
1162
|
+
"read-only",
|
|
1163
|
+
"--model",
|
|
1164
|
+
modelId,
|
|
1165
|
+
"--",
|
|
1166
|
+
large ? "-" : prompt
|
|
1167
|
+
];
|
|
1168
|
+
},
|
|
1169
|
+
parseOutput(stdout) {
|
|
1170
|
+
let content;
|
|
1171
|
+
let tokens = null;
|
|
1172
|
+
for (const event of parseEvents(stdout)) {
|
|
1173
|
+
if (event.type === "item.completed" && event.item?.type === "agent_message") {
|
|
1174
|
+
content = event.item.text ?? "";
|
|
1175
|
+
} else if (event.type === "turn.completed" && event.usage) {
|
|
1176
|
+
tokens = {
|
|
1177
|
+
input: event.usage.input_tokens ?? 0,
|
|
1178
|
+
output: event.usage.output_tokens ?? 0
|
|
1179
|
+
};
|
|
1079
1180
|
}
|
|
1080
|
-
return err;
|
|
1081
1181
|
}
|
|
1082
|
-
|
|
1182
|
+
if (content === void 0) {
|
|
1183
|
+
throw new Error("Codex CLI returned no final agent message");
|
|
1184
|
+
}
|
|
1185
|
+
return { content, tokens };
|
|
1186
|
+
},
|
|
1187
|
+
// The CLI's own error text lives in the JSONL stream, not stderr; surface it
|
|
1188
|
+
// verbatim, falling back to stderr when the stream carries none.
|
|
1189
|
+
formatExitError(code, stdout, stderr) {
|
|
1190
|
+
const messages = /* @__PURE__ */ new Set();
|
|
1191
|
+
for (const event of parseEvents(stdout)) {
|
|
1192
|
+
if (event.type === "error" && event.message) messages.add(event.message);
|
|
1193
|
+
if (event.type === "turn.failed" && event.error?.message) messages.add(event.error.message);
|
|
1194
|
+
}
|
|
1195
|
+
const detail = messages.size > 0 ? [...messages].join("\n") : stderr.trim();
|
|
1196
|
+
return `Codex CLI exited with code ${code}${detail ? `: ${detail}` : ""}`;
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
// src/providers/copilot.ts
|
|
1201
|
+
import os4 from "os";
|
|
1202
|
+
import path5 from "path";
|
|
1203
|
+
var COMMON_COPILOT_PATHS = [
|
|
1204
|
+
// Native/Homebrew installs — preferred over npm
|
|
1205
|
+
"/opt/homebrew/bin/copilot",
|
|
1206
|
+
"/usr/local/bin/copilot",
|
|
1207
|
+
// Copilot's install script (non-root) target
|
|
1208
|
+
path5.join(os4.homedir(), ".local", "bin", "copilot"),
|
|
1209
|
+
// npm global installs (`npm install -g @github/copilot`) — fallback
|
|
1210
|
+
path5.join(os4.homedir(), ".npm-global", "bin", "copilot")
|
|
1211
|
+
];
|
|
1212
|
+
function resolveCopilotBinary(customPath) {
|
|
1213
|
+
return resolveCliBinary("copilot", COMMON_COPILOT_PATHS, customPath);
|
|
1214
|
+
}
|
|
1215
|
+
var copilotSpec = {
|
|
1216
|
+
toolName: "Copilot CLI",
|
|
1217
|
+
installHint: "Install it (`npm install -g @github/copilot`) or re-run `gw provider` to choose another provider.",
|
|
1218
|
+
defaultCommand: "copilot",
|
|
1219
|
+
foldSystemPrompt: true,
|
|
1220
|
+
// Full agent turns take longer than a single Claude Code completion.
|
|
1221
|
+
timeoutMs: 3e5,
|
|
1222
|
+
resolveBinary: resolveCopilotBinary,
|
|
1223
|
+
buildArgs({ prompt, modelId, large }) {
|
|
1224
|
+
return [...large ? [] : [`--prompt=${prompt}`], "--no-ask-user", "--silent", "--model", modelId];
|
|
1225
|
+
},
|
|
1226
|
+
parseOutput(stdout) {
|
|
1227
|
+
const content = stdout.trim();
|
|
1228
|
+
if (!content) throw new Error("Copilot CLI returned an empty response");
|
|
1229
|
+
return { content, tokens: null };
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
// src/providers/kiro.ts
|
|
1234
|
+
import os5 from "os";
|
|
1235
|
+
import path6 from "path";
|
|
1236
|
+
import { stripVTControlCharacters } from "util";
|
|
1237
|
+
var COMMON_KIRO_PATHS = [
|
|
1238
|
+
// macOS app bundle and the installer's symlink location — preferred
|
|
1239
|
+
"/Applications/Kiro CLI.app/Contents/MacOS/kiro-cli",
|
|
1240
|
+
path6.join(os5.homedir(), ".local", "bin", "kiro-cli"),
|
|
1241
|
+
"/opt/homebrew/bin/kiro-cli",
|
|
1242
|
+
"/usr/local/bin/kiro-cli"
|
|
1243
|
+
];
|
|
1244
|
+
function resolveKiroBinary(customPath) {
|
|
1245
|
+
return resolveCliBinary("kiro-cli", COMMON_KIRO_PATHS, customPath);
|
|
1246
|
+
}
|
|
1247
|
+
var kiroSpec = {
|
|
1248
|
+
toolName: "Kiro CLI",
|
|
1249
|
+
installHint: "Install it from https://kiro.dev/docs/cli/ or re-run `gw provider` to choose another provider.",
|
|
1250
|
+
defaultCommand: "kiro-cli",
|
|
1251
|
+
foldSystemPrompt: true,
|
|
1252
|
+
// Full agent turns take longer than a single Claude Code completion.
|
|
1253
|
+
timeoutMs: 3e5,
|
|
1254
|
+
resolveBinary: resolveKiroBinary,
|
|
1255
|
+
buildArgs({ prompt, modelId, large }) {
|
|
1256
|
+
return [
|
|
1257
|
+
"chat",
|
|
1258
|
+
"--no-interactive",
|
|
1259
|
+
"--trust-tools=",
|
|
1260
|
+
"--wrap",
|
|
1261
|
+
"never",
|
|
1262
|
+
"--model",
|
|
1263
|
+
modelId,
|
|
1264
|
+
// `--` (standard for kiro-cli's clap-style parser) keeps a prompt that
|
|
1265
|
+
// starts with "-" from being read as an option.
|
|
1266
|
+
...large ? [] : ["--", prompt]
|
|
1267
|
+
];
|
|
1268
|
+
},
|
|
1269
|
+
parseOutput(stdout) {
|
|
1270
|
+
const content = stripVTControlCharacters(stdout).trim();
|
|
1271
|
+
if (!content) throw new Error("Kiro CLI returned an empty response");
|
|
1272
|
+
return { content, tokens: null };
|
|
1273
|
+
},
|
|
1274
|
+
formatExitError(code, stdout, stderr) {
|
|
1275
|
+
const detail = stderr.trim() || stripVTControlCharacters(stdout).trim();
|
|
1276
|
+
return `Kiro CLI exited with code ${code}${detail ? `: ${detail}` : ""}`;
|
|
1083
1277
|
}
|
|
1084
1278
|
};
|
|
1085
1279
|
|
|
1086
1280
|
// src/config/types.ts
|
|
1281
|
+
var CLAUDE_MODELS = {
|
|
1282
|
+
fast: "claude-haiku-4-5-20251001",
|
|
1283
|
+
balanced: "claude-sonnet-4-6",
|
|
1284
|
+
powerful: "claude-opus-4-7"
|
|
1285
|
+
};
|
|
1087
1286
|
var DEFAULT_USER_CONFIG = {
|
|
1088
1287
|
provider: "api",
|
|
1089
1288
|
models: {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1289
|
+
api: { ...CLAUDE_MODELS },
|
|
1290
|
+
"claude-code": { ...CLAUDE_MODELS },
|
|
1291
|
+
codex: {
|
|
1292
|
+
fast: "gpt-6-luna",
|
|
1293
|
+
balanced: "gpt-6-sol",
|
|
1294
|
+
powerful: "gpt-6-astra"
|
|
1295
|
+
},
|
|
1296
|
+
copilot: {
|
|
1297
|
+
fast: "claude-haiku-4.5",
|
|
1298
|
+
balanced: "claude-sonnet-4.6",
|
|
1299
|
+
powerful: "claude-opus-4.7"
|
|
1300
|
+
},
|
|
1301
|
+
kiro: {
|
|
1302
|
+
fast: "claude-haiku-4.5",
|
|
1303
|
+
balanced: "claude-sonnet-4.5",
|
|
1304
|
+
powerful: "claude-sonnet-4.5"
|
|
1305
|
+
}
|
|
1093
1306
|
},
|
|
1094
1307
|
language: "en",
|
|
1095
1308
|
commitConvention: "conventional"
|
|
1096
1309
|
};
|
|
1097
1310
|
|
|
1098
1311
|
// src/config/merge.ts
|
|
1099
|
-
import
|
|
1312
|
+
import os7 from "os";
|
|
1100
1313
|
|
|
1101
1314
|
// src/config/user.ts
|
|
1102
1315
|
import { join as join2 } from "path";
|
|
1103
|
-
import
|
|
1316
|
+
import os6 from "os";
|
|
1317
|
+
|
|
1318
|
+
// src/providers/types.ts
|
|
1319
|
+
var PROVIDER_KINDS = ["api", "claude-code", "codex", "copilot", "kiro"];
|
|
1320
|
+
|
|
1321
|
+
// src/config/user.ts
|
|
1104
1322
|
var GITWISE_DIR = ".gitwise";
|
|
1105
1323
|
var USER_CONFIG_FILE = "config.json";
|
|
1106
1324
|
function getUserConfigPath(homeDir) {
|
|
1107
|
-
return join2(homeDir ??
|
|
1325
|
+
return join2(homeDir ?? os6.homedir(), GITWISE_DIR, USER_CONFIG_FILE);
|
|
1326
|
+
}
|
|
1327
|
+
function isLegacyFlatModels(value) {
|
|
1328
|
+
if (!value || typeof value !== "object") return false;
|
|
1329
|
+
const v = value;
|
|
1330
|
+
return typeof v["fast"] === "string" && typeof v["balanced"] === "string" && typeof v["powerful"] === "string";
|
|
1331
|
+
}
|
|
1332
|
+
function migrateFlatModels(flat, provider) {
|
|
1333
|
+
const target = provider === void 0 ? DEFAULT_USER_CONFIG.provider : provider;
|
|
1334
|
+
const migrated = { ...DEFAULT_USER_CONFIG.models };
|
|
1335
|
+
if (typeof target === "string" && PROVIDER_KINDS.includes(target)) {
|
|
1336
|
+
migrated[target] = { ...flat };
|
|
1337
|
+
}
|
|
1338
|
+
return migrated;
|
|
1108
1339
|
}
|
|
1109
1340
|
function mergeWithDefaults(partial) {
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
...partial
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
...partial.models ?? {}
|
|
1116
|
-
}
|
|
1117
|
-
};
|
|
1341
|
+
const models = {};
|
|
1342
|
+
for (const kind of PROVIDER_KINDS) {
|
|
1343
|
+
models[kind] = { ...DEFAULT_USER_CONFIG.models[kind], ...partial.models?.[kind] ?? {} };
|
|
1344
|
+
}
|
|
1345
|
+
return { ...DEFAULT_USER_CONFIG, ...partial, models };
|
|
1118
1346
|
}
|
|
1119
1347
|
async function readUserConfig(homeDir) {
|
|
1120
1348
|
const configPath = getUserConfigPath(homeDir);
|
|
@@ -1123,6 +1351,17 @@ async function readUserConfig(homeDir) {
|
|
|
1123
1351
|
return { ...DEFAULT_USER_CONFIG };
|
|
1124
1352
|
}
|
|
1125
1353
|
const raw = await readJSON(configPath);
|
|
1354
|
+
if (isLegacyFlatModels(raw.models)) {
|
|
1355
|
+
const migratedModels = migrateFlatModels(raw.models, raw.provider);
|
|
1356
|
+
const merged = mergeWithDefaults({ ...raw, models: migratedModels });
|
|
1357
|
+
debug("Migrated legacy flat models config to per-provider shape", { path: configPath });
|
|
1358
|
+
try {
|
|
1359
|
+
await writeJSON(configPath, merged);
|
|
1360
|
+
} catch (err) {
|
|
1361
|
+
debug("Could not persist migrated config; using it in memory", { path: configPath, error: String(err) });
|
|
1362
|
+
}
|
|
1363
|
+
return merged;
|
|
1364
|
+
}
|
|
1126
1365
|
return mergeWithDefaults(raw);
|
|
1127
1366
|
}
|
|
1128
1367
|
async function writeUserConfig(partial, homeDir) {
|
|
@@ -1133,7 +1372,7 @@ async function writeUserConfig(partial, homeDir) {
|
|
|
1133
1372
|
await writeJSON(configPath, updated);
|
|
1134
1373
|
}
|
|
1135
1374
|
async function writeApiKey(value, homeDir) {
|
|
1136
|
-
const home = homeDir ??
|
|
1375
|
+
const home = homeDir ?? os6.homedir();
|
|
1137
1376
|
await writeEnvVar(home, "ANTHROPIC_API_KEY", value);
|
|
1138
1377
|
}
|
|
1139
1378
|
|
|
@@ -1160,6 +1399,26 @@ async function readRepoConfig(cwd) {
|
|
|
1160
1399
|
}
|
|
1161
1400
|
|
|
1162
1401
|
// src/config/merge.ts
|
|
1402
|
+
function isPlainObject(value) {
|
|
1403
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1404
|
+
}
|
|
1405
|
+
var MODEL_TIERS = ["fast", "balanced", "powerful"];
|
|
1406
|
+
function mergeRepoModels(base, override) {
|
|
1407
|
+
if (!isPlainObject(override)) return base.models;
|
|
1408
|
+
const source = override;
|
|
1409
|
+
const flat = {};
|
|
1410
|
+
for (const tier of MODEL_TIERS) {
|
|
1411
|
+
const value = source[tier];
|
|
1412
|
+
if (typeof value === "string") flat[tier] = value;
|
|
1413
|
+
}
|
|
1414
|
+
const merged = { ...base.models };
|
|
1415
|
+
merged[base.provider] = { ...base.models[base.provider], ...flat };
|
|
1416
|
+
for (const kind of PROVIDER_KINDS) {
|
|
1417
|
+
const block = source[kind];
|
|
1418
|
+
if (isPlainObject(block)) merged[kind] = { ...merged[kind], ...block };
|
|
1419
|
+
}
|
|
1420
|
+
return merged;
|
|
1421
|
+
}
|
|
1163
1422
|
function deepMerge(base, override) {
|
|
1164
1423
|
return {
|
|
1165
1424
|
...base,
|
|
@@ -1169,10 +1428,7 @@ function deepMerge(base, override) {
|
|
|
1169
1428
|
...override.templatesPath !== void 0 && { templatesPath: override.templatesPath },
|
|
1170
1429
|
...override.releaseStrategy !== void 0 && { releaseStrategy: override.releaseStrategy },
|
|
1171
1430
|
...override.developBranch !== void 0 && { developBranch: override.developBranch },
|
|
1172
|
-
models:
|
|
1173
|
-
...base.models,
|
|
1174
|
-
...override.models ?? {}
|
|
1175
|
-
}
|
|
1431
|
+
models: mergeRepoModels(base, override.models)
|
|
1176
1432
|
};
|
|
1177
1433
|
}
|
|
1178
1434
|
async function getMergedConfig(options) {
|
|
@@ -1185,7 +1441,7 @@ async function getMergedConfig(options) {
|
|
|
1185
1441
|
return deepMerge(userConfig, repoConfig);
|
|
1186
1442
|
}
|
|
1187
1443
|
async function getApiKey(homeDir) {
|
|
1188
|
-
const home = homeDir ??
|
|
1444
|
+
const home = homeDir ?? os7.homedir();
|
|
1189
1445
|
return read("ANTHROPIC_API_KEY", home);
|
|
1190
1446
|
}
|
|
1191
1447
|
|
|
@@ -1193,7 +1449,7 @@ async function getApiKey(homeDir) {
|
|
|
1193
1449
|
import { readFile as readFile4 } from "fs/promises";
|
|
1194
1450
|
import { dirname as dirname2, join as join4 } from "path";
|
|
1195
1451
|
import { fileURLToPath } from "url";
|
|
1196
|
-
import
|
|
1452
|
+
import os8 from "os";
|
|
1197
1453
|
|
|
1198
1454
|
// src/template/interpolate.ts
|
|
1199
1455
|
function interpolate(template, ctx) {
|
|
@@ -1221,7 +1477,7 @@ function validateTemplateName(name) {
|
|
|
1221
1477
|
async function loadTemplate(name, options = {}) {
|
|
1222
1478
|
validateTemplateName(name);
|
|
1223
1479
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
1224
|
-
const userTemplatesPath = options.templatesPath ?? join4(
|
|
1480
|
+
const userTemplatesPath = options.templatesPath ?? join4(os8.homedir(), ".gitwise", "templates");
|
|
1225
1481
|
const repoOverride = join4(repoRoot, ".gitwise", "templates", `${name}.md`);
|
|
1226
1482
|
if (await fileExists(repoOverride)) {
|
|
1227
1483
|
debug("Loading repo-level template override", { path: repoOverride });
|
|
@@ -1462,20 +1718,22 @@ IMPORTANT: Generate exactly 3 different alternative commit messages. Return JSON
|
|
|
1462
1718
|
const response = await provider.chat({ systemPrompt: effectiveSystemPrompt, userMessage, tier });
|
|
1463
1719
|
const parsed = parseCommitResponse(response.content);
|
|
1464
1720
|
const tokens = { input: response.tokens.input, output: response.tokens.output };
|
|
1721
|
+
const tokensAvailable = response.tokensAvailable;
|
|
1465
1722
|
if (opts.generateAlternatives) {
|
|
1466
1723
|
const options = parseAlternativesResponse(response.content);
|
|
1467
1724
|
if (options && options.length > 0) {
|
|
1468
|
-
return { kind: "alternatives", options, tokens };
|
|
1725
|
+
return { kind: "alternatives", options, tokens, tokensAvailable };
|
|
1469
1726
|
}
|
|
1470
1727
|
const fallbackMsg = parsed.type === "single" ? parsed.message : parsed.commits[0]?.message ?? response.content.trim().slice(0, 100);
|
|
1471
|
-
return { kind: "alternatives", options: [fallbackMsg], tokens };
|
|
1728
|
+
return { kind: "alternatives", options: [fallbackMsg], tokens, tokensAvailable };
|
|
1472
1729
|
}
|
|
1473
1730
|
if (split === "never") {
|
|
1474
1731
|
const message2 = parsed.type === "single" ? parsed.message : parsed.commits.map((c) => c.message).join("\n\n");
|
|
1475
1732
|
return {
|
|
1476
1733
|
kind: "single",
|
|
1477
1734
|
commits: [{ message: message2, files: stagedFiles }],
|
|
1478
|
-
tokens
|
|
1735
|
+
tokens,
|
|
1736
|
+
tokensAvailable
|
|
1479
1737
|
};
|
|
1480
1738
|
}
|
|
1481
1739
|
if (parsed.type === "plan" && parsed.commits.length > 1) {
|
|
@@ -1488,7 +1746,8 @@ IMPORTANT: Generate exactly 3 different alternative commit messages. Return JSON
|
|
|
1488
1746
|
return {
|
|
1489
1747
|
kind: "split",
|
|
1490
1748
|
commits: parsed.commits,
|
|
1491
|
-
tokens
|
|
1749
|
+
tokens,
|
|
1750
|
+
tokensAvailable
|
|
1492
1751
|
};
|
|
1493
1752
|
}
|
|
1494
1753
|
}
|
|
@@ -1503,7 +1762,8 @@ IMPORTANT: Generate exactly 3 different alternative commit messages. Return JSON
|
|
|
1503
1762
|
return {
|
|
1504
1763
|
kind: "single",
|
|
1505
1764
|
commits: [{ message, files: stagedFiles }],
|
|
1506
|
-
tokens
|
|
1765
|
+
tokens,
|
|
1766
|
+
tokensAvailable
|
|
1507
1767
|
};
|
|
1508
1768
|
}
|
|
1509
1769
|
function takeNamedStashStep(cwd, stashName) {
|
|
@@ -1713,7 +1973,8 @@ Additional context: ${prompt}` : "");
|
|
|
1713
1973
|
suggestions: parsed.suggestions,
|
|
1714
1974
|
nitpicks: parsed.nitpicks,
|
|
1715
1975
|
markdown,
|
|
1716
|
-
tokens
|
|
1976
|
+
tokens,
|
|
1977
|
+
tokensAvailable: response.tokensAvailable
|
|
1717
1978
|
};
|
|
1718
1979
|
}
|
|
1719
1980
|
async function resolveBaseBranch(cwd) {
|
|
@@ -1740,9 +2001,9 @@ ${stderr}`;
|
|
|
1740
2001
|
}
|
|
1741
2002
|
|
|
1742
2003
|
// src/commands/pr.ts
|
|
1743
|
-
import { execFile as
|
|
1744
|
-
import { promisify as
|
|
1745
|
-
var exec3 =
|
|
2004
|
+
import { execFile as execFile3 } from "child_process";
|
|
2005
|
+
import { promisify as promisify3 } from "util";
|
|
2006
|
+
var exec3 = promisify3(execFile3);
|
|
1746
2007
|
function parsePrResponse(content) {
|
|
1747
2008
|
const titleMatch = content.match(/^TITLE:\s*(.+)$/m);
|
|
1748
2009
|
const title = titleMatch ? titleMatch[1].trim() : "Update";
|
|
@@ -1821,7 +2082,8 @@ Additional context: ${prompt}` : "");
|
|
|
1821
2082
|
title,
|
|
1822
2083
|
body,
|
|
1823
2084
|
existingPrNumber,
|
|
1824
|
-
tokens
|
|
2085
|
+
tokens,
|
|
2086
|
+
tokensAvailable: response.tokensAvailable
|
|
1825
2087
|
};
|
|
1826
2088
|
}
|
|
1827
2089
|
async function applyPr(draft, opts) {
|
|
@@ -1938,7 +2200,7 @@ async function loadReleasePlan(cwd) {
|
|
|
1938
2200
|
exitCode: EXIT_CODES.CONFIG_INVALID
|
|
1939
2201
|
});
|
|
1940
2202
|
}
|
|
1941
|
-
return parsed;
|
|
2203
|
+
return { ...parsed, tokensAvailable: parsed.tokensAvailable ?? true };
|
|
1942
2204
|
}
|
|
1943
2205
|
function isPersistedReleasePlan(value) {
|
|
1944
2206
|
if (!value || typeof value !== "object") return false;
|
|
@@ -1961,6 +2223,7 @@ function isPersistedReleasePlan(value) {
|
|
|
1961
2223
|
const tokens = p.tokens;
|
|
1962
2224
|
if (typeof tokens.input !== "number" || !Number.isFinite(tokens.input)) return false;
|
|
1963
2225
|
if (typeof tokens.output !== "number" || !Number.isFinite(tokens.output)) return false;
|
|
2226
|
+
if (p.tokensAvailable !== void 0 && typeof p.tokensAvailable !== "boolean") return false;
|
|
1964
2227
|
return true;
|
|
1965
2228
|
}
|
|
1966
2229
|
async function deleteReleasePlan(cwd) {
|
|
@@ -2092,6 +2355,7 @@ async function release(opts) {
|
|
|
2092
2355
|
const tier = resolveModelTier("release");
|
|
2093
2356
|
let totalInput = 0;
|
|
2094
2357
|
let totalOutput = 0;
|
|
2358
|
+
let tokensAvailable = true;
|
|
2095
2359
|
let suggestedBump;
|
|
2096
2360
|
if (opts.bump) {
|
|
2097
2361
|
suggestedBump = opts.bump;
|
|
@@ -2109,6 +2373,7 @@ ${commits}`,
|
|
|
2109
2373
|
});
|
|
2110
2374
|
totalInput += versionResponse.tokens.input;
|
|
2111
2375
|
totalOutput += versionResponse.tokens.output;
|
|
2376
|
+
tokensAvailable = versionResponse.tokensAvailable;
|
|
2112
2377
|
const suggestion = parseVersionSuggestion(versionResponse.content);
|
|
2113
2378
|
suggestedBump = suggestion?.suggestion ?? heuristicBump(commits);
|
|
2114
2379
|
}
|
|
@@ -2126,6 +2391,7 @@ ${commits}`,
|
|
|
2126
2391
|
});
|
|
2127
2392
|
totalInput += changelogResponse.tokens.input;
|
|
2128
2393
|
totalOutput += changelogResponse.tokens.output;
|
|
2394
|
+
tokensAvailable = tokensAvailable && changelogResponse.tokensAvailable;
|
|
2129
2395
|
const changelog = changelogResponse.content;
|
|
2130
2396
|
const notesTemplate = await loadTemplate("release-notes", templateOpts);
|
|
2131
2397
|
const notesPrompt = interpolate(notesTemplate, {
|
|
@@ -2144,6 +2410,7 @@ ${commits}`,
|
|
|
2144
2410
|
});
|
|
2145
2411
|
totalInput += notesResponse.tokens.input;
|
|
2146
2412
|
totalOutput += notesResponse.tokens.output;
|
|
2413
|
+
tokensAvailable = tokensAvailable && notesResponse.tokensAvailable;
|
|
2147
2414
|
const notes = notesResponse.content;
|
|
2148
2415
|
return {
|
|
2149
2416
|
suggestedBump,
|
|
@@ -2152,7 +2419,8 @@ ${commits}`,
|
|
|
2152
2419
|
changelog,
|
|
2153
2420
|
notes,
|
|
2154
2421
|
commits,
|
|
2155
|
-
tokens: { input: totalInput, output: totalOutput }
|
|
2422
|
+
tokens: { input: totalInput, output: totalOutput },
|
|
2423
|
+
tokensAvailable
|
|
2156
2424
|
};
|
|
2157
2425
|
}
|
|
2158
2426
|
function createReleaseBranchStep(cwd, branchName, startPoint) {
|
|
@@ -2300,10 +2568,10 @@ async function prepareRelease(opts) {
|
|
|
2300
2568
|
});
|
|
2301
2569
|
try {
|
|
2302
2570
|
const dirtyEntries = (await status(cwd)).split("\n").map((line) => line.replace(/\s+$/, "")).filter((line) => line.length >= 3).filter((line) => {
|
|
2303
|
-
const
|
|
2304
|
-
if (
|
|
2305
|
-
if (
|
|
2306
|
-
if (
|
|
2571
|
+
const path7 = line.slice(3).trim();
|
|
2572
|
+
if (path7 === ".gitignore") return false;
|
|
2573
|
+
if (path7 === ".gitwise/" || path7 === ".gitwise") return false;
|
|
2574
|
+
if (path7.startsWith(".gitwise/")) return false;
|
|
2307
2575
|
return true;
|
|
2308
2576
|
});
|
|
2309
2577
|
if (dirtyEntries.length > 0) {
|
|
@@ -2401,7 +2669,8 @@ ${dirtyEntries.join("\n")}`,
|
|
|
2401
2669
|
baseCommit,
|
|
2402
2670
|
targetBranch,
|
|
2403
2671
|
releaseBranchCreated: releaseBranch !== null,
|
|
2404
|
-
tokens: plan.tokens
|
|
2672
|
+
tokens: plan.tokens,
|
|
2673
|
+
tokensAvailable: plan.tokensAvailable
|
|
2405
2674
|
};
|
|
2406
2675
|
await tx.run(savePlanStep(cwd, persistedPlan));
|
|
2407
2676
|
debug("release.prepare.plan.saved", {
|
|
@@ -2466,13 +2735,36 @@ ${dirty}`,
|
|
|
2466
2735
|
baseCommit: await headSha(cwd),
|
|
2467
2736
|
targetBranch: await getBranch(cwd),
|
|
2468
2737
|
releaseBranchCreated: false,
|
|
2469
|
-
tokens: plan.tokens
|
|
2738
|
+
tokens: plan.tokens,
|
|
2739
|
+
tokensAvailable: plan.tokensAvailable
|
|
2470
2740
|
};
|
|
2471
2741
|
await ensureGitignored(cwd, RELEASE_PLAN_REL_PATH);
|
|
2472
2742
|
await ensureGitignored(cwd, RELEASE_NOTES_GLOB_REL_PATH);
|
|
2473
2743
|
await saveReleasePlan(cwd, persistedPlan);
|
|
2474
2744
|
await finishRelease({ cwd, tagAndPush, createGhRelease, workspacePropagation, signTags });
|
|
2475
2745
|
}
|
|
2746
|
+
function finishPushFailure(opts) {
|
|
2747
|
+
const { stage, tag, mainBranch, developBranch, newVersion, err } = opts;
|
|
2748
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
2749
|
+
const action = stage === "tag" ? `create the tag "${tag}"` : stage === "push-main" ? `push "${mainBranch}" (with tags) to origin` : `push "${developBranch}" to origin`;
|
|
2750
|
+
const recoverySteps = stage === "tag" ? [
|
|
2751
|
+
`git tag -a ${tag} -F .gitwise/release-${newVersion}.md`,
|
|
2752
|
+
`git push origin ${mainBranch} --follow-tags`
|
|
2753
|
+
] : [
|
|
2754
|
+
`git ls-remote --tags origin ${tag} # check whether the tag already reached origin`,
|
|
2755
|
+
`git fetch origin`,
|
|
2756
|
+
`git merge origin/${mainBranch} # do NOT rebase \u2014 that changes the hash ${tag} points to`,
|
|
2757
|
+
`git push origin ${mainBranch} --follow-tags`
|
|
2758
|
+
];
|
|
2759
|
+
return new GitwiseError({
|
|
2760
|
+
code: "FINISH_PUSH_FAILED",
|
|
2761
|
+
message: `Failed to ${action} while finishing v${newVersion}: ${cause}. The release plan file has already been deleted, so "gw release finish" cannot be re-run, and the release commit already exists locally, so "gw release prepare" will refuse with NO_COMMITS. Recover manually:
|
|
2762
|
+
${recoverySteps.map((s) => ` ${s}`).join("\n")}`,
|
|
2763
|
+
exitCode: EXIT_CODES.GIT_FAILED,
|
|
2764
|
+
cause: err,
|
|
2765
|
+
details: { stage, tag, mainBranch, developBranch, newVersion }
|
|
2766
|
+
});
|
|
2767
|
+
}
|
|
2476
2768
|
async function finishRelease(opts) {
|
|
2477
2769
|
const {
|
|
2478
2770
|
cwd,
|
|
@@ -2663,15 +2955,27 @@ ${cause}`,
|
|
|
2663
2955
|
await checkout(cwd, mainBranch);
|
|
2664
2956
|
}
|
|
2665
2957
|
if (tagAndPush) {
|
|
2666
|
-
|
|
2667
|
-
|
|
2958
|
+
try {
|
|
2959
|
+
await createTag(cwd, tag, notes, { signed: signTags !== false });
|
|
2960
|
+
} catch (err) {
|
|
2961
|
+
throw finishPushFailure({ stage: "tag", tag, mainBranch, newVersion: plan.newVersion, err });
|
|
2962
|
+
}
|
|
2963
|
+
try {
|
|
2964
|
+
await pushWithTags(cwd, "origin", mainBranch);
|
|
2965
|
+
} catch (err) {
|
|
2966
|
+
throw finishPushFailure({ stage: "push-main", tag, mainBranch, newVersion: plan.newVersion, err });
|
|
2967
|
+
}
|
|
2668
2968
|
debug("release.finish.tag.pushed", {
|
|
2669
2969
|
tag,
|
|
2670
2970
|
branch: mainBranch,
|
|
2671
2971
|
remote: "origin"
|
|
2672
2972
|
});
|
|
2673
2973
|
if (strategy.requiresDevelop()) {
|
|
2674
|
-
|
|
2974
|
+
try {
|
|
2975
|
+
await push(cwd, "origin", developBranch);
|
|
2976
|
+
} catch (err) {
|
|
2977
|
+
throw finishPushFailure({ stage: "push-develop", tag, mainBranch, developBranch, newVersion: plan.newVersion, err });
|
|
2978
|
+
}
|
|
2675
2979
|
}
|
|
2676
2980
|
}
|
|
2677
2981
|
if (createGhRelease) {
|
|
@@ -2781,13 +3085,35 @@ async function gitignoreMatchesPrepareOutput(cwd) {
|
|
|
2781
3085
|
expected = applyGitignoreEntry(expected, RELEASE_NOTES_GLOB_REL_PATH);
|
|
2782
3086
|
return currentContent === expected;
|
|
2783
3087
|
}
|
|
2784
|
-
|
|
3088
|
+
var CROSS_WORKSPACE_DEPENDENCY_FIELDS = [
|
|
3089
|
+
"dependencies",
|
|
3090
|
+
"devDependencies",
|
|
3091
|
+
"optionalDependencies",
|
|
3092
|
+
"peerDependencies"
|
|
3093
|
+
];
|
|
3094
|
+
function updateCrossWorkspaceDependencies(parsed, workspaceNames, newVersion) {
|
|
3095
|
+
for (const field of CROSS_WORKSPACE_DEPENDENCY_FIELDS) {
|
|
3096
|
+
const deps = parsed[field];
|
|
3097
|
+
if (!deps || typeof deps !== "object") continue;
|
|
3098
|
+
const depsRecord = deps;
|
|
3099
|
+
for (const depName of Object.keys(depsRecord)) {
|
|
3100
|
+
const spec = depsRecord[depName];
|
|
3101
|
+
if (!workspaceNames.has(depName) || typeof spec !== "string") continue;
|
|
3102
|
+
const prefix = spec.startsWith("^") || spec.startsWith("~") ? spec[0] : "";
|
|
3103
|
+
depsRecord[depName] = `${prefix}${newVersion}`;
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
function writeWorkspaceVersionStep(manifestPath, newVersion, workspaceNames) {
|
|
2785
3108
|
return {
|
|
2786
3109
|
name: `write-version:${manifestPath}`,
|
|
2787
3110
|
apply: async () => {
|
|
2788
3111
|
const priorBytes = await readFile6(manifestPath);
|
|
2789
3112
|
const parsed = JSON.parse(priorBytes.toString("utf-8"));
|
|
2790
3113
|
parsed["version"] = newVersion;
|
|
3114
|
+
if (workspaceNames && workspaceNames.size > 0) {
|
|
3115
|
+
updateCrossWorkspaceDependencies(parsed, workspaceNames, newVersion);
|
|
3116
|
+
}
|
|
2791
3117
|
await writeJSON(manifestPath, parsed);
|
|
2792
3118
|
return priorBytes;
|
|
2793
3119
|
},
|
|
@@ -2826,17 +3152,28 @@ async function propagateVersionToWorkspaces(cwd, version2) {
|
|
|
2826
3152
|
async function runWorkspaceVersionStepsInto(tx, cwd, version2) {
|
|
2827
3153
|
const patterns = await readWorkspacePatterns(cwd);
|
|
2828
3154
|
const workspaceDirs = (await expandWorkspacePatterns(cwd, patterns)).sort();
|
|
3155
|
+
const workspaceNames = /* @__PURE__ */ new Set();
|
|
3156
|
+
for (const dir of workspaceDirs) {
|
|
3157
|
+
const pkgPath = join6(dir, "package.json");
|
|
3158
|
+
if (!await fileExists(pkgPath)) continue;
|
|
3159
|
+
const parsed = await readJSON(pkgPath);
|
|
3160
|
+
if (typeof parsed.name === "string") workspaceNames.add(parsed.name);
|
|
3161
|
+
}
|
|
2829
3162
|
const modified = [];
|
|
2830
3163
|
for (const dir of workspaceDirs) {
|
|
2831
3164
|
const pkgPath = join6(dir, "package.json");
|
|
2832
3165
|
if (await fileExists(pkgPath)) {
|
|
2833
|
-
await tx.run(writeWorkspaceVersionStep(pkgPath, version2));
|
|
3166
|
+
await tx.run(writeWorkspaceVersionStep(pkgPath, version2, workspaceNames));
|
|
2834
3167
|
modified.push(relative(cwd, pkgPath));
|
|
2835
3168
|
}
|
|
2836
|
-
const pluginPath
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
3169
|
+
for (const pluginPath of [
|
|
3170
|
+
join6(dir, ".claude-plugin", "plugin.json"),
|
|
3171
|
+
join6(dir, "plugin.json")
|
|
3172
|
+
]) {
|
|
3173
|
+
if (await fileExists(pluginPath)) {
|
|
3174
|
+
await tx.run(writeWorkspaceVersionStep(pluginPath, version2));
|
|
3175
|
+
modified.push(relative(cwd, pluginPath));
|
|
3176
|
+
}
|
|
2840
3177
|
}
|
|
2841
3178
|
}
|
|
2842
3179
|
return modified;
|
|
@@ -2928,6 +3265,11 @@ function segmentToRegex(segment) {
|
|
|
2928
3265
|
return new RegExp(`^${escaped}$`);
|
|
2929
3266
|
}
|
|
2930
3267
|
|
|
3268
|
+
// src/commands/token-format.ts
|
|
3269
|
+
function formatTokens(tokens, tokensAvailable) {
|
|
3270
|
+
return tokensAvailable ? `${tokens.input} in / ${tokens.output} out` : "n/a";
|
|
3271
|
+
}
|
|
3272
|
+
|
|
2931
3273
|
// src/providers/anthropic.ts
|
|
2932
3274
|
import Anthropic from "@anthropic-ai/sdk";
|
|
2933
3275
|
var DEFAULT_MAX_TOKENS = 4096;
|
|
@@ -2984,7 +3326,8 @@ var AnthropicProvider = class {
|
|
|
2984
3326
|
tokens: {
|
|
2985
3327
|
input: response.usage.input_tokens,
|
|
2986
3328
|
output: response.usage.output_tokens
|
|
2987
|
-
}
|
|
3329
|
+
},
|
|
3330
|
+
tokensAvailable: true
|
|
2988
3331
|
};
|
|
2989
3332
|
}
|
|
2990
3333
|
resolveModel(tier) {
|
|
@@ -3003,10 +3346,36 @@ var AnthropicProvider = class {
|
|
|
3003
3346
|
|
|
3004
3347
|
// src/providers/factory.ts
|
|
3005
3348
|
function createProvider(config) {
|
|
3006
|
-
|
|
3007
|
-
|
|
3349
|
+
switch (config.kind) {
|
|
3350
|
+
case "claude-code":
|
|
3351
|
+
return new ClaudeCodeProvider(config.models, config.claudeCliPath);
|
|
3352
|
+
case "codex":
|
|
3353
|
+
return new CliSubprocessProvider(codexSpec, config.models, config.codexCliPath);
|
|
3354
|
+
case "copilot":
|
|
3355
|
+
return new CliSubprocessProvider(copilotSpec, config.models, config.copilotCliPath);
|
|
3356
|
+
case "kiro":
|
|
3357
|
+
return new CliSubprocessProvider(kiroSpec, config.models, config.kiroCliPath);
|
|
3358
|
+
case "api":
|
|
3359
|
+
return new AnthropicProvider(config.apiKey, config.models);
|
|
3360
|
+
default: {
|
|
3361
|
+
const unhandled = config.kind;
|
|
3362
|
+
throw new GitwiseError({
|
|
3363
|
+
code: "CONFIG_INVALID",
|
|
3364
|
+
message: `Unknown provider "${String(unhandled)}" in config. Re-run \`gw provider\` to choose a supported provider.`
|
|
3365
|
+
});
|
|
3366
|
+
}
|
|
3008
3367
|
}
|
|
3009
|
-
|
|
3368
|
+
}
|
|
3369
|
+
function buildProviderConfig(merged, apiKey) {
|
|
3370
|
+
return {
|
|
3371
|
+
kind: merged.provider,
|
|
3372
|
+
models: merged.models[merged.provider],
|
|
3373
|
+
apiKey,
|
|
3374
|
+
claudeCliPath: merged.claudeCliPath,
|
|
3375
|
+
codexCliPath: merged.codexCliPath,
|
|
3376
|
+
copilotCliPath: merged.copilotCliPath,
|
|
3377
|
+
kiroCliPath: merged.kiroCliPath
|
|
3378
|
+
};
|
|
3010
3379
|
}
|
|
3011
3380
|
|
|
3012
3381
|
// src/index.ts
|
|
@@ -3016,6 +3385,7 @@ export {
|
|
|
3016
3385
|
DEFAULT_USER_CONFIG,
|
|
3017
3386
|
EXIT_CODES,
|
|
3018
3387
|
GitwiseError,
|
|
3388
|
+
PROVIDER_KINDS,
|
|
3019
3389
|
STALE_LOCK_MS,
|
|
3020
3390
|
SUPPORTED_COMMANDS,
|
|
3021
3391
|
Transaction,
|
|
@@ -3026,6 +3396,7 @@ export {
|
|
|
3026
3396
|
applyOneCommitStep,
|
|
3027
3397
|
applyPr,
|
|
3028
3398
|
applyRelease,
|
|
3399
|
+
buildProviderConfig,
|
|
3029
3400
|
bumpVersion,
|
|
3030
3401
|
commit2 as commit,
|
|
3031
3402
|
createProvider,
|
|
@@ -3039,6 +3410,7 @@ export {
|
|
|
3039
3410
|
error,
|
|
3040
3411
|
fileExists,
|
|
3041
3412
|
finishRelease,
|
|
3413
|
+
formatTokens,
|
|
3042
3414
|
getApiKey,
|
|
3043
3415
|
getMergedConfig,
|
|
3044
3416
|
git_exports as git,
|
|
@@ -3059,6 +3431,9 @@ export {
|
|
|
3059
3431
|
readUserConfig,
|
|
3060
3432
|
release,
|
|
3061
3433
|
resolveClaudeBinary,
|
|
3434
|
+
resolveCodexBinary,
|
|
3435
|
+
resolveCopilotBinary,
|
|
3436
|
+
resolveKiroBinary,
|
|
3062
3437
|
resolveModelTier,
|
|
3063
3438
|
review,
|
|
3064
3439
|
runReleaseInProcess,
|