@locusai/cli 0.20.1 → 0.20.3
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 +809 -313
- package/package.json +1 -1
package/bin/locus.js
CHANGED
|
@@ -890,7 +890,8 @@ __export(exports_sandbox, {
|
|
|
890
890
|
getProviderSandboxName: () => getProviderSandboxName,
|
|
891
891
|
getModelSandboxName: () => getModelSandboxName,
|
|
892
892
|
displaySandboxWarning: () => displaySandboxWarning,
|
|
893
|
-
detectSandboxSupport: () => detectSandboxSupport
|
|
893
|
+
detectSandboxSupport: () => detectSandboxSupport,
|
|
894
|
+
checkProviderSandboxMismatch: () => checkProviderSandboxMismatch
|
|
894
895
|
});
|
|
895
896
|
import { execFile } from "node:child_process";
|
|
896
897
|
import { createInterface } from "node:readline";
|
|
@@ -901,6 +902,19 @@ function getModelSandboxName(config, model, fallbackProvider) {
|
|
|
901
902
|
const provider = inferProviderFromModel(model) ?? fallbackProvider;
|
|
902
903
|
return getProviderSandboxName(config, provider);
|
|
903
904
|
}
|
|
905
|
+
function checkProviderSandboxMismatch(config, model, fallbackProvider) {
|
|
906
|
+
if (!config.enabled)
|
|
907
|
+
return null;
|
|
908
|
+
const targetProvider = inferProviderFromModel(model) ?? fallbackProvider;
|
|
909
|
+
const sandboxName = getProviderSandboxName(config, targetProvider);
|
|
910
|
+
if (sandboxName)
|
|
911
|
+
return null;
|
|
912
|
+
const configured = ["claude", "codex"].filter((p) => config.providers[p]);
|
|
913
|
+
if (configured.length > 0) {
|
|
914
|
+
return `Sandbox is configured for ${configured.join(", ")} but not for ${targetProvider}. ` + `Run "locus sandbox" and select ${targetProvider} to create its sandbox.`;
|
|
915
|
+
}
|
|
916
|
+
return `No sandbox is configured for ${targetProvider}. ` + `Run "locus sandbox" to create one.`;
|
|
917
|
+
}
|
|
904
918
|
async function detectSandboxSupport() {
|
|
905
919
|
if (cachedStatus)
|
|
906
920
|
return cachedStatus;
|
|
@@ -1280,6 +1294,177 @@ var init_version_check = __esm(() => {
|
|
|
1280
1294
|
CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
1281
1295
|
});
|
|
1282
1296
|
|
|
1297
|
+
// src/core/ecosystem.ts
|
|
1298
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
1299
|
+
import { join as join5 } from "node:path";
|
|
1300
|
+
function detectProjectEcosystem(projectRoot) {
|
|
1301
|
+
for (const signal of ECOSYSTEM_SIGNALS) {
|
|
1302
|
+
for (const marker of signal.markers) {
|
|
1303
|
+
if (marker.startsWith("*")) {
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
if (existsSync5(join5(projectRoot, marker))) {
|
|
1307
|
+
return signal.ecosystem;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
return "unknown";
|
|
1312
|
+
}
|
|
1313
|
+
function isJavaScriptEcosystem(ecosystem) {
|
|
1314
|
+
return ecosystem === "javascript";
|
|
1315
|
+
}
|
|
1316
|
+
function generateSandboxSetupTemplate(ecosystem) {
|
|
1317
|
+
switch (ecosystem) {
|
|
1318
|
+
case "javascript":
|
|
1319
|
+
return null;
|
|
1320
|
+
case "rust":
|
|
1321
|
+
return `#!/bin/sh
|
|
1322
|
+
# Sandbox setup for Rust projects
|
|
1323
|
+
# This script runs inside the Docker sandbox after creation.
|
|
1324
|
+
# Uncomment or modify the commands below for your project.
|
|
1325
|
+
|
|
1326
|
+
# Install Rust toolchain (if not pre-installed)
|
|
1327
|
+
# curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
|
1328
|
+
# . "$HOME/.cargo/env"
|
|
1329
|
+
|
|
1330
|
+
# Build the project
|
|
1331
|
+
# cargo build
|
|
1332
|
+
|
|
1333
|
+
echo "Rust sandbox setup complete"
|
|
1334
|
+
`;
|
|
1335
|
+
case "go":
|
|
1336
|
+
return `#!/bin/sh
|
|
1337
|
+
# Sandbox setup for Go projects
|
|
1338
|
+
# This script runs inside the Docker sandbox after creation.
|
|
1339
|
+
|
|
1340
|
+
# Download dependencies
|
|
1341
|
+
# go mod download
|
|
1342
|
+
|
|
1343
|
+
# Build the project
|
|
1344
|
+
# go build ./...
|
|
1345
|
+
|
|
1346
|
+
echo "Go sandbox setup complete"
|
|
1347
|
+
`;
|
|
1348
|
+
case "python":
|
|
1349
|
+
return `#!/bin/sh
|
|
1350
|
+
# Sandbox setup for Python projects
|
|
1351
|
+
# This script runs inside the Docker sandbox after creation.
|
|
1352
|
+
|
|
1353
|
+
# Create virtual environment and install dependencies
|
|
1354
|
+
# python3 -m venv .venv
|
|
1355
|
+
# . .venv/bin/activate
|
|
1356
|
+
# pip install -r requirements.txt
|
|
1357
|
+
# OR: pip install -e .
|
|
1358
|
+
|
|
1359
|
+
echo "Python sandbox setup complete"
|
|
1360
|
+
`;
|
|
1361
|
+
case "java":
|
|
1362
|
+
return `#!/bin/sh
|
|
1363
|
+
# Sandbox setup for Java/JVM projects
|
|
1364
|
+
# This script runs inside the Docker sandbox after creation.
|
|
1365
|
+
|
|
1366
|
+
# Maven
|
|
1367
|
+
# mvn install -DskipTests
|
|
1368
|
+
|
|
1369
|
+
# Gradle
|
|
1370
|
+
# ./gradlew build -x test
|
|
1371
|
+
|
|
1372
|
+
echo "Java sandbox setup complete"
|
|
1373
|
+
`;
|
|
1374
|
+
case "ruby":
|
|
1375
|
+
return `#!/bin/sh
|
|
1376
|
+
# Sandbox setup for Ruby projects
|
|
1377
|
+
# This script runs inside the Docker sandbox after creation.
|
|
1378
|
+
|
|
1379
|
+
# Install dependencies
|
|
1380
|
+
# bundle install
|
|
1381
|
+
|
|
1382
|
+
echo "Ruby sandbox setup complete"
|
|
1383
|
+
`;
|
|
1384
|
+
case "elixir":
|
|
1385
|
+
return `#!/bin/sh
|
|
1386
|
+
# Sandbox setup for Elixir projects
|
|
1387
|
+
# This script runs inside the Docker sandbox after creation.
|
|
1388
|
+
|
|
1389
|
+
# Install dependencies
|
|
1390
|
+
# mix deps.get
|
|
1391
|
+
# mix compile
|
|
1392
|
+
|
|
1393
|
+
echo "Elixir sandbox setup complete"
|
|
1394
|
+
`;
|
|
1395
|
+
case "dotnet":
|
|
1396
|
+
return `#!/bin/sh
|
|
1397
|
+
# Sandbox setup for .NET projects
|
|
1398
|
+
# This script runs inside the Docker sandbox after creation.
|
|
1399
|
+
|
|
1400
|
+
# Restore packages
|
|
1401
|
+
# dotnet restore
|
|
1402
|
+
|
|
1403
|
+
# Build
|
|
1404
|
+
# dotnet build
|
|
1405
|
+
|
|
1406
|
+
echo ".NET sandbox setup complete"
|
|
1407
|
+
`;
|
|
1408
|
+
case "unknown":
|
|
1409
|
+
return `#!/bin/sh
|
|
1410
|
+
# Sandbox setup script
|
|
1411
|
+
# This script runs inside the Docker sandbox after creation.
|
|
1412
|
+
# Add any commands needed to prepare the build environment.
|
|
1413
|
+
|
|
1414
|
+
# Example: install system dependencies
|
|
1415
|
+
# apt-get update && apt-get install -y <packages>
|
|
1416
|
+
|
|
1417
|
+
# Example: install project dependencies
|
|
1418
|
+
# <your-package-manager> install
|
|
1419
|
+
|
|
1420
|
+
echo "Sandbox setup complete"
|
|
1421
|
+
`;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
var ECOSYSTEM_SIGNALS;
|
|
1425
|
+
var init_ecosystem = __esm(() => {
|
|
1426
|
+
ECOSYSTEM_SIGNALS = [
|
|
1427
|
+
{
|
|
1428
|
+
ecosystem: "javascript",
|
|
1429
|
+
markers: ["package.json"]
|
|
1430
|
+
},
|
|
1431
|
+
{
|
|
1432
|
+
ecosystem: "rust",
|
|
1433
|
+
markers: ["Cargo.toml"]
|
|
1434
|
+
},
|
|
1435
|
+
{
|
|
1436
|
+
ecosystem: "go",
|
|
1437
|
+
markers: ["go.mod"]
|
|
1438
|
+
},
|
|
1439
|
+
{
|
|
1440
|
+
ecosystem: "python",
|
|
1441
|
+
markers: [
|
|
1442
|
+
"pyproject.toml",
|
|
1443
|
+
"setup.py",
|
|
1444
|
+
"setup.cfg",
|
|
1445
|
+
"Pipfile",
|
|
1446
|
+
"requirements.txt"
|
|
1447
|
+
]
|
|
1448
|
+
},
|
|
1449
|
+
{
|
|
1450
|
+
ecosystem: "java",
|
|
1451
|
+
markers: ["pom.xml", "build.gradle", "build.gradle.kts"]
|
|
1452
|
+
},
|
|
1453
|
+
{
|
|
1454
|
+
ecosystem: "ruby",
|
|
1455
|
+
markers: ["Gemfile"]
|
|
1456
|
+
},
|
|
1457
|
+
{
|
|
1458
|
+
ecosystem: "elixir",
|
|
1459
|
+
markers: ["mix.exs"]
|
|
1460
|
+
},
|
|
1461
|
+
{
|
|
1462
|
+
ecosystem: "dotnet",
|
|
1463
|
+
markers: ["*.csproj", "*.fsproj", "*.sln"]
|
|
1464
|
+
}
|
|
1465
|
+
];
|
|
1466
|
+
});
|
|
1467
|
+
|
|
1283
1468
|
// src/core/github.ts
|
|
1284
1469
|
import {
|
|
1285
1470
|
execFileSync,
|
|
@@ -1476,7 +1661,18 @@ function reopenMilestone(owner, repo, milestoneNumber, options = {}) {
|
|
|
1476
1661
|
gh(`api repos/${owner}/${repo}/milestones/${milestoneNumber} -X PATCH -f state=open`, options);
|
|
1477
1662
|
}
|
|
1478
1663
|
function createPR(title, body, head, base, options = {}) {
|
|
1479
|
-
const result = ghExec([
|
|
1664
|
+
const result = ghExec([
|
|
1665
|
+
"pr",
|
|
1666
|
+
"create",
|
|
1667
|
+
"--title",
|
|
1668
|
+
title,
|
|
1669
|
+
"--body",
|
|
1670
|
+
body,
|
|
1671
|
+
"--head",
|
|
1672
|
+
head,
|
|
1673
|
+
"--base",
|
|
1674
|
+
base
|
|
1675
|
+
], options);
|
|
1480
1676
|
const match = result.match(/\/pull\/(\d+)/);
|
|
1481
1677
|
if (!match) {
|
|
1482
1678
|
throw new Error(`Could not extract PR number from: ${result}`);
|
|
@@ -1630,8 +1826,8 @@ var exports_init = {};
|
|
|
1630
1826
|
__export(exports_init, {
|
|
1631
1827
|
initCommand: () => initCommand
|
|
1632
1828
|
});
|
|
1633
|
-
import { existsSync as
|
|
1634
|
-
import { join as
|
|
1829
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1830
|
+
import { join as join6 } from "node:path";
|
|
1635
1831
|
async function initCommand(cwd) {
|
|
1636
1832
|
const log = getLogger();
|
|
1637
1833
|
process.stderr.write(`
|
|
@@ -1675,17 +1871,17 @@ ${bold("Initializing Locus...")}
|
|
|
1675
1871
|
}
|
|
1676
1872
|
process.stderr.write(`${green("✓")} Repository: ${bold(`${context.owner}/${context.repo}`)} (branch: ${context.defaultBranch})
|
|
1677
1873
|
`);
|
|
1678
|
-
const locusDir =
|
|
1874
|
+
const locusDir = join6(cwd, ".locus");
|
|
1679
1875
|
const dirs = [
|
|
1680
1876
|
locusDir,
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1877
|
+
join6(locusDir, "sessions"),
|
|
1878
|
+
join6(locusDir, "discussions"),
|
|
1879
|
+
join6(locusDir, "artifacts"),
|
|
1880
|
+
join6(locusDir, "plans"),
|
|
1881
|
+
join6(locusDir, "logs")
|
|
1686
1882
|
];
|
|
1687
1883
|
for (const dir of dirs) {
|
|
1688
|
-
if (!
|
|
1884
|
+
if (!existsSync6(dir)) {
|
|
1689
1885
|
mkdirSync5(dir, { recursive: true });
|
|
1690
1886
|
}
|
|
1691
1887
|
}
|
|
@@ -1706,7 +1902,7 @@ ${bold("Initializing Locus...")}
|
|
|
1706
1902
|
};
|
|
1707
1903
|
if (isReInit) {
|
|
1708
1904
|
try {
|
|
1709
|
-
const existing = JSON.parse(readFileSync4(
|
|
1905
|
+
const existing = JSON.parse(readFileSync4(join6(locusDir, "config.json"), "utf-8"));
|
|
1710
1906
|
if (existing.ai)
|
|
1711
1907
|
config.ai = { ...config.ai, ...existing.ai };
|
|
1712
1908
|
if (existing.agent)
|
|
@@ -1725,8 +1921,8 @@ ${bold("Initializing Locus...")}
|
|
|
1725
1921
|
`);
|
|
1726
1922
|
}
|
|
1727
1923
|
saveConfig(cwd, config);
|
|
1728
|
-
const locusMdPath =
|
|
1729
|
-
if (!
|
|
1924
|
+
const locusMdPath = join6(locusDir, "LOCUS.md");
|
|
1925
|
+
if (!existsSync6(locusMdPath)) {
|
|
1730
1926
|
writeFileSync4(locusMdPath, LOCUS_MD_TEMPLATE, "utf-8");
|
|
1731
1927
|
process.stderr.write(`${green("✓")} Generated LOCUS.md (edit to add project context)
|
|
1732
1928
|
`);
|
|
@@ -1734,8 +1930,8 @@ ${bold("Initializing Locus...")}
|
|
|
1734
1930
|
process.stderr.write(`${dim("○")} LOCUS.md already exists (preserved)
|
|
1735
1931
|
`);
|
|
1736
1932
|
}
|
|
1737
|
-
const learningsMdPath =
|
|
1738
|
-
if (!
|
|
1933
|
+
const learningsMdPath = join6(locusDir, "LEARNINGS.md");
|
|
1934
|
+
if (!existsSync6(learningsMdPath)) {
|
|
1739
1935
|
writeFileSync4(learningsMdPath, LEARNINGS_MD_TEMPLATE, "utf-8");
|
|
1740
1936
|
process.stderr.write(`${green("✓")} Generated LEARNINGS.md
|
|
1741
1937
|
`);
|
|
@@ -1743,13 +1939,29 @@ ${bold("Initializing Locus...")}
|
|
|
1743
1939
|
process.stderr.write(`${dim("○")} LEARNINGS.md already exists (preserved)
|
|
1744
1940
|
`);
|
|
1745
1941
|
}
|
|
1746
|
-
const sandboxIgnorePath =
|
|
1747
|
-
if (!
|
|
1942
|
+
const sandboxIgnorePath = join6(cwd, ".sandboxignore");
|
|
1943
|
+
if (!existsSync6(sandboxIgnorePath)) {
|
|
1748
1944
|
writeFileSync4(sandboxIgnorePath, SANDBOXIGNORE_TEMPLATE, "utf-8");
|
|
1749
1945
|
process.stderr.write(`${green("✓")} Generated .sandboxignore
|
|
1750
1946
|
`);
|
|
1751
1947
|
} else {
|
|
1752
1948
|
process.stderr.write(`${dim("○")} .sandboxignore already exists (preserved)
|
|
1949
|
+
`);
|
|
1950
|
+
}
|
|
1951
|
+
const ecosystem = detectProjectEcosystem(cwd);
|
|
1952
|
+
const sandboxSetupPath = join6(locusDir, "sandbox-setup.sh");
|
|
1953
|
+
if (!existsSync6(sandboxSetupPath)) {
|
|
1954
|
+
const template = generateSandboxSetupTemplate(ecosystem);
|
|
1955
|
+
if (template) {
|
|
1956
|
+
writeFileSync4(sandboxSetupPath, template, {
|
|
1957
|
+
encoding: "utf-8",
|
|
1958
|
+
mode: 493
|
|
1959
|
+
});
|
|
1960
|
+
process.stderr.write(`${green("✓")} Generated .locus/sandbox-setup.sh (${ecosystem} project detected)
|
|
1961
|
+
`);
|
|
1962
|
+
}
|
|
1963
|
+
} else {
|
|
1964
|
+
process.stderr.write(`${dim("○")} sandbox-setup.sh already exists (preserved)
|
|
1753
1965
|
`);
|
|
1754
1966
|
}
|
|
1755
1967
|
process.stderr.write(`${cyan("●")} Creating GitHub labels...`);
|
|
@@ -1761,9 +1973,9 @@ ${bold("Initializing Locus...")}
|
|
|
1761
1973
|
process.stderr.write(`\r${yellow("⚠")} Some labels could not be created: ${e.message}
|
|
1762
1974
|
`);
|
|
1763
1975
|
}
|
|
1764
|
-
const gitignorePath =
|
|
1976
|
+
const gitignorePath = join6(cwd, ".gitignore");
|
|
1765
1977
|
let gitignoreContent = "";
|
|
1766
|
-
if (
|
|
1978
|
+
if (existsSync6(gitignorePath)) {
|
|
1767
1979
|
gitignoreContent = readFileSync4(gitignorePath, "utf-8");
|
|
1768
1980
|
}
|
|
1769
1981
|
const entriesToAdd = GITIGNORE_ENTRIES.filter((entry) => entry && !gitignoreContent.includes(entry.trim()));
|
|
@@ -1996,6 +2208,7 @@ It is read by AI agents before every task to avoid repeating mistakes and to fol
|
|
|
1996
2208
|
var init_init = __esm(() => {
|
|
1997
2209
|
init_config();
|
|
1998
2210
|
init_context();
|
|
2211
|
+
init_ecosystem();
|
|
1999
2212
|
init_github();
|
|
2000
2213
|
init_logger();
|
|
2001
2214
|
init_terminal();
|
|
@@ -2017,33 +2230,33 @@ var init_init = __esm(() => {
|
|
|
2017
2230
|
|
|
2018
2231
|
// src/packages/registry.ts
|
|
2019
2232
|
import {
|
|
2020
|
-
existsSync as
|
|
2233
|
+
existsSync as existsSync7,
|
|
2021
2234
|
mkdirSync as mkdirSync6,
|
|
2022
2235
|
readFileSync as readFileSync5,
|
|
2023
2236
|
renameSync,
|
|
2024
2237
|
writeFileSync as writeFileSync5
|
|
2025
2238
|
} from "node:fs";
|
|
2026
2239
|
import { homedir as homedir2 } from "node:os";
|
|
2027
|
-
import { join as
|
|
2240
|
+
import { join as join7 } from "node:path";
|
|
2028
2241
|
function getPackagesDir() {
|
|
2029
2242
|
const home = process.env.HOME || homedir2();
|
|
2030
|
-
const dir =
|
|
2031
|
-
if (!
|
|
2243
|
+
const dir = join7(home, ".locus", "packages");
|
|
2244
|
+
if (!existsSync7(dir)) {
|
|
2032
2245
|
mkdirSync6(dir, { recursive: true });
|
|
2033
2246
|
}
|
|
2034
|
-
const pkgJson =
|
|
2035
|
-
if (!
|
|
2247
|
+
const pkgJson = join7(dir, "package.json");
|
|
2248
|
+
if (!existsSync7(pkgJson)) {
|
|
2036
2249
|
writeFileSync5(pkgJson, `${JSON.stringify({ private: true }, null, 2)}
|
|
2037
2250
|
`, "utf-8");
|
|
2038
2251
|
}
|
|
2039
2252
|
return dir;
|
|
2040
2253
|
}
|
|
2041
2254
|
function getRegistryPath() {
|
|
2042
|
-
return
|
|
2255
|
+
return join7(getPackagesDir(), "registry.json");
|
|
2043
2256
|
}
|
|
2044
2257
|
function loadRegistry() {
|
|
2045
2258
|
const registryPath = getRegistryPath();
|
|
2046
|
-
if (!
|
|
2259
|
+
if (!existsSync7(registryPath)) {
|
|
2047
2260
|
return { packages: {} };
|
|
2048
2261
|
}
|
|
2049
2262
|
try {
|
|
@@ -2066,8 +2279,8 @@ function saveRegistry(registry) {
|
|
|
2066
2279
|
}
|
|
2067
2280
|
function resolvePackageBinary(packageName) {
|
|
2068
2281
|
const fullName = normalizePackageName(packageName);
|
|
2069
|
-
const binPath =
|
|
2070
|
-
return
|
|
2282
|
+
const binPath = join7(getPackagesDir(), "node_modules", ".bin", fullName);
|
|
2283
|
+
return existsSync7(binPath) ? binPath : null;
|
|
2071
2284
|
}
|
|
2072
2285
|
function normalizePackageName(input) {
|
|
2073
2286
|
if (input.startsWith("@")) {
|
|
@@ -2087,7 +2300,7 @@ __export(exports_pkg, {
|
|
|
2087
2300
|
listInstalledPackages: () => listInstalledPackages
|
|
2088
2301
|
});
|
|
2089
2302
|
import { spawn } from "node:child_process";
|
|
2090
|
-
import { existsSync as
|
|
2303
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
2091
2304
|
function listInstalledPackages() {
|
|
2092
2305
|
const registry = loadRegistry();
|
|
2093
2306
|
const entries = Object.values(registry.packages);
|
|
@@ -2165,7 +2378,7 @@ async function pkgCommand(args, _flags) {
|
|
|
2165
2378
|
return;
|
|
2166
2379
|
}
|
|
2167
2380
|
const binaryPath = entry.binaryPath;
|
|
2168
|
-
if (!binaryPath || !
|
|
2381
|
+
if (!binaryPath || !existsSync8(binaryPath)) {
|
|
2169
2382
|
process.stderr.write(`${red("✗")} Binary for ${bold(packageName)} not found on disk.
|
|
2170
2383
|
`);
|
|
2171
2384
|
if (binaryPath) {
|
|
@@ -2291,8 +2504,8 @@ __export(exports_install, {
|
|
|
2291
2504
|
installCommand: () => installCommand
|
|
2292
2505
|
});
|
|
2293
2506
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
2294
|
-
import { existsSync as
|
|
2295
|
-
import { join as
|
|
2507
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
|
|
2508
|
+
import { join as join8 } from "node:path";
|
|
2296
2509
|
function parsePackageArg(raw) {
|
|
2297
2510
|
if (raw.startsWith("@")) {
|
|
2298
2511
|
const slashIdx = raw.indexOf("/");
|
|
@@ -2369,8 +2582,8 @@ ${red("✗")} Failed to install ${bold(packageSpec)}.
|
|
|
2369
2582
|
process.exit(1);
|
|
2370
2583
|
return;
|
|
2371
2584
|
}
|
|
2372
|
-
const installedPkgJsonPath =
|
|
2373
|
-
if (!
|
|
2585
|
+
const installedPkgJsonPath = join8(packagesDir, "node_modules", packageName, "package.json");
|
|
2586
|
+
if (!existsSync9(installedPkgJsonPath)) {
|
|
2374
2587
|
process.stderr.write(`
|
|
2375
2588
|
${red("✗")} Package installed but package.json not found at:
|
|
2376
2589
|
`);
|
|
@@ -2645,16 +2858,16 @@ __export(exports_logs, {
|
|
|
2645
2858
|
logsCommand: () => logsCommand
|
|
2646
2859
|
});
|
|
2647
2860
|
import {
|
|
2648
|
-
existsSync as
|
|
2861
|
+
existsSync as existsSync10,
|
|
2649
2862
|
readdirSync as readdirSync2,
|
|
2650
2863
|
readFileSync as readFileSync7,
|
|
2651
2864
|
statSync as statSync2,
|
|
2652
2865
|
unlinkSync as unlinkSync2
|
|
2653
2866
|
} from "node:fs";
|
|
2654
|
-
import { join as
|
|
2867
|
+
import { join as join9 } from "node:path";
|
|
2655
2868
|
async function logsCommand(cwd, options) {
|
|
2656
|
-
const logsDir =
|
|
2657
|
-
if (!
|
|
2869
|
+
const logsDir = join9(cwd, ".locus", "logs");
|
|
2870
|
+
if (!existsSync10(logsDir)) {
|
|
2658
2871
|
process.stderr.write(`${dim("No logs found.")}
|
|
2659
2872
|
`);
|
|
2660
2873
|
return;
|
|
@@ -2709,8 +2922,8 @@ async function tailLog(logFile, levelFilter) {
|
|
|
2709
2922
|
process.stderr.write(`${bold("Tailing:")} ${dim(logFile)} ${dim("(Ctrl+C to stop)")}
|
|
2710
2923
|
|
|
2711
2924
|
`);
|
|
2712
|
-
let lastSize =
|
|
2713
|
-
if (
|
|
2925
|
+
let lastSize = existsSync10(logFile) ? statSync2(logFile).size : 0;
|
|
2926
|
+
if (existsSync10(logFile)) {
|
|
2714
2927
|
const content = readFileSync7(logFile, "utf-8");
|
|
2715
2928
|
const lines = content.trim().split(`
|
|
2716
2929
|
`).filter(Boolean);
|
|
@@ -2729,7 +2942,7 @@ async function tailLog(logFile, levelFilter) {
|
|
|
2729
2942
|
}
|
|
2730
2943
|
return new Promise((resolve) => {
|
|
2731
2944
|
const interval = setInterval(() => {
|
|
2732
|
-
if (!
|
|
2945
|
+
if (!existsSync10(logFile))
|
|
2733
2946
|
return;
|
|
2734
2947
|
const currentSize = statSync2(logFile).size;
|
|
2735
2948
|
if (currentSize <= lastSize)
|
|
@@ -2789,7 +3002,7 @@ function cleanLogs(logsDir) {
|
|
|
2789
3002
|
`);
|
|
2790
3003
|
}
|
|
2791
3004
|
function getLogFiles(logsDir) {
|
|
2792
|
-
return readdirSync2(logsDir).filter((f) => f.startsWith("locus-") && f.endsWith(".log")).map((f) =>
|
|
3005
|
+
return readdirSync2(logsDir).filter((f) => f.startsWith("locus-") && f.endsWith(".log")).map((f) => join9(logsDir, f)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
|
|
2793
3006
|
}
|
|
2794
3007
|
function formatEntry(entry) {
|
|
2795
3008
|
const time = dim(new Date(entry.ts).toLocaleTimeString());
|
|
@@ -3099,9 +3312,9 @@ var init_stream_renderer = __esm(() => {
|
|
|
3099
3312
|
|
|
3100
3313
|
// src/repl/clipboard.ts
|
|
3101
3314
|
import { execSync as execSync4 } from "node:child_process";
|
|
3102
|
-
import { existsSync as
|
|
3315
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync7 } from "node:fs";
|
|
3103
3316
|
import { tmpdir } from "node:os";
|
|
3104
|
-
import { join as
|
|
3317
|
+
import { join as join10 } from "node:path";
|
|
3105
3318
|
function readClipboardImage() {
|
|
3106
3319
|
if (process.platform === "darwin") {
|
|
3107
3320
|
return readMacOSClipboardImage();
|
|
@@ -3112,14 +3325,14 @@ function readClipboardImage() {
|
|
|
3112
3325
|
return null;
|
|
3113
3326
|
}
|
|
3114
3327
|
function ensureStableDir() {
|
|
3115
|
-
if (!
|
|
3328
|
+
if (!existsSync11(STABLE_DIR)) {
|
|
3116
3329
|
mkdirSync7(STABLE_DIR, { recursive: true });
|
|
3117
3330
|
}
|
|
3118
3331
|
}
|
|
3119
3332
|
function readMacOSClipboardImage() {
|
|
3120
3333
|
try {
|
|
3121
3334
|
ensureStableDir();
|
|
3122
|
-
const destPath =
|
|
3335
|
+
const destPath = join10(STABLE_DIR, `clipboard-${Date.now()}.png`);
|
|
3123
3336
|
const script = [
|
|
3124
3337
|
`set destPath to POSIX file "${destPath}"`,
|
|
3125
3338
|
"try",
|
|
@@ -3143,7 +3356,7 @@ function readMacOSClipboardImage() {
|
|
|
3143
3356
|
timeout: 5000,
|
|
3144
3357
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3145
3358
|
}).trim();
|
|
3146
|
-
if (result === "ok" &&
|
|
3359
|
+
if (result === "ok" && existsSync11(destPath)) {
|
|
3147
3360
|
return destPath;
|
|
3148
3361
|
}
|
|
3149
3362
|
} catch {}
|
|
@@ -3156,9 +3369,9 @@ function readLinuxClipboardImage() {
|
|
|
3156
3369
|
return null;
|
|
3157
3370
|
}
|
|
3158
3371
|
ensureStableDir();
|
|
3159
|
-
const destPath =
|
|
3372
|
+
const destPath = join10(STABLE_DIR, `clipboard-${Date.now()}.png`);
|
|
3160
3373
|
execSync4(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
|
|
3161
|
-
if (
|
|
3374
|
+
if (existsSync11(destPath)) {
|
|
3162
3375
|
return destPath;
|
|
3163
3376
|
}
|
|
3164
3377
|
} catch {}
|
|
@@ -3166,13 +3379,13 @@ function readLinuxClipboardImage() {
|
|
|
3166
3379
|
}
|
|
3167
3380
|
var STABLE_DIR;
|
|
3168
3381
|
var init_clipboard = __esm(() => {
|
|
3169
|
-
STABLE_DIR =
|
|
3382
|
+
STABLE_DIR = join10(tmpdir(), "locus-images");
|
|
3170
3383
|
});
|
|
3171
3384
|
|
|
3172
3385
|
// src/repl/image-detect.ts
|
|
3173
|
-
import { copyFileSync, existsSync as
|
|
3386
|
+
import { copyFileSync, existsSync as existsSync12, mkdirSync as mkdirSync8 } from "node:fs";
|
|
3174
3387
|
import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
|
|
3175
|
-
import { basename, extname, join as
|
|
3388
|
+
import { basename, extname, join as join11, resolve } from "node:path";
|
|
3176
3389
|
function detectImages(input) {
|
|
3177
3390
|
const detected = [];
|
|
3178
3391
|
const byResolved = new Map;
|
|
@@ -3266,15 +3479,15 @@ function collectReferencedAttachments(input, attachments) {
|
|
|
3266
3479
|
return dedupeByResolvedPath(selected);
|
|
3267
3480
|
}
|
|
3268
3481
|
function relocateImages(images, projectRoot) {
|
|
3269
|
-
const targetDir =
|
|
3482
|
+
const targetDir = join11(projectRoot, ".locus", "tmp", "images");
|
|
3270
3483
|
for (const img of images) {
|
|
3271
3484
|
if (!img.exists)
|
|
3272
3485
|
continue;
|
|
3273
3486
|
try {
|
|
3274
|
-
if (!
|
|
3487
|
+
if (!existsSync12(targetDir)) {
|
|
3275
3488
|
mkdirSync8(targetDir, { recursive: true });
|
|
3276
3489
|
}
|
|
3277
|
-
const dest =
|
|
3490
|
+
const dest = join11(targetDir, basename(img.stablePath));
|
|
3278
3491
|
copyFileSync(img.stablePath, dest);
|
|
3279
3492
|
img.stablePath = dest;
|
|
3280
3493
|
} catch {}
|
|
@@ -3286,7 +3499,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
|
|
|
3286
3499
|
return;
|
|
3287
3500
|
let resolved = stripQuotes(rawPath).replace(/\\ /g, " ");
|
|
3288
3501
|
if (resolved.startsWith("~/")) {
|
|
3289
|
-
resolved =
|
|
3502
|
+
resolved = join11(homedir3(), resolved.slice(2));
|
|
3290
3503
|
}
|
|
3291
3504
|
resolved = resolve(resolved);
|
|
3292
3505
|
const existing = byResolved.get(resolved);
|
|
@@ -3299,7 +3512,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
|
|
|
3299
3512
|
]);
|
|
3300
3513
|
return;
|
|
3301
3514
|
}
|
|
3302
|
-
const exists =
|
|
3515
|
+
const exists = existsSync12(resolved);
|
|
3303
3516
|
let stablePath = resolved;
|
|
3304
3517
|
if (exists) {
|
|
3305
3518
|
stablePath = copyToStable(resolved);
|
|
@@ -3353,10 +3566,10 @@ function dedupeByResolvedPath(images) {
|
|
|
3353
3566
|
}
|
|
3354
3567
|
function copyToStable(sourcePath) {
|
|
3355
3568
|
try {
|
|
3356
|
-
if (!
|
|
3569
|
+
if (!existsSync12(STABLE_DIR2)) {
|
|
3357
3570
|
mkdirSync8(STABLE_DIR2, { recursive: true });
|
|
3358
3571
|
}
|
|
3359
|
-
const dest =
|
|
3572
|
+
const dest = join11(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
|
|
3360
3573
|
copyFileSync(sourcePath, dest);
|
|
3361
3574
|
return dest;
|
|
3362
3575
|
} catch {
|
|
@@ -3376,7 +3589,7 @@ var init_image_detect = __esm(() => {
|
|
|
3376
3589
|
".tif",
|
|
3377
3590
|
".tiff"
|
|
3378
3591
|
]);
|
|
3379
|
-
STABLE_DIR2 =
|
|
3592
|
+
STABLE_DIR2 = join11(tmpdir2(), "locus-images");
|
|
3380
3593
|
PLACEHOLDER_ID_PATTERN = /\(locus:\/\/screenshot-(\d+)\)/g;
|
|
3381
3594
|
});
|
|
3382
3595
|
|
|
@@ -4160,10 +4373,7 @@ __export(exports_claude, {
|
|
|
4160
4373
|
});
|
|
4161
4374
|
import { execSync as execSync5, spawn as spawn2 } from "node:child_process";
|
|
4162
4375
|
function buildClaudeArgs(options) {
|
|
4163
|
-
const args = [
|
|
4164
|
-
"--dangerously-skip-permissions",
|
|
4165
|
-
"--no-session-persistence"
|
|
4166
|
-
];
|
|
4376
|
+
const args = ["--dangerously-skip-permissions", "--no-session-persistence"];
|
|
4167
4377
|
if (options.model) {
|
|
4168
4378
|
args.push("--model", options.model);
|
|
4169
4379
|
}
|
|
@@ -4355,11 +4565,11 @@ var init_claude = __esm(() => {
|
|
|
4355
4565
|
|
|
4356
4566
|
// src/core/sandbox-ignore.ts
|
|
4357
4567
|
import { exec } from "node:child_process";
|
|
4358
|
-
import { existsSync as
|
|
4359
|
-
import { join as
|
|
4568
|
+
import { existsSync as existsSync13, readFileSync as readFileSync8 } from "node:fs";
|
|
4569
|
+
import { join as join12 } from "node:path";
|
|
4360
4570
|
import { promisify } from "node:util";
|
|
4361
4571
|
function parseIgnoreFile(filePath) {
|
|
4362
|
-
if (!
|
|
4572
|
+
if (!existsSync13(filePath))
|
|
4363
4573
|
return [];
|
|
4364
4574
|
const content = readFileSync8(filePath, "utf-8");
|
|
4365
4575
|
const rules = [];
|
|
@@ -4406,7 +4616,7 @@ function buildCleanupScript(rules, workspacePath) {
|
|
|
4406
4616
|
}
|
|
4407
4617
|
async function enforceSandboxIgnore(sandboxName, projectRoot) {
|
|
4408
4618
|
const log = getLogger();
|
|
4409
|
-
const ignorePath =
|
|
4619
|
+
const ignorePath = join12(projectRoot, ".sandboxignore");
|
|
4410
4620
|
const rules = parseIgnoreFile(ignorePath);
|
|
4411
4621
|
if (rules.length === 0)
|
|
4412
4622
|
return;
|
|
@@ -5141,7 +5351,7 @@ ${red("✗")} ${dim("Force exit.")}\r
|
|
|
5141
5351
|
return {
|
|
5142
5352
|
success: false,
|
|
5143
5353
|
output: "",
|
|
5144
|
-
error: `
|
|
5354
|
+
error: `No sandbox configured for "${resolvedProvider}". ` + `Run "locus sandbox" and select "${resolvedProvider}" to create its sandbox, ` + `then authenticate via "locus sandbox ${resolvedProvider}".`,
|
|
5145
5355
|
interrupted: false,
|
|
5146
5356
|
exitCode: 1
|
|
5147
5357
|
};
|
|
@@ -6685,8 +6895,8 @@ var init_sprint = __esm(() => {
|
|
|
6685
6895
|
|
|
6686
6896
|
// src/core/prompt-builder.ts
|
|
6687
6897
|
import { execSync as execSync7 } from "node:child_process";
|
|
6688
|
-
import { existsSync as
|
|
6689
|
-
import { join as
|
|
6898
|
+
import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "node:fs";
|
|
6899
|
+
import { join as join13 } from "node:path";
|
|
6690
6900
|
function buildExecutionPrompt(ctx) {
|
|
6691
6901
|
const sections = [];
|
|
6692
6902
|
sections.push(buildSystemContext(ctx.projectRoot));
|
|
@@ -6716,13 +6926,13 @@ function buildFeedbackPrompt(ctx) {
|
|
|
6716
6926
|
}
|
|
6717
6927
|
function buildReplPrompt(userMessage, projectRoot, _config, previousMessages) {
|
|
6718
6928
|
const sections = [];
|
|
6719
|
-
const locusmd = readFileSafe(
|
|
6929
|
+
const locusmd = readFileSafe(join13(projectRoot, ".locus", "LOCUS.md"));
|
|
6720
6930
|
if (locusmd) {
|
|
6721
6931
|
sections.push(`<project-instructions>
|
|
6722
6932
|
${locusmd}
|
|
6723
6933
|
</project-instructions>`);
|
|
6724
6934
|
}
|
|
6725
|
-
const learnings = readFileSafe(
|
|
6935
|
+
const learnings = readFileSafe(join13(projectRoot, ".locus", "LEARNINGS.md"));
|
|
6726
6936
|
if (learnings) {
|
|
6727
6937
|
sections.push(`<past-learnings>
|
|
6728
6938
|
${learnings}
|
|
@@ -6748,24 +6958,24 @@ ${userMessage}
|
|
|
6748
6958
|
}
|
|
6749
6959
|
function buildSystemContext(projectRoot) {
|
|
6750
6960
|
const parts = [];
|
|
6751
|
-
const locusmd = readFileSafe(
|
|
6961
|
+
const locusmd = readFileSafe(join13(projectRoot, ".locus", "LOCUS.md"));
|
|
6752
6962
|
if (locusmd) {
|
|
6753
6963
|
parts.push(`<project-instructions>
|
|
6754
6964
|
${locusmd}
|
|
6755
6965
|
</project-instructions>`);
|
|
6756
6966
|
}
|
|
6757
|
-
const learnings = readFileSafe(
|
|
6967
|
+
const learnings = readFileSafe(join13(projectRoot, ".locus", "LEARNINGS.md"));
|
|
6758
6968
|
if (learnings) {
|
|
6759
6969
|
parts.push(`<past-learnings>
|
|
6760
6970
|
${learnings}
|
|
6761
6971
|
</past-learnings>`);
|
|
6762
6972
|
}
|
|
6763
|
-
const discussionsDir =
|
|
6764
|
-
if (
|
|
6973
|
+
const discussionsDir = join13(projectRoot, ".locus", "discussions");
|
|
6974
|
+
if (existsSync14(discussionsDir)) {
|
|
6765
6975
|
try {
|
|
6766
6976
|
const files = readdirSync3(discussionsDir).filter((f) => f.endsWith(".md")).slice(0, 3);
|
|
6767
6977
|
for (const file of files) {
|
|
6768
|
-
const content = readFileSafe(
|
|
6978
|
+
const content = readFileSafe(join13(discussionsDir, file));
|
|
6769
6979
|
if (content) {
|
|
6770
6980
|
const name = file.replace(".md", "");
|
|
6771
6981
|
parts.push(`<discussion name="${name}">
|
|
@@ -6916,7 +7126,7 @@ function buildFeedbackInstructions() {
|
|
|
6916
7126
|
}
|
|
6917
7127
|
function readFileSafe(path) {
|
|
6918
7128
|
try {
|
|
6919
|
-
if (!
|
|
7129
|
+
if (!existsSync14(path))
|
|
6920
7130
|
return null;
|
|
6921
7131
|
return readFileSync9(path, "utf-8");
|
|
6922
7132
|
} catch {
|
|
@@ -7382,7 +7592,7 @@ var init_commands = __esm(() => {
|
|
|
7382
7592
|
|
|
7383
7593
|
// src/repl/completions.ts
|
|
7384
7594
|
import { readdirSync as readdirSync4 } from "node:fs";
|
|
7385
|
-
import { basename as basename2, dirname as dirname3, join as
|
|
7595
|
+
import { basename as basename2, dirname as dirname3, join as join14 } from "node:path";
|
|
7386
7596
|
|
|
7387
7597
|
class SlashCommandCompletion {
|
|
7388
7598
|
commands;
|
|
@@ -7437,7 +7647,7 @@ class FilePathCompletion {
|
|
|
7437
7647
|
}
|
|
7438
7648
|
findMatches(partial) {
|
|
7439
7649
|
try {
|
|
7440
|
-
const dir = partial.includes("/") ?
|
|
7650
|
+
const dir = partial.includes("/") ? join14(this.projectRoot, dirname3(partial)) : this.projectRoot;
|
|
7441
7651
|
const prefix = basename2(partial);
|
|
7442
7652
|
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
7443
7653
|
return entries.filter((e) => {
|
|
@@ -7473,14 +7683,14 @@ class CombinedCompletion {
|
|
|
7473
7683
|
var init_completions = () => {};
|
|
7474
7684
|
|
|
7475
7685
|
// src/repl/input-history.ts
|
|
7476
|
-
import { existsSync as
|
|
7477
|
-
import { dirname as dirname4, join as
|
|
7686
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
|
|
7687
|
+
import { dirname as dirname4, join as join15 } from "node:path";
|
|
7478
7688
|
|
|
7479
7689
|
class InputHistory {
|
|
7480
7690
|
entries = [];
|
|
7481
7691
|
filePath;
|
|
7482
7692
|
constructor(projectRoot) {
|
|
7483
|
-
this.filePath =
|
|
7693
|
+
this.filePath = join15(projectRoot, ".locus", "sessions", ".input-history");
|
|
7484
7694
|
this.load();
|
|
7485
7695
|
}
|
|
7486
7696
|
add(text) {
|
|
@@ -7519,7 +7729,7 @@ class InputHistory {
|
|
|
7519
7729
|
}
|
|
7520
7730
|
load() {
|
|
7521
7731
|
try {
|
|
7522
|
-
if (!
|
|
7732
|
+
if (!existsSync15(this.filePath))
|
|
7523
7733
|
return;
|
|
7524
7734
|
const content = readFileSync10(this.filePath, "utf-8");
|
|
7525
7735
|
this.entries = content.split(`
|
|
@@ -7529,7 +7739,7 @@ class InputHistory {
|
|
|
7529
7739
|
save() {
|
|
7530
7740
|
try {
|
|
7531
7741
|
const dir = dirname4(this.filePath);
|
|
7532
|
-
if (!
|
|
7742
|
+
if (!existsSync15(dir)) {
|
|
7533
7743
|
mkdirSync9(dir, { recursive: true });
|
|
7534
7744
|
}
|
|
7535
7745
|
const content = this.entries.map((e) => this.escape(e)).join(`
|
|
@@ -7560,20 +7770,20 @@ var init_model_config = __esm(() => {
|
|
|
7560
7770
|
|
|
7561
7771
|
// src/repl/session-manager.ts
|
|
7562
7772
|
import {
|
|
7563
|
-
existsSync as
|
|
7773
|
+
existsSync as existsSync16,
|
|
7564
7774
|
mkdirSync as mkdirSync10,
|
|
7565
7775
|
readdirSync as readdirSync5,
|
|
7566
7776
|
readFileSync as readFileSync11,
|
|
7567
7777
|
unlinkSync as unlinkSync3,
|
|
7568
7778
|
writeFileSync as writeFileSync7
|
|
7569
7779
|
} from "node:fs";
|
|
7570
|
-
import { basename as basename3, join as
|
|
7780
|
+
import { basename as basename3, join as join16 } from "node:path";
|
|
7571
7781
|
|
|
7572
7782
|
class SessionManager {
|
|
7573
7783
|
sessionsDir;
|
|
7574
7784
|
constructor(projectRoot) {
|
|
7575
|
-
this.sessionsDir =
|
|
7576
|
-
if (!
|
|
7785
|
+
this.sessionsDir = join16(projectRoot, ".locus", "sessions");
|
|
7786
|
+
if (!existsSync16(this.sessionsDir)) {
|
|
7577
7787
|
mkdirSync10(this.sessionsDir, { recursive: true });
|
|
7578
7788
|
}
|
|
7579
7789
|
}
|
|
@@ -7599,12 +7809,12 @@ class SessionManager {
|
|
|
7599
7809
|
}
|
|
7600
7810
|
isPersisted(sessionOrId) {
|
|
7601
7811
|
const sessionId = typeof sessionOrId === "string" ? sessionOrId : sessionOrId.id;
|
|
7602
|
-
return
|
|
7812
|
+
return existsSync16(this.getSessionPath(sessionId));
|
|
7603
7813
|
}
|
|
7604
7814
|
load(idOrPrefix) {
|
|
7605
7815
|
const files = this.listSessionFiles();
|
|
7606
7816
|
const exactPath = this.getSessionPath(idOrPrefix);
|
|
7607
|
-
if (
|
|
7817
|
+
if (existsSync16(exactPath)) {
|
|
7608
7818
|
try {
|
|
7609
7819
|
return JSON.parse(readFileSync11(exactPath, "utf-8"));
|
|
7610
7820
|
} catch {
|
|
@@ -7654,7 +7864,7 @@ class SessionManager {
|
|
|
7654
7864
|
}
|
|
7655
7865
|
delete(sessionId) {
|
|
7656
7866
|
const path = this.getSessionPath(sessionId);
|
|
7657
|
-
if (
|
|
7867
|
+
if (existsSync16(path)) {
|
|
7658
7868
|
unlinkSync3(path);
|
|
7659
7869
|
return true;
|
|
7660
7870
|
}
|
|
@@ -7684,7 +7894,7 @@ class SessionManager {
|
|
|
7684
7894
|
const remaining = withStats.length - pruned;
|
|
7685
7895
|
if (remaining > MAX_SESSIONS) {
|
|
7686
7896
|
const toRemove = remaining - MAX_SESSIONS;
|
|
7687
|
-
const alive = withStats.filter((e) =>
|
|
7897
|
+
const alive = withStats.filter((e) => existsSync16(e.path));
|
|
7688
7898
|
for (let i = 0;i < toRemove && i < alive.length; i++) {
|
|
7689
7899
|
try {
|
|
7690
7900
|
unlinkSync3(alive[i].path);
|
|
@@ -7699,7 +7909,7 @@ class SessionManager {
|
|
|
7699
7909
|
}
|
|
7700
7910
|
listSessionFiles() {
|
|
7701
7911
|
try {
|
|
7702
|
-
return readdirSync5(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) =>
|
|
7912
|
+
return readdirSync5(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join16(this.sessionsDir, f));
|
|
7703
7913
|
} catch {
|
|
7704
7914
|
return [];
|
|
7705
7915
|
}
|
|
@@ -7708,7 +7918,7 @@ class SessionManager {
|
|
|
7708
7918
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
7709
7919
|
}
|
|
7710
7920
|
getSessionPath(sessionId) {
|
|
7711
|
-
return
|
|
7921
|
+
return join16(this.sessionsDir, `${sessionId}.json`);
|
|
7712
7922
|
}
|
|
7713
7923
|
}
|
|
7714
7924
|
var MAX_SESSIONS = 50, SESSION_MAX_AGE_MS;
|
|
@@ -7787,7 +7997,8 @@ async function runInteractiveRepl(session, sessionManager, options) {
|
|
|
7787
7997
|
process.stderr.write(`${dim("Using")} ${dim(provider)} ${dim("sandbox")} ${dim(sandboxName)}
|
|
7788
7998
|
`);
|
|
7789
7999
|
} else {
|
|
7790
|
-
|
|
8000
|
+
const mismatch = checkProviderSandboxMismatch(config.sandbox, config.ai.model, config.ai.provider);
|
|
8001
|
+
process.stderr.write(`${yellow("⚠")} ${dim(mismatch ?? `No sandbox configured for ${provider}. Run locus sandbox.`)}
|
|
7791
8002
|
`);
|
|
7792
8003
|
}
|
|
7793
8004
|
}
|
|
@@ -7831,7 +8042,8 @@ async function runInteractiveRepl(session, sessionManager, options) {
|
|
|
7831
8042
|
`);
|
|
7832
8043
|
} else {
|
|
7833
8044
|
sandboxRunner = null;
|
|
7834
|
-
|
|
8045
|
+
const mismatch = checkProviderSandboxMismatch(config.sandbox, model, config.ai.provider);
|
|
8046
|
+
process.stderr.write(`${yellow("⚠")} ${dim(mismatch ?? `No sandbox configured for ${inferredProvider}. Run locus sandbox.`)}
|
|
7835
8047
|
`);
|
|
7836
8048
|
}
|
|
7837
8049
|
}
|
|
@@ -8136,7 +8348,8 @@ async function handleJsonStream(projectRoot, config, args, sessionId) {
|
|
|
8136
8348
|
const sandboxName = getProviderSandboxName(config.sandbox, config.ai.provider);
|
|
8137
8349
|
const runner = config.sandbox.enabled ? sandboxName ? createUserManagedSandboxRunner(config.ai.provider, sandboxName) : null : await createRunnerAsync(config.ai.provider, false);
|
|
8138
8350
|
if (!runner) {
|
|
8139
|
-
|
|
8351
|
+
const mismatch = checkProviderSandboxMismatch(config.sandbox, config.ai.model, config.ai.provider);
|
|
8352
|
+
stream.emitError(mismatch ?? `No sandbox configured for "${config.ai.provider}". Run "locus sandbox" to create one.`, false);
|
|
8140
8353
|
return;
|
|
8141
8354
|
}
|
|
8142
8355
|
const available = await runner.isAvailable();
|
|
@@ -8185,8 +8398,167 @@ var init_exec = __esm(() => {
|
|
|
8185
8398
|
init_session_manager();
|
|
8186
8399
|
});
|
|
8187
8400
|
|
|
8188
|
-
// src/core/
|
|
8401
|
+
// src/core/submodule.ts
|
|
8189
8402
|
import { execSync as execSync10 } from "node:child_process";
|
|
8403
|
+
import { existsSync as existsSync17 } from "node:fs";
|
|
8404
|
+
import { join as join17 } from "node:path";
|
|
8405
|
+
function git2(args, cwd) {
|
|
8406
|
+
return execSync10(`git ${args}`, {
|
|
8407
|
+
cwd,
|
|
8408
|
+
encoding: "utf-8",
|
|
8409
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
8410
|
+
});
|
|
8411
|
+
}
|
|
8412
|
+
function gitSafe(args, cwd) {
|
|
8413
|
+
try {
|
|
8414
|
+
return git2(args, cwd);
|
|
8415
|
+
} catch {
|
|
8416
|
+
return null;
|
|
8417
|
+
}
|
|
8418
|
+
}
|
|
8419
|
+
function hasSubmodules(cwd) {
|
|
8420
|
+
return existsSync17(join17(cwd, ".gitmodules"));
|
|
8421
|
+
}
|
|
8422
|
+
function listSubmodules(cwd) {
|
|
8423
|
+
if (!hasSubmodules(cwd))
|
|
8424
|
+
return [];
|
|
8425
|
+
const log = getLogger();
|
|
8426
|
+
const submodules = [];
|
|
8427
|
+
try {
|
|
8428
|
+
const output = git2("submodule status", cwd);
|
|
8429
|
+
for (const line of output.trim().split(`
|
|
8430
|
+
`)) {
|
|
8431
|
+
if (!line.trim())
|
|
8432
|
+
continue;
|
|
8433
|
+
const dirty = line.startsWith("+");
|
|
8434
|
+
const parts = line.trim().replace(/^[+-]/, "").split(/\s+/);
|
|
8435
|
+
const path = parts[1];
|
|
8436
|
+
if (!path)
|
|
8437
|
+
continue;
|
|
8438
|
+
submodules.push({
|
|
8439
|
+
path,
|
|
8440
|
+
absolutePath: join17(cwd, path),
|
|
8441
|
+
dirty
|
|
8442
|
+
});
|
|
8443
|
+
}
|
|
8444
|
+
} catch (e) {
|
|
8445
|
+
log.warn(`Failed to list submodules: ${e}`);
|
|
8446
|
+
}
|
|
8447
|
+
return submodules;
|
|
8448
|
+
}
|
|
8449
|
+
function getDirtySubmodules(cwd) {
|
|
8450
|
+
const submodules = listSubmodules(cwd);
|
|
8451
|
+
const dirty = [];
|
|
8452
|
+
for (const sub of submodules) {
|
|
8453
|
+
if (!existsSync17(sub.absolutePath))
|
|
8454
|
+
continue;
|
|
8455
|
+
const status = gitSafe("status --porcelain", sub.absolutePath);
|
|
8456
|
+
if (status && status.trim().length > 0) {
|
|
8457
|
+
dirty.push({ ...sub, dirty: true });
|
|
8458
|
+
}
|
|
8459
|
+
}
|
|
8460
|
+
return dirty;
|
|
8461
|
+
}
|
|
8462
|
+
function commitDirtySubmodules(cwd, issueNumber, issueTitle) {
|
|
8463
|
+
const log = getLogger();
|
|
8464
|
+
const dirtySubmodules = getDirtySubmodules(cwd);
|
|
8465
|
+
if (dirtySubmodules.length === 0)
|
|
8466
|
+
return [];
|
|
8467
|
+
const committed = [];
|
|
8468
|
+
for (const sub of dirtySubmodules) {
|
|
8469
|
+
try {
|
|
8470
|
+
git2("add -A", sub.absolutePath);
|
|
8471
|
+
const message = `chore: complete #${issueNumber} - ${issueTitle}
|
|
8472
|
+
|
|
8473
|
+
Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
8474
|
+
execSync10("git commit -F -", {
|
|
8475
|
+
input: message,
|
|
8476
|
+
cwd: sub.absolutePath,
|
|
8477
|
+
encoding: "utf-8",
|
|
8478
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
8479
|
+
});
|
|
8480
|
+
committed.push(sub.path);
|
|
8481
|
+
log.info(`Committed submodule changes: ${sub.path} for #${issueNumber}`);
|
|
8482
|
+
} catch {
|
|
8483
|
+
log.verbose(`No committable changes in submodule ${sub.path}`);
|
|
8484
|
+
}
|
|
8485
|
+
}
|
|
8486
|
+
if (committed.length > 0) {
|
|
8487
|
+
for (const subPath of committed) {
|
|
8488
|
+
gitSafe(`add ${subPath}`, cwd);
|
|
8489
|
+
}
|
|
8490
|
+
}
|
|
8491
|
+
return committed;
|
|
8492
|
+
}
|
|
8493
|
+
function initSubmodules(cwd) {
|
|
8494
|
+
if (!hasSubmodules(cwd))
|
|
8495
|
+
return;
|
|
8496
|
+
const log = getLogger();
|
|
8497
|
+
try {
|
|
8498
|
+
git2("submodule update --init --recursive", cwd);
|
|
8499
|
+
log.info("Initialized submodules");
|
|
8500
|
+
} catch (e) {
|
|
8501
|
+
log.warn(`Failed to initialize submodules: ${e}`);
|
|
8502
|
+
}
|
|
8503
|
+
}
|
|
8504
|
+
function updateSubmodulesAfterRebase(cwd) {
|
|
8505
|
+
if (!hasSubmodules(cwd))
|
|
8506
|
+
return;
|
|
8507
|
+
const log = getLogger();
|
|
8508
|
+
try {
|
|
8509
|
+
git2("submodule update --recursive", cwd);
|
|
8510
|
+
log.info("Updated submodules after rebase");
|
|
8511
|
+
} catch (e) {
|
|
8512
|
+
log.warn(`Failed to update submodules after rebase: ${e}`);
|
|
8513
|
+
}
|
|
8514
|
+
}
|
|
8515
|
+
function getSubmoduleChangeSummary(cwd, baseBranch) {
|
|
8516
|
+
if (!hasSubmodules(cwd))
|
|
8517
|
+
return null;
|
|
8518
|
+
const diff = gitSafe(`diff origin/${baseBranch}..HEAD --submodule=short`, cwd);
|
|
8519
|
+
if (!diff || !diff.trim())
|
|
8520
|
+
return null;
|
|
8521
|
+
const submoduleChanges = [];
|
|
8522
|
+
for (const line of diff.split(`
|
|
8523
|
+
`)) {
|
|
8524
|
+
if (line.startsWith("Submodule ")) {
|
|
8525
|
+
submoduleChanges.push(line.trim());
|
|
8526
|
+
}
|
|
8527
|
+
}
|
|
8528
|
+
if (submoduleChanges.length === 0)
|
|
8529
|
+
return null;
|
|
8530
|
+
return `### Submodule Changes
|
|
8531
|
+
${submoduleChanges.map((c) => `- ${c}`).join(`
|
|
8532
|
+
`)}`;
|
|
8533
|
+
}
|
|
8534
|
+
function pushSubmoduleBranches(cwd) {
|
|
8535
|
+
if (!hasSubmodules(cwd))
|
|
8536
|
+
return;
|
|
8537
|
+
const log = getLogger();
|
|
8538
|
+
const submodules = listSubmodules(cwd);
|
|
8539
|
+
for (const sub of submodules) {
|
|
8540
|
+
if (!existsSync17(sub.absolutePath))
|
|
8541
|
+
continue;
|
|
8542
|
+
const branch = gitSafe("rev-parse --abbrev-ref HEAD", sub.absolutePath)?.trim();
|
|
8543
|
+
if (!branch || branch === "HEAD")
|
|
8544
|
+
continue;
|
|
8545
|
+
const unpushed = gitSafe(`log origin/${branch}..HEAD --oneline`, sub.absolutePath)?.trim();
|
|
8546
|
+
if (unpushed) {
|
|
8547
|
+
try {
|
|
8548
|
+
git2(`push origin ${branch}`, sub.absolutePath);
|
|
8549
|
+
log.info(`Pushed submodule ${sub.path} branch ${branch}`);
|
|
8550
|
+
} catch (e) {
|
|
8551
|
+
log.warn(`Failed to push submodule ${sub.path}: ${e}`);
|
|
8552
|
+
}
|
|
8553
|
+
}
|
|
8554
|
+
}
|
|
8555
|
+
}
|
|
8556
|
+
var init_submodule = __esm(() => {
|
|
8557
|
+
init_logger();
|
|
8558
|
+
});
|
|
8559
|
+
|
|
8560
|
+
// src/core/agent.ts
|
|
8561
|
+
import { execSync as execSync11 } from "node:child_process";
|
|
8190
8562
|
async function executeIssue(projectRoot, options) {
|
|
8191
8563
|
const log = getLogger();
|
|
8192
8564
|
const timer = createTimer();
|
|
@@ -8215,7 +8587,7 @@ ${cyan("●")} ${bold(`#${issueNumber}`)} ${issue.title}
|
|
|
8215
8587
|
}
|
|
8216
8588
|
let issueComments = [];
|
|
8217
8589
|
try {
|
|
8218
|
-
const commentsRaw =
|
|
8590
|
+
const commentsRaw = execSync11(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
8219
8591
|
if (commentsRaw) {
|
|
8220
8592
|
issueComments = commentsRaw.split(`
|
|
8221
8593
|
`).filter(Boolean);
|
|
@@ -8379,12 +8751,12 @@ ${aiResult.success ? green("✓") : red("✗")} Iteration ${aiResult.success ? "
|
|
|
8379
8751
|
}
|
|
8380
8752
|
async function createIssuePR(projectRoot, config, issue) {
|
|
8381
8753
|
try {
|
|
8382
|
-
const currentBranch =
|
|
8754
|
+
const currentBranch = execSync11("git rev-parse --abbrev-ref HEAD", {
|
|
8383
8755
|
cwd: projectRoot,
|
|
8384
8756
|
encoding: "utf-8",
|
|
8385
8757
|
stdio: ["pipe", "pipe", "pipe"]
|
|
8386
8758
|
}).trim();
|
|
8387
|
-
const diff =
|
|
8759
|
+
const diff = execSync11(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
8388
8760
|
cwd: projectRoot,
|
|
8389
8761
|
encoding: "utf-8",
|
|
8390
8762
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8393,17 +8765,25 @@ async function createIssuePR(projectRoot, config, issue) {
|
|
|
8393
8765
|
getLogger().verbose("No changes to create PR for");
|
|
8394
8766
|
return;
|
|
8395
8767
|
}
|
|
8396
|
-
|
|
8768
|
+
pushSubmoduleBranches(projectRoot);
|
|
8769
|
+
execSync11(`git push -u origin ${currentBranch}`, {
|
|
8397
8770
|
cwd: projectRoot,
|
|
8398
8771
|
encoding: "utf-8",
|
|
8399
8772
|
stdio: ["pipe", "pipe", "pipe"]
|
|
8400
8773
|
});
|
|
8401
|
-
const
|
|
8402
|
-
|
|
8774
|
+
const submoduleSummary = getSubmoduleChangeSummary(projectRoot, config.agent.baseBranch);
|
|
8775
|
+
let prBody = `Closes #${issue.number}`;
|
|
8776
|
+
if (submoduleSummary) {
|
|
8777
|
+
prBody += `
|
|
8778
|
+
|
|
8779
|
+
${submoduleSummary}`;
|
|
8780
|
+
}
|
|
8781
|
+
prBody += `
|
|
8403
8782
|
|
|
8404
8783
|
---
|
|
8405
8784
|
|
|
8406
8785
|
\uD83E\uDD16 Automated by [Locus](https://github.com/locusai/locus)`;
|
|
8786
|
+
const prTitle = `${issue.title} (#${issue.number})`;
|
|
8407
8787
|
const prNumber = createPR(prTitle, prBody, currentBranch, config.agent.baseBranch, { cwd: projectRoot });
|
|
8408
8788
|
process.stderr.write(` ${green("✓")} Created PR #${prNumber}
|
|
8409
8789
|
`);
|
|
@@ -8437,20 +8817,21 @@ var init_agent = __esm(() => {
|
|
|
8437
8817
|
init_logger();
|
|
8438
8818
|
init_prompt_builder();
|
|
8439
8819
|
init_sandbox();
|
|
8820
|
+
init_submodule();
|
|
8440
8821
|
});
|
|
8441
8822
|
|
|
8442
8823
|
// src/core/conflict.ts
|
|
8443
|
-
import { execSync as
|
|
8444
|
-
function
|
|
8445
|
-
return
|
|
8824
|
+
import { execSync as execSync12 } from "node:child_process";
|
|
8825
|
+
function git3(args, cwd) {
|
|
8826
|
+
return execSync12(`git ${args}`, {
|
|
8446
8827
|
cwd,
|
|
8447
8828
|
encoding: "utf-8",
|
|
8448
8829
|
stdio: ["pipe", "pipe", "pipe"]
|
|
8449
8830
|
});
|
|
8450
8831
|
}
|
|
8451
|
-
function
|
|
8832
|
+
function gitSafe2(args, cwd) {
|
|
8452
8833
|
try {
|
|
8453
|
-
return
|
|
8834
|
+
return git3(args, cwd);
|
|
8454
8835
|
} catch {
|
|
8455
8836
|
return null;
|
|
8456
8837
|
}
|
|
@@ -8458,7 +8839,7 @@ function gitSafe(args, cwd) {
|
|
|
8458
8839
|
function checkForConflicts(cwd, baseBranch) {
|
|
8459
8840
|
const log = getLogger();
|
|
8460
8841
|
try {
|
|
8461
|
-
|
|
8842
|
+
git3(`fetch origin ${baseBranch}`, cwd);
|
|
8462
8843
|
log.debug("Fetched latest from origin", { baseBranch });
|
|
8463
8844
|
} catch (e) {
|
|
8464
8845
|
log.warn(`Could not fetch origin/${baseBranch}: ${e}`);
|
|
@@ -8469,8 +8850,8 @@ function checkForConflicts(cwd, baseBranch) {
|
|
|
8469
8850
|
newCommits: 0
|
|
8470
8851
|
};
|
|
8471
8852
|
}
|
|
8472
|
-
const currentBranch =
|
|
8473
|
-
const mergeBase =
|
|
8853
|
+
const currentBranch = gitSafe2("rev-parse --abbrev-ref HEAD", cwd)?.trim() ?? "";
|
|
8854
|
+
const mergeBase = gitSafe2(`merge-base ${currentBranch} origin/${baseBranch}`, cwd)?.trim();
|
|
8474
8855
|
if (!mergeBase) {
|
|
8475
8856
|
log.debug("Could not find merge base — branches may be unrelated");
|
|
8476
8857
|
return {
|
|
@@ -8480,7 +8861,7 @@ function checkForConflicts(cwd, baseBranch) {
|
|
|
8480
8861
|
newCommits: 0
|
|
8481
8862
|
};
|
|
8482
8863
|
}
|
|
8483
|
-
const remoteTip =
|
|
8864
|
+
const remoteTip = gitSafe2(`rev-parse origin/${baseBranch}`, cwd)?.trim();
|
|
8484
8865
|
if (!remoteTip || remoteTip === mergeBase) {
|
|
8485
8866
|
log.debug("Base branch has not advanced");
|
|
8486
8867
|
return {
|
|
@@ -8490,16 +8871,16 @@ function checkForConflicts(cwd, baseBranch) {
|
|
|
8490
8871
|
newCommits: 0
|
|
8491
8872
|
};
|
|
8492
8873
|
}
|
|
8493
|
-
const newCommitsOutput =
|
|
8874
|
+
const newCommitsOutput = gitSafe2(`rev-list --count ${mergeBase}..origin/${baseBranch}`, cwd)?.trim() ?? "0";
|
|
8494
8875
|
const newCommits = Number.parseInt(newCommitsOutput, 10);
|
|
8495
8876
|
log.verbose(`Base branch has ${newCommits} new commits`, {
|
|
8496
8877
|
baseBranch,
|
|
8497
8878
|
mergeBase: mergeBase.slice(0, 8),
|
|
8498
8879
|
remoteTip: remoteTip.slice(0, 8)
|
|
8499
8880
|
});
|
|
8500
|
-
const ourChanges =
|
|
8881
|
+
const ourChanges = gitSafe2(`diff --name-only ${mergeBase}..HEAD`, cwd)?.trim().split(`
|
|
8501
8882
|
`).filter(Boolean) ?? [];
|
|
8502
|
-
const theirChanges =
|
|
8883
|
+
const theirChanges = gitSafe2(`diff --name-only ${mergeBase}..origin/${baseBranch}`, cwd)?.trim().split(`
|
|
8503
8884
|
`).filter(Boolean) ?? [];
|
|
8504
8885
|
const overlapping = ourChanges.filter((f) => theirChanges.includes(f));
|
|
8505
8886
|
return {
|
|
@@ -8512,18 +8893,19 @@ function checkForConflicts(cwd, baseBranch) {
|
|
|
8512
8893
|
function attemptRebase(cwd, baseBranch) {
|
|
8513
8894
|
const log = getLogger();
|
|
8514
8895
|
try {
|
|
8515
|
-
|
|
8896
|
+
git3(`rebase origin/${baseBranch}`, cwd);
|
|
8516
8897
|
log.info(`Successfully rebased onto origin/${baseBranch}`);
|
|
8898
|
+
updateSubmodulesAfterRebase(cwd);
|
|
8517
8899
|
return { success: true };
|
|
8518
8900
|
} catch (_e) {
|
|
8519
8901
|
log.warn("Rebase failed, aborting");
|
|
8520
8902
|
const conflicts = [];
|
|
8521
8903
|
try {
|
|
8522
|
-
const status =
|
|
8904
|
+
const status = git3("diff --name-only --diff-filter=U", cwd);
|
|
8523
8905
|
conflicts.push(...status.trim().split(`
|
|
8524
8906
|
`).filter(Boolean));
|
|
8525
8907
|
} catch {}
|
|
8526
|
-
|
|
8908
|
+
gitSafe2("rebase --abort", cwd);
|
|
8527
8909
|
return { success: false, conflicts };
|
|
8528
8910
|
}
|
|
8529
8911
|
}
|
|
@@ -8565,23 +8947,24 @@ ${bold(yellow("⚠"))} Base branch has ${result.newCommits} new commit${result.n
|
|
|
8565
8947
|
var init_conflict = __esm(() => {
|
|
8566
8948
|
init_terminal();
|
|
8567
8949
|
init_logger();
|
|
8950
|
+
init_submodule();
|
|
8568
8951
|
});
|
|
8569
8952
|
|
|
8570
8953
|
// src/core/run-state.ts
|
|
8571
8954
|
import {
|
|
8572
|
-
existsSync as
|
|
8955
|
+
existsSync as existsSync18,
|
|
8573
8956
|
mkdirSync as mkdirSync11,
|
|
8574
8957
|
readFileSync as readFileSync12,
|
|
8575
8958
|
unlinkSync as unlinkSync4,
|
|
8576
8959
|
writeFileSync as writeFileSync8
|
|
8577
8960
|
} from "node:fs";
|
|
8578
|
-
import { dirname as dirname5, join as
|
|
8961
|
+
import { dirname as dirname5, join as join18 } from "node:path";
|
|
8579
8962
|
function getRunStatePath(projectRoot) {
|
|
8580
|
-
return
|
|
8963
|
+
return join18(projectRoot, ".locus", "run-state.json");
|
|
8581
8964
|
}
|
|
8582
8965
|
function loadRunState(projectRoot) {
|
|
8583
8966
|
const path = getRunStatePath(projectRoot);
|
|
8584
|
-
if (!
|
|
8967
|
+
if (!existsSync18(path))
|
|
8585
8968
|
return null;
|
|
8586
8969
|
try {
|
|
8587
8970
|
return JSON.parse(readFileSync12(path, "utf-8"));
|
|
@@ -8593,7 +8976,7 @@ function loadRunState(projectRoot) {
|
|
|
8593
8976
|
function saveRunState(projectRoot, state) {
|
|
8594
8977
|
const path = getRunStatePath(projectRoot);
|
|
8595
8978
|
const dir = dirname5(path);
|
|
8596
|
-
if (!
|
|
8979
|
+
if (!existsSync18(dir)) {
|
|
8597
8980
|
mkdirSync11(dir, { recursive: true });
|
|
8598
8981
|
}
|
|
8599
8982
|
writeFileSync8(path, `${JSON.stringify(state, null, 2)}
|
|
@@ -8601,7 +8984,7 @@ function saveRunState(projectRoot, state) {
|
|
|
8601
8984
|
}
|
|
8602
8985
|
function clearRunState(projectRoot) {
|
|
8603
8986
|
const path = getRunStatePath(projectRoot);
|
|
8604
|
-
if (
|
|
8987
|
+
if (existsSync18(path)) {
|
|
8605
8988
|
unlinkSync4(path);
|
|
8606
8989
|
}
|
|
8607
8990
|
}
|
|
@@ -8741,28 +9124,28 @@ var init_shutdown = __esm(() => {
|
|
|
8741
9124
|
});
|
|
8742
9125
|
|
|
8743
9126
|
// src/core/worktree.ts
|
|
8744
|
-
import { execSync as
|
|
8745
|
-
import { existsSync as
|
|
8746
|
-
import { join as
|
|
8747
|
-
function
|
|
8748
|
-
return
|
|
9127
|
+
import { execSync as execSync13 } from "node:child_process";
|
|
9128
|
+
import { existsSync as existsSync19, readdirSync as readdirSync6, realpathSync, statSync as statSync3 } from "node:fs";
|
|
9129
|
+
import { join as join19 } from "node:path";
|
|
9130
|
+
function git4(args, cwd) {
|
|
9131
|
+
return execSync13(`git ${args}`, {
|
|
8749
9132
|
cwd,
|
|
8750
9133
|
encoding: "utf-8",
|
|
8751
9134
|
stdio: ["pipe", "pipe", "pipe"]
|
|
8752
9135
|
});
|
|
8753
9136
|
}
|
|
8754
|
-
function
|
|
9137
|
+
function gitSafe3(args, cwd) {
|
|
8755
9138
|
try {
|
|
8756
|
-
return
|
|
9139
|
+
return git4(args, cwd);
|
|
8757
9140
|
} catch {
|
|
8758
9141
|
return null;
|
|
8759
9142
|
}
|
|
8760
9143
|
}
|
|
8761
9144
|
function getWorktreeDir(projectRoot) {
|
|
8762
|
-
return
|
|
9145
|
+
return join19(projectRoot, ".locus", "worktrees");
|
|
8763
9146
|
}
|
|
8764
9147
|
function getWorktreePath(projectRoot, issueNumber) {
|
|
8765
|
-
return
|
|
9148
|
+
return join19(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
|
|
8766
9149
|
}
|
|
8767
9150
|
function generateBranchName(issueNumber) {
|
|
8768
9151
|
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
@@ -8770,7 +9153,7 @@ function generateBranchName(issueNumber) {
|
|
|
8770
9153
|
}
|
|
8771
9154
|
function getWorktreeBranch(worktreePath) {
|
|
8772
9155
|
try {
|
|
8773
|
-
return
|
|
9156
|
+
return execSync13("git branch --show-current", {
|
|
8774
9157
|
cwd: worktreePath,
|
|
8775
9158
|
encoding: "utf-8",
|
|
8776
9159
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8782,7 +9165,7 @@ function getWorktreeBranch(worktreePath) {
|
|
|
8782
9165
|
function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
8783
9166
|
const log = getLogger();
|
|
8784
9167
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
8785
|
-
if (
|
|
9168
|
+
if (existsSync19(worktreePath)) {
|
|
8786
9169
|
log.verbose(`Worktree already exists for issue #${issueNumber}`);
|
|
8787
9170
|
const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
|
|
8788
9171
|
return {
|
|
@@ -8793,7 +9176,8 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
|
8793
9176
|
};
|
|
8794
9177
|
}
|
|
8795
9178
|
const branch = generateBranchName(issueNumber);
|
|
8796
|
-
|
|
9179
|
+
git4(`worktree add ${JSON.stringify(worktreePath)} -b ${branch} ${baseBranch}`, projectRoot);
|
|
9180
|
+
initSubmodules(worktreePath);
|
|
8797
9181
|
log.info(`Created worktree for issue #${issueNumber}`, {
|
|
8798
9182
|
path: worktreePath,
|
|
8799
9183
|
branch,
|
|
@@ -8809,30 +9193,30 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
|
8809
9193
|
function removeWorktree(projectRoot, issueNumber) {
|
|
8810
9194
|
const log = getLogger();
|
|
8811
9195
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
8812
|
-
if (!
|
|
9196
|
+
if (!existsSync19(worktreePath)) {
|
|
8813
9197
|
log.verbose(`Worktree for issue #${issueNumber} does not exist`);
|
|
8814
9198
|
return;
|
|
8815
9199
|
}
|
|
8816
9200
|
const branch = getWorktreeBranch(worktreePath);
|
|
8817
9201
|
try {
|
|
8818
|
-
|
|
9202
|
+
git4(`worktree remove ${JSON.stringify(worktreePath)} --force`, projectRoot);
|
|
8819
9203
|
log.info(`Removed worktree for issue #${issueNumber}`);
|
|
8820
9204
|
} catch (e) {
|
|
8821
9205
|
log.warn(`Failed to remove worktree: ${e}`);
|
|
8822
|
-
|
|
9206
|
+
gitSafe3(`worktree remove ${JSON.stringify(worktreePath)} --force`, projectRoot);
|
|
8823
9207
|
}
|
|
8824
9208
|
if (branch) {
|
|
8825
|
-
|
|
9209
|
+
gitSafe3(`branch -D ${branch}`, projectRoot);
|
|
8826
9210
|
}
|
|
8827
9211
|
}
|
|
8828
9212
|
function listWorktrees(projectRoot) {
|
|
8829
9213
|
const log = getLogger();
|
|
8830
9214
|
const worktreeDir = getWorktreeDir(projectRoot);
|
|
8831
|
-
if (!
|
|
9215
|
+
if (!existsSync19(worktreeDir)) {
|
|
8832
9216
|
return [];
|
|
8833
9217
|
}
|
|
8834
9218
|
const entries = readdirSync6(worktreeDir).filter((entry) => entry.startsWith("issue-"));
|
|
8835
|
-
const gitWorktreeList =
|
|
9219
|
+
const gitWorktreeList = gitSafe3("worktree list --porcelain", projectRoot);
|
|
8836
9220
|
const activeWorktrees = new Set;
|
|
8837
9221
|
if (gitWorktreeList) {
|
|
8838
9222
|
for (const line of gitWorktreeList.split(`
|
|
@@ -8848,7 +9232,7 @@ function listWorktrees(projectRoot) {
|
|
|
8848
9232
|
if (!match)
|
|
8849
9233
|
continue;
|
|
8850
9234
|
const issueNumber = Number.parseInt(match[1], 10);
|
|
8851
|
-
const path =
|
|
9235
|
+
const path = join19(worktreeDir, entry);
|
|
8852
9236
|
const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
|
|
8853
9237
|
let resolvedPath;
|
|
8854
9238
|
try {
|
|
@@ -8882,12 +9266,13 @@ function cleanupStaleWorktrees(projectRoot) {
|
|
|
8882
9266
|
}
|
|
8883
9267
|
}
|
|
8884
9268
|
if (cleaned > 0) {
|
|
8885
|
-
|
|
9269
|
+
gitSafe3("worktree prune", projectRoot);
|
|
8886
9270
|
}
|
|
8887
9271
|
return cleaned;
|
|
8888
9272
|
}
|
|
8889
9273
|
var init_worktree = __esm(() => {
|
|
8890
9274
|
init_logger();
|
|
9275
|
+
init_submodule();
|
|
8891
9276
|
});
|
|
8892
9277
|
|
|
8893
9278
|
// src/commands/run.ts
|
|
@@ -8895,7 +9280,7 @@ var exports_run = {};
|
|
|
8895
9280
|
__export(exports_run, {
|
|
8896
9281
|
runCommand: () => runCommand
|
|
8897
9282
|
});
|
|
8898
|
-
import { execSync as
|
|
9283
|
+
import { execSync as execSync14 } from "node:child_process";
|
|
8899
9284
|
function resolveExecutionContext(config, modelOverride) {
|
|
8900
9285
|
const model = modelOverride ?? config.ai.model;
|
|
8901
9286
|
const provider = inferProviderFromModel(model) ?? config.ai.provider;
|
|
@@ -8967,6 +9352,15 @@ async function runCommand(projectRoot, args, flags = {}) {
|
|
|
8967
9352
|
process.stderr.write(`${yellow("⚠")} Running without sandbox. The AI agent will have unrestricted access to your filesystem, network, and environment variables.
|
|
8968
9353
|
`);
|
|
8969
9354
|
}
|
|
9355
|
+
if (sandboxed) {
|
|
9356
|
+
const model = flags.model ?? config.ai.model;
|
|
9357
|
+
const mismatch = checkProviderSandboxMismatch(config.sandbox, model, config.ai.provider);
|
|
9358
|
+
if (mismatch) {
|
|
9359
|
+
process.stderr.write(`${red("✗")} ${mismatch}
|
|
9360
|
+
`);
|
|
9361
|
+
return;
|
|
9362
|
+
}
|
|
9363
|
+
}
|
|
8970
9364
|
if (flags.resume) {
|
|
8971
9365
|
return handleResume(projectRoot, config, sandboxed);
|
|
8972
9366
|
}
|
|
@@ -9046,7 +9440,7 @@ ${yellow("⚠")} A sprint run is already in progress.
|
|
|
9046
9440
|
}
|
|
9047
9441
|
if (!flags.dryRun) {
|
|
9048
9442
|
try {
|
|
9049
|
-
|
|
9443
|
+
execSync14(`git checkout -B ${branchName}`, {
|
|
9050
9444
|
cwd: projectRoot,
|
|
9051
9445
|
encoding: "utf-8",
|
|
9052
9446
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9096,7 +9490,7 @@ ${red("✗")} Auto-rebase failed. Resolve manually.
|
|
|
9096
9490
|
let sprintContext;
|
|
9097
9491
|
if (i > 0 && !flags.dryRun) {
|
|
9098
9492
|
try {
|
|
9099
|
-
sprintContext =
|
|
9493
|
+
sprintContext = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD`, {
|
|
9100
9494
|
cwd: projectRoot,
|
|
9101
9495
|
encoding: "utf-8",
|
|
9102
9496
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9161,7 +9555,7 @@ ${bold("Summary:")}
|
|
|
9161
9555
|
const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
|
|
9162
9556
|
if (prNumber !== undefined) {
|
|
9163
9557
|
try {
|
|
9164
|
-
|
|
9558
|
+
execSync14(`git checkout ${config.agent.baseBranch}`, {
|
|
9165
9559
|
cwd: projectRoot,
|
|
9166
9560
|
encoding: "utf-8",
|
|
9167
9561
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9176,6 +9570,7 @@ ${bold("Summary:")}
|
|
|
9176
9570
|
}
|
|
9177
9571
|
}
|
|
9178
9572
|
async function handleSingleIssue(projectRoot, config, issueNumber, flags, sandboxed) {
|
|
9573
|
+
const log = getLogger();
|
|
9179
9574
|
const execution = resolveExecutionContext(config, flags.model);
|
|
9180
9575
|
let isSprintIssue = false;
|
|
9181
9576
|
try {
|
|
@@ -9184,7 +9579,7 @@ async function handleSingleIssue(projectRoot, config, issueNumber, flags, sandbo
|
|
|
9184
9579
|
} catch {}
|
|
9185
9580
|
if (isSprintIssue) {
|
|
9186
9581
|
process.stderr.write(`
|
|
9187
|
-
${bold("Running sprint issue")} ${cyan(`#${issueNumber}`)} ${dim("(sequential
|
|
9582
|
+
${bold("Running sprint issue")} ${cyan(`#${issueNumber}`)} ${dim("(sequential)")}
|
|
9188
9583
|
|
|
9189
9584
|
`);
|
|
9190
9585
|
await executeIssue(projectRoot, {
|
|
@@ -9197,42 +9592,48 @@ ${bold("Running sprint issue")} ${cyan(`#${issueNumber}`)} ${dim("(sequential, n
|
|
|
9197
9592
|
});
|
|
9198
9593
|
return;
|
|
9199
9594
|
}
|
|
9595
|
+
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
9596
|
+
const branchName = `locus/issue-${issueNumber}-${randomSuffix}`;
|
|
9200
9597
|
process.stderr.write(`
|
|
9201
|
-
${bold("Running issue")} ${cyan(`#${issueNumber}`)} ${dim(
|
|
9598
|
+
${bold("Running issue")} ${cyan(`#${issueNumber}`)} ${dim(`(branch: ${branchName})`)}
|
|
9202
9599
|
|
|
9203
9600
|
`);
|
|
9204
|
-
let worktreePath;
|
|
9205
9601
|
if (!flags.dryRun) {
|
|
9206
9602
|
try {
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
|
|
9603
|
+
execSync14(`git checkout -B ${branchName} ${config.agent.baseBranch}`, {
|
|
9604
|
+
cwd: projectRoot,
|
|
9605
|
+
encoding: "utf-8",
|
|
9606
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
9607
|
+
});
|
|
9608
|
+
log.info(`Checked out branch ${branchName}`);
|
|
9212
9609
|
} catch (e) {
|
|
9213
|
-
process.stderr.write(`${yellow("⚠")} Could not create
|
|
9610
|
+
process.stderr.write(`${yellow("⚠")} Could not create branch: ${e}
|
|
9214
9611
|
`);
|
|
9215
|
-
process.stderr.write(` ${dim("
|
|
9612
|
+
process.stderr.write(` ${dim("Running on current branch instead.")}
|
|
9216
9613
|
|
|
9217
9614
|
`);
|
|
9218
9615
|
}
|
|
9219
9616
|
}
|
|
9220
9617
|
const result = await executeIssue(projectRoot, {
|
|
9221
9618
|
issueNumber,
|
|
9222
|
-
worktreePath,
|
|
9223
9619
|
provider: execution.provider,
|
|
9224
9620
|
model: execution.model,
|
|
9225
9621
|
dryRun: flags.dryRun,
|
|
9226
9622
|
sandboxed,
|
|
9227
9623
|
sandboxName: execution.sandboxName
|
|
9228
9624
|
});
|
|
9229
|
-
if (
|
|
9625
|
+
if (!flags.dryRun) {
|
|
9230
9626
|
if (result.success) {
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9627
|
+
try {
|
|
9628
|
+
execSync14(`git checkout ${config.agent.baseBranch}`, {
|
|
9629
|
+
cwd: projectRoot,
|
|
9630
|
+
encoding: "utf-8",
|
|
9631
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
9632
|
+
});
|
|
9633
|
+
log.info(`Checked out ${config.agent.baseBranch}`);
|
|
9634
|
+
} catch {}
|
|
9234
9635
|
} else {
|
|
9235
|
-
process.stderr.write(` ${yellow("⚠")}
|
|
9636
|
+
process.stderr.write(` ${yellow("⚠")} Branch ${dim(branchName)} preserved for debugging.
|
|
9236
9637
|
`);
|
|
9237
9638
|
}
|
|
9238
9639
|
}
|
|
@@ -9361,13 +9762,13 @@ ${bold("Resuming")} ${state.type} run ${dim(state.runId)}
|
|
|
9361
9762
|
`);
|
|
9362
9763
|
if (state.type === "sprint" && state.branch) {
|
|
9363
9764
|
try {
|
|
9364
|
-
const currentBranch =
|
|
9765
|
+
const currentBranch = execSync14("git rev-parse --abbrev-ref HEAD", {
|
|
9365
9766
|
cwd: projectRoot,
|
|
9366
9767
|
encoding: "utf-8",
|
|
9367
9768
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9368
9769
|
}).trim();
|
|
9369
9770
|
if (currentBranch !== state.branch) {
|
|
9370
|
-
|
|
9771
|
+
execSync14(`git checkout ${state.branch}`, {
|
|
9371
9772
|
cwd: projectRoot,
|
|
9372
9773
|
encoding: "utf-8",
|
|
9373
9774
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9434,7 +9835,7 @@ ${bold("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fail
|
|
|
9434
9835
|
const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
|
|
9435
9836
|
if (prNumber !== undefined) {
|
|
9436
9837
|
try {
|
|
9437
|
-
|
|
9838
|
+
execSync14(`git checkout ${config.agent.baseBranch}`, {
|
|
9438
9839
|
cwd: projectRoot,
|
|
9439
9840
|
encoding: "utf-8",
|
|
9440
9841
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9465,14 +9866,19 @@ function getOrder2(issue) {
|
|
|
9465
9866
|
}
|
|
9466
9867
|
function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
9467
9868
|
try {
|
|
9468
|
-
const
|
|
9869
|
+
const committedSubmodules = commitDirtySubmodules(projectRoot, issueNumber, issueTitle);
|
|
9870
|
+
if (committedSubmodules.length > 0) {
|
|
9871
|
+
process.stderr.write(` ${dim(`Committed submodule changes: ${committedSubmodules.join(", ")}`)}
|
|
9872
|
+
`);
|
|
9873
|
+
}
|
|
9874
|
+
const status = execSync14("git status --porcelain", {
|
|
9469
9875
|
cwd: projectRoot,
|
|
9470
9876
|
encoding: "utf-8",
|
|
9471
9877
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9472
9878
|
}).trim();
|
|
9473
9879
|
if (!status)
|
|
9474
9880
|
return;
|
|
9475
|
-
|
|
9881
|
+
execSync14("git add -A", {
|
|
9476
9882
|
cwd: projectRoot,
|
|
9477
9883
|
encoding: "utf-8",
|
|
9478
9884
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9480,7 +9886,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
|
9480
9886
|
const message = `chore: complete #${issueNumber} - ${issueTitle}
|
|
9481
9887
|
|
|
9482
9888
|
Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
9483
|
-
|
|
9889
|
+
execSync14(`git commit -F -`, {
|
|
9484
9890
|
input: message,
|
|
9485
9891
|
cwd: projectRoot,
|
|
9486
9892
|
encoding: "utf-8",
|
|
@@ -9494,7 +9900,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
9494
9900
|
if (!config.agent.autoPR)
|
|
9495
9901
|
return;
|
|
9496
9902
|
try {
|
|
9497
|
-
const diff =
|
|
9903
|
+
const diff = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
9498
9904
|
cwd: projectRoot,
|
|
9499
9905
|
encoding: "utf-8",
|
|
9500
9906
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9504,16 +9910,24 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
9504
9910
|
`);
|
|
9505
9911
|
return;
|
|
9506
9912
|
}
|
|
9507
|
-
|
|
9913
|
+
pushSubmoduleBranches(projectRoot);
|
|
9914
|
+
execSync14(`git push -u origin ${branchName}`, {
|
|
9508
9915
|
cwd: projectRoot,
|
|
9509
9916
|
encoding: "utf-8",
|
|
9510
9917
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9511
9918
|
});
|
|
9512
9919
|
const taskLines = tasks.map((t) => `- Closes #${t.issue}${t.title ? `: ${t.title}` : ""}`).join(`
|
|
9513
9920
|
`);
|
|
9514
|
-
const
|
|
9921
|
+
const submoduleSummary = getSubmoduleChangeSummary(projectRoot, config.agent.baseBranch);
|
|
9922
|
+
let prBody = `## Sprint: ${sprintName}
|
|
9923
|
+
|
|
9924
|
+
${taskLines}`;
|
|
9925
|
+
if (submoduleSummary) {
|
|
9926
|
+
prBody += `
|
|
9515
9927
|
|
|
9516
|
-
${
|
|
9928
|
+
${submoduleSummary}`;
|
|
9929
|
+
}
|
|
9930
|
+
prBody += `
|
|
9517
9931
|
|
|
9518
9932
|
---
|
|
9519
9933
|
|
|
@@ -9530,8 +9944,8 @@ ${taskLines}
|
|
|
9530
9944
|
}
|
|
9531
9945
|
}
|
|
9532
9946
|
var init_run = __esm(() => {
|
|
9533
|
-
init_ai_models();
|
|
9534
9947
|
init_agent();
|
|
9948
|
+
init_ai_models();
|
|
9535
9949
|
init_config();
|
|
9536
9950
|
init_conflict();
|
|
9537
9951
|
init_github();
|
|
@@ -9540,6 +9954,7 @@ var init_run = __esm(() => {
|
|
|
9540
9954
|
init_run_state();
|
|
9541
9955
|
init_sandbox();
|
|
9542
9956
|
init_shutdown();
|
|
9957
|
+
init_submodule();
|
|
9543
9958
|
init_worktree();
|
|
9544
9959
|
init_progress();
|
|
9545
9960
|
init_terminal();
|
|
@@ -9654,13 +10069,13 @@ __export(exports_plan, {
|
|
|
9654
10069
|
parsePlanArgs: () => parsePlanArgs
|
|
9655
10070
|
});
|
|
9656
10071
|
import {
|
|
9657
|
-
existsSync as
|
|
10072
|
+
existsSync as existsSync20,
|
|
9658
10073
|
mkdirSync as mkdirSync12,
|
|
9659
10074
|
readdirSync as readdirSync7,
|
|
9660
10075
|
readFileSync as readFileSync13,
|
|
9661
10076
|
writeFileSync as writeFileSync9
|
|
9662
10077
|
} from "node:fs";
|
|
9663
|
-
import { join as
|
|
10078
|
+
import { join as join20 } from "node:path";
|
|
9664
10079
|
function printHelp() {
|
|
9665
10080
|
process.stderr.write(`
|
|
9666
10081
|
${bold("locus plan")} — AI-powered sprint planning
|
|
@@ -9691,11 +10106,11 @@ function normalizeSprintName(name) {
|
|
|
9691
10106
|
return name.trim().toLowerCase();
|
|
9692
10107
|
}
|
|
9693
10108
|
function getPlansDir(projectRoot) {
|
|
9694
|
-
return
|
|
10109
|
+
return join20(projectRoot, ".locus", "plans");
|
|
9695
10110
|
}
|
|
9696
10111
|
function ensurePlansDir(projectRoot) {
|
|
9697
10112
|
const dir = getPlansDir(projectRoot);
|
|
9698
|
-
if (!
|
|
10113
|
+
if (!existsSync20(dir)) {
|
|
9699
10114
|
mkdirSync12(dir, { recursive: true });
|
|
9700
10115
|
}
|
|
9701
10116
|
return dir;
|
|
@@ -9705,14 +10120,14 @@ function generateId() {
|
|
|
9705
10120
|
}
|
|
9706
10121
|
function loadPlanFile(projectRoot, id) {
|
|
9707
10122
|
const dir = getPlansDir(projectRoot);
|
|
9708
|
-
if (!
|
|
10123
|
+
if (!existsSync20(dir))
|
|
9709
10124
|
return null;
|
|
9710
10125
|
const files = readdirSync7(dir).filter((f) => f.endsWith(".json"));
|
|
9711
10126
|
const match = files.find((f) => f.startsWith(id));
|
|
9712
10127
|
if (!match)
|
|
9713
10128
|
return null;
|
|
9714
10129
|
try {
|
|
9715
|
-
const content = readFileSync13(
|
|
10130
|
+
const content = readFileSync13(join20(dir, match), "utf-8");
|
|
9716
10131
|
return JSON.parse(content);
|
|
9717
10132
|
} catch {
|
|
9718
10133
|
return null;
|
|
@@ -9758,7 +10173,7 @@ async function planCommand(projectRoot, args, flags = {}) {
|
|
|
9758
10173
|
}
|
|
9759
10174
|
function handleListPlans(projectRoot) {
|
|
9760
10175
|
const dir = getPlansDir(projectRoot);
|
|
9761
|
-
if (!
|
|
10176
|
+
if (!existsSync20(dir)) {
|
|
9762
10177
|
process.stderr.write(`${dim("No saved plans yet.")}
|
|
9763
10178
|
`);
|
|
9764
10179
|
return;
|
|
@@ -9776,7 +10191,7 @@ ${bold("Saved Plans:")}
|
|
|
9776
10191
|
for (const file of files) {
|
|
9777
10192
|
const id = file.replace(".json", "");
|
|
9778
10193
|
try {
|
|
9779
|
-
const content = readFileSync13(
|
|
10194
|
+
const content = readFileSync13(join20(dir, file), "utf-8");
|
|
9780
10195
|
const plan = JSON.parse(content);
|
|
9781
10196
|
const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
|
|
9782
10197
|
const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
|
|
@@ -9887,7 +10302,7 @@ ${bold("Approving plan:")}
|
|
|
9887
10302
|
async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
|
|
9888
10303
|
const id = generateId();
|
|
9889
10304
|
const plansDir = ensurePlansDir(projectRoot);
|
|
9890
|
-
const planPath =
|
|
10305
|
+
const planPath = join20(plansDir, `${id}.json`);
|
|
9891
10306
|
const planPathRelative = `.locus/plans/${id}.json`;
|
|
9892
10307
|
const displayDirective = directive;
|
|
9893
10308
|
process.stderr.write(`
|
|
@@ -9899,6 +10314,14 @@ ${bold("Planning:")} ${cyan(displayDirective)}
|
|
|
9899
10314
|
}
|
|
9900
10315
|
process.stderr.write(`
|
|
9901
10316
|
`);
|
|
10317
|
+
if (config.sandbox.enabled) {
|
|
10318
|
+
const mismatch = checkProviderSandboxMismatch(config.sandbox, flags.model ?? config.ai.model, config.ai.provider);
|
|
10319
|
+
if (mismatch) {
|
|
10320
|
+
process.stderr.write(`${red("✗")} ${mismatch}
|
|
10321
|
+
`);
|
|
10322
|
+
return;
|
|
10323
|
+
}
|
|
10324
|
+
}
|
|
9902
10325
|
const prompt = buildPlanningPrompt(projectRoot, config, directive, sprintName, id, planPathRelative);
|
|
9903
10326
|
const aiResult = await runAI({
|
|
9904
10327
|
prompt,
|
|
@@ -9921,7 +10344,7 @@ ${red("✗")} Planning failed: ${aiResult.error}
|
|
|
9921
10344
|
`);
|
|
9922
10345
|
return;
|
|
9923
10346
|
}
|
|
9924
|
-
if (!
|
|
10347
|
+
if (!existsSync20(planPath)) {
|
|
9925
10348
|
process.stderr.write(`
|
|
9926
10349
|
${yellow("⚠")} Plan file was not created at ${bold(planPathRelative)}.
|
|
9927
10350
|
`);
|
|
@@ -9994,6 +10417,14 @@ ${bold("Organizing issues for:")} ${cyan(sprintName)}
|
|
|
9994
10417
|
`);
|
|
9995
10418
|
return;
|
|
9996
10419
|
}
|
|
10420
|
+
if (config.sandbox.enabled) {
|
|
10421
|
+
const mismatch = checkProviderSandboxMismatch(config.sandbox, flags.model ?? config.ai.model, config.ai.provider);
|
|
10422
|
+
if (mismatch) {
|
|
10423
|
+
process.stderr.write(`${red("✗")} ${mismatch}
|
|
10424
|
+
`);
|
|
10425
|
+
return;
|
|
10426
|
+
}
|
|
10427
|
+
}
|
|
9997
10428
|
const issueDescriptions = issues.map((i) => `#${i.number}: ${i.title}
|
|
9998
10429
|
${i.body?.slice(0, 300) ?? ""}`).join(`
|
|
9999
10430
|
|
|
@@ -10094,15 +10525,15 @@ ${directive}${sprintName ? `
|
|
|
10094
10525
|
|
|
10095
10526
|
**Sprint:** ${sprintName}` : ""}
|
|
10096
10527
|
</directive>`);
|
|
10097
|
-
const locusPath =
|
|
10098
|
-
if (
|
|
10528
|
+
const locusPath = join20(projectRoot, ".locus", "LOCUS.md");
|
|
10529
|
+
if (existsSync20(locusPath)) {
|
|
10099
10530
|
const content = readFileSync13(locusPath, "utf-8");
|
|
10100
10531
|
parts.push(`<project-context>
|
|
10101
10532
|
${content.slice(0, 3000)}
|
|
10102
10533
|
</project-context>`);
|
|
10103
10534
|
}
|
|
10104
|
-
const learningsPath =
|
|
10105
|
-
if (
|
|
10535
|
+
const learningsPath = join20(projectRoot, ".locus", "LEARNINGS.md");
|
|
10536
|
+
if (existsSync20(learningsPath)) {
|
|
10106
10537
|
const content = readFileSync13(learningsPath, "utf-8");
|
|
10107
10538
|
parts.push(`<past-learnings>
|
|
10108
10539
|
${content.slice(0, 2000)}
|
|
@@ -10273,8 +10704,8 @@ var init_plan = __esm(() => {
|
|
|
10273
10704
|
init_run_ai();
|
|
10274
10705
|
init_config();
|
|
10275
10706
|
init_github();
|
|
10276
|
-
init_terminal();
|
|
10277
10707
|
init_sandbox();
|
|
10708
|
+
init_terminal();
|
|
10278
10709
|
});
|
|
10279
10710
|
|
|
10280
10711
|
// src/commands/review.ts
|
|
@@ -10282,9 +10713,9 @@ var exports_review = {};
|
|
|
10282
10713
|
__export(exports_review, {
|
|
10283
10714
|
reviewCommand: () => reviewCommand
|
|
10284
10715
|
});
|
|
10285
|
-
import { execSync as
|
|
10286
|
-
import { existsSync as
|
|
10287
|
-
import { join as
|
|
10716
|
+
import { execSync as execSync15 } from "node:child_process";
|
|
10717
|
+
import { existsSync as existsSync21, readFileSync as readFileSync14 } from "node:fs";
|
|
10718
|
+
import { join as join21 } from "node:path";
|
|
10288
10719
|
function printHelp2() {
|
|
10289
10720
|
process.stderr.write(`
|
|
10290
10721
|
${bold("locus review")} — AI-powered code review
|
|
@@ -10323,6 +10754,14 @@ async function reviewCommand(projectRoot, args, flags = {}) {
|
|
|
10323
10754
|
prNumber = Number.parseInt(args[i], 10);
|
|
10324
10755
|
}
|
|
10325
10756
|
}
|
|
10757
|
+
if (config.sandbox.enabled) {
|
|
10758
|
+
const mismatch = checkProviderSandboxMismatch(config.sandbox, flags.model ?? config.ai.model, config.ai.provider);
|
|
10759
|
+
if (mismatch) {
|
|
10760
|
+
process.stderr.write(`${red("✗")} ${mismatch}
|
|
10761
|
+
`);
|
|
10762
|
+
return;
|
|
10763
|
+
}
|
|
10764
|
+
}
|
|
10326
10765
|
if (prNumber) {
|
|
10327
10766
|
return reviewSinglePR(projectRoot, config, prNumber, focus, flags);
|
|
10328
10767
|
}
|
|
@@ -10360,7 +10799,7 @@ ${bold("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red(`
|
|
|
10360
10799
|
async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
|
|
10361
10800
|
let prInfo;
|
|
10362
10801
|
try {
|
|
10363
|
-
const result =
|
|
10802
|
+
const result = execSync15(`gh pr view ${prNumber} --json number,title,body,state,headRefName,baseRefName,labels,url,createdAt`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10364
10803
|
const raw = JSON.parse(result);
|
|
10365
10804
|
prInfo = {
|
|
10366
10805
|
number: raw.number,
|
|
@@ -10426,7 +10865,7 @@ ${output.slice(0, 60000)}
|
|
|
10426
10865
|
|
|
10427
10866
|
---
|
|
10428
10867
|
_Reviewed by Locus AI (${config.ai.provider}/${flags.model ?? config.ai.model})_`;
|
|
10429
|
-
|
|
10868
|
+
execSync15(`gh pr comment ${pr.number} --body ${JSON.stringify(reviewBody)}`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10430
10869
|
process.stderr.write(` ${green("✓")} Review posted ${dim(`(${timer.formatted()})`)}
|
|
10431
10870
|
`);
|
|
10432
10871
|
} catch (e) {
|
|
@@ -10444,8 +10883,8 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
|
|
|
10444
10883
|
parts.push(`<role>
|
|
10445
10884
|
You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.
|
|
10446
10885
|
</role>`);
|
|
10447
|
-
const locusPath =
|
|
10448
|
-
if (
|
|
10886
|
+
const locusPath = join21(projectRoot, ".locus", "LOCUS.md");
|
|
10887
|
+
if (existsSync21(locusPath)) {
|
|
10449
10888
|
const content = readFileSync14(locusPath, "utf-8");
|
|
10450
10889
|
parts.push(`<project-context>
|
|
10451
10890
|
${content.slice(0, 2000)}
|
|
@@ -10506,7 +10945,7 @@ var exports_iterate = {};
|
|
|
10506
10945
|
__export(exports_iterate, {
|
|
10507
10946
|
iterateCommand: () => iterateCommand
|
|
10508
10947
|
});
|
|
10509
|
-
import { execSync as
|
|
10948
|
+
import { execSync as execSync16 } from "node:child_process";
|
|
10510
10949
|
function printHelp3() {
|
|
10511
10950
|
process.stderr.write(`
|
|
10512
10951
|
${bold("locus iterate")} — Re-execute tasks with PR feedback
|
|
@@ -10550,6 +10989,14 @@ async function iterateCommand(projectRoot, args, flags = {}) {
|
|
|
10550
10989
|
issueNumber = Number.parseInt(args[i], 10);
|
|
10551
10990
|
}
|
|
10552
10991
|
}
|
|
10992
|
+
if (config.sandbox.enabled) {
|
|
10993
|
+
const mismatch = checkProviderSandboxMismatch(config.sandbox, config.ai.model, config.ai.provider);
|
|
10994
|
+
if (mismatch) {
|
|
10995
|
+
process.stderr.write(`${red("✗")} ${mismatch}
|
|
10996
|
+
`);
|
|
10997
|
+
return;
|
|
10998
|
+
}
|
|
10999
|
+
}
|
|
10553
11000
|
if (prNumber) {
|
|
10554
11001
|
return handleSinglePR(projectRoot, config, prNumber, flags);
|
|
10555
11002
|
}
|
|
@@ -10716,12 +11163,12 @@ ${bold("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red(`✗ ${fa
|
|
|
10716
11163
|
}
|
|
10717
11164
|
function findPRForIssue(projectRoot, issueNumber) {
|
|
10718
11165
|
try {
|
|
10719
|
-
const result =
|
|
11166
|
+
const result = execSync16(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10720
11167
|
const parsed = JSON.parse(result);
|
|
10721
11168
|
if (parsed.length > 0) {
|
|
10722
11169
|
return parsed[0].number;
|
|
10723
11170
|
}
|
|
10724
|
-
const branchResult =
|
|
11171
|
+
const branchResult = execSync16(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
10725
11172
|
const branchParsed = JSON.parse(branchResult);
|
|
10726
11173
|
if (branchParsed.length > 0) {
|
|
10727
11174
|
return branchParsed[0].number;
|
|
@@ -10746,6 +11193,7 @@ var init_iterate = __esm(() => {
|
|
|
10746
11193
|
init_agent();
|
|
10747
11194
|
init_config();
|
|
10748
11195
|
init_github();
|
|
11196
|
+
init_sandbox();
|
|
10749
11197
|
init_progress();
|
|
10750
11198
|
init_terminal();
|
|
10751
11199
|
});
|
|
@@ -10756,14 +11204,14 @@ __export(exports_discuss, {
|
|
|
10756
11204
|
discussCommand: () => discussCommand
|
|
10757
11205
|
});
|
|
10758
11206
|
import {
|
|
10759
|
-
existsSync as
|
|
11207
|
+
existsSync as existsSync22,
|
|
10760
11208
|
mkdirSync as mkdirSync13,
|
|
10761
11209
|
readdirSync as readdirSync8,
|
|
10762
11210
|
readFileSync as readFileSync15,
|
|
10763
11211
|
unlinkSync as unlinkSync5,
|
|
10764
11212
|
writeFileSync as writeFileSync10
|
|
10765
11213
|
} from "node:fs";
|
|
10766
|
-
import { join as
|
|
11214
|
+
import { join as join22 } from "node:path";
|
|
10767
11215
|
function printHelp4() {
|
|
10768
11216
|
process.stderr.write(`
|
|
10769
11217
|
${bold("locus discuss")} — AI-powered architectural discussions
|
|
@@ -10785,11 +11233,11 @@ ${bold("Examples:")}
|
|
|
10785
11233
|
`);
|
|
10786
11234
|
}
|
|
10787
11235
|
function getDiscussionsDir(projectRoot) {
|
|
10788
|
-
return
|
|
11236
|
+
return join22(projectRoot, ".locus", "discussions");
|
|
10789
11237
|
}
|
|
10790
11238
|
function ensureDiscussionsDir(projectRoot) {
|
|
10791
11239
|
const dir = getDiscussionsDir(projectRoot);
|
|
10792
|
-
if (!
|
|
11240
|
+
if (!existsSync22(dir)) {
|
|
10793
11241
|
mkdirSync13(dir, { recursive: true });
|
|
10794
11242
|
}
|
|
10795
11243
|
return dir;
|
|
@@ -10824,7 +11272,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
|
|
|
10824
11272
|
}
|
|
10825
11273
|
function listDiscussions(projectRoot) {
|
|
10826
11274
|
const dir = getDiscussionsDir(projectRoot);
|
|
10827
|
-
if (!
|
|
11275
|
+
if (!existsSync22(dir)) {
|
|
10828
11276
|
process.stderr.write(`${dim("No discussions yet.")}
|
|
10829
11277
|
`);
|
|
10830
11278
|
return;
|
|
@@ -10841,7 +11289,7 @@ ${bold("Discussions:")}
|
|
|
10841
11289
|
`);
|
|
10842
11290
|
for (const file of files) {
|
|
10843
11291
|
const id = file.replace(".md", "");
|
|
10844
|
-
const content = readFileSync15(
|
|
11292
|
+
const content = readFileSync15(join22(dir, file), "utf-8");
|
|
10845
11293
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
10846
11294
|
const title = titleMatch ? titleMatch[1] : id;
|
|
10847
11295
|
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
|
|
@@ -10859,7 +11307,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
10859
11307
|
return;
|
|
10860
11308
|
}
|
|
10861
11309
|
const dir = getDiscussionsDir(projectRoot);
|
|
10862
|
-
if (!
|
|
11310
|
+
if (!existsSync22(dir)) {
|
|
10863
11311
|
process.stderr.write(`${red("✗")} No discussions found.
|
|
10864
11312
|
`);
|
|
10865
11313
|
return;
|
|
@@ -10871,7 +11319,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
10871
11319
|
`);
|
|
10872
11320
|
return;
|
|
10873
11321
|
}
|
|
10874
|
-
const content = readFileSync15(
|
|
11322
|
+
const content = readFileSync15(join22(dir, match), "utf-8");
|
|
10875
11323
|
process.stdout.write(`${content}
|
|
10876
11324
|
`);
|
|
10877
11325
|
}
|
|
@@ -10882,7 +11330,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
10882
11330
|
return;
|
|
10883
11331
|
}
|
|
10884
11332
|
const dir = getDiscussionsDir(projectRoot);
|
|
10885
|
-
if (!
|
|
11333
|
+
if (!existsSync22(dir)) {
|
|
10886
11334
|
process.stderr.write(`${red("✗")} No discussions found.
|
|
10887
11335
|
`);
|
|
10888
11336
|
return;
|
|
@@ -10894,7 +11342,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
10894
11342
|
`);
|
|
10895
11343
|
return;
|
|
10896
11344
|
}
|
|
10897
|
-
unlinkSync5(
|
|
11345
|
+
unlinkSync5(join22(dir, match));
|
|
10898
11346
|
process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
|
|
10899
11347
|
`);
|
|
10900
11348
|
}
|
|
@@ -10907,7 +11355,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
10907
11355
|
return;
|
|
10908
11356
|
}
|
|
10909
11357
|
const dir = getDiscussionsDir(projectRoot);
|
|
10910
|
-
if (!
|
|
11358
|
+
if (!existsSync22(dir)) {
|
|
10911
11359
|
process.stderr.write(`${red("✗")} No discussions found.
|
|
10912
11360
|
`);
|
|
10913
11361
|
return;
|
|
@@ -10919,7 +11367,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
10919
11367
|
`);
|
|
10920
11368
|
return;
|
|
10921
11369
|
}
|
|
10922
|
-
const content = readFileSync15(
|
|
11370
|
+
const content = readFileSync15(join22(dir, match), "utf-8");
|
|
10923
11371
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
10924
11372
|
const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
|
|
10925
11373
|
await planCommand(projectRoot, [
|
|
@@ -10950,6 +11398,14 @@ async function startDiscussion(projectRoot, topic, flags) {
|
|
|
10950
11398
|
const config = loadConfig(projectRoot);
|
|
10951
11399
|
const timer = createTimer();
|
|
10952
11400
|
const id = generateId2();
|
|
11401
|
+
if (config.sandbox.enabled) {
|
|
11402
|
+
const mismatch = checkProviderSandboxMismatch(config.sandbox, flags.model ?? config.ai.model, config.ai.provider);
|
|
11403
|
+
if (mismatch) {
|
|
11404
|
+
process.stderr.write(`${red("✗")} ${mismatch}
|
|
11405
|
+
`);
|
|
11406
|
+
return;
|
|
11407
|
+
}
|
|
11408
|
+
}
|
|
10953
11409
|
process.stderr.write(`
|
|
10954
11410
|
${bold("Discussion:")} ${cyan(topic)}
|
|
10955
11411
|
|
|
@@ -11033,7 +11489,7 @@ ${turn.content}`;
|
|
|
11033
11489
|
...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
|
|
11034
11490
|
].join(`
|
|
11035
11491
|
`);
|
|
11036
|
-
writeFileSync10(
|
|
11492
|
+
writeFileSync10(join22(dir, `${id}.md`), markdown, "utf-8");
|
|
11037
11493
|
process.stderr.write(`
|
|
11038
11494
|
${green("✓")} Discussion saved: ${cyan(id)} ${dim(`(${timer.formatted()})`)}
|
|
11039
11495
|
`);
|
|
@@ -11048,15 +11504,15 @@ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFi
|
|
|
11048
11504
|
parts.push(`<role>
|
|
11049
11505
|
You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.
|
|
11050
11506
|
</role>`);
|
|
11051
|
-
const locusPath =
|
|
11052
|
-
if (
|
|
11507
|
+
const locusPath = join22(projectRoot, ".locus", "LOCUS.md");
|
|
11508
|
+
if (existsSync22(locusPath)) {
|
|
11053
11509
|
const content = readFileSync15(locusPath, "utf-8");
|
|
11054
11510
|
parts.push(`<project-context>
|
|
11055
11511
|
${content.slice(0, 3000)}
|
|
11056
11512
|
</project-context>`);
|
|
11057
11513
|
}
|
|
11058
|
-
const learningsPath =
|
|
11059
|
-
if (
|
|
11514
|
+
const learningsPath = join22(projectRoot, ".locus", "LEARNINGS.md");
|
|
11515
|
+
if (existsSync22(learningsPath)) {
|
|
11060
11516
|
const content = readFileSync15(learningsPath, "utf-8");
|
|
11061
11517
|
parts.push(`<past-learnings>
|
|
11062
11518
|
${content.slice(0, 2000)}
|
|
@@ -11128,8 +11584,8 @@ __export(exports_artifacts, {
|
|
|
11128
11584
|
formatDate: () => formatDate2,
|
|
11129
11585
|
artifactsCommand: () => artifactsCommand
|
|
11130
11586
|
});
|
|
11131
|
-
import { existsSync as
|
|
11132
|
-
import { join as
|
|
11587
|
+
import { existsSync as existsSync23, readdirSync as readdirSync9, readFileSync as readFileSync16, statSync as statSync4 } from "node:fs";
|
|
11588
|
+
import { join as join23 } from "node:path";
|
|
11133
11589
|
function printHelp5() {
|
|
11134
11590
|
process.stderr.write(`
|
|
11135
11591
|
${bold("locus artifacts")} — View and manage AI-generated artifacts
|
|
@@ -11149,14 +11605,14 @@ ${dim("Artifact names support partial matching.")}
|
|
|
11149
11605
|
`);
|
|
11150
11606
|
}
|
|
11151
11607
|
function getArtifactsDir(projectRoot) {
|
|
11152
|
-
return
|
|
11608
|
+
return join23(projectRoot, ".locus", "artifacts");
|
|
11153
11609
|
}
|
|
11154
11610
|
function listArtifacts(projectRoot) {
|
|
11155
11611
|
const dir = getArtifactsDir(projectRoot);
|
|
11156
|
-
if (!
|
|
11612
|
+
if (!existsSync23(dir))
|
|
11157
11613
|
return [];
|
|
11158
11614
|
return readdirSync9(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
|
|
11159
|
-
const filePath =
|
|
11615
|
+
const filePath = join23(dir, fileName);
|
|
11160
11616
|
const stat = statSync4(filePath);
|
|
11161
11617
|
return {
|
|
11162
11618
|
name: fileName.replace(/\.md$/, ""),
|
|
@@ -11169,8 +11625,8 @@ function listArtifacts(projectRoot) {
|
|
|
11169
11625
|
function readArtifact(projectRoot, name) {
|
|
11170
11626
|
const dir = getArtifactsDir(projectRoot);
|
|
11171
11627
|
const fileName = name.endsWith(".md") ? name : `${name}.md`;
|
|
11172
|
-
const filePath =
|
|
11173
|
-
if (!
|
|
11628
|
+
const filePath = join23(dir, fileName);
|
|
11629
|
+
if (!existsSync23(filePath))
|
|
11174
11630
|
return null;
|
|
11175
11631
|
const stat = statSync4(filePath);
|
|
11176
11632
|
return {
|
|
@@ -11340,16 +11796,17 @@ __export(exports_sandbox2, {
|
|
|
11340
11796
|
parseSandboxInstallArgs: () => parseSandboxInstallArgs,
|
|
11341
11797
|
parseSandboxExecArgs: () => parseSandboxExecArgs
|
|
11342
11798
|
});
|
|
11343
|
-
import { execSync as
|
|
11799
|
+
import { execSync as execSync17, spawn as spawn6 } from "node:child_process";
|
|
11344
11800
|
import { createHash } from "node:crypto";
|
|
11345
|
-
import { existsSync as
|
|
11346
|
-
import { basename as basename4, join as
|
|
11801
|
+
import { existsSync as existsSync24, readFileSync as readFileSync17 } from "node:fs";
|
|
11802
|
+
import { basename as basename4, join as join24 } from "node:path";
|
|
11803
|
+
import { createInterface as createInterface3 } from "node:readline";
|
|
11347
11804
|
function printSandboxHelp() {
|
|
11348
11805
|
process.stderr.write(`
|
|
11349
11806
|
${bold("locus sandbox")} — Manage Docker sandbox lifecycle
|
|
11350
11807
|
|
|
11351
11808
|
${bold("Usage:")}
|
|
11352
|
-
locus sandbox ${dim("#
|
|
11809
|
+
locus sandbox ${dim("# Select a provider and create its sandbox")}
|
|
11353
11810
|
locus sandbox claude ${dim("# Run claude interactively (for login)")}
|
|
11354
11811
|
locus sandbox codex ${dim("# Run codex interactively (for login)")}
|
|
11355
11812
|
locus sandbox setup ${dim("# Re-run dependency install in sandbox(es)")}
|
|
@@ -11360,10 +11817,10 @@ ${bold("Usage:")}
|
|
|
11360
11817
|
locus sandbox status ${dim("# Show current sandbox state")}
|
|
11361
11818
|
|
|
11362
11819
|
${bold("Flow:")}
|
|
11363
|
-
1. ${cyan("locus sandbox")}
|
|
11364
|
-
2. ${cyan("locus sandbox
|
|
11365
|
-
3. ${cyan("locus sandbox
|
|
11366
|
-
4. ${cyan("locus sandbox
|
|
11820
|
+
1. ${cyan("locus sandbox")} Select a provider and create its sandbox
|
|
11821
|
+
2. ${cyan("locus sandbox <provider>")} Login to the provider inside its sandbox
|
|
11822
|
+
3. ${cyan("locus sandbox install bun")} Install extra tools (optional)
|
|
11823
|
+
4. Run ${cyan("locus sandbox")} again to add another provider (optional)
|
|
11367
11824
|
|
|
11368
11825
|
`);
|
|
11369
11826
|
}
|
|
@@ -11397,6 +11854,41 @@ async function sandboxCommand(projectRoot, args) {
|
|
|
11397
11854
|
`);
|
|
11398
11855
|
}
|
|
11399
11856
|
}
|
|
11857
|
+
async function promptProviderSelection() {
|
|
11858
|
+
process.stderr.write(`
|
|
11859
|
+
${bold("Select a provider to create a sandbox for:")}
|
|
11860
|
+
|
|
11861
|
+
`);
|
|
11862
|
+
for (let i = 0;i < PROVIDERS.length; i++) {
|
|
11863
|
+
process.stderr.write(` ${bold(String(i + 1))}. ${PROVIDERS[i]}
|
|
11864
|
+
`);
|
|
11865
|
+
}
|
|
11866
|
+
process.stderr.write(`
|
|
11867
|
+
`);
|
|
11868
|
+
const rl = createInterface3({
|
|
11869
|
+
input: process.stdin,
|
|
11870
|
+
output: process.stderr
|
|
11871
|
+
});
|
|
11872
|
+
return new Promise((resolve2) => {
|
|
11873
|
+
rl.question("Enter choice (1-2): ", (answer) => {
|
|
11874
|
+
rl.close();
|
|
11875
|
+
const trimmed = answer.trim().toLowerCase();
|
|
11876
|
+
if (trimmed === "claude" || trimmed === "codex") {
|
|
11877
|
+
resolve2(trimmed);
|
|
11878
|
+
return;
|
|
11879
|
+
}
|
|
11880
|
+
const num = Number.parseInt(trimmed, 10);
|
|
11881
|
+
if (num >= 1 && num <= PROVIDERS.length) {
|
|
11882
|
+
resolve2(PROVIDERS[num - 1]);
|
|
11883
|
+
return;
|
|
11884
|
+
}
|
|
11885
|
+
process.stderr.write(`${red("✗")} Invalid selection.
|
|
11886
|
+
`);
|
|
11887
|
+
resolve2(null);
|
|
11888
|
+
});
|
|
11889
|
+
rl.on("close", () => resolve2(null));
|
|
11890
|
+
});
|
|
11891
|
+
}
|
|
11400
11892
|
async function handleCreate(projectRoot) {
|
|
11401
11893
|
const config = loadConfig(projectRoot);
|
|
11402
11894
|
const status = await detectSandboxSupport();
|
|
@@ -11407,54 +11899,44 @@ async function handleCreate(projectRoot) {
|
|
|
11407
11899
|
`);
|
|
11408
11900
|
return;
|
|
11409
11901
|
}
|
|
11902
|
+
const provider = await promptProviderSelection();
|
|
11903
|
+
if (!provider)
|
|
11904
|
+
return;
|
|
11410
11905
|
const sandboxNames = buildProviderSandboxNames(projectRoot);
|
|
11411
|
-
const
|
|
11412
|
-
const
|
|
11413
|
-
|
|
11414
|
-
|
|
11415
|
-
|
|
11416
|
-
|
|
11417
|
-
|
|
11906
|
+
const name = sandboxNames[provider];
|
|
11907
|
+
const readySandboxes = { ...config.sandbox.providers };
|
|
11908
|
+
if (isSandboxAlive(name)) {
|
|
11909
|
+
process.stderr.write(`${green("✓")} ${provider} sandbox already exists: ${bold(name)}
|
|
11910
|
+
`);
|
|
11911
|
+
readySandboxes[provider] = name;
|
|
11912
|
+
config.sandbox.enabled = true;
|
|
11913
|
+
config.sandbox.providers = readySandboxes;
|
|
11914
|
+
saveConfig(projectRoot, config);
|
|
11915
|
+
process.stderr.write(` Next: run ${cyan(`locus sandbox ${provider}`)} to authenticate.
|
|
11418
11916
|
`);
|
|
11419
|
-
|
|
11420
|
-
|
|
11421
|
-
|
|
11917
|
+
return;
|
|
11918
|
+
}
|
|
11919
|
+
process.stderr.write(`Creating ${bold(provider)} sandbox ${dim(name)} with workspace ${dim(projectRoot)}...
|
|
11422
11920
|
`);
|
|
11423
|
-
|
|
11424
|
-
|
|
11425
|
-
|
|
11921
|
+
const created = await createProviderSandbox(provider, name, projectRoot);
|
|
11922
|
+
if (!created) {
|
|
11923
|
+
process.stderr.write(`${red("✗")} Failed to create ${provider} sandbox (${name}).
|
|
11426
11924
|
`);
|
|
11427
|
-
|
|
11428
|
-
}
|
|
11429
|
-
process.stderr.write(`${green("✓")} ${provider} sandbox created: ${bold(name)}
|
|
11925
|
+
process.stderr.write(` Re-run ${cyan("locus sandbox")} after resolving Docker issues.
|
|
11430
11926
|
`);
|
|
11431
|
-
return
|
|
11432
|
-
}));
|
|
11433
|
-
for (const result of createResults) {
|
|
11434
|
-
if (result.created || result.existed) {
|
|
11435
|
-
readySandboxes[result.provider] = result.name;
|
|
11436
|
-
if (result.created)
|
|
11437
|
-
newlyCreated.add(result.name);
|
|
11438
|
-
} else {
|
|
11439
|
-
failed = true;
|
|
11440
|
-
}
|
|
11927
|
+
return;
|
|
11441
11928
|
}
|
|
11929
|
+
process.stderr.write(`${green("✓")} ${provider} sandbox created: ${bold(name)}
|
|
11930
|
+
`);
|
|
11931
|
+
readySandboxes[provider] = name;
|
|
11442
11932
|
config.sandbox.enabled = true;
|
|
11443
11933
|
config.sandbox.providers = readySandboxes;
|
|
11444
11934
|
saveConfig(projectRoot, config);
|
|
11445
|
-
await
|
|
11446
|
-
const sandboxName = readySandboxes[provider];
|
|
11447
|
-
return sandboxName && newlyCreated.has(sandboxName);
|
|
11448
|
-
}).map((provider) => runSandboxSetup(readySandboxes[provider], projectRoot)));
|
|
11449
|
-
if (failed) {
|
|
11450
|
-
process.stderr.write(`
|
|
11451
|
-
${yellow("⚠")} Some sandboxes failed to create. Re-run ${cyan("locus sandbox")} after resolving Docker issues.
|
|
11452
|
-
`);
|
|
11453
|
-
}
|
|
11935
|
+
await runSandboxSetup(name, projectRoot);
|
|
11454
11936
|
process.stderr.write(`
|
|
11455
|
-
${green("✓")} Sandbox mode enabled
|
|
11937
|
+
${green("✓")} Sandbox mode enabled for ${bold(provider)}.
|
|
11456
11938
|
`);
|
|
11457
|
-
process.stderr.write(` Next: run ${cyan(
|
|
11939
|
+
process.stderr.write(` Next: run ${cyan(`locus sandbox ${provider}`)} to authenticate.
|
|
11458
11940
|
`);
|
|
11459
11941
|
}
|
|
11460
11942
|
async function handleAgentLogin(projectRoot, agent) {
|
|
@@ -11519,7 +12001,7 @@ function handleRemove(projectRoot) {
|
|
|
11519
12001
|
process.stderr.write(`Removing sandbox ${bold(sandboxName)}...
|
|
11520
12002
|
`);
|
|
11521
12003
|
try {
|
|
11522
|
-
|
|
12004
|
+
execSync17(`docker sandbox rm ${sandboxName}`, {
|
|
11523
12005
|
encoding: "utf-8",
|
|
11524
12006
|
stdio: ["pipe", "pipe", "pipe"],
|
|
11525
12007
|
timeout: 15000
|
|
@@ -11550,9 +12032,9 @@ ${bold("Sandbox Status")}
|
|
|
11550
12032
|
`);
|
|
11551
12033
|
}
|
|
11552
12034
|
}
|
|
11553
|
-
if (!config.sandbox.providers.claude
|
|
12035
|
+
if (!config.sandbox.providers.claude && !config.sandbox.providers.codex) {
|
|
11554
12036
|
process.stderr.write(`
|
|
11555
|
-
${yellow("⚠")}
|
|
12037
|
+
${yellow("⚠")} No provider sandboxes configured. Run ${bold("locus sandbox")} to create one.
|
|
11556
12038
|
`);
|
|
11557
12039
|
}
|
|
11558
12040
|
process.stderr.write(`
|
|
@@ -11793,7 +12275,7 @@ async function handleLogs(projectRoot, args) {
|
|
|
11793
12275
|
}
|
|
11794
12276
|
function detectPackageManager(projectRoot) {
|
|
11795
12277
|
try {
|
|
11796
|
-
const raw = readFileSync17(
|
|
12278
|
+
const raw = readFileSync17(join24(projectRoot, "package.json"), "utf-8");
|
|
11797
12279
|
const pkgJson = JSON.parse(raw);
|
|
11798
12280
|
if (typeof pkgJson.packageManager === "string") {
|
|
11799
12281
|
const name = pkgJson.packageManager.split("@")[0];
|
|
@@ -11802,13 +12284,13 @@ function detectPackageManager(projectRoot) {
|
|
|
11802
12284
|
}
|
|
11803
12285
|
}
|
|
11804
12286
|
} catch {}
|
|
11805
|
-
if (
|
|
12287
|
+
if (existsSync24(join24(projectRoot, "bun.lock")) || existsSync24(join24(projectRoot, "bun.lockb"))) {
|
|
11806
12288
|
return "bun";
|
|
11807
12289
|
}
|
|
11808
|
-
if (
|
|
12290
|
+
if (existsSync24(join24(projectRoot, "yarn.lock"))) {
|
|
11809
12291
|
return "yarn";
|
|
11810
12292
|
}
|
|
11811
|
-
if (
|
|
12293
|
+
if (existsSync24(join24(projectRoot, "pnpm-lock.yaml"))) {
|
|
11812
12294
|
return "pnpm";
|
|
11813
12295
|
}
|
|
11814
12296
|
return "npm";
|
|
@@ -11826,31 +12308,39 @@ function getInstallCommand(pm) {
|
|
|
11826
12308
|
}
|
|
11827
12309
|
}
|
|
11828
12310
|
async function runSandboxSetup(sandboxName, projectRoot) {
|
|
11829
|
-
const
|
|
11830
|
-
|
|
11831
|
-
|
|
11832
|
-
|
|
11833
|
-
|
|
11834
|
-
|
|
12311
|
+
const ecosystem = detectProjectEcosystem(projectRoot);
|
|
12312
|
+
const isJS = isJavaScriptEcosystem(ecosystem);
|
|
12313
|
+
if (isJS) {
|
|
12314
|
+
const pm = detectPackageManager(projectRoot);
|
|
12315
|
+
if (pm !== "npm") {
|
|
12316
|
+
await ensurePackageManagerInSandbox(sandboxName, pm);
|
|
12317
|
+
}
|
|
12318
|
+
const installCmd = getInstallCommand(pm);
|
|
12319
|
+
process.stderr.write(`
|
|
11835
12320
|
Installing dependencies (${bold(installCmd.join(" "))}) in sandbox ${dim(sandboxName)}...
|
|
11836
12321
|
`);
|
|
11837
|
-
|
|
11838
|
-
|
|
11839
|
-
|
|
11840
|
-
|
|
11841
|
-
|
|
11842
|
-
|
|
11843
|
-
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
12322
|
+
const installOk = await runInteractiveCommand("docker", [
|
|
12323
|
+
"sandbox",
|
|
12324
|
+
"exec",
|
|
12325
|
+
"-w",
|
|
12326
|
+
projectRoot,
|
|
12327
|
+
sandboxName,
|
|
12328
|
+
...installCmd
|
|
12329
|
+
]);
|
|
12330
|
+
if (!installOk) {
|
|
12331
|
+
process.stderr.write(`${red("✗")} Dependency install failed in sandbox ${dim(sandboxName)}.
|
|
11847
12332
|
`);
|
|
11848
|
-
|
|
11849
|
-
|
|
11850
|
-
|
|
12333
|
+
return false;
|
|
12334
|
+
}
|
|
12335
|
+
process.stderr.write(`${green("✓")} Dependencies installed in sandbox ${dim(sandboxName)}.
|
|
12336
|
+
`);
|
|
12337
|
+
} else {
|
|
12338
|
+
process.stderr.write(`
|
|
12339
|
+
${dim(`Detected ${ecosystem} project — skipping JS package install.`)}
|
|
11851
12340
|
`);
|
|
11852
|
-
|
|
11853
|
-
|
|
12341
|
+
}
|
|
12342
|
+
const setupScript = join24(projectRoot, ".locus", "sandbox-setup.sh");
|
|
12343
|
+
if (existsSync24(setupScript)) {
|
|
11854
12344
|
process.stderr.write(`Running ${bold(".locus/sandbox-setup.sh")} in sandbox ${dim(sandboxName)}...
|
|
11855
12345
|
`);
|
|
11856
12346
|
const hookOk = await runInteractiveCommand("docker", [
|
|
@@ -11866,6 +12356,11 @@ Installing dependencies (${bold(installCmd.join(" "))}) in sandbox ${dim(sandbox
|
|
|
11866
12356
|
process.stderr.write(`${yellow("⚠")} Setup hook failed in sandbox ${dim(sandboxName)}.
|
|
11867
12357
|
`);
|
|
11868
12358
|
}
|
|
12359
|
+
} else if (!isJS) {
|
|
12360
|
+
process.stderr.write(`${yellow("⚠")} No ${bold(".locus/sandbox-setup.sh")} found. Create one to install ${ecosystem} toolchain in the sandbox.
|
|
12361
|
+
`);
|
|
12362
|
+
process.stderr.write(` Re-run ${cyan("locus init")} to auto-generate a template, or create it manually.
|
|
12363
|
+
`);
|
|
11869
12364
|
}
|
|
11870
12365
|
return true;
|
|
11871
12366
|
}
|
|
@@ -11933,7 +12428,7 @@ function runInteractiveCommand(command, args) {
|
|
|
11933
12428
|
}
|
|
11934
12429
|
async function createProviderSandbox(provider, sandboxName, projectRoot) {
|
|
11935
12430
|
try {
|
|
11936
|
-
|
|
12431
|
+
execSync17(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
|
|
11937
12432
|
stdio: ["pipe", "pipe", "pipe"],
|
|
11938
12433
|
timeout: 120000
|
|
11939
12434
|
});
|
|
@@ -11949,7 +12444,7 @@ async function createProviderSandbox(provider, sandboxName, projectRoot) {
|
|
|
11949
12444
|
}
|
|
11950
12445
|
async function ensurePackageManagerInSandbox(sandboxName, pm) {
|
|
11951
12446
|
try {
|
|
11952
|
-
|
|
12447
|
+
execSync17(`docker sandbox exec ${sandboxName} which ${pm}`, {
|
|
11953
12448
|
stdio: ["pipe", "pipe", "pipe"],
|
|
11954
12449
|
timeout: 5000
|
|
11955
12450
|
});
|
|
@@ -11958,7 +12453,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
|
|
|
11958
12453
|
process.stderr.write(`Installing ${bold(pm)} in sandbox...
|
|
11959
12454
|
`);
|
|
11960
12455
|
try {
|
|
11961
|
-
|
|
12456
|
+
execSync17(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
|
|
11962
12457
|
stdio: "inherit",
|
|
11963
12458
|
timeout: 120000
|
|
11964
12459
|
});
|
|
@@ -11970,7 +12465,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
|
|
|
11970
12465
|
}
|
|
11971
12466
|
async function ensureCodexInSandbox(sandboxName) {
|
|
11972
12467
|
try {
|
|
11973
|
-
|
|
12468
|
+
execSync17(`docker sandbox exec ${sandboxName} which codex`, {
|
|
11974
12469
|
stdio: ["pipe", "pipe", "pipe"],
|
|
11975
12470
|
timeout: 5000
|
|
11976
12471
|
});
|
|
@@ -11978,7 +12473,7 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
11978
12473
|
process.stderr.write(`Installing codex in sandbox...
|
|
11979
12474
|
`);
|
|
11980
12475
|
try {
|
|
11981
|
-
|
|
12476
|
+
execSync17(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
|
|
11982
12477
|
} catch {
|
|
11983
12478
|
process.stderr.write(`${red("✗")} Failed to install codex in sandbox.
|
|
11984
12479
|
`);
|
|
@@ -11987,7 +12482,7 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
11987
12482
|
}
|
|
11988
12483
|
function isSandboxAlive(name) {
|
|
11989
12484
|
try {
|
|
11990
|
-
const output =
|
|
12485
|
+
const output = execSync17("docker sandbox ls", {
|
|
11991
12486
|
encoding: "utf-8",
|
|
11992
12487
|
stdio: ["pipe", "pipe", "pipe"],
|
|
11993
12488
|
timeout: 5000
|
|
@@ -12000,6 +12495,7 @@ function isSandboxAlive(name) {
|
|
|
12000
12495
|
var PROVIDERS;
|
|
12001
12496
|
var init_sandbox2 = __esm(() => {
|
|
12002
12497
|
init_config();
|
|
12498
|
+
init_ecosystem();
|
|
12003
12499
|
init_sandbox();
|
|
12004
12500
|
init_sandbox_ignore();
|
|
12005
12501
|
init_terminal();
|
|
@@ -12012,13 +12508,13 @@ init_context();
|
|
|
12012
12508
|
init_logger();
|
|
12013
12509
|
init_rate_limiter();
|
|
12014
12510
|
init_terminal();
|
|
12015
|
-
import { existsSync as
|
|
12016
|
-
import { join as
|
|
12511
|
+
import { existsSync as existsSync25, readFileSync as readFileSync18 } from "node:fs";
|
|
12512
|
+
import { join as join25 } from "node:path";
|
|
12017
12513
|
import { fileURLToPath } from "node:url";
|
|
12018
12514
|
function getCliVersion() {
|
|
12019
12515
|
const fallbackVersion = "0.0.0";
|
|
12020
|
-
const packageJsonPath =
|
|
12021
|
-
if (!
|
|
12516
|
+
const packageJsonPath = join25(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
|
|
12517
|
+
if (!existsSync25(packageJsonPath)) {
|
|
12022
12518
|
return fallbackVersion;
|
|
12023
12519
|
}
|
|
12024
12520
|
try {
|
|
@@ -12283,7 +12779,7 @@ async function main() {
|
|
|
12283
12779
|
try {
|
|
12284
12780
|
const root = getGitRoot(cwd);
|
|
12285
12781
|
if (isInitialized(root)) {
|
|
12286
|
-
logDir =
|
|
12782
|
+
logDir = join25(root, ".locus", "logs");
|
|
12287
12783
|
getRateLimiter(root);
|
|
12288
12784
|
}
|
|
12289
12785
|
} catch {}
|