@locusai/cli 0.22.9 → 0.22.11
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/bin/locus.js +335 -75
- package/package.json +2 -2
package/bin/locus.js
CHANGED
|
@@ -1952,7 +1952,8 @@ ${bold2("Initializing Locus...")}
|
|
|
1952
1952
|
join6(locusDir, "discussions"),
|
|
1953
1953
|
join6(locusDir, "artifacts"),
|
|
1954
1954
|
join6(locusDir, "plans"),
|
|
1955
|
-
join6(locusDir, "logs")
|
|
1955
|
+
join6(locusDir, "logs"),
|
|
1956
|
+
join6(locusDir, "run-state")
|
|
1956
1957
|
];
|
|
1957
1958
|
for (const dir of dirs) {
|
|
1958
1959
|
if (!existsSync6(dir)) {
|
|
@@ -1987,6 +1988,8 @@ ${bold2("Initializing Locus...")}
|
|
|
1987
1988
|
config.logging = { ...config.logging, ...existing.logging };
|
|
1988
1989
|
if (existing.sandbox)
|
|
1989
1990
|
config.sandbox = { ...config.sandbox, ...existing.sandbox };
|
|
1991
|
+
if (existing.packages)
|
|
1992
|
+
config.packages = existing.packages;
|
|
1990
1993
|
} catch {}
|
|
1991
1994
|
process.stderr.write(`${green("✓")} Updated config.json (preserved existing settings)
|
|
1992
1995
|
`);
|
|
@@ -4893,39 +4896,49 @@ class ClaudeRunner {
|
|
|
4893
4896
|
stdio: ["pipe", "pipe", "pipe"],
|
|
4894
4897
|
env
|
|
4895
4898
|
});
|
|
4899
|
+
let flushLineBuffer = null;
|
|
4896
4900
|
if (options.verbose) {
|
|
4897
4901
|
let lineBuffer = "";
|
|
4898
4902
|
const seenToolIds = new Set;
|
|
4903
|
+
const processLine = (line) => {
|
|
4904
|
+
if (!line.trim())
|
|
4905
|
+
return;
|
|
4906
|
+
try {
|
|
4907
|
+
const event = JSON.parse(line);
|
|
4908
|
+
if (event.type === "assistant" && event.message?.content) {
|
|
4909
|
+
for (const item of event.message.content) {
|
|
4910
|
+
if (item.type === "tool_use" && item.id && !seenToolIds.has(item.id)) {
|
|
4911
|
+
seenToolIds.add(item.id);
|
|
4912
|
+
options.onToolActivity?.(formatToolCall(item.name ?? "", item.input ?? {}));
|
|
4913
|
+
}
|
|
4914
|
+
}
|
|
4915
|
+
} else if (event.type === "result") {
|
|
4916
|
+
const text = event.result ?? "";
|
|
4917
|
+
output = text;
|
|
4918
|
+
options.onOutput?.(text);
|
|
4919
|
+
}
|
|
4920
|
+
} catch {
|
|
4921
|
+
const newLine = `${line}
|
|
4922
|
+
`;
|
|
4923
|
+
output += newLine;
|
|
4924
|
+
options.onOutput?.(newLine);
|
|
4925
|
+
}
|
|
4926
|
+
};
|
|
4899
4927
|
this.process.stdout?.on("data", (chunk) => {
|
|
4900
4928
|
lineBuffer += chunk.toString();
|
|
4901
4929
|
const lines = lineBuffer.split(`
|
|
4902
4930
|
`);
|
|
4903
4931
|
lineBuffer = lines.pop() ?? "";
|
|
4904
4932
|
for (const line of lines) {
|
|
4905
|
-
|
|
4906
|
-
continue;
|
|
4907
|
-
try {
|
|
4908
|
-
const event = JSON.parse(line);
|
|
4909
|
-
if (event.type === "assistant" && event.message?.content) {
|
|
4910
|
-
for (const item of event.message.content) {
|
|
4911
|
-
if (item.type === "tool_use" && item.id && !seenToolIds.has(item.id)) {
|
|
4912
|
-
seenToolIds.add(item.id);
|
|
4913
|
-
options.onToolActivity?.(formatToolCall(item.name ?? "", item.input ?? {}));
|
|
4914
|
-
}
|
|
4915
|
-
}
|
|
4916
|
-
} else if (event.type === "result") {
|
|
4917
|
-
const text = event.result ?? "";
|
|
4918
|
-
output = text;
|
|
4919
|
-
options.onOutput?.(text);
|
|
4920
|
-
}
|
|
4921
|
-
} catch {
|
|
4922
|
-
const newLine = `${line}
|
|
4923
|
-
`;
|
|
4924
|
-
output += newLine;
|
|
4925
|
-
options.onOutput?.(newLine);
|
|
4926
|
-
}
|
|
4933
|
+
processLine(line);
|
|
4927
4934
|
}
|
|
4928
4935
|
});
|
|
4936
|
+
flushLineBuffer = () => {
|
|
4937
|
+
if (lineBuffer.trim()) {
|
|
4938
|
+
processLine(lineBuffer);
|
|
4939
|
+
lineBuffer = "";
|
|
4940
|
+
}
|
|
4941
|
+
};
|
|
4929
4942
|
} else {
|
|
4930
4943
|
this.process.stdout?.on("data", (chunk) => {
|
|
4931
4944
|
const text = chunk.toString();
|
|
@@ -4940,6 +4953,7 @@ class ClaudeRunner {
|
|
|
4940
4953
|
});
|
|
4941
4954
|
this.process.on("close", (code) => {
|
|
4942
4955
|
this.process = null;
|
|
4956
|
+
flushLineBuffer?.();
|
|
4943
4957
|
if (this.aborted) {
|
|
4944
4958
|
resolve2({
|
|
4945
4959
|
success: false,
|
|
@@ -5314,39 +5328,49 @@ class SandboxedClaudeRunner {
|
|
|
5314
5328
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5315
5329
|
env: process.env
|
|
5316
5330
|
});
|
|
5331
|
+
let flushLineBuffer = null;
|
|
5317
5332
|
if (options.verbose) {
|
|
5318
5333
|
let lineBuffer = "";
|
|
5319
5334
|
const seenToolIds = new Set;
|
|
5335
|
+
const processLine = (line) => {
|
|
5336
|
+
if (!line.trim())
|
|
5337
|
+
return;
|
|
5338
|
+
try {
|
|
5339
|
+
const event = JSON.parse(line);
|
|
5340
|
+
if (event.type === "assistant" && event.message?.content) {
|
|
5341
|
+
for (const item of event.message.content) {
|
|
5342
|
+
if (item.type === "tool_use" && item.id && !seenToolIds.has(item.id)) {
|
|
5343
|
+
seenToolIds.add(item.id);
|
|
5344
|
+
options.onToolActivity?.(formatToolCall2(item.name ?? "", item.input ?? {}));
|
|
5345
|
+
}
|
|
5346
|
+
}
|
|
5347
|
+
} else if (event.type === "result") {
|
|
5348
|
+
const text = event.result ?? "";
|
|
5349
|
+
output = text;
|
|
5350
|
+
options.onOutput?.(text);
|
|
5351
|
+
}
|
|
5352
|
+
} catch {
|
|
5353
|
+
const newLine = `${line}
|
|
5354
|
+
`;
|
|
5355
|
+
output += newLine;
|
|
5356
|
+
options.onOutput?.(newLine);
|
|
5357
|
+
}
|
|
5358
|
+
};
|
|
5320
5359
|
this.process.stdout?.on("data", (chunk) => {
|
|
5321
5360
|
lineBuffer += chunk.toString();
|
|
5322
5361
|
const lines = lineBuffer.split(`
|
|
5323
5362
|
`);
|
|
5324
5363
|
lineBuffer = lines.pop() ?? "";
|
|
5325
5364
|
for (const line of lines) {
|
|
5326
|
-
|
|
5327
|
-
continue;
|
|
5328
|
-
try {
|
|
5329
|
-
const event = JSON.parse(line);
|
|
5330
|
-
if (event.type === "assistant" && event.message?.content) {
|
|
5331
|
-
for (const item of event.message.content) {
|
|
5332
|
-
if (item.type === "tool_use" && item.id && !seenToolIds.has(item.id)) {
|
|
5333
|
-
seenToolIds.add(item.id);
|
|
5334
|
-
options.onToolActivity?.(formatToolCall2(item.name ?? "", item.input ?? {}));
|
|
5335
|
-
}
|
|
5336
|
-
}
|
|
5337
|
-
} else if (event.type === "result") {
|
|
5338
|
-
const text = event.result ?? "";
|
|
5339
|
-
output = text;
|
|
5340
|
-
options.onOutput?.(text);
|
|
5341
|
-
}
|
|
5342
|
-
} catch {
|
|
5343
|
-
const newLine = `${line}
|
|
5344
|
-
`;
|
|
5345
|
-
output += newLine;
|
|
5346
|
-
options.onOutput?.(newLine);
|
|
5347
|
-
}
|
|
5365
|
+
processLine(line);
|
|
5348
5366
|
}
|
|
5349
5367
|
});
|
|
5368
|
+
flushLineBuffer = () => {
|
|
5369
|
+
if (lineBuffer.trim()) {
|
|
5370
|
+
processLine(lineBuffer);
|
|
5371
|
+
lineBuffer = "";
|
|
5372
|
+
}
|
|
5373
|
+
};
|
|
5350
5374
|
} else {
|
|
5351
5375
|
this.process.stdout?.on("data", (chunk) => {
|
|
5352
5376
|
const text = chunk.toString();
|
|
@@ -5361,6 +5385,7 @@ class SandboxedClaudeRunner {
|
|
|
5361
5385
|
});
|
|
5362
5386
|
this.process.on("close", (code) => {
|
|
5363
5387
|
this.process = null;
|
|
5388
|
+
flushLineBuffer?.();
|
|
5364
5389
|
if (this.aborted) {
|
|
5365
5390
|
resolve2({
|
|
5366
5391
|
success: false,
|
|
@@ -10177,22 +10202,32 @@ import {
|
|
|
10177
10202
|
writeFileSync as writeFileSync9
|
|
10178
10203
|
} from "node:fs";
|
|
10179
10204
|
import { dirname as dirname6, join as join20 } from "node:path";
|
|
10180
|
-
function
|
|
10181
|
-
return join20(projectRoot, ".locus", "run-state
|
|
10205
|
+
function getRunStateDir(projectRoot) {
|
|
10206
|
+
return join20(projectRoot, ".locus", "run-state");
|
|
10207
|
+
}
|
|
10208
|
+
function sprintSlug(name) {
|
|
10209
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
10210
|
+
}
|
|
10211
|
+
function getRunStatePath(projectRoot, sprintName) {
|
|
10212
|
+
const dir = getRunStateDir(projectRoot);
|
|
10213
|
+
if (sprintName) {
|
|
10214
|
+
return join20(dir, `${sprintSlug(sprintName)}.json`);
|
|
10215
|
+
}
|
|
10216
|
+
return join20(dir, "_parallel.json");
|
|
10182
10217
|
}
|
|
10183
|
-
function loadRunState(projectRoot) {
|
|
10184
|
-
const path = getRunStatePath(projectRoot);
|
|
10218
|
+
function loadRunState(projectRoot, sprintName) {
|
|
10219
|
+
const path = getRunStatePath(projectRoot, sprintName);
|
|
10185
10220
|
if (!existsSync20(path))
|
|
10186
10221
|
return null;
|
|
10187
10222
|
try {
|
|
10188
10223
|
return JSON.parse(readFileSync12(path, "utf-8"));
|
|
10189
10224
|
} catch {
|
|
10190
|
-
getLogger().warn("Corrupted run
|
|
10225
|
+
getLogger().warn("Corrupted run state file, ignoring");
|
|
10191
10226
|
return null;
|
|
10192
10227
|
}
|
|
10193
10228
|
}
|
|
10194
10229
|
function saveRunState(projectRoot, state) {
|
|
10195
|
-
const path = getRunStatePath(projectRoot);
|
|
10230
|
+
const path = getRunStatePath(projectRoot, state.sprint);
|
|
10196
10231
|
const dir = dirname6(path);
|
|
10197
10232
|
if (!existsSync20(dir)) {
|
|
10198
10233
|
mkdirSync14(dir, { recursive: true });
|
|
@@ -10200,8 +10235,8 @@ function saveRunState(projectRoot, state) {
|
|
|
10200
10235
|
writeFileSync9(path, `${JSON.stringify(state, null, 2)}
|
|
10201
10236
|
`, "utf-8");
|
|
10202
10237
|
}
|
|
10203
|
-
function clearRunState(projectRoot) {
|
|
10204
|
-
const path = getRunStatePath(projectRoot);
|
|
10238
|
+
function clearRunState(projectRoot, sprintName) {
|
|
10239
|
+
const path = getRunStatePath(projectRoot, sprintName);
|
|
10205
10240
|
if (existsSync20(path)) {
|
|
10206
10241
|
unlinkSync5(path);
|
|
10207
10242
|
}
|
|
@@ -10503,7 +10538,12 @@ function resolveExecutionContext(config, modelOverride) {
|
|
|
10503
10538
|
const model = modelOverride ?? config.ai.model;
|
|
10504
10539
|
const provider = inferProviderFromModel(model) ?? config.ai.provider;
|
|
10505
10540
|
const sandboxName = getModelSandboxName(config.sandbox, model, provider);
|
|
10506
|
-
return {
|
|
10541
|
+
return {
|
|
10542
|
+
provider,
|
|
10543
|
+
model,
|
|
10544
|
+
sandboxName,
|
|
10545
|
+
containerWorkdir: config.sandbox.containerWorkdir
|
|
10546
|
+
};
|
|
10507
10547
|
}
|
|
10508
10548
|
function printRunHelp() {
|
|
10509
10549
|
process.stderr.write(`
|
|
@@ -10544,9 +10584,10 @@ async function runCommand(projectRoot, args, flags = {}) {
|
|
|
10544
10584
|
}
|
|
10545
10585
|
const config = loadConfig(projectRoot);
|
|
10546
10586
|
const _log = getLogger();
|
|
10587
|
+
const runRef = { sprintName: undefined };
|
|
10547
10588
|
const cleanupShutdown = registerShutdownHandlers({
|
|
10548
10589
|
projectRoot,
|
|
10549
|
-
getRunState: () => loadRunState(projectRoot)
|
|
10590
|
+
getRunState: () => loadRunState(projectRoot, runRef.sprintName)
|
|
10550
10591
|
});
|
|
10551
10592
|
try {
|
|
10552
10593
|
const sandboxMode = resolveSandboxMode(config.sandbox, flags);
|
|
@@ -10580,11 +10621,11 @@ async function runCommand(projectRoot, args, flags = {}) {
|
|
|
10580
10621
|
}
|
|
10581
10622
|
}
|
|
10582
10623
|
if (flags.resume) {
|
|
10583
|
-
return handleResume(projectRoot, config, sandboxed);
|
|
10624
|
+
return handleResume(projectRoot, config, sandboxed, runRef);
|
|
10584
10625
|
}
|
|
10585
10626
|
const issueNumbers = args.filter((a) => /^\d+$/.test(a)).map(Number);
|
|
10586
10627
|
if (issueNumbers.length === 0) {
|
|
10587
|
-
return handleSprintRun(projectRoot, config, flags, sandboxed);
|
|
10628
|
+
return handleSprintRun(projectRoot, config, flags, sandboxed, runRef);
|
|
10588
10629
|
}
|
|
10589
10630
|
if (issueNumbers.length === 1) {
|
|
10590
10631
|
return handleSingleIssue(projectRoot, config, issueNumbers[0], flags, sandboxed);
|
|
@@ -10594,7 +10635,7 @@ async function runCommand(projectRoot, args, flags = {}) {
|
|
|
10594
10635
|
cleanupShutdown();
|
|
10595
10636
|
}
|
|
10596
10637
|
}
|
|
10597
|
-
async function handleSprintRun(projectRoot, config, flags, sandboxed) {
|
|
10638
|
+
async function handleSprintRun(projectRoot, config, flags, sandboxed, runRef) {
|
|
10598
10639
|
const log = getLogger();
|
|
10599
10640
|
const execution = resolveExecutionContext(config, flags.model);
|
|
10600
10641
|
if (!config.sprint.active) {
|
|
@@ -10605,15 +10646,16 @@ async function handleSprintRun(projectRoot, config, flags, sandboxed) {
|
|
|
10605
10646
|
return;
|
|
10606
10647
|
}
|
|
10607
10648
|
const sprintName = config.sprint.active;
|
|
10649
|
+
runRef.sprintName = sprintName;
|
|
10608
10650
|
process.stderr.write(`
|
|
10609
10651
|
${bold2("Sprint:")} ${cyan2(sprintName)}
|
|
10610
10652
|
`);
|
|
10611
|
-
const existingState = loadRunState(projectRoot);
|
|
10612
|
-
if (existingState
|
|
10653
|
+
const existingState = loadRunState(projectRoot, sprintName);
|
|
10654
|
+
if (existingState) {
|
|
10613
10655
|
const stats2 = getRunStats(existingState);
|
|
10614
10656
|
if (stats2.inProgress > 0 || stats2.pending > 0) {
|
|
10615
10657
|
process.stderr.write(`
|
|
10616
|
-
${yellow2("⚠")} A sprint
|
|
10658
|
+
${yellow2("⚠")} A run for this sprint is already in progress.
|
|
10617
10659
|
`);
|
|
10618
10660
|
process.stderr.write(` Use ${bold2("locus run --resume")} to continue.
|
|
10619
10661
|
`);
|
|
@@ -10785,7 +10827,7 @@ ${bold2("Summary:")}
|
|
|
10785
10827
|
}
|
|
10786
10828
|
}
|
|
10787
10829
|
if (stats.failed === 0) {
|
|
10788
|
-
clearRunState(projectRoot);
|
|
10830
|
+
clearRunState(projectRoot, sprintName);
|
|
10789
10831
|
}
|
|
10790
10832
|
}
|
|
10791
10833
|
async function handleSingleIssue(projectRoot, config, issueNumber, flags, sandboxed) {
|
|
@@ -10966,14 +11008,19 @@ ${yellow2("⚠")} Failed worktrees preserved for debugging:
|
|
|
10966
11008
|
clearRunState(projectRoot);
|
|
10967
11009
|
}
|
|
10968
11010
|
}
|
|
10969
|
-
async function handleResume(projectRoot, config, sandboxed) {
|
|
11011
|
+
async function handleResume(projectRoot, config, sandboxed, runRef) {
|
|
10970
11012
|
const execution = resolveExecutionContext(config);
|
|
10971
|
-
const
|
|
11013
|
+
const sprintName = config.sprint.active ?? undefined;
|
|
11014
|
+
let state = sprintName ? loadRunState(projectRoot, sprintName) : null;
|
|
11015
|
+
if (!state) {
|
|
11016
|
+
state = loadRunState(projectRoot);
|
|
11017
|
+
}
|
|
10972
11018
|
if (!state) {
|
|
10973
11019
|
process.stderr.write(`${red2("✗")} No run state found. Nothing to resume.
|
|
10974
11020
|
`);
|
|
10975
11021
|
return;
|
|
10976
11022
|
}
|
|
11023
|
+
runRef.sprintName = state.sprint;
|
|
10977
11024
|
const stats = getRunStats(state);
|
|
10978
11025
|
process.stderr.write(`
|
|
10979
11026
|
${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}
|
|
@@ -11068,7 +11115,7 @@ ${bold2("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fai
|
|
|
11068
11115
|
}
|
|
11069
11116
|
}
|
|
11070
11117
|
if (finalStats.failed === 0) {
|
|
11071
|
-
clearRunState(projectRoot);
|
|
11118
|
+
clearRunState(projectRoot, state.sprint);
|
|
11072
11119
|
}
|
|
11073
11120
|
}
|
|
11074
11121
|
function sortByOrder2(issues) {
|
|
@@ -13018,6 +13065,196 @@ var init_artifacts = __esm(() => {
|
|
|
13018
13065
|
init_terminal();
|
|
13019
13066
|
});
|
|
13020
13067
|
|
|
13068
|
+
// src/commands/commit.ts
|
|
13069
|
+
var exports_commit = {};
|
|
13070
|
+
__export(exports_commit, {
|
|
13071
|
+
commitCommand: () => commitCommand
|
|
13072
|
+
});
|
|
13073
|
+
import { execSync as execSync20 } from "node:child_process";
|
|
13074
|
+
function printCommitHelp() {
|
|
13075
|
+
process.stderr.write(`
|
|
13076
|
+
${bold2("locus commit")} — AI-powered commit message generation
|
|
13077
|
+
|
|
13078
|
+
${bold2("Usage:")}
|
|
13079
|
+
locus commit ${dim2("# Analyze staged changes and commit")}
|
|
13080
|
+
locus commit --dry-run ${dim2("# Preview message without committing")}
|
|
13081
|
+
locus commit --model <name> ${dim2("# Override AI model")}
|
|
13082
|
+
|
|
13083
|
+
${bold2("Options:")}
|
|
13084
|
+
--dry-run Show the generated message without committing
|
|
13085
|
+
--model <name> Override the AI model for message generation
|
|
13086
|
+
|
|
13087
|
+
${bold2("How it works:")}
|
|
13088
|
+
1. Reads your staged changes (git diff --cached)
|
|
13089
|
+
2. Reads recent commit history for style matching
|
|
13090
|
+
3. Uses AI to generate a conventional commit message
|
|
13091
|
+
4. Appends Co-Authored-By trailer and commits
|
|
13092
|
+
|
|
13093
|
+
${bold2("Examples:")}
|
|
13094
|
+
locus commit ${dim2("# Stage changes, then run")}
|
|
13095
|
+
locus commit --dry-run ${dim2("# Preview before committing")}
|
|
13096
|
+
|
|
13097
|
+
`);
|
|
13098
|
+
}
|
|
13099
|
+
async function commitCommand(projectRoot, args, flags = {}) {
|
|
13100
|
+
if (args[0] === "help") {
|
|
13101
|
+
printCommitHelp();
|
|
13102
|
+
return;
|
|
13103
|
+
}
|
|
13104
|
+
const config = loadConfig(projectRoot);
|
|
13105
|
+
let stagedDiff;
|
|
13106
|
+
try {
|
|
13107
|
+
stagedDiff = execSync20("git diff --cached", {
|
|
13108
|
+
cwd: projectRoot,
|
|
13109
|
+
encoding: "utf-8",
|
|
13110
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
13111
|
+
}).trim();
|
|
13112
|
+
} catch {
|
|
13113
|
+
process.stderr.write(`${red2("✗")} Failed to read staged changes.
|
|
13114
|
+
`);
|
|
13115
|
+
return;
|
|
13116
|
+
}
|
|
13117
|
+
if (!stagedDiff) {
|
|
13118
|
+
process.stderr.write(`${red2("✗")} No staged changes. Stage files first with ${bold2("git add")}.
|
|
13119
|
+
`);
|
|
13120
|
+
return;
|
|
13121
|
+
}
|
|
13122
|
+
let stagedStat;
|
|
13123
|
+
try {
|
|
13124
|
+
stagedStat = execSync20("git diff --cached --stat", {
|
|
13125
|
+
cwd: projectRoot,
|
|
13126
|
+
encoding: "utf-8",
|
|
13127
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
13128
|
+
}).trim();
|
|
13129
|
+
} catch {
|
|
13130
|
+
stagedStat = "";
|
|
13131
|
+
}
|
|
13132
|
+
let recentCommits;
|
|
13133
|
+
try {
|
|
13134
|
+
recentCommits = execSync20("git log --oneline -10", {
|
|
13135
|
+
cwd: projectRoot,
|
|
13136
|
+
encoding: "utf-8",
|
|
13137
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
13138
|
+
}).trim();
|
|
13139
|
+
} catch {
|
|
13140
|
+
recentCommits = "";
|
|
13141
|
+
}
|
|
13142
|
+
process.stderr.write(`
|
|
13143
|
+
${bold2("Analyzing staged changes...")}
|
|
13144
|
+
`);
|
|
13145
|
+
process.stderr.write(`${dim2(stagedStat)}
|
|
13146
|
+
|
|
13147
|
+
`);
|
|
13148
|
+
const prompt = buildCommitPrompt(stagedDiff, stagedStat, recentCommits);
|
|
13149
|
+
const model = flags.model ?? config.ai.model;
|
|
13150
|
+
const provider = inferProviderFromModel(model) ?? config.ai.provider;
|
|
13151
|
+
const sandboxName = getModelSandboxName(config.sandbox, model, provider);
|
|
13152
|
+
const result = await runAI({
|
|
13153
|
+
prompt,
|
|
13154
|
+
provider,
|
|
13155
|
+
model,
|
|
13156
|
+
cwd: projectRoot,
|
|
13157
|
+
activity: "Generating commit message",
|
|
13158
|
+
silent: true,
|
|
13159
|
+
noInterrupt: true,
|
|
13160
|
+
sandboxed: !!sandboxName,
|
|
13161
|
+
sandboxName,
|
|
13162
|
+
containerWorkdir: config.sandbox.containerWorkdir
|
|
13163
|
+
});
|
|
13164
|
+
if (!result.success) {
|
|
13165
|
+
process.stderr.write(`${red2("✗")} Failed to generate commit message: ${result.error}
|
|
13166
|
+
`);
|
|
13167
|
+
return;
|
|
13168
|
+
}
|
|
13169
|
+
const commitMessage = extractCommitMessage(result.output);
|
|
13170
|
+
if (!commitMessage) {
|
|
13171
|
+
process.stderr.write(`${red2("✗")} Could not extract a commit message from AI response.
|
|
13172
|
+
`);
|
|
13173
|
+
return;
|
|
13174
|
+
}
|
|
13175
|
+
const fullMessage = `${commitMessage}
|
|
13176
|
+
|
|
13177
|
+
Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
13178
|
+
process.stderr.write(`${bold2("Generated commit message:")}
|
|
13179
|
+
`);
|
|
13180
|
+
process.stderr.write(`${cyan2("─".repeat(50))}
|
|
13181
|
+
`);
|
|
13182
|
+
process.stderr.write(`${fullMessage}
|
|
13183
|
+
`);
|
|
13184
|
+
process.stderr.write(`${cyan2("─".repeat(50))}
|
|
13185
|
+
|
|
13186
|
+
`);
|
|
13187
|
+
if (flags.dryRun) {
|
|
13188
|
+
process.stderr.write(`${yellow2("⚠")} ${bold2("Dry run")} — no commit created.
|
|
13189
|
+
|
|
13190
|
+
`);
|
|
13191
|
+
return;
|
|
13192
|
+
}
|
|
13193
|
+
try {
|
|
13194
|
+
execSync20("git commit -F -", {
|
|
13195
|
+
input: fullMessage,
|
|
13196
|
+
cwd: projectRoot,
|
|
13197
|
+
encoding: "utf-8",
|
|
13198
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
13199
|
+
});
|
|
13200
|
+
process.stderr.write(`${green("✓")} Committed successfully.
|
|
13201
|
+
|
|
13202
|
+
`);
|
|
13203
|
+
} catch (e) {
|
|
13204
|
+
process.stderr.write(`${red2("✗")} Commit failed: ${e.message}
|
|
13205
|
+
`);
|
|
13206
|
+
}
|
|
13207
|
+
}
|
|
13208
|
+
function buildCommitPrompt(diff, stat, recentCommits) {
|
|
13209
|
+
const maxDiffLength = 15000;
|
|
13210
|
+
const truncatedDiff = diff.length > maxDiffLength ? `${diff.slice(0, maxDiffLength)}
|
|
13211
|
+
|
|
13212
|
+
... [diff truncated — ${diff.length - maxDiffLength} chars omitted]` : diff;
|
|
13213
|
+
let prompt = `You are a commit message generator. Analyze the following staged git changes and generate a single, concise conventional commit message.
|
|
13214
|
+
|
|
13215
|
+
Rules:
|
|
13216
|
+
- Use conventional commit format: type(scope): description
|
|
13217
|
+
- Types: feat, fix, chore, refactor, docs, test, style, perf, ci, build
|
|
13218
|
+
- Keep the first line under 72 characters
|
|
13219
|
+
- Add a body paragraph (separated by blank line) ONLY if the changes are complex enough to warrant explanation
|
|
13220
|
+
- Do NOT include any markdown formatting, code blocks, or extra commentary
|
|
13221
|
+
- Output ONLY the commit message text — nothing else
|
|
13222
|
+
|
|
13223
|
+
`;
|
|
13224
|
+
if (recentCommits) {
|
|
13225
|
+
prompt += `Recent commits (for style reference):
|
|
13226
|
+
${recentCommits}
|
|
13227
|
+
|
|
13228
|
+
`;
|
|
13229
|
+
}
|
|
13230
|
+
prompt += `File summary:
|
|
13231
|
+
${stat}
|
|
13232
|
+
|
|
13233
|
+
Diff:
|
|
13234
|
+
${truncatedDiff}`;
|
|
13235
|
+
return prompt;
|
|
13236
|
+
}
|
|
13237
|
+
function extractCommitMessage(output) {
|
|
13238
|
+
const trimmed = output.trim();
|
|
13239
|
+
if (!trimmed)
|
|
13240
|
+
return null;
|
|
13241
|
+
let cleaned = trimmed;
|
|
13242
|
+
if (cleaned.startsWith("```")) {
|
|
13243
|
+
cleaned = cleaned.replace(/^```[^\n]*\n?/, "").replace(/\n?```$/, "");
|
|
13244
|
+
}
|
|
13245
|
+
if (cleaned.startsWith('"') && cleaned.endsWith('"') || cleaned.startsWith("'") && cleaned.endsWith("'")) {
|
|
13246
|
+
cleaned = cleaned.slice(1, -1);
|
|
13247
|
+
}
|
|
13248
|
+
return cleaned.trim() || null;
|
|
13249
|
+
}
|
|
13250
|
+
var init_commit = __esm(() => {
|
|
13251
|
+
init_run_ai();
|
|
13252
|
+
init_ai_models();
|
|
13253
|
+
init_config();
|
|
13254
|
+
init_sandbox();
|
|
13255
|
+
init_terminal();
|
|
13256
|
+
});
|
|
13257
|
+
|
|
13021
13258
|
// src/commands/sandbox.ts
|
|
13022
13259
|
var exports_sandbox2 = {};
|
|
13023
13260
|
__export(exports_sandbox2, {
|
|
@@ -13025,7 +13262,7 @@ __export(exports_sandbox2, {
|
|
|
13025
13262
|
parseSandboxLogsArgs: () => parseSandboxLogsArgs,
|
|
13026
13263
|
parseSandboxInstallArgs: () => parseSandboxInstallArgs
|
|
13027
13264
|
});
|
|
13028
|
-
import { execSync as
|
|
13265
|
+
import { execSync as execSync21, spawn as spawn7 } from "node:child_process";
|
|
13029
13266
|
import { createHash } from "node:crypto";
|
|
13030
13267
|
import { existsSync as existsSync26, readFileSync as readFileSync17 } from "node:fs";
|
|
13031
13268
|
import { basename as basename4, join as join26 } from "node:path";
|
|
@@ -13261,7 +13498,7 @@ function handleRemove(projectRoot) {
|
|
|
13261
13498
|
process.stderr.write(`Removing sandbox ${bold2(sandboxName)}...
|
|
13262
13499
|
`);
|
|
13263
13500
|
try {
|
|
13264
|
-
|
|
13501
|
+
execSync21(`docker sandbox rm ${sandboxName}`, {
|
|
13265
13502
|
encoding: "utf-8",
|
|
13266
13503
|
stdio: ["pipe", "pipe", "pipe"],
|
|
13267
13504
|
timeout: 15000
|
|
@@ -13553,7 +13790,7 @@ function getInstallCommand(pm) {
|
|
|
13553
13790
|
function verifyBinEntries(sandboxName, workdir) {
|
|
13554
13791
|
try {
|
|
13555
13792
|
const binDir = `${workdir}/node_modules/.bin/`;
|
|
13556
|
-
const result =
|
|
13793
|
+
const result = execSync21(`docker sandbox exec --privileged ${sandboxName} sh -c ${JSON.stringify(`ls ${JSON.stringify(binDir)} 2>/dev/null | head -20`)}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
|
|
13557
13794
|
if (!result) {
|
|
13558
13795
|
process.stderr.write(`${yellow2("⚠")} node_modules/.bin is empty — binaries like biome may not be available.
|
|
13559
13796
|
`);
|
|
@@ -13766,7 +14003,7 @@ function runInteractiveCommand(command, args) {
|
|
|
13766
14003
|
}
|
|
13767
14004
|
async function createProviderSandbox(provider, sandboxName, projectRoot) {
|
|
13768
14005
|
try {
|
|
13769
|
-
|
|
14006
|
+
execSync21(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
|
|
13770
14007
|
stdio: ["pipe", "pipe", "pipe"],
|
|
13771
14008
|
timeout: 120000
|
|
13772
14009
|
});
|
|
@@ -13781,7 +14018,7 @@ async function createProviderSandbox(provider, sandboxName, projectRoot) {
|
|
|
13781
14018
|
}
|
|
13782
14019
|
async function ensurePackageManagerInSandbox(sandboxName, pm) {
|
|
13783
14020
|
try {
|
|
13784
|
-
|
|
14021
|
+
execSync21(`docker sandbox exec --privileged ${sandboxName} which ${pm}`, {
|
|
13785
14022
|
stdio: ["pipe", "pipe", "pipe"],
|
|
13786
14023
|
timeout: 5000
|
|
13787
14024
|
});
|
|
@@ -13790,7 +14027,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
|
|
|
13790
14027
|
process.stderr.write(`Installing ${bold2(pm)} in sandbox...
|
|
13791
14028
|
`);
|
|
13792
14029
|
try {
|
|
13793
|
-
|
|
14030
|
+
execSync21(`docker sandbox exec --privileged ${sandboxName} npm install -g ${npmPkg}`, {
|
|
13794
14031
|
stdio: "inherit",
|
|
13795
14032
|
timeout: 120000
|
|
13796
14033
|
});
|
|
@@ -13802,7 +14039,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
|
|
|
13802
14039
|
}
|
|
13803
14040
|
async function ensureCodexInSandbox(sandboxName) {
|
|
13804
14041
|
try {
|
|
13805
|
-
|
|
14042
|
+
execSync21(`docker sandbox exec --privileged ${sandboxName} which codex`, {
|
|
13806
14043
|
stdio: ["pipe", "pipe", "pipe"],
|
|
13807
14044
|
timeout: 5000
|
|
13808
14045
|
});
|
|
@@ -13810,7 +14047,7 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
13810
14047
|
process.stderr.write(`Installing codex in sandbox...
|
|
13811
14048
|
`);
|
|
13812
14049
|
try {
|
|
13813
|
-
|
|
14050
|
+
execSync21(`docker sandbox exec --privileged ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
|
|
13814
14051
|
} catch {
|
|
13815
14052
|
process.stderr.write(`${red2("✗")} Failed to install codex in sandbox.
|
|
13816
14053
|
`);
|
|
@@ -13819,7 +14056,7 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
13819
14056
|
}
|
|
13820
14057
|
function isSandboxAlive(name) {
|
|
13821
14058
|
try {
|
|
13822
|
-
const output =
|
|
14059
|
+
const output = execSync21("docker sandbox ls", {
|
|
13823
14060
|
encoding: "utf-8",
|
|
13824
14061
|
stdio: ["pipe", "pipe", "pipe"],
|
|
13825
14062
|
timeout: 5000
|
|
@@ -13862,8 +14099,17 @@ function getCliVersion() {
|
|
|
13862
14099
|
}
|
|
13863
14100
|
}
|
|
13864
14101
|
var VERSION = getCliVersion();
|
|
14102
|
+
function normalizeArgs(args) {
|
|
14103
|
+
return args.map((arg) => {
|
|
14104
|
+
if (arg.startsWith("--") || !arg.startsWith("-"))
|
|
14105
|
+
return arg;
|
|
14106
|
+
if (arg.length <= 2)
|
|
14107
|
+
return arg;
|
|
14108
|
+
return `-${arg}`;
|
|
14109
|
+
});
|
|
14110
|
+
}
|
|
13865
14111
|
function parseArgs(argv) {
|
|
13866
|
-
const rawArgs = argv.slice(2);
|
|
14112
|
+
const rawArgs = normalizeArgs(argv.slice(2));
|
|
13867
14113
|
const flags = {
|
|
13868
14114
|
debug: false,
|
|
13869
14115
|
help: false,
|
|
@@ -14020,6 +14266,7 @@ ${bold2("Commands:")}
|
|
|
14020
14266
|
${cyan2("discuss")} AI-powered architectural discussions
|
|
14021
14267
|
${cyan2("artifacts")} View and manage AI-generated artifacts
|
|
14022
14268
|
${cyan2("status")} Dashboard view of current state
|
|
14269
|
+
${cyan2("commit")} AI-powered commit message generation
|
|
14023
14270
|
${cyan2("config")} View and manage settings
|
|
14024
14271
|
${cyan2("logs")} View, tail, and manage execution logs
|
|
14025
14272
|
${cyan2("create")} ${dim2("<name>")} Scaffold a new community package
|
|
@@ -14067,6 +14314,7 @@ function requiresSandboxSync(command, args, flags) {
|
|
|
14067
14314
|
case "run":
|
|
14068
14315
|
case "review":
|
|
14069
14316
|
case "iterate":
|
|
14317
|
+
case "commit":
|
|
14070
14318
|
return true;
|
|
14071
14319
|
case "exec":
|
|
14072
14320
|
return args[0] !== "sessions" && args[0] !== "help";
|
|
@@ -14307,6 +14555,15 @@ async function main() {
|
|
|
14307
14555
|
await artifactsCommand2(projectRoot, artifactsArgs);
|
|
14308
14556
|
break;
|
|
14309
14557
|
}
|
|
14558
|
+
case "commit": {
|
|
14559
|
+
const { commitCommand: commitCommand2 } = await Promise.resolve().then(() => (init_commit(), exports_commit));
|
|
14560
|
+
const commitArgs = parsed.flags.help ? ["help"] : parsed.args;
|
|
14561
|
+
await commitCommand2(projectRoot, commitArgs, {
|
|
14562
|
+
dryRun: parsed.flags.dryRun,
|
|
14563
|
+
model: parsed.flags.model
|
|
14564
|
+
});
|
|
14565
|
+
break;
|
|
14566
|
+
}
|
|
14310
14567
|
case "sandbox": {
|
|
14311
14568
|
const { sandboxCommand: sandboxCommand2 } = await Promise.resolve().then(() => (init_sandbox2(), exports_sandbox2));
|
|
14312
14569
|
const sandboxArgs = parsed.flags.help ? ["help"] : parsed.args;
|
|
@@ -14343,3 +14600,6 @@ ${dim2(error.stack)}
|
|
|
14343
14600
|
}
|
|
14344
14601
|
process.exit(1);
|
|
14345
14602
|
});
|
|
14603
|
+
export {
|
|
14604
|
+
normalizeArgs
|
|
14605
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@locusai/cli",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.11",
|
|
4
4
|
"description": "GitHub-native AI engineering assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"license": "MIT",
|
|
37
37
|
"dependencies": {},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@locusai/sdk": "^0.22.
|
|
39
|
+
"@locusai/sdk": "^0.22.11",
|
|
40
40
|
"@types/bun": "latest",
|
|
41
41
|
"typescript": "^5.8.3"
|
|
42
42
|
},
|