@locusai/cli 0.22.0 → 0.22.2
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 +768 -345
- package/package.json +2 -2
package/bin/locus.js
CHANGED
|
@@ -29,7 +29,7 @@ var init_dist = __esm(() => {
|
|
|
29
29
|
},
|
|
30
30
|
ai: {
|
|
31
31
|
provider: "claude",
|
|
32
|
-
model: "
|
|
32
|
+
model: "opus"
|
|
33
33
|
},
|
|
34
34
|
agent: {
|
|
35
35
|
maxParallel: 3,
|
|
@@ -896,9 +896,10 @@ __export(exports_sandbox, {
|
|
|
896
896
|
getModelSandboxName: () => getModelSandboxName,
|
|
897
897
|
displaySandboxWarning: () => displaySandboxWarning,
|
|
898
898
|
detectSandboxSupport: () => detectSandboxSupport,
|
|
899
|
+
detectContainerWorkdir: () => detectContainerWorkdir,
|
|
899
900
|
checkProviderSandboxMismatch: () => checkProviderSandboxMismatch
|
|
900
901
|
});
|
|
901
|
-
import { execFile } from "node:child_process";
|
|
902
|
+
import { execFile, execSync as execSync2 } from "node:child_process";
|
|
902
903
|
import { createInterface } from "node:readline";
|
|
903
904
|
function getProviderSandboxName(config, provider) {
|
|
904
905
|
return config.providers[provider];
|
|
@@ -1033,6 +1034,34 @@ ${yellow2("⚠")} Docker sandbox not available. Install Docker Desktop 4.58+ fo
|
|
|
1033
1034
|
}
|
|
1034
1035
|
return true;
|
|
1035
1036
|
}
|
|
1037
|
+
function detectContainerWorkdir(sandboxName, hostProjectRoot) {
|
|
1038
|
+
const log = getLogger();
|
|
1039
|
+
try {
|
|
1040
|
+
execSync2(`docker sandbox exec ${sandboxName} test -d ${JSON.stringify(hostProjectRoot)}`, { stdio: ["pipe", "pipe", "pipe"], timeout: 5000 });
|
|
1041
|
+
log.debug("Container workdir matches host path", { hostProjectRoot });
|
|
1042
|
+
return null;
|
|
1043
|
+
} catch {}
|
|
1044
|
+
try {
|
|
1045
|
+
const result = execSync2(`docker sandbox exec ${sandboxName} find / -maxdepth 5 -path '*/.locus/config.json' -type f 2>/dev/null`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 }).trim();
|
|
1046
|
+
if (result) {
|
|
1047
|
+
const configPath = result.split(`
|
|
1048
|
+
`)[0].trim();
|
|
1049
|
+
const workdir = configPath.replace(/\/.locus\/config\.json$/, "");
|
|
1050
|
+
if (workdir) {
|
|
1051
|
+
log.debug("Detected container workdir", {
|
|
1052
|
+
hostProjectRoot,
|
|
1053
|
+
containerWorkdir: workdir
|
|
1054
|
+
});
|
|
1055
|
+
return workdir;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
log.debug("Container workdir probe failed", {
|
|
1060
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1036
1065
|
function waitForEnter() {
|
|
1037
1066
|
return new Promise((resolve) => {
|
|
1038
1067
|
const rl = createInterface({
|
|
@@ -1062,7 +1091,7 @@ __export(exports_upgrade, {
|
|
|
1062
1091
|
fetchLatestVersion: () => fetchLatestVersion,
|
|
1063
1092
|
compareSemver: () => compareSemver
|
|
1064
1093
|
});
|
|
1065
|
-
import { execSync as
|
|
1094
|
+
import { execSync as execSync3 } from "node:child_process";
|
|
1066
1095
|
function compareSemver(a, b) {
|
|
1067
1096
|
const partsA = a.replace(/^v/, "").split(".").map(Number);
|
|
1068
1097
|
const partsB = b.replace(/^v/, "").split(".").map(Number);
|
|
@@ -1078,7 +1107,7 @@ function compareSemver(a, b) {
|
|
|
1078
1107
|
}
|
|
1079
1108
|
function fetchLatestVersion() {
|
|
1080
1109
|
try {
|
|
1081
|
-
const result =
|
|
1110
|
+
const result = execSync3(`npm view ${PACKAGE_NAME} version`, {
|
|
1082
1111
|
encoding: "utf-8",
|
|
1083
1112
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1084
1113
|
timeout: 15000
|
|
@@ -1090,14 +1119,14 @@ function fetchLatestVersion() {
|
|
|
1090
1119
|
}
|
|
1091
1120
|
function installVersion(version) {
|
|
1092
1121
|
try {
|
|
1093
|
-
|
|
1122
|
+
execSync3("npm cache clean --force", {
|
|
1094
1123
|
encoding: "utf-8",
|
|
1095
1124
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1096
1125
|
timeout: 30000
|
|
1097
1126
|
});
|
|
1098
1127
|
} catch {}
|
|
1099
1128
|
try {
|
|
1100
|
-
|
|
1129
|
+
execSync3(`npm install -g ${PACKAGE_NAME}@${version}`, {
|
|
1101
1130
|
encoding: "utf-8",
|
|
1102
1131
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1103
1132
|
timeout: 120000
|
|
@@ -1109,7 +1138,7 @@ function installVersion(version) {
|
|
|
1109
1138
|
}
|
|
1110
1139
|
function verifyInstalled(expectedVersion) {
|
|
1111
1140
|
try {
|
|
1112
|
-
const installed =
|
|
1141
|
+
const installed = execSync3(`npm list -g ${PACKAGE_NAME} --depth=0 --json`, {
|
|
1113
1142
|
encoding: "utf-8",
|
|
1114
1143
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1115
1144
|
timeout: 1e4
|
|
@@ -1473,7 +1502,7 @@ var init_ecosystem = __esm(() => {
|
|
|
1473
1502
|
// src/core/github.ts
|
|
1474
1503
|
import {
|
|
1475
1504
|
execFileSync,
|
|
1476
|
-
execSync as
|
|
1505
|
+
execSync as execSync4
|
|
1477
1506
|
} from "node:child_process";
|
|
1478
1507
|
function gh(args, options = {}) {
|
|
1479
1508
|
const log = getLogger();
|
|
@@ -1482,7 +1511,7 @@ function gh(args, options = {}) {
|
|
|
1482
1511
|
log.debug(`gh ${args}`, { cwd });
|
|
1483
1512
|
const startTime = Date.now();
|
|
1484
1513
|
try {
|
|
1485
|
-
const result =
|
|
1514
|
+
const result = execSync4(`gh ${args}`, {
|
|
1486
1515
|
cwd,
|
|
1487
1516
|
encoding: "utf-8",
|
|
1488
1517
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -2226,35 +2255,384 @@ var init_init = __esm(() => {
|
|
|
2226
2255
|
GITIGNORE_ENTRIES = ["", "# Locus", ".locus/", "!.locus/LEARNINGS.md"];
|
|
2227
2256
|
});
|
|
2228
2257
|
|
|
2258
|
+
// src/commands/create.ts
|
|
2259
|
+
var exports_create = {};
|
|
2260
|
+
__export(exports_create, {
|
|
2261
|
+
createCommand: () => createCommand
|
|
2262
|
+
});
|
|
2263
|
+
import { execSync as execSync5 } from "node:child_process";
|
|
2264
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2265
|
+
import { join as join7 } from "node:path";
|
|
2266
|
+
function validateName(name) {
|
|
2267
|
+
if (!name)
|
|
2268
|
+
return "Package name is required.";
|
|
2269
|
+
if (!NAME_PATTERN.test(name))
|
|
2270
|
+
return "Name must be lowercase, start with a letter, and contain only letters, numbers, and hyphens.";
|
|
2271
|
+
if (name.startsWith("locus-"))
|
|
2272
|
+
return `Don't include the "locus-" prefix — it's added automatically.`;
|
|
2273
|
+
if (name.length > 50)
|
|
2274
|
+
return "Name must be 50 characters or fewer.";
|
|
2275
|
+
return null;
|
|
2276
|
+
}
|
|
2277
|
+
function capitalize(str) {
|
|
2278
|
+
return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
2279
|
+
}
|
|
2280
|
+
function checkNpmExists(fullName) {
|
|
2281
|
+
try {
|
|
2282
|
+
execSync5(`npm view ${fullName} version 2>/dev/null`, {
|
|
2283
|
+
stdio: "pipe",
|
|
2284
|
+
timeout: 1e4
|
|
2285
|
+
});
|
|
2286
|
+
return true;
|
|
2287
|
+
} catch {
|
|
2288
|
+
return false;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
function parseCreateArgs(args) {
|
|
2292
|
+
let name = "";
|
|
2293
|
+
let description = "";
|
|
2294
|
+
let i = 0;
|
|
2295
|
+
while (i < args.length) {
|
|
2296
|
+
const arg = args[i];
|
|
2297
|
+
if (arg === "--description" || arg === "-D") {
|
|
2298
|
+
description = args[++i] ?? "";
|
|
2299
|
+
} else if (arg === "help" || arg === "--help" || arg === "-h") {
|
|
2300
|
+
return null;
|
|
2301
|
+
} else if (!arg.startsWith("-")) {
|
|
2302
|
+
name = arg;
|
|
2303
|
+
}
|
|
2304
|
+
i++;
|
|
2305
|
+
}
|
|
2306
|
+
return { name, description };
|
|
2307
|
+
}
|
|
2308
|
+
function generatePackageJson(name, displayName, description, sdkVersion) {
|
|
2309
|
+
const pkg = {
|
|
2310
|
+
name: `@locusai/locus-${name}`,
|
|
2311
|
+
version: "0.1.0",
|
|
2312
|
+
description,
|
|
2313
|
+
type: "module",
|
|
2314
|
+
bin: {
|
|
2315
|
+
[`locus-${name}`]: `./bin/locus-${name}.js`
|
|
2316
|
+
},
|
|
2317
|
+
files: ["bin", "package.json", "README.md"],
|
|
2318
|
+
locus: {
|
|
2319
|
+
displayName,
|
|
2320
|
+
description,
|
|
2321
|
+
commands: [name],
|
|
2322
|
+
version: "0.1.0"
|
|
2323
|
+
},
|
|
2324
|
+
scripts: {
|
|
2325
|
+
build: `bun build src/cli.ts --outfile bin/locus-${name}.js --target node`,
|
|
2326
|
+
typecheck: "tsc --noEmit",
|
|
2327
|
+
lint: "biome lint .",
|
|
2328
|
+
format: "biome format --write ."
|
|
2329
|
+
},
|
|
2330
|
+
dependencies: {
|
|
2331
|
+
"@locusai/sdk": `^${sdkVersion}`
|
|
2332
|
+
},
|
|
2333
|
+
devDependencies: {
|
|
2334
|
+
typescript: "^5.8.3"
|
|
2335
|
+
},
|
|
2336
|
+
keywords: ["locusai-package", "locus", name],
|
|
2337
|
+
engines: {
|
|
2338
|
+
node: ">=18"
|
|
2339
|
+
},
|
|
2340
|
+
license: "MIT"
|
|
2341
|
+
};
|
|
2342
|
+
return `${JSON.stringify(pkg, null, 2)}
|
|
2343
|
+
`;
|
|
2344
|
+
}
|
|
2345
|
+
function generateTsconfig() {
|
|
2346
|
+
const config = {
|
|
2347
|
+
compilerOptions: {
|
|
2348
|
+
target: "ES2022",
|
|
2349
|
+
module: "ESNext",
|
|
2350
|
+
moduleResolution: "bundler",
|
|
2351
|
+
strict: true,
|
|
2352
|
+
skipLibCheck: true,
|
|
2353
|
+
esModuleInterop: true,
|
|
2354
|
+
isolatedModules: true,
|
|
2355
|
+
resolveJsonModule: true,
|
|
2356
|
+
noEmit: true,
|
|
2357
|
+
rootDir: "./src"
|
|
2358
|
+
},
|
|
2359
|
+
include: ["src/**/*"],
|
|
2360
|
+
exclude: ["node_modules", "dist", "bin"]
|
|
2361
|
+
};
|
|
2362
|
+
return `${JSON.stringify(config, null, 2)}
|
|
2363
|
+
`;
|
|
2364
|
+
}
|
|
2365
|
+
function generateCliTs() {
|
|
2366
|
+
return `#!/usr/bin/env node
|
|
2367
|
+
|
|
2368
|
+
import { main } from "./index.js";
|
|
2369
|
+
|
|
2370
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
2371
|
+
console.error(\`Fatal error: \${error.message}\`);
|
|
2372
|
+
process.exit(1);
|
|
2373
|
+
});
|
|
2374
|
+
`;
|
|
2375
|
+
}
|
|
2376
|
+
function generateIndexTs(name) {
|
|
2377
|
+
return `import { createLogger, readLocusConfig } from "@locusai/sdk";
|
|
2378
|
+
|
|
2379
|
+
const logger = createLogger("${name}");
|
|
2380
|
+
|
|
2381
|
+
export async function main(args: string[]): Promise<void> {
|
|
2382
|
+
const command = args[0] ?? "help";
|
|
2383
|
+
|
|
2384
|
+
switch (command) {
|
|
2385
|
+
case "start":
|
|
2386
|
+
return handleStart();
|
|
2387
|
+
case "help":
|
|
2388
|
+
case "--help":
|
|
2389
|
+
case "-h":
|
|
2390
|
+
return printHelp();
|
|
2391
|
+
default:
|
|
2392
|
+
console.error(\`Unknown command: \${command}\`);
|
|
2393
|
+
printHelp();
|
|
2394
|
+
process.exit(1);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
// ─── Commands ────────────────────────────────────────────────────────────────
|
|
2399
|
+
|
|
2400
|
+
function handleStart(): void {
|
|
2401
|
+
const config = readLocusConfig();
|
|
2402
|
+
logger.info(\`Hello from locus-${name}! Repo: \${config.github.owner}/\${config.github.repo}\`);
|
|
2403
|
+
// TODO: Implement your package logic here
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
// ─── Help ────────────────────────────────────────────────────────────────────
|
|
2407
|
+
|
|
2408
|
+
function printHelp(): void {
|
|
2409
|
+
console.log(\`
|
|
2410
|
+
locus-${name}
|
|
2411
|
+
|
|
2412
|
+
Usage:
|
|
2413
|
+
locus pkg ${name} <command>
|
|
2414
|
+
|
|
2415
|
+
Commands:
|
|
2416
|
+
start Start the ${name} integration
|
|
2417
|
+
help Show this help message
|
|
2418
|
+
\`);
|
|
2419
|
+
}
|
|
2420
|
+
`;
|
|
2421
|
+
}
|
|
2422
|
+
function generateReadme(name, description) {
|
|
2423
|
+
return `# @locusai/locus-${name}
|
|
2424
|
+
|
|
2425
|
+
${description}
|
|
2426
|
+
|
|
2427
|
+
## Installation
|
|
2428
|
+
|
|
2429
|
+
\`\`\`bash
|
|
2430
|
+
locus install ${name}
|
|
2431
|
+
\`\`\`
|
|
2432
|
+
|
|
2433
|
+
## Usage
|
|
2434
|
+
|
|
2435
|
+
\`\`\`bash
|
|
2436
|
+
locus pkg ${name} start # Start the integration
|
|
2437
|
+
locus pkg ${name} help # Show help
|
|
2438
|
+
\`\`\`
|
|
2439
|
+
|
|
2440
|
+
## Configuration
|
|
2441
|
+
|
|
2442
|
+
Configure via \`locus config\`:
|
|
2443
|
+
|
|
2444
|
+
\`\`\`bash
|
|
2445
|
+
locus config set packages.${name}.apiKey "your-api-key"
|
|
2446
|
+
\`\`\`
|
|
2447
|
+
|
|
2448
|
+
## Development
|
|
2449
|
+
|
|
2450
|
+
\`\`\`bash
|
|
2451
|
+
# Build
|
|
2452
|
+
bun run build
|
|
2453
|
+
|
|
2454
|
+
# Type check
|
|
2455
|
+
bun run typecheck
|
|
2456
|
+
|
|
2457
|
+
# Lint
|
|
2458
|
+
bun run lint
|
|
2459
|
+
|
|
2460
|
+
# Test locally
|
|
2461
|
+
locus pkg ${name}
|
|
2462
|
+
\`\`\`
|
|
2463
|
+
|
|
2464
|
+
## License
|
|
2465
|
+
|
|
2466
|
+
MIT
|
|
2467
|
+
`;
|
|
2468
|
+
}
|
|
2469
|
+
function printHelp() {
|
|
2470
|
+
process.stderr.write(`
|
|
2471
|
+
${bold2("locus create")} — Scaffold a new Locus community package
|
|
2472
|
+
|
|
2473
|
+
${bold2("Usage:")}
|
|
2474
|
+
locus create <name> [options]
|
|
2475
|
+
|
|
2476
|
+
${bold2("Arguments:")}
|
|
2477
|
+
${cyan2("<name>")} Package short name (e.g. slack, discord, jira)
|
|
2478
|
+
|
|
2479
|
+
${bold2("Options:")}
|
|
2480
|
+
${dim2("--description, -D")} Package description (default: auto-generated)
|
|
2481
|
+
${dim2("--help, -h")} Show this help
|
|
2482
|
+
|
|
2483
|
+
${bold2("Examples:")}
|
|
2484
|
+
locus create slack ${dim2("# Create packages/slack/")}
|
|
2485
|
+
locus create discord -D "Control Locus via Discord" ${dim2("# With custom description")}
|
|
2486
|
+
|
|
2487
|
+
${bold2("What gets created:")}
|
|
2488
|
+
packages/<name>/
|
|
2489
|
+
├── src/
|
|
2490
|
+
│ ├── cli.ts ${dim2("# Entry point")}
|
|
2491
|
+
│ └── index.ts ${dim2("# Main logic with command dispatch")}
|
|
2492
|
+
├── package.json ${dim2("# Full config with locus manifest")}
|
|
2493
|
+
├── tsconfig.json ${dim2("# TypeScript configuration")}
|
|
2494
|
+
└── README.md ${dim2("# Package documentation")}
|
|
2495
|
+
|
|
2496
|
+
${bold2("Next steps after creation:")}
|
|
2497
|
+
${gray2("1.")} cd packages/<name>
|
|
2498
|
+
${gray2("2.")} Implement your logic in src/index.ts
|
|
2499
|
+
${gray2("3.")} bun install && bun run build
|
|
2500
|
+
${gray2("4.")} Test locally with: locus pkg <name>
|
|
2501
|
+
${gray2("5.")} Submit a pull request
|
|
2502
|
+
|
|
2503
|
+
`);
|
|
2504
|
+
}
|
|
2505
|
+
async function createCommand(args) {
|
|
2506
|
+
const parsed = parseCreateArgs(args);
|
|
2507
|
+
if (!parsed || !parsed.name) {
|
|
2508
|
+
printHelp();
|
|
2509
|
+
if (parsed && !parsed.name) {
|
|
2510
|
+
process.stderr.write(`${red2("✗")} Package name is required.
|
|
2511
|
+
|
|
2512
|
+
`);
|
|
2513
|
+
process.stderr.write(` Usage: ${bold2("locus create <name>")}
|
|
2514
|
+
|
|
2515
|
+
`);
|
|
2516
|
+
process.exit(1);
|
|
2517
|
+
}
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
const { name } = parsed;
|
|
2521
|
+
const displayName = capitalize(name);
|
|
2522
|
+
const description = parsed.description || `${displayName} integration for Locus`;
|
|
2523
|
+
const fullNpmName = `@locusai/locus-${name}`;
|
|
2524
|
+
process.stderr.write(`
|
|
2525
|
+
${bold2("Creating package:")} ${cyan2(fullNpmName)}
|
|
2526
|
+
|
|
2527
|
+
`);
|
|
2528
|
+
const nameError = validateName(name);
|
|
2529
|
+
if (nameError) {
|
|
2530
|
+
process.stderr.write(`${red2("✗")} ${nameError}
|
|
2531
|
+
`);
|
|
2532
|
+
process.exit(1);
|
|
2533
|
+
}
|
|
2534
|
+
process.stderr.write(`${green("✓")} Name is valid: ${bold2(name)}
|
|
2535
|
+
`);
|
|
2536
|
+
const packagesDir = join7(process.cwd(), "packages", name);
|
|
2537
|
+
if (existsSync7(packagesDir)) {
|
|
2538
|
+
process.stderr.write(`${red2("✗")} Directory already exists: ${bold2(`packages/${name}/`)}
|
|
2539
|
+
`);
|
|
2540
|
+
process.exit(1);
|
|
2541
|
+
}
|
|
2542
|
+
process.stderr.write(`${cyan2("●")} Checking npm registry...`);
|
|
2543
|
+
const existsOnNpm = checkNpmExists(fullNpmName);
|
|
2544
|
+
if (existsOnNpm) {
|
|
2545
|
+
process.stderr.write(`\r${yellow2("⚠")} Package ${bold2(fullNpmName)} already exists on npm. You may want to choose a different name.
|
|
2546
|
+
`);
|
|
2547
|
+
} else {
|
|
2548
|
+
process.stderr.write(`\r${green("✓")} Name is available on npm
|
|
2549
|
+
`);
|
|
2550
|
+
}
|
|
2551
|
+
let sdkVersion = "0.22.0";
|
|
2552
|
+
try {
|
|
2553
|
+
const sdkPkgPath = join7(process.cwd(), "packages", "sdk", "package.json");
|
|
2554
|
+
if (existsSync7(sdkPkgPath)) {
|
|
2555
|
+
const { readFileSync: readFileSync5 } = await import("node:fs");
|
|
2556
|
+
const sdkPkg = JSON.parse(readFileSync5(sdkPkgPath, "utf-8"));
|
|
2557
|
+
if (sdkPkg.version)
|
|
2558
|
+
sdkVersion = sdkPkg.version;
|
|
2559
|
+
}
|
|
2560
|
+
} catch {}
|
|
2561
|
+
mkdirSync6(join7(packagesDir, "src"), { recursive: true });
|
|
2562
|
+
mkdirSync6(join7(packagesDir, "bin"), { recursive: true });
|
|
2563
|
+
process.stderr.write(`${green("✓")} Created directory structure
|
|
2564
|
+
`);
|
|
2565
|
+
writeFileSync5(join7(packagesDir, "package.json"), generatePackageJson(name, displayName, description, sdkVersion), "utf-8");
|
|
2566
|
+
process.stderr.write(`${green("✓")} Generated package.json
|
|
2567
|
+
`);
|
|
2568
|
+
writeFileSync5(join7(packagesDir, "tsconfig.json"), generateTsconfig(), "utf-8");
|
|
2569
|
+
process.stderr.write(`${green("✓")} Generated tsconfig.json
|
|
2570
|
+
`);
|
|
2571
|
+
writeFileSync5(join7(packagesDir, "src", "cli.ts"), generateCliTs(), "utf-8");
|
|
2572
|
+
process.stderr.write(`${green("✓")} Generated src/cli.ts
|
|
2573
|
+
`);
|
|
2574
|
+
writeFileSync5(join7(packagesDir, "src", "index.ts"), generateIndexTs(name), "utf-8");
|
|
2575
|
+
process.stderr.write(`${green("✓")} Generated src/index.ts
|
|
2576
|
+
`);
|
|
2577
|
+
writeFileSync5(join7(packagesDir, "README.md"), generateReadme(name, description), "utf-8");
|
|
2578
|
+
process.stderr.write(`${green("✓")} Generated README.md
|
|
2579
|
+
`);
|
|
2580
|
+
process.stderr.write(`
|
|
2581
|
+
${bold2(green("Package created!"))}
|
|
2582
|
+
|
|
2583
|
+
`);
|
|
2584
|
+
process.stderr.write(`${bold2("Next steps:")}
|
|
2585
|
+
`);
|
|
2586
|
+
process.stderr.write(` ${gray2("1.")} Implement your logic in ${bold2(`packages/${name}/src/index.ts`)}
|
|
2587
|
+
`);
|
|
2588
|
+
process.stderr.write(` ${gray2("2.")} Install dependencies: ${bold2("bun install")}
|
|
2589
|
+
`);
|
|
2590
|
+
process.stderr.write(` ${gray2("3.")} Build your package: ${bold2(`cd packages/${name} && bun run build`)}
|
|
2591
|
+
`);
|
|
2592
|
+
process.stderr.write(` ${gray2("4.")} Test locally: ${bold2(`locus pkg ${name}`)}
|
|
2593
|
+
`);
|
|
2594
|
+
process.stderr.write(` ${gray2("5.")} Submit a pull request
|
|
2595
|
+
`);
|
|
2596
|
+
process.stderr.write(`
|
|
2597
|
+
${dim2("See the full guide:")} ${cyan2("packages/sdk/PACKAGE_GUIDE.md")}
|
|
2598
|
+
|
|
2599
|
+
`);
|
|
2600
|
+
}
|
|
2601
|
+
var NAME_PATTERN;
|
|
2602
|
+
var init_create = __esm(() => {
|
|
2603
|
+
init_terminal();
|
|
2604
|
+
NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
2605
|
+
});
|
|
2606
|
+
|
|
2229
2607
|
// src/packages/registry.ts
|
|
2230
2608
|
import {
|
|
2231
|
-
existsSync as
|
|
2232
|
-
mkdirSync as
|
|
2609
|
+
existsSync as existsSync8,
|
|
2610
|
+
mkdirSync as mkdirSync7,
|
|
2233
2611
|
readFileSync as readFileSync5,
|
|
2234
2612
|
renameSync,
|
|
2235
|
-
writeFileSync as
|
|
2613
|
+
writeFileSync as writeFileSync6
|
|
2236
2614
|
} from "node:fs";
|
|
2237
2615
|
import { homedir as homedir2 } from "node:os";
|
|
2238
|
-
import { join as
|
|
2616
|
+
import { join as join8 } from "node:path";
|
|
2239
2617
|
function getPackagesDir() {
|
|
2240
2618
|
const home = process.env.HOME || homedir2();
|
|
2241
|
-
const dir =
|
|
2242
|
-
if (!
|
|
2243
|
-
|
|
2619
|
+
const dir = join8(home, ".locus", "packages");
|
|
2620
|
+
if (!existsSync8(dir)) {
|
|
2621
|
+
mkdirSync7(dir, { recursive: true });
|
|
2244
2622
|
}
|
|
2245
|
-
const pkgJson =
|
|
2246
|
-
if (!
|
|
2247
|
-
|
|
2623
|
+
const pkgJson = join8(dir, "package.json");
|
|
2624
|
+
if (!existsSync8(pkgJson)) {
|
|
2625
|
+
writeFileSync6(pkgJson, `${JSON.stringify({ private: true }, null, 2)}
|
|
2248
2626
|
`, "utf-8");
|
|
2249
2627
|
}
|
|
2250
2628
|
return dir;
|
|
2251
2629
|
}
|
|
2252
2630
|
function getRegistryPath() {
|
|
2253
|
-
return
|
|
2631
|
+
return join8(getPackagesDir(), "registry.json");
|
|
2254
2632
|
}
|
|
2255
2633
|
function loadRegistry() {
|
|
2256
2634
|
const registryPath = getRegistryPath();
|
|
2257
|
-
if (!
|
|
2635
|
+
if (!existsSync8(registryPath)) {
|
|
2258
2636
|
return { packages: {} };
|
|
2259
2637
|
}
|
|
2260
2638
|
try {
|
|
@@ -2271,7 +2649,7 @@ function loadRegistry() {
|
|
|
2271
2649
|
}
|
|
2272
2650
|
if (pruned) {
|
|
2273
2651
|
const tmp = `${registryPath}.tmp`;
|
|
2274
|
-
|
|
2652
|
+
writeFileSync6(tmp, `${JSON.stringify(registry, null, 2)}
|
|
2275
2653
|
`, "utf-8");
|
|
2276
2654
|
renameSync(tmp, registryPath);
|
|
2277
2655
|
}
|
|
@@ -2285,15 +2663,15 @@ function loadRegistry() {
|
|
|
2285
2663
|
function saveRegistry(registry) {
|
|
2286
2664
|
const registryPath = getRegistryPath();
|
|
2287
2665
|
const tmp = `${registryPath}.tmp`;
|
|
2288
|
-
|
|
2666
|
+
writeFileSync6(tmp, `${JSON.stringify(registry, null, 2)}
|
|
2289
2667
|
`, "utf-8");
|
|
2290
2668
|
renameSync(tmp, registryPath);
|
|
2291
2669
|
}
|
|
2292
2670
|
function resolvePackageBinary(packageName) {
|
|
2293
2671
|
const fullName = normalizePackageName(packageName);
|
|
2294
2672
|
const binName = fullName.includes("/") ? fullName.split("/").pop() : fullName;
|
|
2295
|
-
const binPath =
|
|
2296
|
-
return
|
|
2673
|
+
const binPath = join8(getPackagesDir(), "node_modules", ".bin", binName);
|
|
2674
|
+
return existsSync8(binPath) ? binPath : null;
|
|
2297
2675
|
}
|
|
2298
2676
|
function normalizePackageName(input) {
|
|
2299
2677
|
if (input.startsWith(SCOPED_PREFIX)) {
|
|
@@ -2319,7 +2697,7 @@ __export(exports_pkg, {
|
|
|
2319
2697
|
listInstalledPackages: () => listInstalledPackages
|
|
2320
2698
|
});
|
|
2321
2699
|
import { spawn } from "node:child_process";
|
|
2322
|
-
import { existsSync as
|
|
2700
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
2323
2701
|
function listInstalledPackages() {
|
|
2324
2702
|
const registry = loadRegistry();
|
|
2325
2703
|
const entries = Object.values(registry.packages);
|
|
@@ -2398,7 +2776,7 @@ async function pkgCommand(args, _flags) {
|
|
|
2398
2776
|
return;
|
|
2399
2777
|
}
|
|
2400
2778
|
let binaryPath = entry.binaryPath;
|
|
2401
|
-
if (!binaryPath || !
|
|
2779
|
+
if (!binaryPath || !existsSync9(binaryPath)) {
|
|
2402
2780
|
const resolved = resolvePackageBinary(packageName);
|
|
2403
2781
|
if (resolved) {
|
|
2404
2782
|
binaryPath = resolved;
|
|
@@ -2531,8 +2909,8 @@ __export(exports_install, {
|
|
|
2531
2909
|
installCommand: () => installCommand
|
|
2532
2910
|
});
|
|
2533
2911
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
2534
|
-
import { existsSync as
|
|
2535
|
-
import { join as
|
|
2912
|
+
import { existsSync as existsSync10, readFileSync as readFileSync6 } from "node:fs";
|
|
2913
|
+
import { join as join9 } from "node:path";
|
|
2536
2914
|
function parsePackageArg(raw) {
|
|
2537
2915
|
if (raw.startsWith("@")) {
|
|
2538
2916
|
const slashIdx = raw.indexOf("/");
|
|
@@ -2609,8 +2987,8 @@ ${red2("✗")} Failed to install ${bold2(packageSpec)}.
|
|
|
2609
2987
|
process.exit(1);
|
|
2610
2988
|
return;
|
|
2611
2989
|
}
|
|
2612
|
-
const installedPkgJsonPath =
|
|
2613
|
-
if (!
|
|
2990
|
+
const installedPkgJsonPath = join9(packagesDir, "node_modules", packageName, "package.json");
|
|
2991
|
+
if (!existsSync10(installedPkgJsonPath)) {
|
|
2614
2992
|
process.stderr.write(`
|
|
2615
2993
|
${red2("✗")} Package installed but package.json not found at:
|
|
2616
2994
|
`);
|
|
@@ -2888,16 +3266,16 @@ __export(exports_logs, {
|
|
|
2888
3266
|
logsCommand: () => logsCommand
|
|
2889
3267
|
});
|
|
2890
3268
|
import {
|
|
2891
|
-
existsSync as
|
|
3269
|
+
existsSync as existsSync11,
|
|
2892
3270
|
readdirSync as readdirSync2,
|
|
2893
3271
|
readFileSync as readFileSync7,
|
|
2894
3272
|
statSync as statSync2,
|
|
2895
3273
|
unlinkSync as unlinkSync2
|
|
2896
3274
|
} from "node:fs";
|
|
2897
|
-
import { join as
|
|
3275
|
+
import { join as join10 } from "node:path";
|
|
2898
3276
|
async function logsCommand(cwd, options) {
|
|
2899
|
-
const logsDir =
|
|
2900
|
-
if (!
|
|
3277
|
+
const logsDir = join10(cwd, ".locus", "logs");
|
|
3278
|
+
if (!existsSync11(logsDir)) {
|
|
2901
3279
|
process.stderr.write(`${dim2("No logs found.")}
|
|
2902
3280
|
`);
|
|
2903
3281
|
return;
|
|
@@ -2952,8 +3330,8 @@ async function tailLog(logFile, levelFilter) {
|
|
|
2952
3330
|
process.stderr.write(`${bold2("Tailing:")} ${dim2(logFile)} ${dim2("(Ctrl+C to stop)")}
|
|
2953
3331
|
|
|
2954
3332
|
`);
|
|
2955
|
-
let lastSize =
|
|
2956
|
-
if (
|
|
3333
|
+
let lastSize = existsSync11(logFile) ? statSync2(logFile).size : 0;
|
|
3334
|
+
if (existsSync11(logFile)) {
|
|
2957
3335
|
const content = readFileSync7(logFile, "utf-8");
|
|
2958
3336
|
const lines = content.trim().split(`
|
|
2959
3337
|
`).filter(Boolean);
|
|
@@ -2972,7 +3350,7 @@ async function tailLog(logFile, levelFilter) {
|
|
|
2972
3350
|
}
|
|
2973
3351
|
return new Promise((resolve) => {
|
|
2974
3352
|
const interval = setInterval(() => {
|
|
2975
|
-
if (!
|
|
3353
|
+
if (!existsSync11(logFile))
|
|
2976
3354
|
return;
|
|
2977
3355
|
const currentSize = statSync2(logFile).size;
|
|
2978
3356
|
if (currentSize <= lastSize)
|
|
@@ -3032,7 +3410,7 @@ function cleanLogs(logsDir) {
|
|
|
3032
3410
|
`);
|
|
3033
3411
|
}
|
|
3034
3412
|
function getLogFiles(logsDir) {
|
|
3035
|
-
return readdirSync2(logsDir).filter((f) => f.startsWith("locus-") && f.endsWith(".log")).map((f) =>
|
|
3413
|
+
return readdirSync2(logsDir).filter((f) => f.startsWith("locus-") && f.endsWith(".log")).map((f) => join10(logsDir, f)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
|
|
3036
3414
|
}
|
|
3037
3415
|
function formatEntry(entry) {
|
|
3038
3416
|
const time = dim2(new Date(entry.ts).toLocaleTimeString());
|
|
@@ -3341,10 +3719,10 @@ var init_stream_renderer = __esm(() => {
|
|
|
3341
3719
|
});
|
|
3342
3720
|
|
|
3343
3721
|
// src/repl/clipboard.ts
|
|
3344
|
-
import { execSync as
|
|
3345
|
-
import { existsSync as
|
|
3722
|
+
import { execSync as execSync6 } from "node:child_process";
|
|
3723
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync8 } from "node:fs";
|
|
3346
3724
|
import { tmpdir } from "node:os";
|
|
3347
|
-
import { join as
|
|
3725
|
+
import { join as join11 } from "node:path";
|
|
3348
3726
|
function readClipboardImage() {
|
|
3349
3727
|
if (process.platform === "darwin") {
|
|
3350
3728
|
return readMacOSClipboardImage();
|
|
@@ -3355,14 +3733,14 @@ function readClipboardImage() {
|
|
|
3355
3733
|
return null;
|
|
3356
3734
|
}
|
|
3357
3735
|
function ensureStableDir() {
|
|
3358
|
-
if (!
|
|
3359
|
-
|
|
3736
|
+
if (!existsSync12(STABLE_DIR)) {
|
|
3737
|
+
mkdirSync8(STABLE_DIR, { recursive: true });
|
|
3360
3738
|
}
|
|
3361
3739
|
}
|
|
3362
3740
|
function readMacOSClipboardImage() {
|
|
3363
3741
|
try {
|
|
3364
3742
|
ensureStableDir();
|
|
3365
|
-
const destPath =
|
|
3743
|
+
const destPath = join11(STABLE_DIR, `clipboard-${Date.now()}.png`);
|
|
3366
3744
|
const script = [
|
|
3367
3745
|
`set destPath to POSIX file "${destPath}"`,
|
|
3368
3746
|
"try",
|
|
@@ -3380,13 +3758,13 @@ function readMacOSClipboardImage() {
|
|
|
3380
3758
|
`return "ok"`
|
|
3381
3759
|
].join(`
|
|
3382
3760
|
`);
|
|
3383
|
-
const result =
|
|
3761
|
+
const result = execSync6("osascript", {
|
|
3384
3762
|
input: script,
|
|
3385
3763
|
encoding: "utf-8",
|
|
3386
3764
|
timeout: 5000,
|
|
3387
3765
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3388
3766
|
}).trim();
|
|
3389
|
-
if (result === "ok" &&
|
|
3767
|
+
if (result === "ok" && existsSync12(destPath)) {
|
|
3390
3768
|
return destPath;
|
|
3391
3769
|
}
|
|
3392
3770
|
} catch {}
|
|
@@ -3394,14 +3772,14 @@ function readMacOSClipboardImage() {
|
|
|
3394
3772
|
}
|
|
3395
3773
|
function readLinuxClipboardImage() {
|
|
3396
3774
|
try {
|
|
3397
|
-
const targets =
|
|
3775
|
+
const targets = execSync6("xclip -selection clipboard -t TARGETS -o 2>/dev/null", { encoding: "utf-8", timeout: 3000 });
|
|
3398
3776
|
if (!targets.includes("image/png")) {
|
|
3399
3777
|
return null;
|
|
3400
3778
|
}
|
|
3401
3779
|
ensureStableDir();
|
|
3402
|
-
const destPath =
|
|
3403
|
-
|
|
3404
|
-
if (
|
|
3780
|
+
const destPath = join11(STABLE_DIR, `clipboard-${Date.now()}.png`);
|
|
3781
|
+
execSync6(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
|
|
3782
|
+
if (existsSync12(destPath)) {
|
|
3405
3783
|
return destPath;
|
|
3406
3784
|
}
|
|
3407
3785
|
} catch {}
|
|
@@ -3409,13 +3787,13 @@ function readLinuxClipboardImage() {
|
|
|
3409
3787
|
}
|
|
3410
3788
|
var STABLE_DIR;
|
|
3411
3789
|
var init_clipboard = __esm(() => {
|
|
3412
|
-
STABLE_DIR =
|
|
3790
|
+
STABLE_DIR = join11(tmpdir(), "locus-images");
|
|
3413
3791
|
});
|
|
3414
3792
|
|
|
3415
3793
|
// src/repl/image-detect.ts
|
|
3416
|
-
import { copyFileSync, existsSync as
|
|
3794
|
+
import { copyFileSync, existsSync as existsSync13, mkdirSync as mkdirSync9 } from "node:fs";
|
|
3417
3795
|
import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
|
|
3418
|
-
import { basename, extname, join as
|
|
3796
|
+
import { basename, extname, join as join12, resolve } from "node:path";
|
|
3419
3797
|
function detectImages(input) {
|
|
3420
3798
|
const detected = [];
|
|
3421
3799
|
const byResolved = new Map;
|
|
@@ -3509,15 +3887,15 @@ function collectReferencedAttachments(input, attachments) {
|
|
|
3509
3887
|
return dedupeByResolvedPath(selected);
|
|
3510
3888
|
}
|
|
3511
3889
|
function relocateImages(images, projectRoot) {
|
|
3512
|
-
const targetDir =
|
|
3890
|
+
const targetDir = join12(projectRoot, ".locus", "tmp", "images");
|
|
3513
3891
|
for (const img of images) {
|
|
3514
3892
|
if (!img.exists)
|
|
3515
3893
|
continue;
|
|
3516
3894
|
try {
|
|
3517
|
-
if (!
|
|
3518
|
-
|
|
3895
|
+
if (!existsSync13(targetDir)) {
|
|
3896
|
+
mkdirSync9(targetDir, { recursive: true });
|
|
3519
3897
|
}
|
|
3520
|
-
const dest =
|
|
3898
|
+
const dest = join12(targetDir, basename(img.stablePath));
|
|
3521
3899
|
copyFileSync(img.stablePath, dest);
|
|
3522
3900
|
img.stablePath = dest;
|
|
3523
3901
|
} catch {}
|
|
@@ -3529,7 +3907,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
|
|
|
3529
3907
|
return;
|
|
3530
3908
|
let resolved = stripQuotes(rawPath).replace(/\\ /g, " ");
|
|
3531
3909
|
if (resolved.startsWith("~/")) {
|
|
3532
|
-
resolved =
|
|
3910
|
+
resolved = join12(homedir3(), resolved.slice(2));
|
|
3533
3911
|
}
|
|
3534
3912
|
resolved = resolve(resolved);
|
|
3535
3913
|
const existing = byResolved.get(resolved);
|
|
@@ -3542,7 +3920,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
|
|
|
3542
3920
|
]);
|
|
3543
3921
|
return;
|
|
3544
3922
|
}
|
|
3545
|
-
const exists =
|
|
3923
|
+
const exists = existsSync13(resolved);
|
|
3546
3924
|
let stablePath = resolved;
|
|
3547
3925
|
if (exists) {
|
|
3548
3926
|
stablePath = copyToStable(resolved);
|
|
@@ -3596,10 +3974,10 @@ function dedupeByResolvedPath(images) {
|
|
|
3596
3974
|
}
|
|
3597
3975
|
function copyToStable(sourcePath) {
|
|
3598
3976
|
try {
|
|
3599
|
-
if (!
|
|
3600
|
-
|
|
3977
|
+
if (!existsSync13(STABLE_DIR2)) {
|
|
3978
|
+
mkdirSync9(STABLE_DIR2, { recursive: true });
|
|
3601
3979
|
}
|
|
3602
|
-
const dest =
|
|
3980
|
+
const dest = join12(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
|
|
3603
3981
|
copyFileSync(sourcePath, dest);
|
|
3604
3982
|
return dest;
|
|
3605
3983
|
} catch {
|
|
@@ -3619,7 +3997,7 @@ var init_image_detect = __esm(() => {
|
|
|
3619
3997
|
".tif",
|
|
3620
3998
|
".tiff"
|
|
3621
3999
|
]);
|
|
3622
|
-
STABLE_DIR2 =
|
|
4000
|
+
STABLE_DIR2 = join12(tmpdir2(), "locus-images");
|
|
3623
4001
|
PLACEHOLDER_ID_PATTERN = /\(locus:\/\/screenshot-(\d+)\)/g;
|
|
3624
4002
|
});
|
|
3625
4003
|
|
|
@@ -4421,7 +4799,7 @@ __export(exports_claude, {
|
|
|
4421
4799
|
buildClaudeArgs: () => buildClaudeArgs,
|
|
4422
4800
|
ClaudeRunner: () => ClaudeRunner
|
|
4423
4801
|
});
|
|
4424
|
-
import { execSync as
|
|
4802
|
+
import { execSync as execSync7, spawn as spawn2 } from "node:child_process";
|
|
4425
4803
|
function buildClaudeArgs(options) {
|
|
4426
4804
|
const args = ["--dangerously-skip-permissions", "--no-session-persistence"];
|
|
4427
4805
|
if (options.model) {
|
|
@@ -4439,7 +4817,7 @@ class ClaudeRunner {
|
|
|
4439
4817
|
aborted = false;
|
|
4440
4818
|
async isAvailable() {
|
|
4441
4819
|
try {
|
|
4442
|
-
|
|
4820
|
+
execSync7("claude --version", {
|
|
4443
4821
|
encoding: "utf-8",
|
|
4444
4822
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4445
4823
|
});
|
|
@@ -4450,7 +4828,7 @@ class ClaudeRunner {
|
|
|
4450
4828
|
}
|
|
4451
4829
|
async getVersion() {
|
|
4452
4830
|
try {
|
|
4453
|
-
const output =
|
|
4831
|
+
const output = execSync7("claude --version", {
|
|
4454
4832
|
encoding: "utf-8",
|
|
4455
4833
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4456
4834
|
}).trim();
|
|
@@ -4617,8 +4995,8 @@ var init_claude = __esm(() => {
|
|
|
4617
4995
|
import { exec } from "node:child_process";
|
|
4618
4996
|
import {
|
|
4619
4997
|
cpSync,
|
|
4620
|
-
existsSync as
|
|
4621
|
-
mkdirSync as
|
|
4998
|
+
existsSync as existsSync14,
|
|
4999
|
+
mkdirSync as mkdirSync10,
|
|
4622
5000
|
mkdtempSync,
|
|
4623
5001
|
readdirSync as readdirSync3,
|
|
4624
5002
|
readFileSync as readFileSync8,
|
|
@@ -4626,10 +5004,10 @@ import {
|
|
|
4626
5004
|
statSync as statSync3
|
|
4627
5005
|
} from "node:fs";
|
|
4628
5006
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
4629
|
-
import { dirname as dirname3, join as
|
|
5007
|
+
import { dirname as dirname3, join as join13, relative } from "node:path";
|
|
4630
5008
|
import { promisify } from "node:util";
|
|
4631
5009
|
function parseIgnoreFile(filePath) {
|
|
4632
|
-
if (!
|
|
5010
|
+
if (!existsSync14(filePath))
|
|
4633
5011
|
return [];
|
|
4634
5012
|
const content = readFileSync8(filePath, "utf-8");
|
|
4635
5013
|
const rules = [];
|
|
@@ -4699,7 +5077,7 @@ function findIgnoredPaths(projectRoot, rules) {
|
|
|
4699
5077
|
for (const name of entries) {
|
|
4700
5078
|
if (SKIP_DIRS.has(name))
|
|
4701
5079
|
continue;
|
|
4702
|
-
const fullPath =
|
|
5080
|
+
const fullPath = join13(dir, name);
|
|
4703
5081
|
let stat = null;
|
|
4704
5082
|
try {
|
|
4705
5083
|
stat = statSync3(fullPath);
|
|
@@ -4733,7 +5111,7 @@ function findIgnoredPaths(projectRoot, rules) {
|
|
|
4733
5111
|
}
|
|
4734
5112
|
function backupIgnoredFiles(projectRoot) {
|
|
4735
5113
|
const log = getLogger();
|
|
4736
|
-
const ignorePath =
|
|
5114
|
+
const ignorePath = join13(projectRoot, ".sandboxignore");
|
|
4737
5115
|
const rules = parseIgnoreFile(ignorePath);
|
|
4738
5116
|
if (rules.length === 0)
|
|
4739
5117
|
return NOOP_BACKUP;
|
|
@@ -4742,7 +5120,7 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
4742
5120
|
return NOOP_BACKUP;
|
|
4743
5121
|
let backupDir;
|
|
4744
5122
|
try {
|
|
4745
|
-
backupDir = mkdtempSync(
|
|
5123
|
+
backupDir = mkdtempSync(join13(tmpdir3(), "locus-sandbox-backup-"));
|
|
4746
5124
|
} catch (err) {
|
|
4747
5125
|
log.debug("Failed to create sandbox backup dir", {
|
|
4748
5126
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -4752,9 +5130,9 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
4752
5130
|
const backed = [];
|
|
4753
5131
|
for (const src of paths) {
|
|
4754
5132
|
const rel = relative(projectRoot, src);
|
|
4755
|
-
const dest =
|
|
5133
|
+
const dest = join13(backupDir, rel);
|
|
4756
5134
|
try {
|
|
4757
|
-
|
|
5135
|
+
mkdirSync10(dirname3(dest), { recursive: true });
|
|
4758
5136
|
cpSync(src, dest, { recursive: true, preserveTimestamps: true });
|
|
4759
5137
|
backed.push({ src, dest });
|
|
4760
5138
|
} catch (err) {
|
|
@@ -4776,7 +5154,7 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
4776
5154
|
restore() {
|
|
4777
5155
|
for (const { src, dest } of backed) {
|
|
4778
5156
|
try {
|
|
4779
|
-
|
|
5157
|
+
mkdirSync10(dirname3(src), { recursive: true });
|
|
4780
5158
|
cpSync(dest, src, { recursive: true, preserveTimestamps: true });
|
|
4781
5159
|
} catch (err) {
|
|
4782
5160
|
log.debug("Failed to restore ignored file (potential data loss)", {
|
|
@@ -4792,13 +5170,13 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
4792
5170
|
}
|
|
4793
5171
|
};
|
|
4794
5172
|
}
|
|
4795
|
-
async function enforceSandboxIgnore(sandboxName, projectRoot) {
|
|
5173
|
+
async function enforceSandboxIgnore(sandboxName, projectRoot, containerWorkdir) {
|
|
4796
5174
|
const log = getLogger();
|
|
4797
|
-
const ignorePath =
|
|
5175
|
+
const ignorePath = join13(projectRoot, ".sandboxignore");
|
|
4798
5176
|
const rules = parseIgnoreFile(ignorePath);
|
|
4799
5177
|
if (rules.length === 0)
|
|
4800
5178
|
return;
|
|
4801
|
-
const script = buildCleanupScript(rules, projectRoot);
|
|
5179
|
+
const script = buildCleanupScript(rules, containerWorkdir ?? projectRoot);
|
|
4802
5180
|
if (!script)
|
|
4803
5181
|
return;
|
|
4804
5182
|
log.debug("Enforcing .sandboxignore", {
|
|
@@ -4838,11 +5216,13 @@ import { spawn as spawn3 } from "node:child_process";
|
|
|
4838
5216
|
|
|
4839
5217
|
class SandboxedClaudeRunner {
|
|
4840
5218
|
sandboxName;
|
|
5219
|
+
containerWorkdir;
|
|
4841
5220
|
name = "claude-sandboxed";
|
|
4842
5221
|
process = null;
|
|
4843
5222
|
aborted = false;
|
|
4844
|
-
constructor(sandboxName) {
|
|
5223
|
+
constructor(sandboxName, containerWorkdir) {
|
|
4845
5224
|
this.sandboxName = sandboxName;
|
|
5225
|
+
this.containerWorkdir = containerWorkdir;
|
|
4846
5226
|
}
|
|
4847
5227
|
async isAvailable() {
|
|
4848
5228
|
const { ClaudeRunner: ClaudeRunner2 } = await Promise.resolve().then(() => (init_claude(), exports_claude));
|
|
@@ -4868,13 +5248,14 @@ class SandboxedClaudeRunner {
|
|
|
4868
5248
|
const claudeArgs = ["-p", options.prompt, ...buildClaudeArgs(options)];
|
|
4869
5249
|
options.onStatusChange?.("Syncing sandbox...");
|
|
4870
5250
|
const backup = backupIgnoredFiles(options.cwd);
|
|
4871
|
-
await enforceSandboxIgnore(this.sandboxName, options.cwd);
|
|
5251
|
+
await enforceSandboxIgnore(this.sandboxName, options.cwd, this.containerWorkdir);
|
|
4872
5252
|
options.onStatusChange?.("Thinking...");
|
|
5253
|
+
const workdir = this.containerWorkdir ?? options.cwd;
|
|
4873
5254
|
const dockerArgs = [
|
|
4874
5255
|
"sandbox",
|
|
4875
5256
|
"exec",
|
|
4876
5257
|
"-w",
|
|
4877
|
-
|
|
5258
|
+
workdir,
|
|
4878
5259
|
this.sandboxName,
|
|
4879
5260
|
"claude",
|
|
4880
5261
|
...claudeArgs
|
|
@@ -5039,7 +5420,7 @@ var init_claude_sandbox = __esm(() => {
|
|
|
5039
5420
|
});
|
|
5040
5421
|
|
|
5041
5422
|
// src/ai/codex.ts
|
|
5042
|
-
import { execSync as
|
|
5423
|
+
import { execSync as execSync8, spawn as spawn4 } from "node:child_process";
|
|
5043
5424
|
function buildCodexArgs(model) {
|
|
5044
5425
|
const args = ["exec", "--full-auto", "--skip-git-repo-check", "--json"];
|
|
5045
5426
|
if (model) {
|
|
@@ -5055,7 +5436,7 @@ class CodexRunner {
|
|
|
5055
5436
|
aborted = false;
|
|
5056
5437
|
async isAvailable() {
|
|
5057
5438
|
try {
|
|
5058
|
-
|
|
5439
|
+
execSync8("codex --version", {
|
|
5059
5440
|
encoding: "utf-8",
|
|
5060
5441
|
stdio: ["pipe", "pipe", "pipe"]
|
|
5061
5442
|
});
|
|
@@ -5066,7 +5447,7 @@ class CodexRunner {
|
|
|
5066
5447
|
}
|
|
5067
5448
|
async getVersion() {
|
|
5068
5449
|
try {
|
|
5069
|
-
const output =
|
|
5450
|
+
const output = execSync8("codex --version", {
|
|
5070
5451
|
encoding: "utf-8",
|
|
5071
5452
|
stdio: ["pipe", "pipe", "pipe"]
|
|
5072
5453
|
}).trim();
|
|
@@ -5218,12 +5599,14 @@ import { spawn as spawn5 } from "node:child_process";
|
|
|
5218
5599
|
|
|
5219
5600
|
class SandboxedCodexRunner {
|
|
5220
5601
|
sandboxName;
|
|
5602
|
+
containerWorkdir;
|
|
5221
5603
|
name = "codex-sandboxed";
|
|
5222
5604
|
process = null;
|
|
5223
5605
|
aborted = false;
|
|
5224
5606
|
codexInstalled = false;
|
|
5225
|
-
constructor(sandboxName) {
|
|
5607
|
+
constructor(sandboxName, containerWorkdir) {
|
|
5226
5608
|
this.sandboxName = sandboxName;
|
|
5609
|
+
this.containerWorkdir = containerWorkdir;
|
|
5227
5610
|
}
|
|
5228
5611
|
async isAvailable() {
|
|
5229
5612
|
const delegate = new CodexRunner;
|
|
@@ -5247,19 +5630,20 @@ class SandboxedCodexRunner {
|
|
|
5247
5630
|
const codexArgs = buildCodexArgs(options.model);
|
|
5248
5631
|
options.onStatusChange?.("Syncing sandbox...");
|
|
5249
5632
|
const backup = backupIgnoredFiles(options.cwd);
|
|
5250
|
-
await enforceSandboxIgnore(this.sandboxName, options.cwd);
|
|
5633
|
+
await enforceSandboxIgnore(this.sandboxName, options.cwd, this.containerWorkdir);
|
|
5251
5634
|
if (!this.codexInstalled) {
|
|
5252
5635
|
options.onStatusChange?.("Checking codex...");
|
|
5253
5636
|
await this.ensureCodexInstalled(this.sandboxName);
|
|
5254
5637
|
this.codexInstalled = true;
|
|
5255
5638
|
}
|
|
5256
5639
|
options.onStatusChange?.("Thinking...");
|
|
5640
|
+
const workdir = this.containerWorkdir ?? options.cwd;
|
|
5257
5641
|
const dockerArgs = [
|
|
5258
5642
|
"sandbox",
|
|
5259
5643
|
"exec",
|
|
5260
5644
|
"-i",
|
|
5261
5645
|
"-w",
|
|
5262
|
-
|
|
5646
|
+
workdir,
|
|
5263
5647
|
this.sandboxName,
|
|
5264
5648
|
"codex",
|
|
5265
5649
|
...codexArgs
|
|
@@ -5440,12 +5824,12 @@ async function createRunnerAsync(provider, sandboxed) {
|
|
|
5440
5824
|
throw new Error(`Unknown AI provider: ${provider}`);
|
|
5441
5825
|
}
|
|
5442
5826
|
}
|
|
5443
|
-
function createUserManagedSandboxRunner(provider, sandboxName) {
|
|
5827
|
+
function createUserManagedSandboxRunner(provider, sandboxName, containerWorkdir) {
|
|
5444
5828
|
switch (provider) {
|
|
5445
5829
|
case "claude":
|
|
5446
|
-
return new SandboxedClaudeRunner(sandboxName);
|
|
5830
|
+
return new SandboxedClaudeRunner(sandboxName, containerWorkdir);
|
|
5447
5831
|
case "codex":
|
|
5448
|
-
return new SandboxedCodexRunner(sandboxName);
|
|
5832
|
+
return new SandboxedCodexRunner(sandboxName, containerWorkdir);
|
|
5449
5833
|
default:
|
|
5450
5834
|
throw new Error(`Unknown AI provider: ${provider}`);
|
|
5451
5835
|
}
|
|
@@ -5556,7 +5940,7 @@ ${red2("✗")} ${dim2("Force exit.")}\r
|
|
|
5556
5940
|
exitCode: 1
|
|
5557
5941
|
};
|
|
5558
5942
|
}
|
|
5559
|
-
runner = createUserManagedSandboxRunner(resolvedProvider, options.sandboxName);
|
|
5943
|
+
runner = createUserManagedSandboxRunner(resolvedProvider, options.sandboxName, options.containerWorkdir);
|
|
5560
5944
|
} else {
|
|
5561
5945
|
runner = await createRunnerAsync(resolvedProvider, false);
|
|
5562
5946
|
}
|
|
@@ -5891,7 +6275,8 @@ async function issueCreate(projectRoot, parsed) {
|
|
|
5891
6275
|
silent: true,
|
|
5892
6276
|
activity: "generating issue",
|
|
5893
6277
|
sandboxed: config.sandbox.enabled,
|
|
5894
|
-
sandboxName: getModelSandboxName(config.sandbox, config.ai.model, config.ai.provider)
|
|
6278
|
+
sandboxName: getModelSandboxName(config.sandbox, config.ai.model, config.ai.provider),
|
|
6279
|
+
containerWorkdir: config.sandbox.containerWorkdir
|
|
5895
6280
|
});
|
|
5896
6281
|
if (!aiResult.success && !aiResult.interrupted) {
|
|
5897
6282
|
process.stderr.write(`${red2("✗")} Failed to generate issue: ${aiResult.error}
|
|
@@ -7094,9 +7479,9 @@ var init_sprint = __esm(() => {
|
|
|
7094
7479
|
});
|
|
7095
7480
|
|
|
7096
7481
|
// src/core/prompt-builder.ts
|
|
7097
|
-
import { execSync as
|
|
7098
|
-
import { existsSync as
|
|
7099
|
-
import { join as
|
|
7482
|
+
import { execSync as execSync9 } from "node:child_process";
|
|
7483
|
+
import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as readFileSync9 } from "node:fs";
|
|
7484
|
+
import { join as join14 } from "node:path";
|
|
7100
7485
|
function buildExecutionPrompt(ctx) {
|
|
7101
7486
|
const sections = [];
|
|
7102
7487
|
sections.push(buildSystemContext(ctx.projectRoot));
|
|
@@ -7126,13 +7511,13 @@ function buildFeedbackPrompt(ctx) {
|
|
|
7126
7511
|
}
|
|
7127
7512
|
function buildReplPrompt(userMessage, projectRoot, _config, previousMessages) {
|
|
7128
7513
|
const sections = [];
|
|
7129
|
-
const locusmd = readFileSafe(
|
|
7514
|
+
const locusmd = readFileSafe(join14(projectRoot, ".locus", "LOCUS.md"));
|
|
7130
7515
|
if (locusmd) {
|
|
7131
7516
|
sections.push(`<project-instructions>
|
|
7132
7517
|
${locusmd}
|
|
7133
7518
|
</project-instructions>`);
|
|
7134
7519
|
}
|
|
7135
|
-
const learnings = readFileSafe(
|
|
7520
|
+
const learnings = readFileSafe(join14(projectRoot, ".locus", "LEARNINGS.md"));
|
|
7136
7521
|
if (learnings) {
|
|
7137
7522
|
sections.push(`<past-learnings>
|
|
7138
7523
|
${learnings}
|
|
@@ -7159,24 +7544,24 @@ ${userMessage}
|
|
|
7159
7544
|
}
|
|
7160
7545
|
function buildSystemContext(projectRoot) {
|
|
7161
7546
|
const parts = [];
|
|
7162
|
-
const locusmd = readFileSafe(
|
|
7547
|
+
const locusmd = readFileSafe(join14(projectRoot, ".locus", "LOCUS.md"));
|
|
7163
7548
|
if (locusmd) {
|
|
7164
7549
|
parts.push(`<project-instructions>
|
|
7165
7550
|
${locusmd}
|
|
7166
7551
|
</project-instructions>`);
|
|
7167
7552
|
}
|
|
7168
|
-
const learnings = readFileSafe(
|
|
7553
|
+
const learnings = readFileSafe(join14(projectRoot, ".locus", "LEARNINGS.md"));
|
|
7169
7554
|
if (learnings) {
|
|
7170
7555
|
parts.push(`<past-learnings>
|
|
7171
7556
|
${learnings}
|
|
7172
7557
|
</past-learnings>`);
|
|
7173
7558
|
}
|
|
7174
|
-
const discussionsDir =
|
|
7175
|
-
if (
|
|
7559
|
+
const discussionsDir = join14(projectRoot, ".locus", "discussions");
|
|
7560
|
+
if (existsSync15(discussionsDir)) {
|
|
7176
7561
|
try {
|
|
7177
7562
|
const files = readdirSync4(discussionsDir).filter((f) => f.endsWith(".md")).slice(0, 3);
|
|
7178
7563
|
for (const file of files) {
|
|
7179
|
-
const content = readFileSafe(
|
|
7564
|
+
const content = readFileSafe(join14(discussionsDir, file));
|
|
7180
7565
|
if (content) {
|
|
7181
7566
|
const name = file.replace(".md", "");
|
|
7182
7567
|
parts.push(`<discussion name="${name}">
|
|
@@ -7243,7 +7628,7 @@ ${parts.join(`
|
|
|
7243
7628
|
function buildRepoContext(projectRoot) {
|
|
7244
7629
|
const parts = [];
|
|
7245
7630
|
try {
|
|
7246
|
-
const tree =
|
|
7631
|
+
const tree = execSync9("find . -maxdepth 2 -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/.locus/*' -not -path '*/dist/*' -not -path '*/build/*' | head -80", { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
7247
7632
|
if (tree) {
|
|
7248
7633
|
parts.push(`<file-tree>
|
|
7249
7634
|
\`\`\`
|
|
@@ -7253,7 +7638,7 @@ ${tree}
|
|
|
7253
7638
|
}
|
|
7254
7639
|
} catch {}
|
|
7255
7640
|
try {
|
|
7256
|
-
const gitLog =
|
|
7641
|
+
const gitLog = execSync9("git log --oneline -10", {
|
|
7257
7642
|
cwd: projectRoot,
|
|
7258
7643
|
encoding: "utf-8",
|
|
7259
7644
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7267,7 +7652,7 @@ ${gitLog}
|
|
|
7267
7652
|
}
|
|
7268
7653
|
} catch {}
|
|
7269
7654
|
try {
|
|
7270
|
-
const branch =
|
|
7655
|
+
const branch = execSync9("git rev-parse --abbrev-ref HEAD", {
|
|
7271
7656
|
cwd: projectRoot,
|
|
7272
7657
|
encoding: "utf-8",
|
|
7273
7658
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7329,7 +7714,7 @@ function buildFeedbackInstructions() {
|
|
|
7329
7714
|
}
|
|
7330
7715
|
function readFileSafe(path) {
|
|
7331
7716
|
try {
|
|
7332
|
-
if (!
|
|
7717
|
+
if (!existsSync15(path))
|
|
7333
7718
|
return null;
|
|
7334
7719
|
return readFileSync9(path, "utf-8");
|
|
7335
7720
|
} catch {
|
|
@@ -7523,7 +7908,7 @@ var init_diff_renderer = __esm(() => {
|
|
|
7523
7908
|
});
|
|
7524
7909
|
|
|
7525
7910
|
// src/repl/commands.ts
|
|
7526
|
-
import { execSync as
|
|
7911
|
+
import { execSync as execSync10 } from "node:child_process";
|
|
7527
7912
|
function getSlashCommands() {
|
|
7528
7913
|
return [
|
|
7529
7914
|
{
|
|
@@ -7721,7 +8106,7 @@ function cmdModel(args, ctx) {
|
|
|
7721
8106
|
}
|
|
7722
8107
|
function cmdDiff(_args, ctx) {
|
|
7723
8108
|
try {
|
|
7724
|
-
const diff =
|
|
8109
|
+
const diff = execSync10("git diff", {
|
|
7725
8110
|
cwd: ctx.projectRoot,
|
|
7726
8111
|
encoding: "utf-8",
|
|
7727
8112
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7757,7 +8142,7 @@ function cmdDiff(_args, ctx) {
|
|
|
7757
8142
|
}
|
|
7758
8143
|
function cmdUndo(_args, ctx) {
|
|
7759
8144
|
try {
|
|
7760
|
-
const status =
|
|
8145
|
+
const status = execSync10("git status --porcelain", {
|
|
7761
8146
|
cwd: ctx.projectRoot,
|
|
7762
8147
|
encoding: "utf-8",
|
|
7763
8148
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7767,7 +8152,7 @@ function cmdUndo(_args, ctx) {
|
|
|
7767
8152
|
`);
|
|
7768
8153
|
return;
|
|
7769
8154
|
}
|
|
7770
|
-
|
|
8155
|
+
execSync10("git checkout .", {
|
|
7771
8156
|
cwd: ctx.projectRoot,
|
|
7772
8157
|
encoding: "utf-8",
|
|
7773
8158
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -7809,7 +8194,7 @@ var init_commands = __esm(() => {
|
|
|
7809
8194
|
|
|
7810
8195
|
// src/repl/completions.ts
|
|
7811
8196
|
import { readdirSync as readdirSync5 } from "node:fs";
|
|
7812
|
-
import { basename as basename2, dirname as dirname4, join as
|
|
8197
|
+
import { basename as basename2, dirname as dirname4, join as join15 } from "node:path";
|
|
7813
8198
|
|
|
7814
8199
|
class SlashCommandCompletion {
|
|
7815
8200
|
commands;
|
|
@@ -7864,7 +8249,7 @@ class FilePathCompletion {
|
|
|
7864
8249
|
}
|
|
7865
8250
|
findMatches(partial) {
|
|
7866
8251
|
try {
|
|
7867
|
-
const dir = partial.includes("/") ?
|
|
8252
|
+
const dir = partial.includes("/") ? join15(this.projectRoot, dirname4(partial)) : this.projectRoot;
|
|
7868
8253
|
const prefix = basename2(partial);
|
|
7869
8254
|
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
7870
8255
|
return entries.filter((e) => {
|
|
@@ -7900,14 +8285,14 @@ class CombinedCompletion {
|
|
|
7900
8285
|
var init_completions = () => {};
|
|
7901
8286
|
|
|
7902
8287
|
// src/repl/input-history.ts
|
|
7903
|
-
import { existsSync as
|
|
7904
|
-
import { dirname as dirname5, join as
|
|
8288
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "node:fs";
|
|
8289
|
+
import { dirname as dirname5, join as join16 } from "node:path";
|
|
7905
8290
|
|
|
7906
8291
|
class InputHistory {
|
|
7907
8292
|
entries = [];
|
|
7908
8293
|
filePath;
|
|
7909
8294
|
constructor(projectRoot) {
|
|
7910
|
-
this.filePath =
|
|
8295
|
+
this.filePath = join16(projectRoot, ".locus", "sessions", ".input-history");
|
|
7911
8296
|
this.load();
|
|
7912
8297
|
}
|
|
7913
8298
|
add(text) {
|
|
@@ -7946,7 +8331,7 @@ class InputHistory {
|
|
|
7946
8331
|
}
|
|
7947
8332
|
load() {
|
|
7948
8333
|
try {
|
|
7949
|
-
if (!
|
|
8334
|
+
if (!existsSync16(this.filePath))
|
|
7950
8335
|
return;
|
|
7951
8336
|
const content = readFileSync10(this.filePath, "utf-8");
|
|
7952
8337
|
this.entries = content.split(`
|
|
@@ -7956,12 +8341,12 @@ class InputHistory {
|
|
|
7956
8341
|
save() {
|
|
7957
8342
|
try {
|
|
7958
8343
|
const dir = dirname5(this.filePath);
|
|
7959
|
-
if (!
|
|
7960
|
-
|
|
8344
|
+
if (!existsSync16(dir)) {
|
|
8345
|
+
mkdirSync11(dir, { recursive: true });
|
|
7961
8346
|
}
|
|
7962
8347
|
const content = this.entries.map((e) => this.escape(e)).join(`
|
|
7963
8348
|
`);
|
|
7964
|
-
|
|
8349
|
+
writeFileSync7(this.filePath, content, "utf-8");
|
|
7965
8350
|
} catch {}
|
|
7966
8351
|
}
|
|
7967
8352
|
escape(text) {
|
|
@@ -7987,21 +8372,21 @@ var init_model_config = __esm(() => {
|
|
|
7987
8372
|
|
|
7988
8373
|
// src/repl/session-manager.ts
|
|
7989
8374
|
import {
|
|
7990
|
-
existsSync as
|
|
7991
|
-
mkdirSync as
|
|
8375
|
+
existsSync as existsSync17,
|
|
8376
|
+
mkdirSync as mkdirSync12,
|
|
7992
8377
|
readdirSync as readdirSync6,
|
|
7993
8378
|
readFileSync as readFileSync11,
|
|
7994
8379
|
unlinkSync as unlinkSync3,
|
|
7995
|
-
writeFileSync as
|
|
8380
|
+
writeFileSync as writeFileSync8
|
|
7996
8381
|
} from "node:fs";
|
|
7997
|
-
import { basename as basename3, join as
|
|
8382
|
+
import { basename as basename3, join as join17 } from "node:path";
|
|
7998
8383
|
|
|
7999
8384
|
class SessionManager {
|
|
8000
8385
|
sessionsDir;
|
|
8001
8386
|
constructor(projectRoot) {
|
|
8002
|
-
this.sessionsDir =
|
|
8003
|
-
if (!
|
|
8004
|
-
|
|
8387
|
+
this.sessionsDir = join17(projectRoot, ".locus", "sessions");
|
|
8388
|
+
if (!existsSync17(this.sessionsDir)) {
|
|
8389
|
+
mkdirSync12(this.sessionsDir, { recursive: true });
|
|
8005
8390
|
}
|
|
8006
8391
|
}
|
|
8007
8392
|
create(options) {
|
|
@@ -8026,12 +8411,12 @@ class SessionManager {
|
|
|
8026
8411
|
}
|
|
8027
8412
|
isPersisted(sessionOrId) {
|
|
8028
8413
|
const sessionId = typeof sessionOrId === "string" ? sessionOrId : sessionOrId.id;
|
|
8029
|
-
return
|
|
8414
|
+
return existsSync17(this.getSessionPath(sessionId));
|
|
8030
8415
|
}
|
|
8031
8416
|
load(idOrPrefix) {
|
|
8032
8417
|
const files = this.listSessionFiles();
|
|
8033
8418
|
const exactPath = this.getSessionPath(idOrPrefix);
|
|
8034
|
-
if (
|
|
8419
|
+
if (existsSync17(exactPath)) {
|
|
8035
8420
|
try {
|
|
8036
8421
|
return JSON.parse(readFileSync11(exactPath, "utf-8"));
|
|
8037
8422
|
} catch {
|
|
@@ -8054,7 +8439,7 @@ class SessionManager {
|
|
|
8054
8439
|
save(session) {
|
|
8055
8440
|
session.updated = new Date().toISOString();
|
|
8056
8441
|
const path = this.getSessionPath(session.id);
|
|
8057
|
-
|
|
8442
|
+
writeFileSync8(path, `${JSON.stringify(session, null, 2)}
|
|
8058
8443
|
`, "utf-8");
|
|
8059
8444
|
}
|
|
8060
8445
|
addMessage(session, message) {
|
|
@@ -8081,7 +8466,7 @@ class SessionManager {
|
|
|
8081
8466
|
}
|
|
8082
8467
|
delete(sessionId) {
|
|
8083
8468
|
const path = this.getSessionPath(sessionId);
|
|
8084
|
-
if (
|
|
8469
|
+
if (existsSync17(path)) {
|
|
8085
8470
|
unlinkSync3(path);
|
|
8086
8471
|
return true;
|
|
8087
8472
|
}
|
|
@@ -8111,7 +8496,7 @@ class SessionManager {
|
|
|
8111
8496
|
const remaining = withStats.length - pruned;
|
|
8112
8497
|
if (remaining > MAX_SESSIONS) {
|
|
8113
8498
|
const toRemove = remaining - MAX_SESSIONS;
|
|
8114
|
-
const alive = withStats.filter((e) =>
|
|
8499
|
+
const alive = withStats.filter((e) => existsSync17(e.path));
|
|
8115
8500
|
for (let i = 0;i < toRemove && i < alive.length; i++) {
|
|
8116
8501
|
try {
|
|
8117
8502
|
unlinkSync3(alive[i].path);
|
|
@@ -8126,7 +8511,7 @@ class SessionManager {
|
|
|
8126
8511
|
}
|
|
8127
8512
|
listSessionFiles() {
|
|
8128
8513
|
try {
|
|
8129
|
-
return readdirSync6(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) =>
|
|
8514
|
+
return readdirSync6(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join17(this.sessionsDir, f));
|
|
8130
8515
|
} catch {
|
|
8131
8516
|
return [];
|
|
8132
8517
|
}
|
|
@@ -8135,7 +8520,7 @@ class SessionManager {
|
|
|
8135
8520
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
8136
8521
|
}
|
|
8137
8522
|
getSessionPath(sessionId) {
|
|
8138
|
-
return
|
|
8523
|
+
return join17(this.sessionsDir, `${sessionId}.json`);
|
|
8139
8524
|
}
|
|
8140
8525
|
}
|
|
8141
8526
|
var MAX_SESSIONS = 50, SESSION_MAX_AGE_MS;
|
|
@@ -8145,17 +8530,17 @@ var init_session_manager = __esm(() => {
|
|
|
8145
8530
|
});
|
|
8146
8531
|
|
|
8147
8532
|
// src/repl/voice.ts
|
|
8148
|
-
import { execSync as
|
|
8149
|
-
import { existsSync as
|
|
8533
|
+
import { execSync as execSync11, spawn as spawn6 } from "node:child_process";
|
|
8534
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync13, unlinkSync as unlinkSync4 } from "node:fs";
|
|
8150
8535
|
import { cpus, homedir as homedir4, platform, tmpdir as tmpdir4 } from "node:os";
|
|
8151
|
-
import { join as
|
|
8536
|
+
import { join as join18 } from "node:path";
|
|
8152
8537
|
function getWhisperModelPath() {
|
|
8153
|
-
return
|
|
8538
|
+
return join18(WHISPER_MODELS_DIR, `ggml-${WHISPER_MODEL}.bin`);
|
|
8154
8539
|
}
|
|
8155
8540
|
function commandExists(cmd) {
|
|
8156
8541
|
try {
|
|
8157
8542
|
const which = platform() === "win32" ? "where" : "which";
|
|
8158
|
-
|
|
8543
|
+
execSync11(`${which} ${cmd}`, { stdio: "pipe" });
|
|
8159
8544
|
return true;
|
|
8160
8545
|
} catch {
|
|
8161
8546
|
return false;
|
|
@@ -8168,16 +8553,16 @@ function findWhisperBinary() {
|
|
|
8168
8553
|
return name;
|
|
8169
8554
|
}
|
|
8170
8555
|
for (const name of candidates) {
|
|
8171
|
-
const fullPath =
|
|
8172
|
-
if (
|
|
8556
|
+
const fullPath = join18(LOCUS_BIN_DIR, name);
|
|
8557
|
+
if (existsSync18(fullPath))
|
|
8173
8558
|
return fullPath;
|
|
8174
8559
|
}
|
|
8175
8560
|
if (platform() === "darwin") {
|
|
8176
8561
|
const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
|
|
8177
8562
|
for (const dir of brewDirs) {
|
|
8178
8563
|
for (const name of candidates) {
|
|
8179
|
-
const fullPath =
|
|
8180
|
-
if (
|
|
8564
|
+
const fullPath = join18(dir, name);
|
|
8565
|
+
if (existsSync18(fullPath))
|
|
8181
8566
|
return fullPath;
|
|
8182
8567
|
}
|
|
8183
8568
|
}
|
|
@@ -8192,11 +8577,11 @@ function findSoxRecBinary() {
|
|
|
8192
8577
|
if (platform() === "darwin") {
|
|
8193
8578
|
const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
|
|
8194
8579
|
for (const dir of brewDirs) {
|
|
8195
|
-
const recPath =
|
|
8196
|
-
if (
|
|
8580
|
+
const recPath = join18(dir, "rec");
|
|
8581
|
+
if (existsSync18(recPath))
|
|
8197
8582
|
return recPath;
|
|
8198
|
-
const soxPath =
|
|
8199
|
-
if (
|
|
8583
|
+
const soxPath = join18(dir, "sox");
|
|
8584
|
+
if (existsSync18(soxPath))
|
|
8200
8585
|
return soxPath;
|
|
8201
8586
|
}
|
|
8202
8587
|
}
|
|
@@ -8205,7 +8590,7 @@ function findSoxRecBinary() {
|
|
|
8205
8590
|
function checkDependencies() {
|
|
8206
8591
|
const soxBinary = findSoxRecBinary();
|
|
8207
8592
|
const whisperBinary = findWhisperBinary();
|
|
8208
|
-
const modelDownloaded =
|
|
8593
|
+
const modelDownloaded = existsSync18(getWhisperModelPath());
|
|
8209
8594
|
return {
|
|
8210
8595
|
sox: soxBinary !== null,
|
|
8211
8596
|
whisper: whisperBinary !== null,
|
|
@@ -8281,22 +8666,22 @@ function installSox(pm) {
|
|
|
8281
8666
|
try {
|
|
8282
8667
|
switch (pm) {
|
|
8283
8668
|
case "brew":
|
|
8284
|
-
|
|
8669
|
+
execSync11("brew install sox", { stdio: "inherit", timeout: 300000 });
|
|
8285
8670
|
break;
|
|
8286
8671
|
case "apt":
|
|
8287
|
-
|
|
8672
|
+
execSync11("sudo apt-get install -y sox", {
|
|
8288
8673
|
stdio: "inherit",
|
|
8289
8674
|
timeout: 300000
|
|
8290
8675
|
});
|
|
8291
8676
|
break;
|
|
8292
8677
|
case "dnf":
|
|
8293
|
-
|
|
8678
|
+
execSync11("sudo dnf install -y sox", {
|
|
8294
8679
|
stdio: "inherit",
|
|
8295
8680
|
timeout: 300000
|
|
8296
8681
|
});
|
|
8297
8682
|
break;
|
|
8298
8683
|
case "pacman":
|
|
8299
|
-
|
|
8684
|
+
execSync11("sudo pacman -S --noconfirm sox", {
|
|
8300
8685
|
stdio: "inherit",
|
|
8301
8686
|
timeout: 300000
|
|
8302
8687
|
});
|
|
@@ -8310,7 +8695,7 @@ function installSox(pm) {
|
|
|
8310
8695
|
function installWhisperCpp(pm) {
|
|
8311
8696
|
if (pm === "brew") {
|
|
8312
8697
|
try {
|
|
8313
|
-
|
|
8698
|
+
execSync11("brew install whisper-cpp", {
|
|
8314
8699
|
stdio: "inherit",
|
|
8315
8700
|
timeout: 300000
|
|
8316
8701
|
});
|
|
@@ -8332,19 +8717,19 @@ function ensureBuildDeps(pm) {
|
|
|
8332
8717
|
try {
|
|
8333
8718
|
switch (pm) {
|
|
8334
8719
|
case "apt":
|
|
8335
|
-
|
|
8720
|
+
execSync11("sudo apt-get install -y cmake g++ make git", {
|
|
8336
8721
|
stdio: "inherit",
|
|
8337
8722
|
timeout: 300000
|
|
8338
8723
|
});
|
|
8339
8724
|
break;
|
|
8340
8725
|
case "dnf":
|
|
8341
|
-
|
|
8726
|
+
execSync11("sudo dnf install -y cmake gcc-c++ make git", {
|
|
8342
8727
|
stdio: "inherit",
|
|
8343
8728
|
timeout: 300000
|
|
8344
8729
|
});
|
|
8345
8730
|
break;
|
|
8346
8731
|
case "pacman":
|
|
8347
|
-
|
|
8732
|
+
execSync11("sudo pacman -S --noconfirm cmake gcc make git", {
|
|
8348
8733
|
stdio: "inherit",
|
|
8349
8734
|
timeout: 300000
|
|
8350
8735
|
});
|
|
@@ -8359,40 +8744,40 @@ function ensureBuildDeps(pm) {
|
|
|
8359
8744
|
}
|
|
8360
8745
|
function buildWhisperFromSource(pm) {
|
|
8361
8746
|
const out = process.stderr;
|
|
8362
|
-
const buildDir =
|
|
8747
|
+
const buildDir = join18(tmpdir4(), `locus-whisper-build-${process.pid}`);
|
|
8363
8748
|
if (!ensureBuildDeps(pm)) {
|
|
8364
8749
|
out.write(` ${red2("✗")} Could not install build tools (cmake, g++, git).
|
|
8365
8750
|
`);
|
|
8366
8751
|
return false;
|
|
8367
8752
|
}
|
|
8368
8753
|
try {
|
|
8369
|
-
|
|
8370
|
-
|
|
8754
|
+
mkdirSync13(buildDir, { recursive: true });
|
|
8755
|
+
mkdirSync13(LOCUS_BIN_DIR, { recursive: true });
|
|
8371
8756
|
out.write(` ${dim2("Cloning whisper.cpp...")}
|
|
8372
8757
|
`);
|
|
8373
|
-
|
|
8374
|
-
const srcDir =
|
|
8758
|
+
execSync11(`git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git "${join18(buildDir, "whisper.cpp")}"`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
|
|
8759
|
+
const srcDir = join18(buildDir, "whisper.cpp");
|
|
8375
8760
|
const numCpus = cpus().length || 2;
|
|
8376
8761
|
out.write(` ${dim2("Building whisper.cpp (this may take a few minutes)...")}
|
|
8377
8762
|
`);
|
|
8378
|
-
|
|
8763
|
+
execSync11("cmake -B build -DCMAKE_BUILD_TYPE=Release", {
|
|
8379
8764
|
cwd: srcDir,
|
|
8380
8765
|
stdio: ["pipe", "pipe", "pipe"],
|
|
8381
8766
|
timeout: 120000
|
|
8382
8767
|
});
|
|
8383
|
-
|
|
8768
|
+
execSync11(`cmake --build build --config Release -j${numCpus}`, {
|
|
8384
8769
|
cwd: srcDir,
|
|
8385
8770
|
stdio: ["pipe", "pipe", "pipe"],
|
|
8386
8771
|
timeout: 600000
|
|
8387
8772
|
});
|
|
8388
|
-
const destPath =
|
|
8773
|
+
const destPath = join18(LOCUS_BIN_DIR, "whisper-cli");
|
|
8389
8774
|
const binaryCandidates = [
|
|
8390
|
-
|
|
8391
|
-
|
|
8775
|
+
join18(srcDir, "build", "bin", "whisper-cli"),
|
|
8776
|
+
join18(srcDir, "build", "bin", "main")
|
|
8392
8777
|
];
|
|
8393
8778
|
for (const candidate of binaryCandidates) {
|
|
8394
|
-
if (
|
|
8395
|
-
|
|
8779
|
+
if (existsSync18(candidate)) {
|
|
8780
|
+
execSync11(`cp "${candidate}" "${destPath}" && chmod +x "${destPath}"`, {
|
|
8396
8781
|
stdio: "pipe"
|
|
8397
8782
|
});
|
|
8398
8783
|
return true;
|
|
@@ -8407,7 +8792,7 @@ function buildWhisperFromSource(pm) {
|
|
|
8407
8792
|
return false;
|
|
8408
8793
|
} finally {
|
|
8409
8794
|
try {
|
|
8410
|
-
|
|
8795
|
+
execSync11(`rm -rf "${buildDir}"`, { stdio: "pipe" });
|
|
8411
8796
|
} catch {}
|
|
8412
8797
|
}
|
|
8413
8798
|
}
|
|
@@ -8470,20 +8855,20 @@ ${bold2("Installing voice dependencies...")}
|
|
|
8470
8855
|
}
|
|
8471
8856
|
function downloadModel() {
|
|
8472
8857
|
const modelPath = getWhisperModelPath();
|
|
8473
|
-
if (
|
|
8858
|
+
if (existsSync18(modelPath))
|
|
8474
8859
|
return true;
|
|
8475
|
-
|
|
8860
|
+
mkdirSync13(WHISPER_MODELS_DIR, { recursive: true });
|
|
8476
8861
|
const url = `https://huggingface.co/ggerganov/whisper.cpp/resolve/main/${WHISPER_MODEL === "base.en" ? "ggml-base.en.bin" : `ggml-${WHISPER_MODEL}.bin`}`;
|
|
8477
8862
|
process.stderr.write(`${dim2("Downloading whisper model")} ${bold2(WHISPER_MODEL)} ${dim2("(~150MB)...")}
|
|
8478
8863
|
`);
|
|
8479
8864
|
try {
|
|
8480
8865
|
if (commandExists("curl")) {
|
|
8481
|
-
|
|
8866
|
+
execSync11(`curl -L -o "${modelPath}" "${url}"`, {
|
|
8482
8867
|
stdio: ["pipe", "pipe", "pipe"],
|
|
8483
8868
|
timeout: 300000
|
|
8484
8869
|
});
|
|
8485
8870
|
} else if (commandExists("wget")) {
|
|
8486
|
-
|
|
8871
|
+
execSync11(`wget -O "${modelPath}" "${url}"`, {
|
|
8487
8872
|
stdio: ["pipe", "pipe", "pipe"],
|
|
8488
8873
|
timeout: 300000
|
|
8489
8874
|
});
|
|
@@ -8517,7 +8902,7 @@ class VoiceController {
|
|
|
8517
8902
|
onStateChange;
|
|
8518
8903
|
constructor(options) {
|
|
8519
8904
|
this.onStateChange = options.onStateChange;
|
|
8520
|
-
this.tempFile =
|
|
8905
|
+
this.tempFile = join18(tmpdir4(), `locus-voice-${process.pid}.wav`);
|
|
8521
8906
|
this.deps = checkDependencies();
|
|
8522
8907
|
}
|
|
8523
8908
|
getState() {
|
|
@@ -8585,7 +8970,7 @@ ${red2("✗")} Recording failed: ${err.message}\r
|
|
|
8585
8970
|
this.recordProcess = null;
|
|
8586
8971
|
this.setState("idle");
|
|
8587
8972
|
await sleep2(200);
|
|
8588
|
-
if (!
|
|
8973
|
+
if (!existsSync18(this.tempFile)) {
|
|
8589
8974
|
return null;
|
|
8590
8975
|
}
|
|
8591
8976
|
try {
|
|
@@ -8676,12 +9061,12 @@ function sleep2(ms) {
|
|
|
8676
9061
|
var WHISPER_MODEL = "base.en", WHISPER_MODELS_DIR, LOCUS_BIN_DIR;
|
|
8677
9062
|
var init_voice = __esm(() => {
|
|
8678
9063
|
init_terminal();
|
|
8679
|
-
WHISPER_MODELS_DIR =
|
|
8680
|
-
LOCUS_BIN_DIR =
|
|
9064
|
+
WHISPER_MODELS_DIR = join18(homedir4(), ".locus", "whisper-models");
|
|
9065
|
+
LOCUS_BIN_DIR = join18(homedir4(), ".locus", "bin");
|
|
8681
9066
|
});
|
|
8682
9067
|
|
|
8683
9068
|
// src/repl/repl.ts
|
|
8684
|
-
import { execSync as
|
|
9069
|
+
import { execSync as execSync12 } from "node:child_process";
|
|
8685
9070
|
async function startRepl(options) {
|
|
8686
9071
|
const { projectRoot, config } = options;
|
|
8687
9072
|
const sessionManager = new SessionManager(projectRoot);
|
|
@@ -8699,7 +9084,7 @@ async function startRepl(options) {
|
|
|
8699
9084
|
} else {
|
|
8700
9085
|
let branch = "main";
|
|
8701
9086
|
try {
|
|
8702
|
-
branch =
|
|
9087
|
+
branch = execSync12("git rev-parse --abbrev-ref HEAD", {
|
|
8703
9088
|
cwd: projectRoot,
|
|
8704
9089
|
encoding: "utf-8",
|
|
8705
9090
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8751,7 +9136,7 @@ async function runInteractiveRepl(session, sessionManager, options) {
|
|
|
8751
9136
|
const provider = inferProviderFromModel(config.ai.model) || config.ai.provider;
|
|
8752
9137
|
const sandboxName = getProviderSandboxName(config.sandbox, provider);
|
|
8753
9138
|
if (sandboxName) {
|
|
8754
|
-
sandboxRunner = createUserManagedSandboxRunner(provider, sandboxName);
|
|
9139
|
+
sandboxRunner = createUserManagedSandboxRunner(provider, sandboxName, config.sandbox.containerWorkdir);
|
|
8755
9140
|
process.stderr.write(`${dim2("Using")} ${dim2(provider)} ${dim2("sandbox")} ${dim2(sandboxName)}
|
|
8756
9141
|
`);
|
|
8757
9142
|
} else {
|
|
@@ -8802,7 +9187,7 @@ async function runInteractiveRepl(session, sessionManager, options) {
|
|
|
8802
9187
|
if (providerChanged && config.sandbox.enabled) {
|
|
8803
9188
|
const sandboxName = getProviderSandboxName(config.sandbox, inferredProvider);
|
|
8804
9189
|
if (sandboxName) {
|
|
8805
|
-
sandboxRunner = createUserManagedSandboxRunner(inferredProvider, sandboxName);
|
|
9190
|
+
sandboxRunner = createUserManagedSandboxRunner(inferredProvider, sandboxName, config.sandbox.containerWorkdir);
|
|
8806
9191
|
process.stderr.write(`${dim2("Switched sandbox agent to")} ${dim2(inferredProvider)} ${dim2(`(${sandboxName})`)}
|
|
8807
9192
|
`);
|
|
8808
9193
|
} else {
|
|
@@ -8930,6 +9315,7 @@ async function executeAITurn(prompt, session, options, verbose = false, runner)
|
|
|
8930
9315
|
verbose,
|
|
8931
9316
|
sandboxed: config.sandbox.enabled,
|
|
8932
9317
|
sandboxName,
|
|
9318
|
+
containerWorkdir: config.sandbox.containerWorkdir,
|
|
8933
9319
|
runner
|
|
8934
9320
|
});
|
|
8935
9321
|
if (aiResult.interrupted) {
|
|
@@ -9134,7 +9520,7 @@ async function handleJsonStream(projectRoot, config, args, sessionId) {
|
|
|
9134
9520
|
try {
|
|
9135
9521
|
const fullPrompt = buildReplPrompt(prompt, projectRoot, config);
|
|
9136
9522
|
const sandboxName = getProviderSandboxName(config.sandbox, config.ai.provider);
|
|
9137
|
-
const runner = config.sandbox.enabled ? sandboxName ? createUserManagedSandboxRunner(config.ai.provider, sandboxName) : null : await createRunnerAsync(config.ai.provider, false);
|
|
9523
|
+
const runner = config.sandbox.enabled ? sandboxName ? createUserManagedSandboxRunner(config.ai.provider, sandboxName, config.sandbox.containerWorkdir) : null : await createRunnerAsync(config.ai.provider, false);
|
|
9138
9524
|
if (!runner) {
|
|
9139
9525
|
const mismatch = checkProviderSandboxMismatch(config.sandbox, config.ai.model, config.ai.provider);
|
|
9140
9526
|
stream.emitError(mismatch ?? `No sandbox configured for "${config.ai.provider}". Run "locus sandbox" to create one.`, false);
|
|
@@ -9187,11 +9573,11 @@ var init_exec = __esm(() => {
|
|
|
9187
9573
|
});
|
|
9188
9574
|
|
|
9189
9575
|
// src/core/submodule.ts
|
|
9190
|
-
import { execSync as
|
|
9191
|
-
import { existsSync as
|
|
9192
|
-
import { join as
|
|
9576
|
+
import { execSync as execSync13 } from "node:child_process";
|
|
9577
|
+
import { existsSync as existsSync19 } from "node:fs";
|
|
9578
|
+
import { join as join19 } from "node:path";
|
|
9193
9579
|
function git2(args, cwd) {
|
|
9194
|
-
return
|
|
9580
|
+
return execSync13(`git ${args}`, {
|
|
9195
9581
|
cwd,
|
|
9196
9582
|
encoding: "utf-8",
|
|
9197
9583
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9205,7 +9591,7 @@ function gitSafe(args, cwd) {
|
|
|
9205
9591
|
}
|
|
9206
9592
|
}
|
|
9207
9593
|
function hasSubmodules(cwd) {
|
|
9208
|
-
return
|
|
9594
|
+
return existsSync19(join19(cwd, ".gitmodules"));
|
|
9209
9595
|
}
|
|
9210
9596
|
function listSubmodules(cwd) {
|
|
9211
9597
|
if (!hasSubmodules(cwd))
|
|
@@ -9225,7 +9611,7 @@ function listSubmodules(cwd) {
|
|
|
9225
9611
|
continue;
|
|
9226
9612
|
submodules.push({
|
|
9227
9613
|
path,
|
|
9228
|
-
absolutePath:
|
|
9614
|
+
absolutePath: join19(cwd, path),
|
|
9229
9615
|
dirty
|
|
9230
9616
|
});
|
|
9231
9617
|
}
|
|
@@ -9238,7 +9624,7 @@ function getDirtySubmodules(cwd) {
|
|
|
9238
9624
|
const submodules = listSubmodules(cwd);
|
|
9239
9625
|
const dirty = [];
|
|
9240
9626
|
for (const sub of submodules) {
|
|
9241
|
-
if (!
|
|
9627
|
+
if (!existsSync19(sub.absolutePath))
|
|
9242
9628
|
continue;
|
|
9243
9629
|
const status = gitSafe("status --porcelain", sub.absolutePath);
|
|
9244
9630
|
if (status && status.trim().length > 0) {
|
|
@@ -9259,7 +9645,7 @@ function commitDirtySubmodules(cwd, issueNumber, issueTitle) {
|
|
|
9259
9645
|
const message = `chore: complete #${issueNumber} - ${issueTitle}
|
|
9260
9646
|
|
|
9261
9647
|
Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
9262
|
-
|
|
9648
|
+
execSync13("git commit -F -", {
|
|
9263
9649
|
input: message,
|
|
9264
9650
|
cwd: sub.absolutePath,
|
|
9265
9651
|
encoding: "utf-8",
|
|
@@ -9325,7 +9711,7 @@ function pushSubmoduleBranches(cwd) {
|
|
|
9325
9711
|
const log = getLogger();
|
|
9326
9712
|
const submodules = listSubmodules(cwd);
|
|
9327
9713
|
for (const sub of submodules) {
|
|
9328
|
-
if (!
|
|
9714
|
+
if (!existsSync19(sub.absolutePath))
|
|
9329
9715
|
continue;
|
|
9330
9716
|
const branch = gitSafe("rev-parse --abbrev-ref HEAD", sub.absolutePath)?.trim();
|
|
9331
9717
|
if (!branch || branch === "HEAD")
|
|
@@ -9346,7 +9732,7 @@ var init_submodule = __esm(() => {
|
|
|
9346
9732
|
});
|
|
9347
9733
|
|
|
9348
9734
|
// src/core/agent.ts
|
|
9349
|
-
import { execSync as
|
|
9735
|
+
import { execSync as execSync14 } from "node:child_process";
|
|
9350
9736
|
async function executeIssue(projectRoot, options) {
|
|
9351
9737
|
const log = getLogger();
|
|
9352
9738
|
const timer = createTimer();
|
|
@@ -9375,7 +9761,7 @@ ${cyan2("●")} ${bold2(`#${issueNumber}`)} ${issue.title}
|
|
|
9375
9761
|
}
|
|
9376
9762
|
let issueComments = [];
|
|
9377
9763
|
try {
|
|
9378
|
-
const commentsRaw =
|
|
9764
|
+
const commentsRaw = execSync14(`gh issue view ${issueNumber} --json comments --jq '.comments[].body'`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
9379
9765
|
if (commentsRaw) {
|
|
9380
9766
|
issueComments = commentsRaw.split(`
|
|
9381
9767
|
`).filter(Boolean);
|
|
@@ -9412,7 +9798,8 @@ ${yellow2("⚠")} ${bold2("Dry run")} — would execute with:
|
|
|
9412
9798
|
cwd: options.worktreePath ?? projectRoot,
|
|
9413
9799
|
activity: `issue #${issueNumber}`,
|
|
9414
9800
|
sandboxed: options.sandboxed,
|
|
9415
|
-
sandboxName: options.sandboxName
|
|
9801
|
+
sandboxName: options.sandboxName,
|
|
9802
|
+
containerWorkdir: options.containerWorkdir
|
|
9416
9803
|
});
|
|
9417
9804
|
const output = aiResult.output;
|
|
9418
9805
|
if (aiResult.interrupted) {
|
|
@@ -9518,7 +9905,8 @@ ${c.body}`),
|
|
|
9518
9905
|
cwd: projectRoot,
|
|
9519
9906
|
activity: `iterating on PR #${prNumber}`,
|
|
9520
9907
|
sandboxed: config.sandbox.enabled,
|
|
9521
|
-
sandboxName: getModelSandboxName(config.sandbox, config.ai.model, config.ai.provider)
|
|
9908
|
+
sandboxName: getModelSandboxName(config.sandbox, config.ai.model, config.ai.provider),
|
|
9909
|
+
containerWorkdir: config.sandbox.containerWorkdir
|
|
9522
9910
|
});
|
|
9523
9911
|
if (aiResult.interrupted) {
|
|
9524
9912
|
process.stderr.write(`
|
|
@@ -9539,12 +9927,12 @@ ${aiResult.success ? green("✓") : red2("✗")} Iteration ${aiResult.success ?
|
|
|
9539
9927
|
}
|
|
9540
9928
|
async function createIssuePR(projectRoot, config, issue) {
|
|
9541
9929
|
try {
|
|
9542
|
-
const currentBranch =
|
|
9930
|
+
const currentBranch = execSync14("git rev-parse --abbrev-ref HEAD", {
|
|
9543
9931
|
cwd: projectRoot,
|
|
9544
9932
|
encoding: "utf-8",
|
|
9545
9933
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9546
9934
|
}).trim();
|
|
9547
|
-
const diff =
|
|
9935
|
+
const diff = execSync14(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
9548
9936
|
cwd: projectRoot,
|
|
9549
9937
|
encoding: "utf-8",
|
|
9550
9938
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9554,7 +9942,7 @@ async function createIssuePR(projectRoot, config, issue) {
|
|
|
9554
9942
|
return;
|
|
9555
9943
|
}
|
|
9556
9944
|
pushSubmoduleBranches(projectRoot);
|
|
9557
|
-
|
|
9945
|
+
execSync14(`git push -u origin ${currentBranch}`, {
|
|
9558
9946
|
cwd: projectRoot,
|
|
9559
9947
|
encoding: "utf-8",
|
|
9560
9948
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9609,9 +9997,9 @@ var init_agent = __esm(() => {
|
|
|
9609
9997
|
});
|
|
9610
9998
|
|
|
9611
9999
|
// src/core/conflict.ts
|
|
9612
|
-
import { execSync as
|
|
10000
|
+
import { execSync as execSync15 } from "node:child_process";
|
|
9613
10001
|
function git3(args, cwd) {
|
|
9614
|
-
return
|
|
10002
|
+
return execSync15(`git ${args}`, {
|
|
9615
10003
|
cwd,
|
|
9616
10004
|
encoding: "utf-8",
|
|
9617
10005
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9740,19 +10128,19 @@ var init_conflict = __esm(() => {
|
|
|
9740
10128
|
|
|
9741
10129
|
// src/core/run-state.ts
|
|
9742
10130
|
import {
|
|
9743
|
-
existsSync as
|
|
9744
|
-
mkdirSync as
|
|
10131
|
+
existsSync as existsSync20,
|
|
10132
|
+
mkdirSync as mkdirSync14,
|
|
9745
10133
|
readFileSync as readFileSync12,
|
|
9746
10134
|
unlinkSync as unlinkSync5,
|
|
9747
|
-
writeFileSync as
|
|
10135
|
+
writeFileSync as writeFileSync9
|
|
9748
10136
|
} from "node:fs";
|
|
9749
|
-
import { dirname as dirname6, join as
|
|
10137
|
+
import { dirname as dirname6, join as join20 } from "node:path";
|
|
9750
10138
|
function getRunStatePath(projectRoot) {
|
|
9751
|
-
return
|
|
10139
|
+
return join20(projectRoot, ".locus", "run-state.json");
|
|
9752
10140
|
}
|
|
9753
10141
|
function loadRunState(projectRoot) {
|
|
9754
10142
|
const path = getRunStatePath(projectRoot);
|
|
9755
|
-
if (!
|
|
10143
|
+
if (!existsSync20(path))
|
|
9756
10144
|
return null;
|
|
9757
10145
|
try {
|
|
9758
10146
|
return JSON.parse(readFileSync12(path, "utf-8"));
|
|
@@ -9764,15 +10152,15 @@ function loadRunState(projectRoot) {
|
|
|
9764
10152
|
function saveRunState(projectRoot, state) {
|
|
9765
10153
|
const path = getRunStatePath(projectRoot);
|
|
9766
10154
|
const dir = dirname6(path);
|
|
9767
|
-
if (!
|
|
9768
|
-
|
|
10155
|
+
if (!existsSync20(dir)) {
|
|
10156
|
+
mkdirSync14(dir, { recursive: true });
|
|
9769
10157
|
}
|
|
9770
|
-
|
|
10158
|
+
writeFileSync9(path, `${JSON.stringify(state, null, 2)}
|
|
9771
10159
|
`, "utf-8");
|
|
9772
10160
|
}
|
|
9773
10161
|
function clearRunState(projectRoot) {
|
|
9774
10162
|
const path = getRunStatePath(projectRoot);
|
|
9775
|
-
if (
|
|
10163
|
+
if (existsSync20(path)) {
|
|
9776
10164
|
unlinkSync5(path);
|
|
9777
10165
|
}
|
|
9778
10166
|
}
|
|
@@ -9912,11 +10300,11 @@ var init_shutdown = __esm(() => {
|
|
|
9912
10300
|
});
|
|
9913
10301
|
|
|
9914
10302
|
// src/core/worktree.ts
|
|
9915
|
-
import { execSync as
|
|
9916
|
-
import { existsSync as
|
|
9917
|
-
import { join as
|
|
10303
|
+
import { execSync as execSync16 } from "node:child_process";
|
|
10304
|
+
import { existsSync as existsSync21, readdirSync as readdirSync7, realpathSync, statSync as statSync4 } from "node:fs";
|
|
10305
|
+
import { join as join21 } from "node:path";
|
|
9918
10306
|
function git4(args, cwd) {
|
|
9919
|
-
return
|
|
10307
|
+
return execSync16(`git ${args}`, {
|
|
9920
10308
|
cwd,
|
|
9921
10309
|
encoding: "utf-8",
|
|
9922
10310
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9930,10 +10318,10 @@ function gitSafe3(args, cwd) {
|
|
|
9930
10318
|
}
|
|
9931
10319
|
}
|
|
9932
10320
|
function getWorktreeDir(projectRoot) {
|
|
9933
|
-
return
|
|
10321
|
+
return join21(projectRoot, ".locus", "worktrees");
|
|
9934
10322
|
}
|
|
9935
10323
|
function getWorktreePath(projectRoot, issueNumber) {
|
|
9936
|
-
return
|
|
10324
|
+
return join21(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
|
|
9937
10325
|
}
|
|
9938
10326
|
function generateBranchName(issueNumber) {
|
|
9939
10327
|
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
@@ -9941,7 +10329,7 @@ function generateBranchName(issueNumber) {
|
|
|
9941
10329
|
}
|
|
9942
10330
|
function getWorktreeBranch(worktreePath) {
|
|
9943
10331
|
try {
|
|
9944
|
-
return
|
|
10332
|
+
return execSync16("git branch --show-current", {
|
|
9945
10333
|
cwd: worktreePath,
|
|
9946
10334
|
encoding: "utf-8",
|
|
9947
10335
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9953,7 +10341,7 @@ function getWorktreeBranch(worktreePath) {
|
|
|
9953
10341
|
function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
9954
10342
|
const log = getLogger();
|
|
9955
10343
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
9956
|
-
if (
|
|
10344
|
+
if (existsSync21(worktreePath)) {
|
|
9957
10345
|
log.verbose(`Worktree already exists for issue #${issueNumber}`);
|
|
9958
10346
|
const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
|
|
9959
10347
|
return {
|
|
@@ -9981,7 +10369,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
|
9981
10369
|
function removeWorktree(projectRoot, issueNumber) {
|
|
9982
10370
|
const log = getLogger();
|
|
9983
10371
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
9984
|
-
if (!
|
|
10372
|
+
if (!existsSync21(worktreePath)) {
|
|
9985
10373
|
log.verbose(`Worktree for issue #${issueNumber} does not exist`);
|
|
9986
10374
|
return;
|
|
9987
10375
|
}
|
|
@@ -10000,7 +10388,7 @@ function removeWorktree(projectRoot, issueNumber) {
|
|
|
10000
10388
|
function listWorktrees(projectRoot) {
|
|
10001
10389
|
const log = getLogger();
|
|
10002
10390
|
const worktreeDir = getWorktreeDir(projectRoot);
|
|
10003
|
-
if (!
|
|
10391
|
+
if (!existsSync21(worktreeDir)) {
|
|
10004
10392
|
return [];
|
|
10005
10393
|
}
|
|
10006
10394
|
const entries = readdirSync7(worktreeDir).filter((entry) => entry.startsWith("issue-"));
|
|
@@ -10020,7 +10408,7 @@ function listWorktrees(projectRoot) {
|
|
|
10020
10408
|
if (!match)
|
|
10021
10409
|
continue;
|
|
10022
10410
|
const issueNumber = Number.parseInt(match[1], 10);
|
|
10023
|
-
const path =
|
|
10411
|
+
const path = join21(worktreeDir, entry);
|
|
10024
10412
|
const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
|
|
10025
10413
|
let resolvedPath;
|
|
10026
10414
|
try {
|
|
@@ -10068,12 +10456,12 @@ var exports_run = {};
|
|
|
10068
10456
|
__export(exports_run, {
|
|
10069
10457
|
runCommand: () => runCommand
|
|
10070
10458
|
});
|
|
10071
|
-
import { execSync as
|
|
10459
|
+
import { execSync as execSync17 } from "node:child_process";
|
|
10072
10460
|
function resolveExecutionContext(config, modelOverride) {
|
|
10073
10461
|
const model = modelOverride ?? config.ai.model;
|
|
10074
10462
|
const provider = inferProviderFromModel(model) ?? config.ai.provider;
|
|
10075
10463
|
const sandboxName = getModelSandboxName(config.sandbox, model, provider);
|
|
10076
|
-
return { provider, model, sandboxName };
|
|
10464
|
+
return { provider, model, sandboxName, containerWorkdir: config.sandbox.containerWorkdir };
|
|
10077
10465
|
}
|
|
10078
10466
|
function printRunHelp() {
|
|
10079
10467
|
process.stderr.write(`
|
|
@@ -10228,7 +10616,7 @@ ${yellow2("⚠")} A sprint run is already in progress.
|
|
|
10228
10616
|
}
|
|
10229
10617
|
if (!flags.dryRun) {
|
|
10230
10618
|
try {
|
|
10231
|
-
|
|
10619
|
+
execSync17(`git checkout -B ${branchName}`, {
|
|
10232
10620
|
cwd: projectRoot,
|
|
10233
10621
|
encoding: "utf-8",
|
|
10234
10622
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -10278,7 +10666,7 @@ ${red2("✗")} Auto-rebase failed. Resolve manually.
|
|
|
10278
10666
|
let sprintContext;
|
|
10279
10667
|
if (i > 0 && !flags.dryRun) {
|
|
10280
10668
|
try {
|
|
10281
|
-
sprintContext =
|
|
10669
|
+
sprintContext = execSync17(`git diff origin/${config.agent.baseBranch}..HEAD`, {
|
|
10282
10670
|
cwd: projectRoot,
|
|
10283
10671
|
encoding: "utf-8",
|
|
10284
10672
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -10299,7 +10687,8 @@ ${progressBar(i, state.tasks.length, { label: "Sprint Progress" })}
|
|
|
10299
10687
|
sprintContext,
|
|
10300
10688
|
skipPR: true,
|
|
10301
10689
|
sandboxed,
|
|
10302
|
-
sandboxName: execution.sandboxName
|
|
10690
|
+
sandboxName: execution.sandboxName,
|
|
10691
|
+
containerWorkdir: execution.containerWorkdir
|
|
10303
10692
|
});
|
|
10304
10693
|
if (result.success) {
|
|
10305
10694
|
if (!flags.dryRun) {
|
|
@@ -10343,7 +10732,7 @@ ${bold2("Summary:")}
|
|
|
10343
10732
|
const prNumber = await createSprintPR(projectRoot, config, sprintName, branchName, completedTasks);
|
|
10344
10733
|
if (prNumber !== undefined) {
|
|
10345
10734
|
try {
|
|
10346
|
-
|
|
10735
|
+
execSync17(`git checkout ${config.agent.baseBranch}`, {
|
|
10347
10736
|
cwd: projectRoot,
|
|
10348
10737
|
encoding: "utf-8",
|
|
10349
10738
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -10376,7 +10765,8 @@ ${bold2("Running sprint issue")} ${cyan2(`#${issueNumber}`)} ${dim2("(sequential
|
|
|
10376
10765
|
model: execution.model,
|
|
10377
10766
|
dryRun: flags.dryRun,
|
|
10378
10767
|
sandboxed,
|
|
10379
|
-
sandboxName: execution.sandboxName
|
|
10768
|
+
sandboxName: execution.sandboxName,
|
|
10769
|
+
containerWorkdir: execution.containerWorkdir
|
|
10380
10770
|
});
|
|
10381
10771
|
return;
|
|
10382
10772
|
}
|
|
@@ -10388,7 +10778,7 @@ ${bold2("Running issue")} ${cyan2(`#${issueNumber}`)} ${dim2(`(branch: ${branchN
|
|
|
10388
10778
|
`);
|
|
10389
10779
|
if (!flags.dryRun) {
|
|
10390
10780
|
try {
|
|
10391
|
-
|
|
10781
|
+
execSync17(`git checkout -B ${branchName} ${config.agent.baseBranch}`, {
|
|
10392
10782
|
cwd: projectRoot,
|
|
10393
10783
|
encoding: "utf-8",
|
|
10394
10784
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -10413,7 +10803,7 @@ ${bold2("Running issue")} ${cyan2(`#${issueNumber}`)} ${dim2(`(branch: ${branchN
|
|
|
10413
10803
|
if (!flags.dryRun) {
|
|
10414
10804
|
if (result.success) {
|
|
10415
10805
|
try {
|
|
10416
|
-
|
|
10806
|
+
execSync17(`git checkout ${config.agent.baseBranch}`, {
|
|
10417
10807
|
cwd: projectRoot,
|
|
10418
10808
|
encoding: "utf-8",
|
|
10419
10809
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -10484,7 +10874,8 @@ ${bold2("Running")} ${cyan2(`${issueNumbers.length} issues`)} ${dim2(`(max ${max
|
|
|
10484
10874
|
model: execution.model,
|
|
10485
10875
|
dryRun: flags.dryRun,
|
|
10486
10876
|
sandboxed,
|
|
10487
|
-
sandboxName: execution.sandboxName
|
|
10877
|
+
sandboxName: execution.sandboxName,
|
|
10878
|
+
containerWorkdir: execution.containerWorkdir
|
|
10488
10879
|
});
|
|
10489
10880
|
if (result.success) {
|
|
10490
10881
|
markTaskDone(state, issueNumber, result.prNumber);
|
|
@@ -10550,13 +10941,13 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}
|
|
|
10550
10941
|
`);
|
|
10551
10942
|
if (state.type === "sprint" && state.branch) {
|
|
10552
10943
|
try {
|
|
10553
|
-
const currentBranch =
|
|
10944
|
+
const currentBranch = execSync17("git rev-parse --abbrev-ref HEAD", {
|
|
10554
10945
|
cwd: projectRoot,
|
|
10555
10946
|
encoding: "utf-8",
|
|
10556
10947
|
stdio: ["pipe", "pipe", "pipe"]
|
|
10557
10948
|
}).trim();
|
|
10558
10949
|
if (currentBranch !== state.branch) {
|
|
10559
|
-
|
|
10950
|
+
execSync17(`git checkout ${state.branch}`, {
|
|
10560
10951
|
cwd: projectRoot,
|
|
10561
10952
|
encoding: "utf-8",
|
|
10562
10953
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -10584,7 +10975,8 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}
|
|
|
10584
10975
|
model: execution.model,
|
|
10585
10976
|
skipPR: isSprintRun,
|
|
10586
10977
|
sandboxed,
|
|
10587
|
-
sandboxName: execution.sandboxName
|
|
10978
|
+
sandboxName: execution.sandboxName,
|
|
10979
|
+
containerWorkdir: execution.containerWorkdir
|
|
10588
10980
|
});
|
|
10589
10981
|
if (result.success) {
|
|
10590
10982
|
if (isSprintRun) {
|
|
@@ -10623,7 +11015,7 @@ ${bold2("Resume complete:")} ${green(`✓ ${finalStats.done}`)} ${finalStats.fai
|
|
|
10623
11015
|
const prNumber = await createSprintPR(projectRoot, config, state.sprint, state.branch, completedTasks);
|
|
10624
11016
|
if (prNumber !== undefined) {
|
|
10625
11017
|
try {
|
|
10626
|
-
|
|
11018
|
+
execSync17(`git checkout ${config.agent.baseBranch}`, {
|
|
10627
11019
|
cwd: projectRoot,
|
|
10628
11020
|
encoding: "utf-8",
|
|
10629
11021
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -10659,14 +11051,14 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
|
10659
11051
|
process.stderr.write(` ${dim2(`Committed submodule changes: ${committedSubmodules.join(", ")}`)}
|
|
10660
11052
|
`);
|
|
10661
11053
|
}
|
|
10662
|
-
const status =
|
|
11054
|
+
const status = execSync17("git status --porcelain", {
|
|
10663
11055
|
cwd: projectRoot,
|
|
10664
11056
|
encoding: "utf-8",
|
|
10665
11057
|
stdio: ["pipe", "pipe", "pipe"]
|
|
10666
11058
|
}).trim();
|
|
10667
11059
|
if (!status)
|
|
10668
11060
|
return;
|
|
10669
|
-
|
|
11061
|
+
execSync17("git add -A", {
|
|
10670
11062
|
cwd: projectRoot,
|
|
10671
11063
|
encoding: "utf-8",
|
|
10672
11064
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -10674,7 +11066,7 @@ function ensureTaskCommit(projectRoot, issueNumber, issueTitle) {
|
|
|
10674
11066
|
const message = `chore: complete #${issueNumber} - ${issueTitle}
|
|
10675
11067
|
|
|
10676
11068
|
Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
10677
|
-
|
|
11069
|
+
execSync17(`git commit -F -`, {
|
|
10678
11070
|
input: message,
|
|
10679
11071
|
cwd: projectRoot,
|
|
10680
11072
|
encoding: "utf-8",
|
|
@@ -10688,7 +11080,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
10688
11080
|
if (!config.agent.autoPR)
|
|
10689
11081
|
return;
|
|
10690
11082
|
try {
|
|
10691
|
-
const diff =
|
|
11083
|
+
const diff = execSync17(`git diff origin/${config.agent.baseBranch}..HEAD --stat`, {
|
|
10692
11084
|
cwd: projectRoot,
|
|
10693
11085
|
encoding: "utf-8",
|
|
10694
11086
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -10699,7 +11091,7 @@ async function createSprintPR(projectRoot, config, sprintName, branchName, tasks
|
|
|
10699
11091
|
return;
|
|
10700
11092
|
}
|
|
10701
11093
|
pushSubmoduleBranches(projectRoot);
|
|
10702
|
-
|
|
11094
|
+
execSync17(`git push -u origin ${branchName}`, {
|
|
10703
11095
|
cwd: projectRoot,
|
|
10704
11096
|
encoding: "utf-8",
|
|
10705
11097
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -10857,14 +11249,14 @@ __export(exports_plan, {
|
|
|
10857
11249
|
parsePlanArgs: () => parsePlanArgs
|
|
10858
11250
|
});
|
|
10859
11251
|
import {
|
|
10860
|
-
existsSync as
|
|
10861
|
-
mkdirSync as
|
|
11252
|
+
existsSync as existsSync22,
|
|
11253
|
+
mkdirSync as mkdirSync15,
|
|
10862
11254
|
readdirSync as readdirSync8,
|
|
10863
11255
|
readFileSync as readFileSync13,
|
|
10864
|
-
writeFileSync as
|
|
11256
|
+
writeFileSync as writeFileSync10
|
|
10865
11257
|
} from "node:fs";
|
|
10866
|
-
import { join as
|
|
10867
|
-
function
|
|
11258
|
+
import { join as join22 } from "node:path";
|
|
11259
|
+
function printHelp2() {
|
|
10868
11260
|
process.stderr.write(`
|
|
10869
11261
|
${bold2("locus plan")} — AI-powered sprint planning
|
|
10870
11262
|
|
|
@@ -10894,12 +11286,12 @@ function normalizeSprintName(name) {
|
|
|
10894
11286
|
return name.trim().toLowerCase();
|
|
10895
11287
|
}
|
|
10896
11288
|
function getPlansDir(projectRoot) {
|
|
10897
|
-
return
|
|
11289
|
+
return join22(projectRoot, ".locus", "plans");
|
|
10898
11290
|
}
|
|
10899
11291
|
function ensurePlansDir(projectRoot) {
|
|
10900
11292
|
const dir = getPlansDir(projectRoot);
|
|
10901
|
-
if (!
|
|
10902
|
-
|
|
11293
|
+
if (!existsSync22(dir)) {
|
|
11294
|
+
mkdirSync15(dir, { recursive: true });
|
|
10903
11295
|
}
|
|
10904
11296
|
return dir;
|
|
10905
11297
|
}
|
|
@@ -10908,14 +11300,14 @@ function generateId() {
|
|
|
10908
11300
|
}
|
|
10909
11301
|
function loadPlanFile(projectRoot, id) {
|
|
10910
11302
|
const dir = getPlansDir(projectRoot);
|
|
10911
|
-
if (!
|
|
11303
|
+
if (!existsSync22(dir))
|
|
10912
11304
|
return null;
|
|
10913
11305
|
const files = readdirSync8(dir).filter((f) => f.endsWith(".json"));
|
|
10914
11306
|
const match = files.find((f) => f.startsWith(id));
|
|
10915
11307
|
if (!match)
|
|
10916
11308
|
return null;
|
|
10917
11309
|
try {
|
|
10918
|
-
const content = readFileSync13(
|
|
11310
|
+
const content = readFileSync13(join22(dir, match), "utf-8");
|
|
10919
11311
|
return JSON.parse(content);
|
|
10920
11312
|
} catch {
|
|
10921
11313
|
return null;
|
|
@@ -10923,7 +11315,7 @@ function loadPlanFile(projectRoot, id) {
|
|
|
10923
11315
|
}
|
|
10924
11316
|
async function planCommand(projectRoot, args, flags = {}) {
|
|
10925
11317
|
if (args[0] === "help" || args.length === 0) {
|
|
10926
|
-
|
|
11318
|
+
printHelp2();
|
|
10927
11319
|
return;
|
|
10928
11320
|
}
|
|
10929
11321
|
if (args[0] === "list") {
|
|
@@ -10961,7 +11353,7 @@ async function planCommand(projectRoot, args, flags = {}) {
|
|
|
10961
11353
|
}
|
|
10962
11354
|
function handleListPlans(projectRoot) {
|
|
10963
11355
|
const dir = getPlansDir(projectRoot);
|
|
10964
|
-
if (!
|
|
11356
|
+
if (!existsSync22(dir)) {
|
|
10965
11357
|
process.stderr.write(`${dim2("No saved plans yet.")}
|
|
10966
11358
|
`);
|
|
10967
11359
|
return;
|
|
@@ -10979,7 +11371,7 @@ ${bold2("Saved Plans:")}
|
|
|
10979
11371
|
for (const file of files) {
|
|
10980
11372
|
const id = file.replace(".json", "");
|
|
10981
11373
|
try {
|
|
10982
|
-
const content = readFileSync13(
|
|
11374
|
+
const content = readFileSync13(join22(dir, file), "utf-8");
|
|
10983
11375
|
const plan = JSON.parse(content);
|
|
10984
11376
|
const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
|
|
10985
11377
|
const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
|
|
@@ -11090,7 +11482,7 @@ ${bold2("Approving plan:")}
|
|
|
11090
11482
|
async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
|
|
11091
11483
|
const id = generateId();
|
|
11092
11484
|
const plansDir = ensurePlansDir(projectRoot);
|
|
11093
|
-
const planPath =
|
|
11485
|
+
const planPath = join22(plansDir, `${id}.json`);
|
|
11094
11486
|
const planPathRelative = `.locus/plans/${id}.json`;
|
|
11095
11487
|
const displayDirective = directive;
|
|
11096
11488
|
process.stderr.write(`
|
|
@@ -11118,7 +11510,8 @@ ${bold2("Planning:")} ${cyan2(displayDirective)}
|
|
|
11118
11510
|
cwd: projectRoot,
|
|
11119
11511
|
activity: "planning",
|
|
11120
11512
|
sandboxed: config.sandbox.enabled,
|
|
11121
|
-
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
|
|
11513
|
+
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider),
|
|
11514
|
+
containerWorkdir: config.sandbox.containerWorkdir
|
|
11122
11515
|
});
|
|
11123
11516
|
if (aiResult.interrupted) {
|
|
11124
11517
|
process.stderr.write(`
|
|
@@ -11132,7 +11525,7 @@ ${red2("✗")} Planning failed: ${aiResult.error}
|
|
|
11132
11525
|
`);
|
|
11133
11526
|
return;
|
|
11134
11527
|
}
|
|
11135
|
-
if (!
|
|
11528
|
+
if (!existsSync22(planPath)) {
|
|
11136
11529
|
process.stderr.write(`
|
|
11137
11530
|
${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
|
|
11138
11531
|
`);
|
|
@@ -11164,7 +11557,7 @@ ${yellow2("⚠")} Plan file has no issues.
|
|
|
11164
11557
|
plan.sprint = sprintName;
|
|
11165
11558
|
if (!plan.createdAt)
|
|
11166
11559
|
plan.createdAt = new Date().toISOString();
|
|
11167
|
-
|
|
11560
|
+
writeFileSync10(planPath, JSON.stringify(plan, null, 2), "utf-8");
|
|
11168
11561
|
process.stderr.write(`
|
|
11169
11562
|
${bold2("Plan saved:")} ${cyan2(id)}
|
|
11170
11563
|
|
|
@@ -11241,7 +11634,8 @@ Start with foundational/setup tasks, then core features, then integration/testin
|
|
|
11241
11634
|
activity: "issue ordering",
|
|
11242
11635
|
silent: true,
|
|
11243
11636
|
sandboxed: config.sandbox.enabled,
|
|
11244
|
-
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
|
|
11637
|
+
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider),
|
|
11638
|
+
containerWorkdir: config.sandbox.containerWorkdir
|
|
11245
11639
|
});
|
|
11246
11640
|
if (aiResult.interrupted) {
|
|
11247
11641
|
process.stderr.write(`
|
|
@@ -11313,15 +11707,15 @@ ${directive}${sprintName ? `
|
|
|
11313
11707
|
|
|
11314
11708
|
**Sprint:** ${sprintName}` : ""}
|
|
11315
11709
|
</directive>`);
|
|
11316
|
-
const locusPath =
|
|
11317
|
-
if (
|
|
11710
|
+
const locusPath = join22(projectRoot, ".locus", "LOCUS.md");
|
|
11711
|
+
if (existsSync22(locusPath)) {
|
|
11318
11712
|
const content = readFileSync13(locusPath, "utf-8");
|
|
11319
11713
|
parts.push(`<project-context>
|
|
11320
11714
|
${content.slice(0, 3000)}
|
|
11321
11715
|
</project-context>`);
|
|
11322
11716
|
}
|
|
11323
|
-
const learningsPath =
|
|
11324
|
-
if (
|
|
11717
|
+
const learningsPath = join22(projectRoot, ".locus", "LEARNINGS.md");
|
|
11718
|
+
if (existsSync22(learningsPath)) {
|
|
11325
11719
|
const content = readFileSync13(learningsPath, "utf-8");
|
|
11326
11720
|
parts.push(`<past-learnings>
|
|
11327
11721
|
${content.slice(0, 2000)}
|
|
@@ -11501,10 +11895,10 @@ var exports_review = {};
|
|
|
11501
11895
|
__export(exports_review, {
|
|
11502
11896
|
reviewCommand: () => reviewCommand
|
|
11503
11897
|
});
|
|
11504
|
-
import { execFileSync as execFileSync2, execSync as
|
|
11505
|
-
import { existsSync as
|
|
11506
|
-
import { join as
|
|
11507
|
-
function
|
|
11898
|
+
import { execFileSync as execFileSync2, execSync as execSync18 } from "node:child_process";
|
|
11899
|
+
import { existsSync as existsSync23, readFileSync as readFileSync14 } from "node:fs";
|
|
11900
|
+
import { join as join23 } from "node:path";
|
|
11901
|
+
function printHelp3() {
|
|
11508
11902
|
process.stderr.write(`
|
|
11509
11903
|
${bold2("locus review")} — AI-powered code review
|
|
11510
11904
|
|
|
@@ -11527,7 +11921,7 @@ ${bold2("Examples:")}
|
|
|
11527
11921
|
}
|
|
11528
11922
|
async function reviewCommand(projectRoot, args, flags = {}) {
|
|
11529
11923
|
if (args[0] === "help") {
|
|
11530
|
-
|
|
11924
|
+
printHelp3();
|
|
11531
11925
|
return;
|
|
11532
11926
|
}
|
|
11533
11927
|
const config = loadConfig(projectRoot);
|
|
@@ -11587,7 +11981,7 @@ ${bold2("Review complete:")} ${green(`✓ ${reviewed}`)}${failed > 0 ? ` ${red2(
|
|
|
11587
11981
|
async function reviewSinglePR(projectRoot, config, prNumber, focus, flags) {
|
|
11588
11982
|
let prInfo;
|
|
11589
11983
|
try {
|
|
11590
|
-
const result =
|
|
11984
|
+
const result = execSync18(`gh pr view ${prNumber} --json number,title,body,state,headRefName,baseRefName,labels,url,createdAt`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
11591
11985
|
const raw = JSON.parse(result);
|
|
11592
11986
|
prInfo = {
|
|
11593
11987
|
number: raw.number,
|
|
@@ -11632,7 +12026,8 @@ async function reviewPR(projectRoot, config, pr, focus, flags) {
|
|
|
11632
12026
|
cwd: projectRoot,
|
|
11633
12027
|
activity: `PR #${pr.number}`,
|
|
11634
12028
|
sandboxed: config.sandbox.enabled,
|
|
11635
|
-
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
|
|
12029
|
+
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider),
|
|
12030
|
+
containerWorkdir: config.sandbox.containerWorkdir
|
|
11636
12031
|
});
|
|
11637
12032
|
if (aiResult.interrupted) {
|
|
11638
12033
|
process.stderr.write(` ${yellow2("⚡")} Review interrupted.
|
|
@@ -11671,8 +12066,8 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
|
|
|
11671
12066
|
parts.push(`<role>
|
|
11672
12067
|
You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.
|
|
11673
12068
|
</role>`);
|
|
11674
|
-
const locusPath =
|
|
11675
|
-
if (
|
|
12069
|
+
const locusPath = join23(projectRoot, ".locus", "LOCUS.md");
|
|
12070
|
+
if (existsSync23(locusPath)) {
|
|
11676
12071
|
const content = readFileSync14(locusPath, "utf-8");
|
|
11677
12072
|
parts.push(`<project-context>
|
|
11678
12073
|
${content.slice(0, 2000)}
|
|
@@ -11734,8 +12129,8 @@ var exports_iterate = {};
|
|
|
11734
12129
|
__export(exports_iterate, {
|
|
11735
12130
|
iterateCommand: () => iterateCommand
|
|
11736
12131
|
});
|
|
11737
|
-
import { execSync as
|
|
11738
|
-
function
|
|
12132
|
+
import { execSync as execSync19 } from "node:child_process";
|
|
12133
|
+
function printHelp4() {
|
|
11739
12134
|
process.stderr.write(`
|
|
11740
12135
|
${bold2("locus iterate")} — Re-execute tasks with PR feedback
|
|
11741
12136
|
|
|
@@ -11760,7 +12155,7 @@ ${bold2("Examples:")}
|
|
|
11760
12155
|
}
|
|
11761
12156
|
async function iterateCommand(projectRoot, args, flags = {}) {
|
|
11762
12157
|
if (args[0] === "help") {
|
|
11763
|
-
|
|
12158
|
+
printHelp4();
|
|
11764
12159
|
return;
|
|
11765
12160
|
}
|
|
11766
12161
|
const config = loadConfig(projectRoot);
|
|
@@ -11952,12 +12347,12 @@ ${bold2("Summary:")} ${green(`✓ ${succeeded}`)}${failed > 0 ? ` ${red2(`✗ ${
|
|
|
11952
12347
|
}
|
|
11953
12348
|
function findPRForIssue(projectRoot, issueNumber) {
|
|
11954
12349
|
try {
|
|
11955
|
-
const result =
|
|
12350
|
+
const result = execSync19(`gh pr list --search "Closes #${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
11956
12351
|
const parsed = JSON.parse(result);
|
|
11957
12352
|
if (parsed.length > 0) {
|
|
11958
12353
|
return parsed[0].number;
|
|
11959
12354
|
}
|
|
11960
|
-
const branchResult =
|
|
12355
|
+
const branchResult = execSync19(`gh pr list --head "locus/issue-${issueNumber}" --json number --state open`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
11961
12356
|
const branchParsed = JSON.parse(branchResult);
|
|
11962
12357
|
if (branchParsed.length > 0) {
|
|
11963
12358
|
return branchParsed[0].number;
|
|
@@ -11993,15 +12388,15 @@ __export(exports_discuss, {
|
|
|
11993
12388
|
discussCommand: () => discussCommand
|
|
11994
12389
|
});
|
|
11995
12390
|
import {
|
|
11996
|
-
existsSync as
|
|
11997
|
-
mkdirSync as
|
|
12391
|
+
existsSync as existsSync24,
|
|
12392
|
+
mkdirSync as mkdirSync16,
|
|
11998
12393
|
readdirSync as readdirSync9,
|
|
11999
12394
|
readFileSync as readFileSync15,
|
|
12000
12395
|
unlinkSync as unlinkSync6,
|
|
12001
|
-
writeFileSync as
|
|
12396
|
+
writeFileSync as writeFileSync11
|
|
12002
12397
|
} from "node:fs";
|
|
12003
|
-
import { join as
|
|
12004
|
-
function
|
|
12398
|
+
import { join as join24 } from "node:path";
|
|
12399
|
+
function printHelp5() {
|
|
12005
12400
|
process.stderr.write(`
|
|
12006
12401
|
${bold2("locus discuss")} — AI-powered architectural discussions
|
|
12007
12402
|
|
|
@@ -12022,12 +12417,12 @@ ${bold2("Examples:")}
|
|
|
12022
12417
|
`);
|
|
12023
12418
|
}
|
|
12024
12419
|
function getDiscussionsDir(projectRoot) {
|
|
12025
|
-
return
|
|
12420
|
+
return join24(projectRoot, ".locus", "discussions");
|
|
12026
12421
|
}
|
|
12027
12422
|
function ensureDiscussionsDir(projectRoot) {
|
|
12028
12423
|
const dir = getDiscussionsDir(projectRoot);
|
|
12029
|
-
if (!
|
|
12030
|
-
|
|
12424
|
+
if (!existsSync24(dir)) {
|
|
12425
|
+
mkdirSync16(dir, { recursive: true });
|
|
12031
12426
|
}
|
|
12032
12427
|
return dir;
|
|
12033
12428
|
}
|
|
@@ -12036,7 +12431,7 @@ function generateId2() {
|
|
|
12036
12431
|
}
|
|
12037
12432
|
async function discussCommand(projectRoot, args, flags = {}) {
|
|
12038
12433
|
if (args[0] === "help") {
|
|
12039
|
-
|
|
12434
|
+
printHelp5();
|
|
12040
12435
|
return;
|
|
12041
12436
|
}
|
|
12042
12437
|
const subcommand = args[0];
|
|
@@ -12053,7 +12448,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
|
|
|
12053
12448
|
return deleteDiscussion(projectRoot, args[1]);
|
|
12054
12449
|
}
|
|
12055
12450
|
if (args.length === 0) {
|
|
12056
|
-
|
|
12451
|
+
printHelp5();
|
|
12057
12452
|
return;
|
|
12058
12453
|
}
|
|
12059
12454
|
const topic = args.join(" ").trim();
|
|
@@ -12061,7 +12456,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
|
|
|
12061
12456
|
}
|
|
12062
12457
|
function listDiscussions(projectRoot) {
|
|
12063
12458
|
const dir = getDiscussionsDir(projectRoot);
|
|
12064
|
-
if (!
|
|
12459
|
+
if (!existsSync24(dir)) {
|
|
12065
12460
|
process.stderr.write(`${dim2("No discussions yet.")}
|
|
12066
12461
|
`);
|
|
12067
12462
|
return;
|
|
@@ -12078,7 +12473,7 @@ ${bold2("Discussions:")}
|
|
|
12078
12473
|
`);
|
|
12079
12474
|
for (const file of files) {
|
|
12080
12475
|
const id = file.replace(".md", "");
|
|
12081
|
-
const content = readFileSync15(
|
|
12476
|
+
const content = readFileSync15(join24(dir, file), "utf-8");
|
|
12082
12477
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
12083
12478
|
const title = titleMatch ? titleMatch[1] : id;
|
|
12084
12479
|
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
|
|
@@ -12096,7 +12491,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
12096
12491
|
return;
|
|
12097
12492
|
}
|
|
12098
12493
|
const dir = getDiscussionsDir(projectRoot);
|
|
12099
|
-
if (!
|
|
12494
|
+
if (!existsSync24(dir)) {
|
|
12100
12495
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
12101
12496
|
`);
|
|
12102
12497
|
return;
|
|
@@ -12108,7 +12503,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
12108
12503
|
`);
|
|
12109
12504
|
return;
|
|
12110
12505
|
}
|
|
12111
|
-
const content = readFileSync15(
|
|
12506
|
+
const content = readFileSync15(join24(dir, match), "utf-8");
|
|
12112
12507
|
process.stdout.write(`${content}
|
|
12113
12508
|
`);
|
|
12114
12509
|
}
|
|
@@ -12119,7 +12514,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
12119
12514
|
return;
|
|
12120
12515
|
}
|
|
12121
12516
|
const dir = getDiscussionsDir(projectRoot);
|
|
12122
|
-
if (!
|
|
12517
|
+
if (!existsSync24(dir)) {
|
|
12123
12518
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
12124
12519
|
`);
|
|
12125
12520
|
return;
|
|
@@ -12131,7 +12526,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
12131
12526
|
`);
|
|
12132
12527
|
return;
|
|
12133
12528
|
}
|
|
12134
|
-
unlinkSync6(
|
|
12529
|
+
unlinkSync6(join24(dir, match));
|
|
12135
12530
|
process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
|
|
12136
12531
|
`);
|
|
12137
12532
|
}
|
|
@@ -12144,7 +12539,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
12144
12539
|
return;
|
|
12145
12540
|
}
|
|
12146
12541
|
const dir = getDiscussionsDir(projectRoot);
|
|
12147
|
-
if (!
|
|
12542
|
+
if (!existsSync24(dir)) {
|
|
12148
12543
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
12149
12544
|
`);
|
|
12150
12545
|
return;
|
|
@@ -12156,7 +12551,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
12156
12551
|
`);
|
|
12157
12552
|
return;
|
|
12158
12553
|
}
|
|
12159
|
-
const content = readFileSync15(
|
|
12554
|
+
const content = readFileSync15(join24(dir, match), "utf-8");
|
|
12160
12555
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
12161
12556
|
const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
|
|
12162
12557
|
await planCommand(projectRoot, [
|
|
@@ -12214,7 +12609,8 @@ ${bold2("Discussion:")} ${cyan2(topic)}
|
|
|
12214
12609
|
cwd: projectRoot,
|
|
12215
12610
|
activity: "discussion",
|
|
12216
12611
|
sandboxed: config.sandbox.enabled,
|
|
12217
|
-
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider)
|
|
12612
|
+
sandboxName: getModelSandboxName(config.sandbox, flags.model ?? config.ai.model, config.ai.provider),
|
|
12613
|
+
containerWorkdir: config.sandbox.containerWorkdir
|
|
12218
12614
|
});
|
|
12219
12615
|
if (aiResult.interrupted) {
|
|
12220
12616
|
process.stderr.write(`
|
|
@@ -12281,7 +12677,7 @@ ${turn.content}`;
|
|
|
12281
12677
|
...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
|
|
12282
12678
|
].join(`
|
|
12283
12679
|
`);
|
|
12284
|
-
|
|
12680
|
+
writeFileSync11(join24(dir, `${id}.md`), markdown, "utf-8");
|
|
12285
12681
|
process.stderr.write(`
|
|
12286
12682
|
${green("✓")} Discussion saved: ${cyan2(id)} ${dim2(`(${timer.formatted()})`)}
|
|
12287
12683
|
`);
|
|
@@ -12296,15 +12692,15 @@ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFi
|
|
|
12296
12692
|
parts.push(`<role>
|
|
12297
12693
|
You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.
|
|
12298
12694
|
</role>`);
|
|
12299
|
-
const locusPath =
|
|
12300
|
-
if (
|
|
12695
|
+
const locusPath = join24(projectRoot, ".locus", "LOCUS.md");
|
|
12696
|
+
if (existsSync24(locusPath)) {
|
|
12301
12697
|
const content = readFileSync15(locusPath, "utf-8");
|
|
12302
12698
|
parts.push(`<project-context>
|
|
12303
12699
|
${content.slice(0, 3000)}
|
|
12304
12700
|
</project-context>`);
|
|
12305
12701
|
}
|
|
12306
|
-
const learningsPath =
|
|
12307
|
-
if (
|
|
12702
|
+
const learningsPath = join24(projectRoot, ".locus", "LEARNINGS.md");
|
|
12703
|
+
if (existsSync24(learningsPath)) {
|
|
12308
12704
|
const content = readFileSync15(learningsPath, "utf-8");
|
|
12309
12705
|
parts.push(`<past-learnings>
|
|
12310
12706
|
${content.slice(0, 2000)}
|
|
@@ -12376,9 +12772,9 @@ __export(exports_artifacts, {
|
|
|
12376
12772
|
formatDate: () => formatDate2,
|
|
12377
12773
|
artifactsCommand: () => artifactsCommand
|
|
12378
12774
|
});
|
|
12379
|
-
import { existsSync as
|
|
12380
|
-
import { join as
|
|
12381
|
-
function
|
|
12775
|
+
import { existsSync as existsSync25, readdirSync as readdirSync10, readFileSync as readFileSync16, statSync as statSync5 } from "node:fs";
|
|
12776
|
+
import { join as join25 } from "node:path";
|
|
12777
|
+
function printHelp6() {
|
|
12382
12778
|
process.stderr.write(`
|
|
12383
12779
|
${bold2("locus artifacts")} — View and manage AI-generated artifacts
|
|
12384
12780
|
|
|
@@ -12397,14 +12793,14 @@ ${dim2("Artifact names support partial matching.")}
|
|
|
12397
12793
|
`);
|
|
12398
12794
|
}
|
|
12399
12795
|
function getArtifactsDir(projectRoot) {
|
|
12400
|
-
return
|
|
12796
|
+
return join25(projectRoot, ".locus", "artifacts");
|
|
12401
12797
|
}
|
|
12402
12798
|
function listArtifacts(projectRoot) {
|
|
12403
12799
|
const dir = getArtifactsDir(projectRoot);
|
|
12404
|
-
if (!
|
|
12800
|
+
if (!existsSync25(dir))
|
|
12405
12801
|
return [];
|
|
12406
12802
|
return readdirSync10(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
|
|
12407
|
-
const filePath =
|
|
12803
|
+
const filePath = join25(dir, fileName);
|
|
12408
12804
|
const stat = statSync5(filePath);
|
|
12409
12805
|
return {
|
|
12410
12806
|
name: fileName.replace(/\.md$/, ""),
|
|
@@ -12417,8 +12813,8 @@ function listArtifacts(projectRoot) {
|
|
|
12417
12813
|
function readArtifact(projectRoot, name) {
|
|
12418
12814
|
const dir = getArtifactsDir(projectRoot);
|
|
12419
12815
|
const fileName = name.endsWith(".md") ? name : `${name}.md`;
|
|
12420
|
-
const filePath =
|
|
12421
|
-
if (!
|
|
12816
|
+
const filePath = join25(dir, fileName);
|
|
12817
|
+
if (!existsSync25(filePath))
|
|
12422
12818
|
return null;
|
|
12423
12819
|
const stat = statSync5(filePath);
|
|
12424
12820
|
return {
|
|
@@ -12450,7 +12846,7 @@ function formatDate2(date) {
|
|
|
12450
12846
|
}
|
|
12451
12847
|
async function artifactsCommand(projectRoot, args) {
|
|
12452
12848
|
if (args[0] === "help") {
|
|
12453
|
-
|
|
12849
|
+
printHelp6();
|
|
12454
12850
|
return;
|
|
12455
12851
|
}
|
|
12456
12852
|
const subcommand = args[0];
|
|
@@ -12587,10 +12983,10 @@ __export(exports_sandbox2, {
|
|
|
12587
12983
|
parseSandboxLogsArgs: () => parseSandboxLogsArgs,
|
|
12588
12984
|
parseSandboxInstallArgs: () => parseSandboxInstallArgs
|
|
12589
12985
|
});
|
|
12590
|
-
import { execSync as
|
|
12986
|
+
import { execSync as execSync20, spawn as spawn7 } from "node:child_process";
|
|
12591
12987
|
import { createHash } from "node:crypto";
|
|
12592
|
-
import { existsSync as
|
|
12593
|
-
import { basename as basename4, join as
|
|
12988
|
+
import { existsSync as existsSync26, readFileSync as readFileSync17 } from "node:fs";
|
|
12989
|
+
import { basename as basename4, join as join26 } from "node:path";
|
|
12594
12990
|
import { createInterface as createInterface3 } from "node:readline";
|
|
12595
12991
|
function printSandboxHelp() {
|
|
12596
12992
|
process.stderr.write(`
|
|
@@ -12721,11 +13117,20 @@ async function handleCreate(projectRoot) {
|
|
|
12721
13117
|
}
|
|
12722
13118
|
process.stderr.write(`${green("✓")} ${provider} sandbox created: ${bold2(name)}
|
|
12723
13119
|
`);
|
|
13120
|
+
if (!config.sandbox.containerWorkdir) {
|
|
13121
|
+
const containerWorkdir = detectContainerWorkdir(name, projectRoot);
|
|
13122
|
+
if (containerWorkdir) {
|
|
13123
|
+
config.sandbox.containerWorkdir = containerWorkdir;
|
|
13124
|
+
process.stderr.write(` ${dim2(`Container workdir: ${containerWorkdir}`)}
|
|
13125
|
+
`);
|
|
13126
|
+
}
|
|
13127
|
+
}
|
|
12724
13128
|
readySandboxes[provider] = name;
|
|
12725
13129
|
config.sandbox.enabled = true;
|
|
12726
13130
|
config.sandbox.providers = readySandboxes;
|
|
12727
13131
|
saveConfig(projectRoot, config);
|
|
12728
|
-
|
|
13132
|
+
const workdir = config.sandbox.containerWorkdir ?? projectRoot;
|
|
13133
|
+
await runSandboxSetup(name, projectRoot, workdir);
|
|
12729
13134
|
process.stderr.write(`
|
|
12730
13135
|
${green("✓")} Sandbox mode enabled for ${bold2(provider)}.
|
|
12731
13136
|
`);
|
|
@@ -12750,19 +13155,20 @@ async function handleAgentLogin(projectRoot, agent) {
|
|
|
12750
13155
|
if (agent === "codex") {
|
|
12751
13156
|
await ensureCodexInSandbox(sandboxName);
|
|
12752
13157
|
}
|
|
13158
|
+
const workdir = config.sandbox.containerWorkdir ?? projectRoot;
|
|
12753
13159
|
process.stderr.write(`Connecting to ${agent} sandbox ${dim2(sandboxName)}...
|
|
12754
13160
|
`);
|
|
12755
13161
|
process.stderr.write(`${dim2("Login and then exit when ready.")}
|
|
12756
13162
|
|
|
12757
13163
|
`);
|
|
12758
|
-
const child = spawn7("docker", ["sandbox", "exec", "-it", "-w",
|
|
13164
|
+
const child = spawn7("docker", ["sandbox", "exec", "-it", "-w", workdir, sandboxName, agent], {
|
|
12759
13165
|
stdio: "inherit"
|
|
12760
13166
|
});
|
|
12761
13167
|
await new Promise((resolve2) => {
|
|
12762
13168
|
child.on("close", async (code) => {
|
|
12763
13169
|
const backup = backupIgnoredFiles(projectRoot);
|
|
12764
13170
|
try {
|
|
12765
|
-
await enforceSandboxIgnore(sandboxName, projectRoot);
|
|
13171
|
+
await enforceSandboxIgnore(sandboxName, projectRoot, config.sandbox.containerWorkdir);
|
|
12766
13172
|
} finally {
|
|
12767
13173
|
backup.restore();
|
|
12768
13174
|
}
|
|
@@ -12799,7 +13205,7 @@ function handleRemove(projectRoot) {
|
|
|
12799
13205
|
process.stderr.write(`Removing sandbox ${bold2(sandboxName)}...
|
|
12800
13206
|
`);
|
|
12801
13207
|
try {
|
|
12802
|
-
|
|
13208
|
+
execSync20(`docker sandbox rm ${sandboxName}`, {
|
|
12803
13209
|
encoding: "utf-8",
|
|
12804
13210
|
stdio: ["pipe", "pipe", "pipe"],
|
|
12805
13211
|
timeout: 15000
|
|
@@ -12808,6 +13214,7 @@ function handleRemove(projectRoot) {
|
|
|
12808
13214
|
}
|
|
12809
13215
|
config.sandbox.providers = {};
|
|
12810
13216
|
config.sandbox.enabled = false;
|
|
13217
|
+
delete config.sandbox.containerWorkdir;
|
|
12811
13218
|
saveConfig(projectRoot, config);
|
|
12812
13219
|
process.stderr.write(`${green("✓")} Provider sandboxes removed. Sandbox mode disabled.
|
|
12813
13220
|
`);
|
|
@@ -12820,6 +13227,10 @@ ${bold2("Sandbox Status")}
|
|
|
12820
13227
|
`);
|
|
12821
13228
|
process.stderr.write(` ${dim2("Enabled:")} ${config.sandbox.enabled ? green("yes") : red2("no")}
|
|
12822
13229
|
`);
|
|
13230
|
+
if (config.sandbox.containerWorkdir) {
|
|
13231
|
+
process.stderr.write(` ${dim2("Container workdir:")} ${config.sandbox.containerWorkdir}
|
|
13232
|
+
`);
|
|
13233
|
+
}
|
|
12823
13234
|
for (const provider of PROVIDERS) {
|
|
12824
13235
|
const name = config.sandbox.providers[provider];
|
|
12825
13236
|
process.stderr.write(` ${dim2(`${provider}:`).padEnd(15)}${name ? bold2(name) : dim2("(not configured)")}
|
|
@@ -12956,10 +13367,12 @@ async function handleShell(projectRoot, args) {
|
|
|
12956
13367
|
`);
|
|
12957
13368
|
return;
|
|
12958
13369
|
}
|
|
13370
|
+
const config = loadConfig(projectRoot);
|
|
12959
13371
|
const sandboxName = getActiveProviderSandbox(projectRoot, provider);
|
|
12960
13372
|
if (!sandboxName) {
|
|
12961
13373
|
return;
|
|
12962
13374
|
}
|
|
13375
|
+
const workdir = config.sandbox.containerWorkdir ?? projectRoot;
|
|
12963
13376
|
process.stderr.write(`Opening shell in ${provider} sandbox ${dim2(sandboxName)}...
|
|
12964
13377
|
`);
|
|
12965
13378
|
await runInteractiveCommand("docker", [
|
|
@@ -12967,7 +13380,7 @@ async function handleShell(projectRoot, args) {
|
|
|
12967
13380
|
"exec",
|
|
12968
13381
|
"-it",
|
|
12969
13382
|
"-w",
|
|
12970
|
-
|
|
13383
|
+
workdir,
|
|
12971
13384
|
sandboxName,
|
|
12972
13385
|
"sh"
|
|
12973
13386
|
]);
|
|
@@ -13048,7 +13461,7 @@ async function handleLogs(projectRoot, args) {
|
|
|
13048
13461
|
}
|
|
13049
13462
|
function detectPackageManager2(projectRoot) {
|
|
13050
13463
|
try {
|
|
13051
|
-
const raw = readFileSync17(
|
|
13464
|
+
const raw = readFileSync17(join26(projectRoot, "package.json"), "utf-8");
|
|
13052
13465
|
const pkgJson = JSON.parse(raw);
|
|
13053
13466
|
if (typeof pkgJson.packageManager === "string") {
|
|
13054
13467
|
const name = pkgJson.packageManager.split("@")[0];
|
|
@@ -13057,13 +13470,13 @@ function detectPackageManager2(projectRoot) {
|
|
|
13057
13470
|
}
|
|
13058
13471
|
}
|
|
13059
13472
|
} catch {}
|
|
13060
|
-
if (
|
|
13473
|
+
if (existsSync26(join26(projectRoot, "bun.lock")) || existsSync26(join26(projectRoot, "bun.lockb"))) {
|
|
13061
13474
|
return "bun";
|
|
13062
13475
|
}
|
|
13063
|
-
if (
|
|
13476
|
+
if (existsSync26(join26(projectRoot, "yarn.lock"))) {
|
|
13064
13477
|
return "yarn";
|
|
13065
13478
|
}
|
|
13066
|
-
if (
|
|
13479
|
+
if (existsSync26(join26(projectRoot, "pnpm-lock.yaml"))) {
|
|
13067
13480
|
return "pnpm";
|
|
13068
13481
|
}
|
|
13069
13482
|
return "npm";
|
|
@@ -13080,7 +13493,8 @@ function getInstallCommand(pm) {
|
|
|
13080
13493
|
return ["npm", "install"];
|
|
13081
13494
|
}
|
|
13082
13495
|
}
|
|
13083
|
-
async function runSandboxSetup(sandboxName, projectRoot) {
|
|
13496
|
+
async function runSandboxSetup(sandboxName, projectRoot, containerWorkdir) {
|
|
13497
|
+
const workdir = containerWorkdir ?? projectRoot;
|
|
13084
13498
|
const ecosystem = detectProjectEcosystem(projectRoot);
|
|
13085
13499
|
const isJS = isJavaScriptEcosystem(ecosystem);
|
|
13086
13500
|
if (isJS) {
|
|
@@ -13096,7 +13510,7 @@ Installing dependencies (${bold2(installCmd.join(" "))}) in sandbox ${dim2(sandb
|
|
|
13096
13510
|
"sandbox",
|
|
13097
13511
|
"exec",
|
|
13098
13512
|
"-w",
|
|
13099
|
-
|
|
13513
|
+
workdir,
|
|
13100
13514
|
sandboxName,
|
|
13101
13515
|
...installCmd
|
|
13102
13516
|
]);
|
|
@@ -13112,18 +13526,19 @@ Installing dependencies (${bold2(installCmd.join(" "))}) in sandbox ${dim2(sandb
|
|
|
13112
13526
|
${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
|
|
13113
13527
|
`);
|
|
13114
13528
|
}
|
|
13115
|
-
const setupScript =
|
|
13116
|
-
|
|
13529
|
+
const setupScript = join26(projectRoot, ".locus", "sandbox-setup.sh");
|
|
13530
|
+
const containerSetupScript = containerWorkdir ? join26(containerWorkdir, ".locus", "sandbox-setup.sh") : setupScript;
|
|
13531
|
+
if (existsSync26(setupScript)) {
|
|
13117
13532
|
process.stderr.write(`Running ${bold2(".locus/sandbox-setup.sh")} in sandbox ${dim2(sandboxName)}...
|
|
13118
13533
|
`);
|
|
13119
13534
|
const hookOk = await runInteractiveCommand("docker", [
|
|
13120
13535
|
"sandbox",
|
|
13121
13536
|
"exec",
|
|
13122
13537
|
"-w",
|
|
13123
|
-
|
|
13538
|
+
workdir,
|
|
13124
13539
|
sandboxName,
|
|
13125
13540
|
"sh",
|
|
13126
|
-
|
|
13541
|
+
containerSetupScript
|
|
13127
13542
|
]);
|
|
13128
13543
|
if (!hookOk) {
|
|
13129
13544
|
process.stderr.write(`${yellow2("⚠")} Setup hook failed in sandbox ${dim2(sandboxName)}.
|
|
@@ -13154,7 +13569,7 @@ async function handleSetup(projectRoot) {
|
|
|
13154
13569
|
`);
|
|
13155
13570
|
continue;
|
|
13156
13571
|
}
|
|
13157
|
-
await runSandboxSetup(sandboxName, projectRoot);
|
|
13572
|
+
await runSandboxSetup(sandboxName, projectRoot, config.sandbox.containerWorkdir);
|
|
13158
13573
|
}
|
|
13159
13574
|
}
|
|
13160
13575
|
function buildProviderSandboxNames(projectRoot) {
|
|
@@ -13199,9 +13614,9 @@ function runInteractiveCommand(command, args) {
|
|
|
13199
13614
|
child.on("error", () => resolve2(false));
|
|
13200
13615
|
});
|
|
13201
13616
|
}
|
|
13202
|
-
async function createProviderSandbox(provider, sandboxName, projectRoot) {
|
|
13617
|
+
async function createProviderSandbox(provider, sandboxName, projectRoot, containerWorkdir) {
|
|
13203
13618
|
try {
|
|
13204
|
-
|
|
13619
|
+
execSync20(`docker sandbox create --name ${sandboxName} claude ${projectRoot}`, {
|
|
13205
13620
|
stdio: ["pipe", "pipe", "pipe"],
|
|
13206
13621
|
timeout: 120000
|
|
13207
13622
|
});
|
|
@@ -13214,7 +13629,7 @@ async function createProviderSandbox(provider, sandboxName, projectRoot) {
|
|
|
13214
13629
|
}
|
|
13215
13630
|
const backup = backupIgnoredFiles(projectRoot);
|
|
13216
13631
|
try {
|
|
13217
|
-
await enforceSandboxIgnore(sandboxName, projectRoot);
|
|
13632
|
+
await enforceSandboxIgnore(sandboxName, projectRoot, containerWorkdir);
|
|
13218
13633
|
} finally {
|
|
13219
13634
|
backup.restore();
|
|
13220
13635
|
}
|
|
@@ -13222,7 +13637,7 @@ async function createProviderSandbox(provider, sandboxName, projectRoot) {
|
|
|
13222
13637
|
}
|
|
13223
13638
|
async function ensurePackageManagerInSandbox(sandboxName, pm) {
|
|
13224
13639
|
try {
|
|
13225
|
-
|
|
13640
|
+
execSync20(`docker sandbox exec ${sandboxName} which ${pm}`, {
|
|
13226
13641
|
stdio: ["pipe", "pipe", "pipe"],
|
|
13227
13642
|
timeout: 5000
|
|
13228
13643
|
});
|
|
@@ -13231,7 +13646,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
|
|
|
13231
13646
|
process.stderr.write(`Installing ${bold2(pm)} in sandbox...
|
|
13232
13647
|
`);
|
|
13233
13648
|
try {
|
|
13234
|
-
|
|
13649
|
+
execSync20(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
|
|
13235
13650
|
stdio: "inherit",
|
|
13236
13651
|
timeout: 120000
|
|
13237
13652
|
});
|
|
@@ -13243,7 +13658,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
|
|
|
13243
13658
|
}
|
|
13244
13659
|
async function ensureCodexInSandbox(sandboxName) {
|
|
13245
13660
|
try {
|
|
13246
|
-
|
|
13661
|
+
execSync20(`docker sandbox exec ${sandboxName} which codex`, {
|
|
13247
13662
|
stdio: ["pipe", "pipe", "pipe"],
|
|
13248
13663
|
timeout: 5000
|
|
13249
13664
|
});
|
|
@@ -13251,7 +13666,7 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
13251
13666
|
process.stderr.write(`Installing codex in sandbox...
|
|
13252
13667
|
`);
|
|
13253
13668
|
try {
|
|
13254
|
-
|
|
13669
|
+
execSync20(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
|
|
13255
13670
|
} catch {
|
|
13256
13671
|
process.stderr.write(`${red2("✗")} Failed to install codex in sandbox.
|
|
13257
13672
|
`);
|
|
@@ -13260,7 +13675,7 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
13260
13675
|
}
|
|
13261
13676
|
function isSandboxAlive(name) {
|
|
13262
13677
|
try {
|
|
13263
|
-
const output =
|
|
13678
|
+
const output = execSync20("docker sandbox ls", {
|
|
13264
13679
|
encoding: "utf-8",
|
|
13265
13680
|
stdio: ["pipe", "pipe", "pipe"],
|
|
13266
13681
|
timeout: 5000
|
|
@@ -13286,13 +13701,13 @@ init_context();
|
|
|
13286
13701
|
init_logger();
|
|
13287
13702
|
init_rate_limiter();
|
|
13288
13703
|
init_terminal();
|
|
13289
|
-
import { existsSync as
|
|
13290
|
-
import { join as
|
|
13704
|
+
import { existsSync as existsSync27, readFileSync as readFileSync18 } from "node:fs";
|
|
13705
|
+
import { join as join27 } from "node:path";
|
|
13291
13706
|
import { fileURLToPath } from "node:url";
|
|
13292
13707
|
function getCliVersion() {
|
|
13293
13708
|
const fallbackVersion = "0.0.0";
|
|
13294
|
-
const packageJsonPath =
|
|
13295
|
-
if (!
|
|
13709
|
+
const packageJsonPath = join27(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
|
|
13710
|
+
if (!existsSync27(packageJsonPath)) {
|
|
13296
13711
|
return fallbackVersion;
|
|
13297
13712
|
}
|
|
13298
13713
|
try {
|
|
@@ -13442,7 +13857,7 @@ function printLogo() {
|
|
|
13442
13857
|
`);
|
|
13443
13858
|
}
|
|
13444
13859
|
}
|
|
13445
|
-
function
|
|
13860
|
+
function printHelp7() {
|
|
13446
13861
|
printLogo();
|
|
13447
13862
|
process.stderr.write(`
|
|
13448
13863
|
|
|
@@ -13463,6 +13878,7 @@ ${bold2("Commands:")}
|
|
|
13463
13878
|
${cyan2("status")} Dashboard view of current state
|
|
13464
13879
|
${cyan2("config")} View and manage settings
|
|
13465
13880
|
${cyan2("logs")} View, tail, and manage execution logs
|
|
13881
|
+
${cyan2("create")} ${dim2("<name>")} Scaffold a new community package
|
|
13466
13882
|
${cyan2("install")} Install a community package
|
|
13467
13883
|
${cyan2("uninstall")} Remove an installed package
|
|
13468
13884
|
${cyan2("packages")} Manage installed packages (list, outdated)
|
|
@@ -13546,7 +13962,7 @@ async function main() {
|
|
|
13546
13962
|
process.exit(0);
|
|
13547
13963
|
}
|
|
13548
13964
|
if (parsed.flags.help && !parsed.command) {
|
|
13549
|
-
|
|
13965
|
+
printHelp7();
|
|
13550
13966
|
process.exit(0);
|
|
13551
13967
|
}
|
|
13552
13968
|
const command = resolveAlias(parsed.command);
|
|
@@ -13557,7 +13973,7 @@ async function main() {
|
|
|
13557
13973
|
try {
|
|
13558
13974
|
const root = getGitRoot(cwd);
|
|
13559
13975
|
if (isInitialized(root)) {
|
|
13560
|
-
logDir =
|
|
13976
|
+
logDir = join27(root, ".locus", "logs");
|
|
13561
13977
|
getRateLimiter(root);
|
|
13562
13978
|
}
|
|
13563
13979
|
} catch {}
|
|
@@ -13578,7 +13994,7 @@ async function main() {
|
|
|
13578
13994
|
printVersionNotice = startVersionCheck2(VERSION);
|
|
13579
13995
|
}
|
|
13580
13996
|
if (!command) {
|
|
13581
|
-
|
|
13997
|
+
printHelp7();
|
|
13582
13998
|
process.exit(0);
|
|
13583
13999
|
}
|
|
13584
14000
|
if (command === "init") {
|
|
@@ -13587,6 +14003,13 @@ async function main() {
|
|
|
13587
14003
|
logger.destroy();
|
|
13588
14004
|
return;
|
|
13589
14005
|
}
|
|
14006
|
+
if (command === "create") {
|
|
14007
|
+
const { createCommand: createCommand2 } = await Promise.resolve().then(() => (init_create(), exports_create));
|
|
14008
|
+
const createArgs = parsed.flags.help ? ["help"] : parsed.args;
|
|
14009
|
+
await createCommand2(createArgs);
|
|
14010
|
+
logger.destroy();
|
|
14011
|
+
return;
|
|
14012
|
+
}
|
|
13590
14013
|
if (command === "install") {
|
|
13591
14014
|
if (parsed.flags.list) {
|
|
13592
14015
|
const { packagesCommand: packagesCommand2 } = await Promise.resolve().then(() => (init_packages(), exports_packages));
|