@locusai/cli 0.24.9 → 0.25.1
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/README.md +91 -4
- package/bin/locus.js +975 -387
- package/package.json +2 -2
package/bin/locus.js
CHANGED
|
@@ -3213,6 +3213,602 @@ var init_uninstall = __esm(() => {
|
|
|
3213
3213
|
init_registry();
|
|
3214
3214
|
});
|
|
3215
3215
|
|
|
3216
|
+
// src/display/table.ts
|
|
3217
|
+
function renderTable(columns, rows, options = {}) {
|
|
3218
|
+
const {
|
|
3219
|
+
indent = 2,
|
|
3220
|
+
headerSeparator = true,
|
|
3221
|
+
maxRows = 0,
|
|
3222
|
+
emptyMessage = "No results."
|
|
3223
|
+
} = options;
|
|
3224
|
+
if (rows.length === 0) {
|
|
3225
|
+
return `${" ".repeat(indent)}${dim2(emptyMessage)}`;
|
|
3226
|
+
}
|
|
3227
|
+
const termWidth = getCapabilities().columns;
|
|
3228
|
+
const indentStr = " ".repeat(indent);
|
|
3229
|
+
const gap = 2;
|
|
3230
|
+
const formattedRows = rows.map((row) => {
|
|
3231
|
+
const formatted = {};
|
|
3232
|
+
for (const col of columns) {
|
|
3233
|
+
if (col.format) {
|
|
3234
|
+
formatted[col.key] = col.format(row[col.key], row);
|
|
3235
|
+
} else {
|
|
3236
|
+
const val = row[col.key];
|
|
3237
|
+
formatted[col.key] = val === null || val === undefined ? "" : String(val);
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
return formatted;
|
|
3241
|
+
});
|
|
3242
|
+
const colWidths = columns.map((col, _i) => {
|
|
3243
|
+
const headerWidth = stripAnsi(col.header).length;
|
|
3244
|
+
const minWidth = col.minWidth ?? headerWidth;
|
|
3245
|
+
let maxContent = headerWidth;
|
|
3246
|
+
for (const row of formattedRows) {
|
|
3247
|
+
const cellWidth = stripAnsi(row[col.key] ?? "").length;
|
|
3248
|
+
if (cellWidth > maxContent)
|
|
3249
|
+
maxContent = cellWidth;
|
|
3250
|
+
}
|
|
3251
|
+
let width = Math.max(minWidth, maxContent);
|
|
3252
|
+
if (col.maxWidth && col.maxWidth > 0) {
|
|
3253
|
+
width = Math.min(width, col.maxWidth);
|
|
3254
|
+
}
|
|
3255
|
+
return width;
|
|
3256
|
+
});
|
|
3257
|
+
const totalWidth = indent + colWidths.reduce((s, w) => s + w, 0) + gap * (columns.length - 1);
|
|
3258
|
+
if (totalWidth > termWidth && columns.length > 1) {
|
|
3259
|
+
const overflow = totalWidth - termWidth;
|
|
3260
|
+
let widestIdx = 0;
|
|
3261
|
+
let widestSize = 0;
|
|
3262
|
+
for (let i = 0;i < columns.length; i++) {
|
|
3263
|
+
if (!columns[i].maxWidth && colWidths[i] > widestSize) {
|
|
3264
|
+
widestSize = colWidths[i];
|
|
3265
|
+
widestIdx = i;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
colWidths[widestIdx] = Math.max(10, colWidths[widestIdx] - overflow);
|
|
3269
|
+
}
|
|
3270
|
+
const lines = [];
|
|
3271
|
+
const headerParts = columns.map((col, i) => alignCell(bold2(col.header), colWidths[i], col.align ?? "left"));
|
|
3272
|
+
lines.push(`${indentStr}${headerParts.join(" ".repeat(gap))}`);
|
|
3273
|
+
if (headerSeparator) {
|
|
3274
|
+
const sep = columns.map((_, i) => gray2("─".repeat(colWidths[i]))).join(" ".repeat(gap));
|
|
3275
|
+
lines.push(`${indentStr}${sep}`);
|
|
3276
|
+
}
|
|
3277
|
+
const displayRows = maxRows > 0 ? formattedRows.slice(0, maxRows) : formattedRows;
|
|
3278
|
+
for (const row of displayRows) {
|
|
3279
|
+
const cellParts = columns.map((col, i) => {
|
|
3280
|
+
const raw = row[col.key] ?? "";
|
|
3281
|
+
return alignCell(raw, colWidths[i], col.align ?? "left");
|
|
3282
|
+
});
|
|
3283
|
+
lines.push(`${indentStr}${cellParts.join(" ".repeat(gap))}`);
|
|
3284
|
+
}
|
|
3285
|
+
if (maxRows > 0 && formattedRows.length > maxRows) {
|
|
3286
|
+
const remaining = formattedRows.length - maxRows;
|
|
3287
|
+
lines.push(`${indentStr}${dim2(`... and ${remaining} more`)}`);
|
|
3288
|
+
}
|
|
3289
|
+
return lines.join(`
|
|
3290
|
+
`);
|
|
3291
|
+
}
|
|
3292
|
+
function alignCell(text, width, align) {
|
|
3293
|
+
const visual = stripAnsi(text).length;
|
|
3294
|
+
if (visual > width) {
|
|
3295
|
+
const stripped = stripAnsi(text);
|
|
3296
|
+
return `${stripped.slice(0, width - 1)}…`;
|
|
3297
|
+
}
|
|
3298
|
+
const padding = width - visual;
|
|
3299
|
+
switch (align) {
|
|
3300
|
+
case "right":
|
|
3301
|
+
return " ".repeat(padding) + text;
|
|
3302
|
+
case "center": {
|
|
3303
|
+
const left = Math.floor(padding / 2);
|
|
3304
|
+
const right = padding - left;
|
|
3305
|
+
return " ".repeat(left) + text + " ".repeat(right);
|
|
3306
|
+
}
|
|
3307
|
+
default:
|
|
3308
|
+
return text + " ".repeat(padding);
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
function renderDetails(entries, options = {}) {
|
|
3312
|
+
const { indent = 2, labelWidth: fixedWidth } = options;
|
|
3313
|
+
const indentStr = " ".repeat(indent);
|
|
3314
|
+
const labelWidth = fixedWidth ?? Math.max(...entries.map((e) => stripAnsi(e.label).length)) + 1;
|
|
3315
|
+
return entries.map((entry) => {
|
|
3316
|
+
const label = padEnd(dim2(`${entry.label}:`), labelWidth + 1);
|
|
3317
|
+
return `${indentStr}${label} ${entry.value}`;
|
|
3318
|
+
}).join(`
|
|
3319
|
+
`);
|
|
3320
|
+
}
|
|
3321
|
+
var init_table = __esm(() => {
|
|
3322
|
+
init_terminal();
|
|
3323
|
+
});
|
|
3324
|
+
|
|
3325
|
+
// src/skills/types.ts
|
|
3326
|
+
var REGISTRY_REPO = "locusai/skills", REGISTRY_BRANCH = "main", SKILLS_LOCK_FILENAME = "skills-lock.json", CLAUDE_SKILLS_DIR = ".claude/skills", AGENTS_SKILLS_DIR = ".agents/skills";
|
|
3327
|
+
|
|
3328
|
+
// src/skills/lock.ts
|
|
3329
|
+
import { createHash } from "node:crypto";
|
|
3330
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
|
|
3331
|
+
import { join as join10 } from "node:path";
|
|
3332
|
+
function readLockFile(projectRoot) {
|
|
3333
|
+
const filePath = join10(projectRoot, SKILLS_LOCK_FILENAME);
|
|
3334
|
+
try {
|
|
3335
|
+
const raw = readFileSync7(filePath, "utf-8");
|
|
3336
|
+
return JSON.parse(raw);
|
|
3337
|
+
} catch {
|
|
3338
|
+
return { ...DEFAULT_LOCK_FILE, skills: {} };
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
function writeLockFile(projectRoot, lockFile) {
|
|
3342
|
+
const filePath = join10(projectRoot, SKILLS_LOCK_FILENAME);
|
|
3343
|
+
writeFileSync7(filePath, `${JSON.stringify(lockFile, null, 2)}
|
|
3344
|
+
`, "utf-8");
|
|
3345
|
+
}
|
|
3346
|
+
function computeSkillHash(content) {
|
|
3347
|
+
return createHash("sha256").update(content).digest("hex");
|
|
3348
|
+
}
|
|
3349
|
+
var DEFAULT_LOCK_FILE;
|
|
3350
|
+
var init_lock = __esm(() => {
|
|
3351
|
+
DEFAULT_LOCK_FILE = { version: 1, skills: {} };
|
|
3352
|
+
});
|
|
3353
|
+
|
|
3354
|
+
// src/skills/installer.ts
|
|
3355
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, rmSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
3356
|
+
import { join as join11 } from "node:path";
|
|
3357
|
+
async function installSkill(projectRoot, name, content, source) {
|
|
3358
|
+
const claudeDir = join11(projectRoot, CLAUDE_SKILLS_DIR, name);
|
|
3359
|
+
const agentsDir = join11(projectRoot, AGENTS_SKILLS_DIR, name);
|
|
3360
|
+
mkdirSync8(claudeDir, { recursive: true });
|
|
3361
|
+
mkdirSync8(agentsDir, { recursive: true });
|
|
3362
|
+
writeFileSync8(join11(claudeDir, "SKILL.md"), content, "utf-8");
|
|
3363
|
+
writeFileSync8(join11(agentsDir, "SKILL.md"), content, "utf-8");
|
|
3364
|
+
const lock = readLockFile(projectRoot);
|
|
3365
|
+
lock.skills[name] = {
|
|
3366
|
+
source,
|
|
3367
|
+
sourceType: "github",
|
|
3368
|
+
computedHash: computeSkillHash(content)
|
|
3369
|
+
};
|
|
3370
|
+
writeLockFile(projectRoot, lock);
|
|
3371
|
+
}
|
|
3372
|
+
async function removeSkill(projectRoot, name) {
|
|
3373
|
+
if (!isSkillInstalled(projectRoot, name)) {
|
|
3374
|
+
console.warn(`Skill "${name}" is not installed.`);
|
|
3375
|
+
return;
|
|
3376
|
+
}
|
|
3377
|
+
const claudeDir = join11(projectRoot, CLAUDE_SKILLS_DIR, name);
|
|
3378
|
+
const agentsDir = join11(projectRoot, AGENTS_SKILLS_DIR, name);
|
|
3379
|
+
if (existsSync11(claudeDir)) {
|
|
3380
|
+
rmSync(claudeDir, { recursive: true, force: true });
|
|
3381
|
+
}
|
|
3382
|
+
if (existsSync11(agentsDir)) {
|
|
3383
|
+
rmSync(agentsDir, { recursive: true, force: true });
|
|
3384
|
+
}
|
|
3385
|
+
const lock = readLockFile(projectRoot);
|
|
3386
|
+
delete lock.skills[name];
|
|
3387
|
+
writeLockFile(projectRoot, lock);
|
|
3388
|
+
}
|
|
3389
|
+
function isSkillInstalled(projectRoot, name) {
|
|
3390
|
+
const lock = readLockFile(projectRoot);
|
|
3391
|
+
return name in lock.skills;
|
|
3392
|
+
}
|
|
3393
|
+
var init_installer = __esm(() => {
|
|
3394
|
+
init_lock();
|
|
3395
|
+
});
|
|
3396
|
+
|
|
3397
|
+
// src/skills/registry.ts
|
|
3398
|
+
async function fetchRegistry() {
|
|
3399
|
+
const url = `${RAW_BASE}/registry.json`;
|
|
3400
|
+
let res;
|
|
3401
|
+
try {
|
|
3402
|
+
res = await fetch(url);
|
|
3403
|
+
} catch (err) {
|
|
3404
|
+
throw new Error(`Failed to reach the skill registry at ${url}: ${err.message}`);
|
|
3405
|
+
}
|
|
3406
|
+
if (!res.ok) {
|
|
3407
|
+
throw new Error(`Failed to fetch skill registry (HTTP ${res.status}). Check your network connection and try again.`);
|
|
3408
|
+
}
|
|
3409
|
+
let data;
|
|
3410
|
+
try {
|
|
3411
|
+
data = await res.json();
|
|
3412
|
+
} catch {
|
|
3413
|
+
throw new Error("Skill registry returned invalid JSON.");
|
|
3414
|
+
}
|
|
3415
|
+
if (!data || typeof data !== "object" || !Array.isArray(data.skills)) {
|
|
3416
|
+
throw new Error("Skill registry has an unexpected format (missing 'skills' array).");
|
|
3417
|
+
}
|
|
3418
|
+
return data;
|
|
3419
|
+
}
|
|
3420
|
+
async function fetchSkillContent(path) {
|
|
3421
|
+
const url = `${RAW_BASE}/${path}`;
|
|
3422
|
+
let res;
|
|
3423
|
+
try {
|
|
3424
|
+
res = await fetch(url);
|
|
3425
|
+
} catch (err) {
|
|
3426
|
+
throw new Error(`Failed to fetch skill file at ${url}: ${err.message}`);
|
|
3427
|
+
}
|
|
3428
|
+
if (res.status === 404) {
|
|
3429
|
+
throw new Error(`Skill not found: ${path}`);
|
|
3430
|
+
}
|
|
3431
|
+
if (!res.ok) {
|
|
3432
|
+
throw new Error(`Failed to fetch skill file (HTTP ${res.status}): ${path}`);
|
|
3433
|
+
}
|
|
3434
|
+
return res.text();
|
|
3435
|
+
}
|
|
3436
|
+
function findSkillInRegistry(registry, name) {
|
|
3437
|
+
return registry.skills.find((s) => s.name === name);
|
|
3438
|
+
}
|
|
3439
|
+
var RAW_BASE;
|
|
3440
|
+
var init_registry2 = __esm(() => {
|
|
3441
|
+
RAW_BASE = `https://raw.githubusercontent.com/${REGISTRY_REPO}/${REGISTRY_BRANCH}`;
|
|
3442
|
+
});
|
|
3443
|
+
|
|
3444
|
+
// src/commands/skills.ts
|
|
3445
|
+
var exports_skills = {};
|
|
3446
|
+
__export(exports_skills, {
|
|
3447
|
+
skillsCommand: () => skillsCommand
|
|
3448
|
+
});
|
|
3449
|
+
async function listRemoteSkills() {
|
|
3450
|
+
let registry;
|
|
3451
|
+
try {
|
|
3452
|
+
registry = await fetchRegistry();
|
|
3453
|
+
} catch (err) {
|
|
3454
|
+
process.stderr.write(`${red2("✗")} Failed to fetch skills registry. Check your internet connection.
|
|
3455
|
+
`);
|
|
3456
|
+
process.stderr.write(` ${dim2(err.message)}
|
|
3457
|
+
`);
|
|
3458
|
+
process.exit(1);
|
|
3459
|
+
}
|
|
3460
|
+
if (registry.skills.length === 0) {
|
|
3461
|
+
process.stderr.write(`${dim2("No skills available in the registry.")}
|
|
3462
|
+
`);
|
|
3463
|
+
return;
|
|
3464
|
+
}
|
|
3465
|
+
process.stderr.write(`
|
|
3466
|
+
${bold2("Available Skills")}
|
|
3467
|
+
|
|
3468
|
+
`);
|
|
3469
|
+
const columns = [
|
|
3470
|
+
{ key: "name", header: "Name", minWidth: 12, maxWidth: 24 },
|
|
3471
|
+
{ key: "description", header: "Description", minWidth: 20, maxWidth: 50 },
|
|
3472
|
+
{
|
|
3473
|
+
key: "platforms",
|
|
3474
|
+
header: "Platforms",
|
|
3475
|
+
minWidth: 10,
|
|
3476
|
+
maxWidth: 30,
|
|
3477
|
+
format: (val) => dim2(val.join(", "))
|
|
3478
|
+
}
|
|
3479
|
+
];
|
|
3480
|
+
const rows = registry.skills.map((s) => ({
|
|
3481
|
+
name: cyan2(s.name),
|
|
3482
|
+
description: s.description,
|
|
3483
|
+
platforms: s.platforms
|
|
3484
|
+
}));
|
|
3485
|
+
process.stderr.write(`${renderTable(columns, rows)}
|
|
3486
|
+
|
|
3487
|
+
`);
|
|
3488
|
+
process.stderr.write(` ${dim2(`${registry.skills.length} skill(s) available.`)} Install with: ${bold2("locus skills install <name>")}
|
|
3489
|
+
|
|
3490
|
+
`);
|
|
3491
|
+
}
|
|
3492
|
+
function listInstalledSkills() {
|
|
3493
|
+
const cwd = process.cwd();
|
|
3494
|
+
const lockFile = readLockFile(cwd);
|
|
3495
|
+
const entries = Object.entries(lockFile.skills);
|
|
3496
|
+
if (entries.length === 0) {
|
|
3497
|
+
process.stderr.write(`${yellow2("⚠")} No skills installed.
|
|
3498
|
+
`);
|
|
3499
|
+
process.stderr.write(` Browse available skills with: ${bold2("locus skills list")}
|
|
3500
|
+
`);
|
|
3501
|
+
return;
|
|
3502
|
+
}
|
|
3503
|
+
process.stderr.write(`
|
|
3504
|
+
${bold2("Installed Skills")}
|
|
3505
|
+
|
|
3506
|
+
`);
|
|
3507
|
+
const columns = [
|
|
3508
|
+
{ key: "name", header: "Name", minWidth: 12, maxWidth: 24 },
|
|
3509
|
+
{ key: "source", header: "Source", minWidth: 10, maxWidth: 40 },
|
|
3510
|
+
{
|
|
3511
|
+
key: "hash",
|
|
3512
|
+
header: "Hash",
|
|
3513
|
+
minWidth: 10,
|
|
3514
|
+
maxWidth: 16,
|
|
3515
|
+
format: (val) => dim2(val.slice(0, 12))
|
|
3516
|
+
}
|
|
3517
|
+
];
|
|
3518
|
+
const rows = entries.map(([name, entry]) => ({
|
|
3519
|
+
name: cyan2(name),
|
|
3520
|
+
source: entry.source,
|
|
3521
|
+
hash: entry.computedHash
|
|
3522
|
+
}));
|
|
3523
|
+
process.stderr.write(`${renderTable(columns, rows)}
|
|
3524
|
+
|
|
3525
|
+
`);
|
|
3526
|
+
process.stderr.write(` ${dim2(`${entries.length} skill(s) installed.`)}
|
|
3527
|
+
|
|
3528
|
+
`);
|
|
3529
|
+
}
|
|
3530
|
+
async function installRemoteSkill(name) {
|
|
3531
|
+
if (!name) {
|
|
3532
|
+
process.stderr.write(`${red2("✗")} Please specify a skill name.
|
|
3533
|
+
`);
|
|
3534
|
+
process.stderr.write(` Usage: ${bold2("locus skills install <name>")}
|
|
3535
|
+
`);
|
|
3536
|
+
process.exit(1);
|
|
3537
|
+
}
|
|
3538
|
+
let registry;
|
|
3539
|
+
try {
|
|
3540
|
+
registry = await fetchRegistry();
|
|
3541
|
+
} catch (err) {
|
|
3542
|
+
process.stderr.write(`${red2("✗")} Failed to fetch skills registry. Check your internet connection.
|
|
3543
|
+
`);
|
|
3544
|
+
process.stderr.write(` ${dim2(err.message)}
|
|
3545
|
+
`);
|
|
3546
|
+
process.exit(1);
|
|
3547
|
+
}
|
|
3548
|
+
const entry = findSkillInRegistry(registry, name);
|
|
3549
|
+
if (!entry) {
|
|
3550
|
+
process.stderr.write(`${red2("✗")} Skill '${bold2(name)}' not found in the registry.
|
|
3551
|
+
`);
|
|
3552
|
+
process.stderr.write(` Run ${bold2("locus skills list")} to see available skills.
|
|
3553
|
+
`);
|
|
3554
|
+
process.exit(1);
|
|
3555
|
+
}
|
|
3556
|
+
let content;
|
|
3557
|
+
try {
|
|
3558
|
+
content = await fetchSkillContent(entry.path);
|
|
3559
|
+
} catch (err) {
|
|
3560
|
+
process.stderr.write(`${red2("✗")} Failed to download skill '${name}'.
|
|
3561
|
+
`);
|
|
3562
|
+
process.stderr.write(` ${dim2(err.message)}
|
|
3563
|
+
`);
|
|
3564
|
+
process.exit(1);
|
|
3565
|
+
}
|
|
3566
|
+
const cwd = process.cwd();
|
|
3567
|
+
const source = `${REGISTRY_REPO}/${entry.path}`;
|
|
3568
|
+
await installSkill(cwd, name, content, source);
|
|
3569
|
+
process.stderr.write(`
|
|
3570
|
+
${green("✓")} Installed skill '${bold2(name)}' from ${REGISTRY_REPO}
|
|
3571
|
+
`);
|
|
3572
|
+
process.stderr.write(` → ${CLAUDE_SKILLS_DIR}/${name}/SKILL.md
|
|
3573
|
+
`);
|
|
3574
|
+
process.stderr.write(` → ${AGENTS_SKILLS_DIR}/${name}/SKILL.md
|
|
3575
|
+
|
|
3576
|
+
`);
|
|
3577
|
+
}
|
|
3578
|
+
async function removeInstalledSkill(name) {
|
|
3579
|
+
if (!name) {
|
|
3580
|
+
process.stderr.write(`${red2("✗")} Please specify a skill name.
|
|
3581
|
+
`);
|
|
3582
|
+
process.stderr.write(` Usage: ${bold2("locus skills remove <name>")}
|
|
3583
|
+
`);
|
|
3584
|
+
process.exit(1);
|
|
3585
|
+
}
|
|
3586
|
+
const cwd = process.cwd();
|
|
3587
|
+
if (!isSkillInstalled(cwd, name)) {
|
|
3588
|
+
process.stderr.write(`${red2("✗")} Skill '${bold2(name)}' is not installed.
|
|
3589
|
+
`);
|
|
3590
|
+
process.stderr.write(` Run ${bold2("locus skills list --installed")} to see installed skills.
|
|
3591
|
+
`);
|
|
3592
|
+
process.exit(1);
|
|
3593
|
+
}
|
|
3594
|
+
await removeSkill(cwd, name);
|
|
3595
|
+
process.stderr.write(`${green("✓")} Removed skill '${bold2(name)}'
|
|
3596
|
+
`);
|
|
3597
|
+
}
|
|
3598
|
+
async function updateSkills(name) {
|
|
3599
|
+
const cwd = process.cwd();
|
|
3600
|
+
const lockFile = readLockFile(cwd);
|
|
3601
|
+
const installed = Object.entries(lockFile.skills);
|
|
3602
|
+
if (installed.length === 0) {
|
|
3603
|
+
process.stderr.write(`${yellow2("⚠")} No skills installed.
|
|
3604
|
+
`);
|
|
3605
|
+
process.stderr.write(` Install skills with: ${bold2("locus skills install <name>")}
|
|
3606
|
+
`);
|
|
3607
|
+
return;
|
|
3608
|
+
}
|
|
3609
|
+
const targets = name ? installed.filter(([n]) => n === name) : installed;
|
|
3610
|
+
if (name && targets.length === 0) {
|
|
3611
|
+
process.stderr.write(`${red2("✗")} Skill '${bold2(name)}' is not installed.
|
|
3612
|
+
`);
|
|
3613
|
+
process.stderr.write(` Run ${bold2("locus skills list --installed")} to see installed skills.
|
|
3614
|
+
`);
|
|
3615
|
+
process.exit(1);
|
|
3616
|
+
}
|
|
3617
|
+
let registry;
|
|
3618
|
+
try {
|
|
3619
|
+
registry = await fetchRegistry();
|
|
3620
|
+
} catch (err) {
|
|
3621
|
+
process.stderr.write(`${red2("✗")} Failed to fetch skills registry. Check your internet connection.
|
|
3622
|
+
`);
|
|
3623
|
+
process.stderr.write(` ${dim2(err.message)}
|
|
3624
|
+
`);
|
|
3625
|
+
process.exit(1);
|
|
3626
|
+
}
|
|
3627
|
+
process.stderr.write(`
|
|
3628
|
+
`);
|
|
3629
|
+
let updatedCount = 0;
|
|
3630
|
+
for (const [skillName, lockEntry] of targets) {
|
|
3631
|
+
const entry = findSkillInRegistry(registry, skillName);
|
|
3632
|
+
if (!entry) {
|
|
3633
|
+
process.stderr.write(`${yellow2("⚠")} '${bold2(skillName)}' is no longer in the registry (skipped)
|
|
3634
|
+
`);
|
|
3635
|
+
continue;
|
|
3636
|
+
}
|
|
3637
|
+
let content;
|
|
3638
|
+
try {
|
|
3639
|
+
content = await fetchSkillContent(entry.path);
|
|
3640
|
+
} catch (err) {
|
|
3641
|
+
process.stderr.write(`${red2("✗")} Failed to download skill '${skillName}': ${dim2(err.message)}
|
|
3642
|
+
`);
|
|
3643
|
+
continue;
|
|
3644
|
+
}
|
|
3645
|
+
const newHash = computeSkillHash(content);
|
|
3646
|
+
if (newHash === lockEntry.computedHash) {
|
|
3647
|
+
process.stderr.write(` '${cyan2(skillName)}' is already up to date
|
|
3648
|
+
`);
|
|
3649
|
+
continue;
|
|
3650
|
+
}
|
|
3651
|
+
const source = `${REGISTRY_REPO}/${entry.path}`;
|
|
3652
|
+
await installSkill(cwd, skillName, content, source);
|
|
3653
|
+
updatedCount++;
|
|
3654
|
+
process.stderr.write(`${green("✓")} Updated '${bold2(skillName)}' (hash changed)
|
|
3655
|
+
`);
|
|
3656
|
+
}
|
|
3657
|
+
process.stderr.write(`
|
|
3658
|
+
`);
|
|
3659
|
+
if (updatedCount === 0) {
|
|
3660
|
+
process.stderr.write(` ${dim2("All skills are up to date.")}
|
|
3661
|
+
|
|
3662
|
+
`);
|
|
3663
|
+
} else {
|
|
3664
|
+
process.stderr.write(` ${dim2(`${updatedCount} skill(s) updated.`)}
|
|
3665
|
+
|
|
3666
|
+
`);
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
async function infoSkill(name) {
|
|
3670
|
+
if (!name) {
|
|
3671
|
+
process.stderr.write(`${red2("✗")} Please specify a skill name.
|
|
3672
|
+
`);
|
|
3673
|
+
process.stderr.write(` Usage: ${bold2("locus skills info <name>")}
|
|
3674
|
+
`);
|
|
3675
|
+
process.exit(1);
|
|
3676
|
+
}
|
|
3677
|
+
let registry;
|
|
3678
|
+
try {
|
|
3679
|
+
registry = await fetchRegistry();
|
|
3680
|
+
} catch (err) {
|
|
3681
|
+
process.stderr.write(`${red2("✗")} Failed to fetch skills registry. Check your internet connection.
|
|
3682
|
+
`);
|
|
3683
|
+
process.stderr.write(` ${dim2(err.message)}
|
|
3684
|
+
`);
|
|
3685
|
+
process.exit(1);
|
|
3686
|
+
}
|
|
3687
|
+
const entry = findSkillInRegistry(registry, name);
|
|
3688
|
+
const cwd = process.cwd();
|
|
3689
|
+
const lockFile = readLockFile(cwd);
|
|
3690
|
+
const lockEntry = lockFile.skills[name];
|
|
3691
|
+
if (!entry && !lockEntry) {
|
|
3692
|
+
process.stderr.write(`${red2("✗")} Skill '${bold2(name)}' not found in the registry or locally.
|
|
3693
|
+
`);
|
|
3694
|
+
process.stderr.write(` Run ${bold2("locus skills list")} to see available skills.
|
|
3695
|
+
`);
|
|
3696
|
+
process.exit(1);
|
|
3697
|
+
}
|
|
3698
|
+
process.stderr.write(`
|
|
3699
|
+
${bold2("Skill Information")}
|
|
3700
|
+
|
|
3701
|
+
`);
|
|
3702
|
+
if (entry) {
|
|
3703
|
+
process.stderr.write(` ${bold2("Name:")} ${cyan2(entry.name)}
|
|
3704
|
+
`);
|
|
3705
|
+
process.stderr.write(` ${bold2("Description:")} ${entry.description}
|
|
3706
|
+
`);
|
|
3707
|
+
process.stderr.write(` ${bold2("Platforms:")} ${entry.platforms.join(", ")}
|
|
3708
|
+
`);
|
|
3709
|
+
process.stderr.write(` ${bold2("Tags:")} ${entry.tags.join(", ")}
|
|
3710
|
+
`);
|
|
3711
|
+
process.stderr.write(` ${bold2("Author:")} ${entry.author}
|
|
3712
|
+
`);
|
|
3713
|
+
process.stderr.write(` ${bold2("Source:")} ${REGISTRY_REPO}/${entry.path}
|
|
3714
|
+
`);
|
|
3715
|
+
} else {
|
|
3716
|
+
process.stderr.write(` ${bold2("Name:")} ${cyan2(name)}
|
|
3717
|
+
`);
|
|
3718
|
+
process.stderr.write(` ${dim2("(not found in remote registry)")}
|
|
3719
|
+
`);
|
|
3720
|
+
}
|
|
3721
|
+
process.stderr.write(`
|
|
3722
|
+
`);
|
|
3723
|
+
if (lockEntry) {
|
|
3724
|
+
process.stderr.write(` ${bold2("Installed:")} ${green("yes")}
|
|
3725
|
+
`);
|
|
3726
|
+
process.stderr.write(` ${bold2("Hash:")} ${dim2(lockEntry.computedHash.slice(0, 10))}
|
|
3727
|
+
`);
|
|
3728
|
+
process.stderr.write(` ${bold2("Source:")} ${lockEntry.source}
|
|
3729
|
+
`);
|
|
3730
|
+
} else {
|
|
3731
|
+
process.stderr.write(` ${bold2("Installed:")} ${dim2("no")}
|
|
3732
|
+
`);
|
|
3733
|
+
}
|
|
3734
|
+
process.stderr.write(`
|
|
3735
|
+
`);
|
|
3736
|
+
}
|
|
3737
|
+
function printSkillsHelp() {
|
|
3738
|
+
process.stderr.write(`
|
|
3739
|
+
${bold2("Usage:")}
|
|
3740
|
+
locus skills <subcommand> [options]
|
|
3741
|
+
|
|
3742
|
+
${bold2("Subcommands:")}
|
|
3743
|
+
${cyan2("list")} List available skills from the registry
|
|
3744
|
+
${cyan2("list")} ${dim2("--installed")} List locally installed skills
|
|
3745
|
+
${cyan2("install")} ${dim2("<name>")} Install a skill from the registry
|
|
3746
|
+
${cyan2("remove")} ${dim2("<name>")} Remove an installed skill
|
|
3747
|
+
${cyan2("update")} ${dim2("[name]")} Update installed skill(s) from registry
|
|
3748
|
+
${cyan2("info")} ${dim2("<name>")} Show skill metadata and install status
|
|
3749
|
+
|
|
3750
|
+
${bold2("Examples:")}
|
|
3751
|
+
locus skills list ${dim2("# Browse available skills")}
|
|
3752
|
+
locus skills list --installed ${dim2("# Show installed skills")}
|
|
3753
|
+
locus skills install code-review ${dim2("# Install a skill")}
|
|
3754
|
+
locus skills remove code-review ${dim2("# Remove a skill")}
|
|
3755
|
+
locus skills update ${dim2("# Update all installed skills")}
|
|
3756
|
+
locus skills info code-review ${dim2("# Show skill details")}
|
|
3757
|
+
|
|
3758
|
+
`);
|
|
3759
|
+
}
|
|
3760
|
+
async function skillsCommand(args, flags) {
|
|
3761
|
+
const subcommand = args[0] ?? "list";
|
|
3762
|
+
switch (subcommand) {
|
|
3763
|
+
case "help": {
|
|
3764
|
+
printSkillsHelp();
|
|
3765
|
+
break;
|
|
3766
|
+
}
|
|
3767
|
+
case "list": {
|
|
3768
|
+
const isInstalled = flags.installed !== undefined || args.includes("--installed");
|
|
3769
|
+
if (isInstalled) {
|
|
3770
|
+
listInstalledSkills();
|
|
3771
|
+
} else {
|
|
3772
|
+
await listRemoteSkills();
|
|
3773
|
+
}
|
|
3774
|
+
break;
|
|
3775
|
+
}
|
|
3776
|
+
case "install": {
|
|
3777
|
+
const skillName = args[1];
|
|
3778
|
+
await installRemoteSkill(skillName);
|
|
3779
|
+
break;
|
|
3780
|
+
}
|
|
3781
|
+
case "remove": {
|
|
3782
|
+
const skillName = args[1];
|
|
3783
|
+
await removeInstalledSkill(skillName);
|
|
3784
|
+
break;
|
|
3785
|
+
}
|
|
3786
|
+
case "update": {
|
|
3787
|
+
const skillName = args[1];
|
|
3788
|
+
await updateSkills(skillName);
|
|
3789
|
+
break;
|
|
3790
|
+
}
|
|
3791
|
+
case "info": {
|
|
3792
|
+
const skillName = args[1];
|
|
3793
|
+
await infoSkill(skillName);
|
|
3794
|
+
break;
|
|
3795
|
+
}
|
|
3796
|
+
default:
|
|
3797
|
+
process.stderr.write(`${red2("✗")} Unknown subcommand: ${bold2(subcommand)}
|
|
3798
|
+
`);
|
|
3799
|
+
process.stderr.write(` Available: ${bold2("list")}, ${bold2("install")}, ${bold2("remove")}, ${bold2("update")}, ${bold2("info")}
|
|
3800
|
+
`);
|
|
3801
|
+
process.exit(1);
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
var init_skills = __esm(() => {
|
|
3805
|
+
init_table();
|
|
3806
|
+
init_terminal();
|
|
3807
|
+
init_installer();
|
|
3808
|
+
init_lock();
|
|
3809
|
+
init_registry2();
|
|
3810
|
+
});
|
|
3811
|
+
|
|
3216
3812
|
// src/commands/config.ts
|
|
3217
3813
|
var exports_config = {};
|
|
3218
3814
|
__export(exports_config, {
|
|
@@ -3276,9 +3872,7 @@ ${bold2("Locus Configuration")}
|
|
|
3276
3872
|
},
|
|
3277
3873
|
{
|
|
3278
3874
|
title: "Sprint",
|
|
3279
|
-
entries: [
|
|
3280
|
-
["Stop on Failure", String(config.sprint.stopOnFailure)]
|
|
3281
|
-
]
|
|
3875
|
+
entries: [["Stop on Failure", String(config.sprint.stopOnFailure)]]
|
|
3282
3876
|
},
|
|
3283
3877
|
{
|
|
3284
3878
|
title: "Logging",
|
|
@@ -3351,16 +3945,16 @@ __export(exports_logs, {
|
|
|
3351
3945
|
logsCommand: () => logsCommand
|
|
3352
3946
|
});
|
|
3353
3947
|
import {
|
|
3354
|
-
existsSync as
|
|
3948
|
+
existsSync as existsSync12,
|
|
3355
3949
|
readdirSync as readdirSync2,
|
|
3356
|
-
readFileSync as
|
|
3950
|
+
readFileSync as readFileSync8,
|
|
3357
3951
|
statSync as statSync2,
|
|
3358
3952
|
unlinkSync as unlinkSync2
|
|
3359
3953
|
} from "node:fs";
|
|
3360
|
-
import { join as
|
|
3954
|
+
import { join as join12 } from "node:path";
|
|
3361
3955
|
async function logsCommand(cwd, options) {
|
|
3362
|
-
const logsDir =
|
|
3363
|
-
if (!
|
|
3956
|
+
const logsDir = join12(cwd, ".locus", "logs");
|
|
3957
|
+
if (!existsSync12(logsDir)) {
|
|
3364
3958
|
process.stderr.write(`${dim2("No logs found.")}
|
|
3365
3959
|
`);
|
|
3366
3960
|
return;
|
|
@@ -3380,7 +3974,7 @@ async function logsCommand(cwd, options) {
|
|
|
3380
3974
|
return viewLog(logFiles[0], options.level, options.lines ?? 50);
|
|
3381
3975
|
}
|
|
3382
3976
|
function viewLog(logFile, levelFilter, maxLines) {
|
|
3383
|
-
const content =
|
|
3977
|
+
const content = readFileSync8(logFile, "utf-8");
|
|
3384
3978
|
const lines = content.trim().split(`
|
|
3385
3979
|
`).filter(Boolean);
|
|
3386
3980
|
process.stderr.write(`
|
|
@@ -3415,9 +4009,9 @@ async function tailLog(logFile, levelFilter) {
|
|
|
3415
4009
|
process.stderr.write(`${bold2("Tailing:")} ${dim2(logFile)} ${dim2("(Ctrl+C to stop)")}
|
|
3416
4010
|
|
|
3417
4011
|
`);
|
|
3418
|
-
let lastSize =
|
|
3419
|
-
if (
|
|
3420
|
-
const content =
|
|
4012
|
+
let lastSize = existsSync12(logFile) ? statSync2(logFile).size : 0;
|
|
4013
|
+
if (existsSync12(logFile)) {
|
|
4014
|
+
const content = readFileSync8(logFile, "utf-8");
|
|
3421
4015
|
const lines = content.trim().split(`
|
|
3422
4016
|
`).filter(Boolean);
|
|
3423
4017
|
const recent = lines.slice(-10);
|
|
@@ -3435,12 +4029,12 @@ async function tailLog(logFile, levelFilter) {
|
|
|
3435
4029
|
}
|
|
3436
4030
|
return new Promise((resolve) => {
|
|
3437
4031
|
const interval = setInterval(() => {
|
|
3438
|
-
if (!
|
|
4032
|
+
if (!existsSync12(logFile))
|
|
3439
4033
|
return;
|
|
3440
4034
|
const currentSize = statSync2(logFile).size;
|
|
3441
4035
|
if (currentSize <= lastSize)
|
|
3442
4036
|
return;
|
|
3443
|
-
const content =
|
|
4037
|
+
const content = readFileSync8(logFile, "utf-8");
|
|
3444
4038
|
const allLines = content.trim().split(`
|
|
3445
4039
|
`).filter(Boolean);
|
|
3446
4040
|
const oldContent = content.slice(0, lastSize);
|
|
@@ -3495,7 +4089,7 @@ function cleanLogs(logsDir) {
|
|
|
3495
4089
|
`);
|
|
3496
4090
|
}
|
|
3497
4091
|
function getLogFiles(logsDir) {
|
|
3498
|
-
return readdirSync2(logsDir).filter((f) => f.startsWith("locus-") && f.endsWith(".log")).map((f) =>
|
|
4092
|
+
return readdirSync2(logsDir).filter((f) => f.startsWith("locus-") && f.endsWith(".log")).map((f) => join12(logsDir, f)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
|
|
3499
4093
|
}
|
|
3500
4094
|
function formatEntry(entry) {
|
|
3501
4095
|
const time = dim2(new Date(entry.ts).toLocaleTimeString());
|
|
@@ -3805,9 +4399,9 @@ var init_stream_renderer = __esm(() => {
|
|
|
3805
4399
|
|
|
3806
4400
|
// src/repl/clipboard.ts
|
|
3807
4401
|
import { execSync as execSync6 } from "node:child_process";
|
|
3808
|
-
import { existsSync as
|
|
4402
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync9 } from "node:fs";
|
|
3809
4403
|
import { tmpdir } from "node:os";
|
|
3810
|
-
import { join as
|
|
4404
|
+
import { join as join13 } from "node:path";
|
|
3811
4405
|
function readClipboardImage() {
|
|
3812
4406
|
if (process.platform === "darwin") {
|
|
3813
4407
|
return readMacOSClipboardImage();
|
|
@@ -3818,14 +4412,14 @@ function readClipboardImage() {
|
|
|
3818
4412
|
return null;
|
|
3819
4413
|
}
|
|
3820
4414
|
function ensureStableDir() {
|
|
3821
|
-
if (!
|
|
3822
|
-
|
|
4415
|
+
if (!existsSync13(STABLE_DIR)) {
|
|
4416
|
+
mkdirSync9(STABLE_DIR, { recursive: true });
|
|
3823
4417
|
}
|
|
3824
4418
|
}
|
|
3825
4419
|
function readMacOSClipboardImage() {
|
|
3826
4420
|
try {
|
|
3827
4421
|
ensureStableDir();
|
|
3828
|
-
const destPath =
|
|
4422
|
+
const destPath = join13(STABLE_DIR, `clipboard-${Date.now()}.png`);
|
|
3829
4423
|
const script = [
|
|
3830
4424
|
`set destPath to POSIX file "${destPath}"`,
|
|
3831
4425
|
"try",
|
|
@@ -3849,7 +4443,7 @@ function readMacOSClipboardImage() {
|
|
|
3849
4443
|
timeout: 5000,
|
|
3850
4444
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3851
4445
|
}).trim();
|
|
3852
|
-
if (result === "ok" &&
|
|
4446
|
+
if (result === "ok" && existsSync13(destPath)) {
|
|
3853
4447
|
return destPath;
|
|
3854
4448
|
}
|
|
3855
4449
|
} catch {}
|
|
@@ -3862,9 +4456,9 @@ function readLinuxClipboardImage() {
|
|
|
3862
4456
|
return null;
|
|
3863
4457
|
}
|
|
3864
4458
|
ensureStableDir();
|
|
3865
|
-
const destPath =
|
|
4459
|
+
const destPath = join13(STABLE_DIR, `clipboard-${Date.now()}.png`);
|
|
3866
4460
|
execSync6(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
|
|
3867
|
-
if (
|
|
4461
|
+
if (existsSync13(destPath)) {
|
|
3868
4462
|
return destPath;
|
|
3869
4463
|
}
|
|
3870
4464
|
} catch {}
|
|
@@ -3872,13 +4466,13 @@ function readLinuxClipboardImage() {
|
|
|
3872
4466
|
}
|
|
3873
4467
|
var STABLE_DIR;
|
|
3874
4468
|
var init_clipboard = __esm(() => {
|
|
3875
|
-
STABLE_DIR =
|
|
4469
|
+
STABLE_DIR = join13(tmpdir(), "locus-images");
|
|
3876
4470
|
});
|
|
3877
4471
|
|
|
3878
4472
|
// src/repl/image-detect.ts
|
|
3879
|
-
import { copyFileSync, existsSync as
|
|
4473
|
+
import { copyFileSync, existsSync as existsSync14, mkdirSync as mkdirSync10 } from "node:fs";
|
|
3880
4474
|
import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
|
|
3881
|
-
import { basename, extname, join as
|
|
4475
|
+
import { basename, extname, join as join14, resolve } from "node:path";
|
|
3882
4476
|
function detectImages(input) {
|
|
3883
4477
|
const detected = [];
|
|
3884
4478
|
const byResolved = new Map;
|
|
@@ -3972,15 +4566,15 @@ function collectReferencedAttachments(input, attachments) {
|
|
|
3972
4566
|
return dedupeByResolvedPath(selected);
|
|
3973
4567
|
}
|
|
3974
4568
|
function relocateImages(images, projectRoot) {
|
|
3975
|
-
const targetDir =
|
|
4569
|
+
const targetDir = join14(projectRoot, ".locus", "tmp", "images");
|
|
3976
4570
|
for (const img of images) {
|
|
3977
4571
|
if (!img.exists)
|
|
3978
4572
|
continue;
|
|
3979
4573
|
try {
|
|
3980
|
-
if (!
|
|
3981
|
-
|
|
4574
|
+
if (!existsSync14(targetDir)) {
|
|
4575
|
+
mkdirSync10(targetDir, { recursive: true });
|
|
3982
4576
|
}
|
|
3983
|
-
const dest =
|
|
4577
|
+
const dest = join14(targetDir, basename(img.stablePath));
|
|
3984
4578
|
copyFileSync(img.stablePath, dest);
|
|
3985
4579
|
img.stablePath = dest;
|
|
3986
4580
|
} catch {}
|
|
@@ -3992,7 +4586,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
|
|
|
3992
4586
|
return;
|
|
3993
4587
|
let resolved = stripQuotes(rawPath).replace(/\\ /g, " ");
|
|
3994
4588
|
if (resolved.startsWith("~/")) {
|
|
3995
|
-
resolved =
|
|
4589
|
+
resolved = join14(homedir3(), resolved.slice(2));
|
|
3996
4590
|
}
|
|
3997
4591
|
resolved = resolve(resolved);
|
|
3998
4592
|
const existing = byResolved.get(resolved);
|
|
@@ -4005,7 +4599,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
|
|
|
4005
4599
|
]);
|
|
4006
4600
|
return;
|
|
4007
4601
|
}
|
|
4008
|
-
const exists =
|
|
4602
|
+
const exists = existsSync14(resolved);
|
|
4009
4603
|
let stablePath = resolved;
|
|
4010
4604
|
if (exists) {
|
|
4011
4605
|
stablePath = copyToStable(resolved);
|
|
@@ -4059,10 +4653,10 @@ function dedupeByResolvedPath(images) {
|
|
|
4059
4653
|
}
|
|
4060
4654
|
function copyToStable(sourcePath) {
|
|
4061
4655
|
try {
|
|
4062
|
-
if (!
|
|
4063
|
-
|
|
4656
|
+
if (!existsSync14(STABLE_DIR2)) {
|
|
4657
|
+
mkdirSync10(STABLE_DIR2, { recursive: true });
|
|
4064
4658
|
}
|
|
4065
|
-
const dest =
|
|
4659
|
+
const dest = join14(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
|
|
4066
4660
|
copyFileSync(sourcePath, dest);
|
|
4067
4661
|
return dest;
|
|
4068
4662
|
} catch {
|
|
@@ -4082,7 +4676,7 @@ var init_image_detect = __esm(() => {
|
|
|
4082
4676
|
".tif",
|
|
4083
4677
|
".tiff"
|
|
4084
4678
|
]);
|
|
4085
|
-
STABLE_DIR2 =
|
|
4679
|
+
STABLE_DIR2 = join14(tmpdir2(), "locus-images");
|
|
4086
4680
|
PLACEHOLDER_ID_PATTERN = /\(locus:\/\/screenshot-(\d+)\)/g;
|
|
4087
4681
|
});
|
|
4088
4682
|
|
|
@@ -5091,21 +5685,21 @@ var init_claude = __esm(() => {
|
|
|
5091
5685
|
import { exec } from "node:child_process";
|
|
5092
5686
|
import {
|
|
5093
5687
|
cpSync,
|
|
5094
|
-
existsSync as
|
|
5095
|
-
mkdirSync as
|
|
5688
|
+
existsSync as existsSync15,
|
|
5689
|
+
mkdirSync as mkdirSync11,
|
|
5096
5690
|
mkdtempSync,
|
|
5097
5691
|
readdirSync as readdirSync3,
|
|
5098
|
-
readFileSync as
|
|
5099
|
-
rmSync,
|
|
5692
|
+
readFileSync as readFileSync9,
|
|
5693
|
+
rmSync as rmSync2,
|
|
5100
5694
|
statSync as statSync3
|
|
5101
5695
|
} from "node:fs";
|
|
5102
5696
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
5103
|
-
import { dirname as dirname3, join as
|
|
5697
|
+
import { dirname as dirname3, join as join15, relative } from "node:path";
|
|
5104
5698
|
import { promisify } from "node:util";
|
|
5105
5699
|
function parseIgnoreFile(filePath) {
|
|
5106
|
-
if (!
|
|
5700
|
+
if (!existsSync15(filePath))
|
|
5107
5701
|
return [];
|
|
5108
|
-
const content =
|
|
5702
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
5109
5703
|
const rules = [];
|
|
5110
5704
|
for (const rawLine of content.split(`
|
|
5111
5705
|
`)) {
|
|
@@ -5173,7 +5767,7 @@ function findIgnoredPaths(projectRoot, rules) {
|
|
|
5173
5767
|
for (const name of entries) {
|
|
5174
5768
|
if (SKIP_DIRS.has(name))
|
|
5175
5769
|
continue;
|
|
5176
|
-
const fullPath =
|
|
5770
|
+
const fullPath = join15(dir, name);
|
|
5177
5771
|
let stat = null;
|
|
5178
5772
|
try {
|
|
5179
5773
|
stat = statSync3(fullPath);
|
|
@@ -5207,7 +5801,7 @@ function findIgnoredPaths(projectRoot, rules) {
|
|
|
5207
5801
|
}
|
|
5208
5802
|
function backupIgnoredFiles(projectRoot) {
|
|
5209
5803
|
const log = getLogger();
|
|
5210
|
-
const ignorePath =
|
|
5804
|
+
const ignorePath = join15(projectRoot, ".sandboxignore");
|
|
5211
5805
|
const rules = parseIgnoreFile(ignorePath);
|
|
5212
5806
|
if (rules.length === 0)
|
|
5213
5807
|
return NOOP_BACKUP;
|
|
@@ -5216,7 +5810,7 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
5216
5810
|
return NOOP_BACKUP;
|
|
5217
5811
|
let backupDir;
|
|
5218
5812
|
try {
|
|
5219
|
-
backupDir = mkdtempSync(
|
|
5813
|
+
backupDir = mkdtempSync(join15(tmpdir3(), "locus-sandbox-backup-"));
|
|
5220
5814
|
} catch (err) {
|
|
5221
5815
|
log.debug("Failed to create sandbox backup dir", {
|
|
5222
5816
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -5226,9 +5820,9 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
5226
5820
|
const backed = [];
|
|
5227
5821
|
for (const src of paths) {
|
|
5228
5822
|
const rel = relative(projectRoot, src);
|
|
5229
|
-
const dest =
|
|
5823
|
+
const dest = join15(backupDir, rel);
|
|
5230
5824
|
try {
|
|
5231
|
-
|
|
5825
|
+
mkdirSync11(dirname3(dest), { recursive: true });
|
|
5232
5826
|
cpSync(src, dest, { recursive: true, preserveTimestamps: true });
|
|
5233
5827
|
backed.push({ src, dest });
|
|
5234
5828
|
} catch (err) {
|
|
@@ -5239,7 +5833,7 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
5239
5833
|
}
|
|
5240
5834
|
}
|
|
5241
5835
|
if (backed.length === 0) {
|
|
5242
|
-
|
|
5836
|
+
rmSync2(backupDir, { recursive: true, force: true });
|
|
5243
5837
|
return NOOP_BACKUP;
|
|
5244
5838
|
}
|
|
5245
5839
|
log.debug("Backed up sandbox-ignored files", {
|
|
@@ -5250,7 +5844,7 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
5250
5844
|
restore() {
|
|
5251
5845
|
for (const { src, dest } of backed) {
|
|
5252
5846
|
try {
|
|
5253
|
-
|
|
5847
|
+
mkdirSync11(dirname3(src), { recursive: true });
|
|
5254
5848
|
cpSync(dest, src, { recursive: true, preserveTimestamps: true });
|
|
5255
5849
|
} catch (err) {
|
|
5256
5850
|
log.debug("Failed to restore ignored file (potential data loss)", {
|
|
@@ -5260,7 +5854,7 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
5260
5854
|
}
|
|
5261
5855
|
}
|
|
5262
5856
|
try {
|
|
5263
|
-
|
|
5857
|
+
rmSync2(backupDir, { recursive: true, force: true });
|
|
5264
5858
|
} catch {}
|
|
5265
5859
|
log.debug("Restored sandbox-ignored files", { count: backed.length });
|
|
5266
5860
|
}
|
|
@@ -5268,7 +5862,7 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
5268
5862
|
}
|
|
5269
5863
|
async function enforceSandboxIgnore(sandboxName, projectRoot, containerWorkdir) {
|
|
5270
5864
|
const log = getLogger();
|
|
5271
|
-
const ignorePath =
|
|
5865
|
+
const ignorePath = join15(projectRoot, ".sandboxignore");
|
|
5272
5866
|
const rules = parseIgnoreFile(ignorePath);
|
|
5273
5867
|
if (rules.length === 0)
|
|
5274
5868
|
return;
|
|
@@ -5486,17 +6080,22 @@ class SandboxedClaudeRunner {
|
|
|
5486
6080
|
timer.unref();
|
|
5487
6081
|
}
|
|
5488
6082
|
async isSandboxRunning() {
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
6083
|
+
const { promisify: promisify2 } = await import("node:util");
|
|
6084
|
+
const { exec: exec2 } = await import("node:child_process");
|
|
6085
|
+
const execAsync2 = promisify2(exec2);
|
|
6086
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
6087
|
+
try {
|
|
6088
|
+
const { stdout } = await execAsync2("docker sandbox ls", {
|
|
6089
|
+
timeout: 15000
|
|
6090
|
+
});
|
|
6091
|
+
return stdout.includes(this.sandboxName);
|
|
6092
|
+
} catch {
|
|
6093
|
+
if (attempt < 2) {
|
|
6094
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
6095
|
+
}
|
|
6096
|
+
}
|
|
5499
6097
|
}
|
|
6098
|
+
return false;
|
|
5500
6099
|
}
|
|
5501
6100
|
}
|
|
5502
6101
|
function formatToolCall2(name, input) {
|
|
@@ -5897,17 +6496,22 @@ class SandboxedCodexRunner {
|
|
|
5897
6496
|
timer.unref();
|
|
5898
6497
|
}
|
|
5899
6498
|
async isSandboxRunning() {
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
6499
|
+
const { promisify: promisify2 } = await import("node:util");
|
|
6500
|
+
const { exec: exec2 } = await import("node:child_process");
|
|
6501
|
+
const execAsync2 = promisify2(exec2);
|
|
6502
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
6503
|
+
try {
|
|
6504
|
+
const { stdout } = await execAsync2("docker sandbox ls", {
|
|
6505
|
+
timeout: 15000
|
|
6506
|
+
});
|
|
6507
|
+
return stdout.includes(this.sandboxName);
|
|
6508
|
+
} catch {
|
|
6509
|
+
if (attempt < 2) {
|
|
6510
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
6511
|
+
}
|
|
6512
|
+
}
|
|
5910
6513
|
}
|
|
6514
|
+
return false;
|
|
5911
6515
|
}
|
|
5912
6516
|
async ensureCodexInstalled(name) {
|
|
5913
6517
|
const { promisify: promisify2 } = await import("node:util");
|
|
@@ -6164,115 +6768,6 @@ var init_run_ai = __esm(() => {
|
|
|
6164
6768
|
init_runner();
|
|
6165
6769
|
});
|
|
6166
6770
|
|
|
6167
|
-
// src/display/table.ts
|
|
6168
|
-
function renderTable(columns, rows, options = {}) {
|
|
6169
|
-
const {
|
|
6170
|
-
indent = 2,
|
|
6171
|
-
headerSeparator = true,
|
|
6172
|
-
maxRows = 0,
|
|
6173
|
-
emptyMessage = "No results."
|
|
6174
|
-
} = options;
|
|
6175
|
-
if (rows.length === 0) {
|
|
6176
|
-
return `${" ".repeat(indent)}${dim2(emptyMessage)}`;
|
|
6177
|
-
}
|
|
6178
|
-
const termWidth = getCapabilities().columns;
|
|
6179
|
-
const indentStr = " ".repeat(indent);
|
|
6180
|
-
const gap = 2;
|
|
6181
|
-
const formattedRows = rows.map((row) => {
|
|
6182
|
-
const formatted = {};
|
|
6183
|
-
for (const col of columns) {
|
|
6184
|
-
if (col.format) {
|
|
6185
|
-
formatted[col.key] = col.format(row[col.key], row);
|
|
6186
|
-
} else {
|
|
6187
|
-
const val = row[col.key];
|
|
6188
|
-
formatted[col.key] = val === null || val === undefined ? "" : String(val);
|
|
6189
|
-
}
|
|
6190
|
-
}
|
|
6191
|
-
return formatted;
|
|
6192
|
-
});
|
|
6193
|
-
const colWidths = columns.map((col, _i) => {
|
|
6194
|
-
const headerWidth = stripAnsi(col.header).length;
|
|
6195
|
-
const minWidth = col.minWidth ?? headerWidth;
|
|
6196
|
-
let maxContent = headerWidth;
|
|
6197
|
-
for (const row of formattedRows) {
|
|
6198
|
-
const cellWidth = stripAnsi(row[col.key] ?? "").length;
|
|
6199
|
-
if (cellWidth > maxContent)
|
|
6200
|
-
maxContent = cellWidth;
|
|
6201
|
-
}
|
|
6202
|
-
let width = Math.max(minWidth, maxContent);
|
|
6203
|
-
if (col.maxWidth && col.maxWidth > 0) {
|
|
6204
|
-
width = Math.min(width, col.maxWidth);
|
|
6205
|
-
}
|
|
6206
|
-
return width;
|
|
6207
|
-
});
|
|
6208
|
-
const totalWidth = indent + colWidths.reduce((s, w) => s + w, 0) + gap * (columns.length - 1);
|
|
6209
|
-
if (totalWidth > termWidth && columns.length > 1) {
|
|
6210
|
-
const overflow = totalWidth - termWidth;
|
|
6211
|
-
let widestIdx = 0;
|
|
6212
|
-
let widestSize = 0;
|
|
6213
|
-
for (let i = 0;i < columns.length; i++) {
|
|
6214
|
-
if (!columns[i].maxWidth && colWidths[i] > widestSize) {
|
|
6215
|
-
widestSize = colWidths[i];
|
|
6216
|
-
widestIdx = i;
|
|
6217
|
-
}
|
|
6218
|
-
}
|
|
6219
|
-
colWidths[widestIdx] = Math.max(10, colWidths[widestIdx] - overflow);
|
|
6220
|
-
}
|
|
6221
|
-
const lines = [];
|
|
6222
|
-
const headerParts = columns.map((col, i) => alignCell(bold2(col.header), colWidths[i], col.align ?? "left"));
|
|
6223
|
-
lines.push(`${indentStr}${headerParts.join(" ".repeat(gap))}`);
|
|
6224
|
-
if (headerSeparator) {
|
|
6225
|
-
const sep = columns.map((_, i) => gray2("─".repeat(colWidths[i]))).join(" ".repeat(gap));
|
|
6226
|
-
lines.push(`${indentStr}${sep}`);
|
|
6227
|
-
}
|
|
6228
|
-
const displayRows = maxRows > 0 ? formattedRows.slice(0, maxRows) : formattedRows;
|
|
6229
|
-
for (const row of displayRows) {
|
|
6230
|
-
const cellParts = columns.map((col, i) => {
|
|
6231
|
-
const raw = row[col.key] ?? "";
|
|
6232
|
-
return alignCell(raw, colWidths[i], col.align ?? "left");
|
|
6233
|
-
});
|
|
6234
|
-
lines.push(`${indentStr}${cellParts.join(" ".repeat(gap))}`);
|
|
6235
|
-
}
|
|
6236
|
-
if (maxRows > 0 && formattedRows.length > maxRows) {
|
|
6237
|
-
const remaining = formattedRows.length - maxRows;
|
|
6238
|
-
lines.push(`${indentStr}${dim2(`... and ${remaining} more`)}`);
|
|
6239
|
-
}
|
|
6240
|
-
return lines.join(`
|
|
6241
|
-
`);
|
|
6242
|
-
}
|
|
6243
|
-
function alignCell(text, width, align) {
|
|
6244
|
-
const visual = stripAnsi(text).length;
|
|
6245
|
-
if (visual > width) {
|
|
6246
|
-
const stripped = stripAnsi(text);
|
|
6247
|
-
return `${stripped.slice(0, width - 1)}…`;
|
|
6248
|
-
}
|
|
6249
|
-
const padding = width - visual;
|
|
6250
|
-
switch (align) {
|
|
6251
|
-
case "right":
|
|
6252
|
-
return " ".repeat(padding) + text;
|
|
6253
|
-
case "center": {
|
|
6254
|
-
const left = Math.floor(padding / 2);
|
|
6255
|
-
const right = padding - left;
|
|
6256
|
-
return " ".repeat(left) + text + " ".repeat(right);
|
|
6257
|
-
}
|
|
6258
|
-
default:
|
|
6259
|
-
return text + " ".repeat(padding);
|
|
6260
|
-
}
|
|
6261
|
-
}
|
|
6262
|
-
function renderDetails(entries, options = {}) {
|
|
6263
|
-
const { indent = 2, labelWidth: fixedWidth } = options;
|
|
6264
|
-
const indentStr = " ".repeat(indent);
|
|
6265
|
-
const labelWidth = fixedWidth ?? Math.max(...entries.map((e) => stripAnsi(e.label).length)) + 1;
|
|
6266
|
-
return entries.map((entry) => {
|
|
6267
|
-
const label = padEnd(dim2(`${entry.label}:`), labelWidth + 1);
|
|
6268
|
-
return `${indentStr}${label} ${entry.value}`;
|
|
6269
|
-
}).join(`
|
|
6270
|
-
`);
|
|
6271
|
-
}
|
|
6272
|
-
var init_table = __esm(() => {
|
|
6273
|
-
init_terminal();
|
|
6274
|
-
});
|
|
6275
|
-
|
|
6276
6771
|
// src/commands/issue.ts
|
|
6277
6772
|
var exports_issue = {};
|
|
6278
6773
|
__export(exports_issue, {
|
|
@@ -7533,8 +8028,8 @@ var init_sprint = __esm(() => {
|
|
|
7533
8028
|
|
|
7534
8029
|
// src/core/prompt-builder.ts
|
|
7535
8030
|
import { execSync as execSync9 } from "node:child_process";
|
|
7536
|
-
import { existsSync as
|
|
7537
|
-
import { join as
|
|
8031
|
+
import { existsSync as existsSync16, readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
|
|
8032
|
+
import { join as join16 } from "node:path";
|
|
7538
8033
|
function buildExecutionPrompt(ctx) {
|
|
7539
8034
|
const sections = [];
|
|
7540
8035
|
sections.push(buildSystemContext(ctx.projectRoot));
|
|
@@ -7564,7 +8059,7 @@ function buildFeedbackPrompt(ctx) {
|
|
|
7564
8059
|
}
|
|
7565
8060
|
function buildReplPrompt(userMessage, projectRoot, _config, previousMessages) {
|
|
7566
8061
|
const sections = [];
|
|
7567
|
-
const locusmd = readFileSafe(
|
|
8062
|
+
const locusmd = readFileSafe(join16(projectRoot, ".locus", "LOCUS.md"));
|
|
7568
8063
|
if (locusmd) {
|
|
7569
8064
|
sections.push(`<project-instructions>
|
|
7570
8065
|
${locusmd}
|
|
@@ -7593,7 +8088,7 @@ ${userMessage}
|
|
|
7593
8088
|
}
|
|
7594
8089
|
function buildSystemContext(projectRoot) {
|
|
7595
8090
|
const parts = [];
|
|
7596
|
-
const locusmd = readFileSafe(
|
|
8091
|
+
const locusmd = readFileSafe(join16(projectRoot, ".locus", "LOCUS.md"));
|
|
7597
8092
|
if (locusmd) {
|
|
7598
8093
|
parts.push(`<project-instructions>
|
|
7599
8094
|
${locusmd}
|
|
@@ -7601,12 +8096,12 @@ ${locusmd}
|
|
|
7601
8096
|
}
|
|
7602
8097
|
parts.push(`<past-learnings>
|
|
7603
8098
|
Past learnings are located in \`.locus/LEARNINGS.md\`.</past-learnings>`);
|
|
7604
|
-
const discussionsDir =
|
|
7605
|
-
if (
|
|
8099
|
+
const discussionsDir = join16(projectRoot, ".locus", "discussions");
|
|
8100
|
+
if (existsSync16(discussionsDir)) {
|
|
7606
8101
|
try {
|
|
7607
8102
|
const files = readdirSync4(discussionsDir).filter((f) => f.endsWith(".md")).slice(0, 3);
|
|
7608
8103
|
for (const file of files) {
|
|
7609
|
-
const content = readFileSafe(
|
|
8104
|
+
const content = readFileSafe(join16(discussionsDir, file));
|
|
7610
8105
|
if (content) {
|
|
7611
8106
|
const name = file.replace(".md", "");
|
|
7612
8107
|
parts.push(`<discussion name="${name}">
|
|
@@ -7759,9 +8254,9 @@ function buildFeedbackInstructions() {
|
|
|
7759
8254
|
}
|
|
7760
8255
|
function readFileSafe(path) {
|
|
7761
8256
|
try {
|
|
7762
|
-
if (!
|
|
8257
|
+
if (!existsSync16(path))
|
|
7763
8258
|
return null;
|
|
7764
|
-
return
|
|
8259
|
+
return readFileSync10(path, "utf-8");
|
|
7765
8260
|
} catch {
|
|
7766
8261
|
return null;
|
|
7767
8262
|
}
|
|
@@ -8239,7 +8734,7 @@ var init_commands = __esm(() => {
|
|
|
8239
8734
|
|
|
8240
8735
|
// src/repl/completions.ts
|
|
8241
8736
|
import { readdirSync as readdirSync5 } from "node:fs";
|
|
8242
|
-
import { basename as basename2, dirname as dirname4, join as
|
|
8737
|
+
import { basename as basename2, dirname as dirname4, join as join17 } from "node:path";
|
|
8243
8738
|
|
|
8244
8739
|
class SlashCommandCompletion {
|
|
8245
8740
|
commands;
|
|
@@ -8294,7 +8789,7 @@ class FilePathCompletion {
|
|
|
8294
8789
|
}
|
|
8295
8790
|
findMatches(partial) {
|
|
8296
8791
|
try {
|
|
8297
|
-
const dir = partial.includes("/") ?
|
|
8792
|
+
const dir = partial.includes("/") ? join17(this.projectRoot, dirname4(partial)) : this.projectRoot;
|
|
8298
8793
|
const prefix = basename2(partial);
|
|
8299
8794
|
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
8300
8795
|
return entries.filter((e) => {
|
|
@@ -8330,14 +8825,14 @@ class CombinedCompletion {
|
|
|
8330
8825
|
var init_completions = () => {};
|
|
8331
8826
|
|
|
8332
8827
|
// src/repl/input-history.ts
|
|
8333
|
-
import { existsSync as
|
|
8334
|
-
import { dirname as dirname5, join as
|
|
8828
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "node:fs";
|
|
8829
|
+
import { dirname as dirname5, join as join18 } from "node:path";
|
|
8335
8830
|
|
|
8336
8831
|
class InputHistory {
|
|
8337
8832
|
entries = [];
|
|
8338
8833
|
filePath;
|
|
8339
8834
|
constructor(projectRoot) {
|
|
8340
|
-
this.filePath =
|
|
8835
|
+
this.filePath = join18(projectRoot, ".locus", "sessions", ".input-history");
|
|
8341
8836
|
this.load();
|
|
8342
8837
|
}
|
|
8343
8838
|
add(text) {
|
|
@@ -8376,9 +8871,9 @@ class InputHistory {
|
|
|
8376
8871
|
}
|
|
8377
8872
|
load() {
|
|
8378
8873
|
try {
|
|
8379
|
-
if (!
|
|
8874
|
+
if (!existsSync17(this.filePath))
|
|
8380
8875
|
return;
|
|
8381
|
-
const content =
|
|
8876
|
+
const content = readFileSync11(this.filePath, "utf-8");
|
|
8382
8877
|
this.entries = content.split(`
|
|
8383
8878
|
`).map((line) => this.unescape(line)).filter(Boolean);
|
|
8384
8879
|
} catch {}
|
|
@@ -8386,12 +8881,12 @@ class InputHistory {
|
|
|
8386
8881
|
save() {
|
|
8387
8882
|
try {
|
|
8388
8883
|
const dir = dirname5(this.filePath);
|
|
8389
|
-
if (!
|
|
8390
|
-
|
|
8884
|
+
if (!existsSync17(dir)) {
|
|
8885
|
+
mkdirSync12(dir, { recursive: true });
|
|
8391
8886
|
}
|
|
8392
8887
|
const content = this.entries.map((e) => this.escape(e)).join(`
|
|
8393
8888
|
`);
|
|
8394
|
-
|
|
8889
|
+
writeFileSync9(this.filePath, content, "utf-8");
|
|
8395
8890
|
} catch {}
|
|
8396
8891
|
}
|
|
8397
8892
|
escape(text) {
|
|
@@ -8417,21 +8912,21 @@ var init_model_config = __esm(() => {
|
|
|
8417
8912
|
|
|
8418
8913
|
// src/repl/session-manager.ts
|
|
8419
8914
|
import {
|
|
8420
|
-
existsSync as
|
|
8421
|
-
mkdirSync as
|
|
8915
|
+
existsSync as existsSync18,
|
|
8916
|
+
mkdirSync as mkdirSync13,
|
|
8422
8917
|
readdirSync as readdirSync6,
|
|
8423
|
-
readFileSync as
|
|
8918
|
+
readFileSync as readFileSync12,
|
|
8424
8919
|
unlinkSync as unlinkSync3,
|
|
8425
|
-
writeFileSync as
|
|
8920
|
+
writeFileSync as writeFileSync10
|
|
8426
8921
|
} from "node:fs";
|
|
8427
|
-
import { basename as basename3, join as
|
|
8922
|
+
import { basename as basename3, join as join19 } from "node:path";
|
|
8428
8923
|
|
|
8429
8924
|
class SessionManager {
|
|
8430
8925
|
sessionsDir;
|
|
8431
8926
|
constructor(projectRoot) {
|
|
8432
|
-
this.sessionsDir =
|
|
8433
|
-
if (!
|
|
8434
|
-
|
|
8927
|
+
this.sessionsDir = join19(projectRoot, ".locus", "sessions");
|
|
8928
|
+
if (!existsSync18(this.sessionsDir)) {
|
|
8929
|
+
mkdirSync13(this.sessionsDir, { recursive: true });
|
|
8435
8930
|
}
|
|
8436
8931
|
}
|
|
8437
8932
|
create(options) {
|
|
@@ -8456,14 +8951,14 @@ class SessionManager {
|
|
|
8456
8951
|
}
|
|
8457
8952
|
isPersisted(sessionOrId) {
|
|
8458
8953
|
const sessionId = typeof sessionOrId === "string" ? sessionOrId : sessionOrId.id;
|
|
8459
|
-
return
|
|
8954
|
+
return existsSync18(this.getSessionPath(sessionId));
|
|
8460
8955
|
}
|
|
8461
8956
|
load(idOrPrefix) {
|
|
8462
8957
|
const files = this.listSessionFiles();
|
|
8463
8958
|
const exactPath = this.getSessionPath(idOrPrefix);
|
|
8464
|
-
if (
|
|
8959
|
+
if (existsSync18(exactPath)) {
|
|
8465
8960
|
try {
|
|
8466
|
-
return JSON.parse(
|
|
8961
|
+
return JSON.parse(readFileSync12(exactPath, "utf-8"));
|
|
8467
8962
|
} catch {
|
|
8468
8963
|
return null;
|
|
8469
8964
|
}
|
|
@@ -8471,7 +8966,7 @@ class SessionManager {
|
|
|
8471
8966
|
const matches = files.filter((f) => basename3(f, ".json").startsWith(idOrPrefix));
|
|
8472
8967
|
if (matches.length === 1) {
|
|
8473
8968
|
try {
|
|
8474
|
-
return JSON.parse(
|
|
8969
|
+
return JSON.parse(readFileSync12(matches[0], "utf-8"));
|
|
8475
8970
|
} catch {
|
|
8476
8971
|
return null;
|
|
8477
8972
|
}
|
|
@@ -8484,7 +8979,7 @@ class SessionManager {
|
|
|
8484
8979
|
save(session) {
|
|
8485
8980
|
session.updated = new Date().toISOString();
|
|
8486
8981
|
const path = this.getSessionPath(session.id);
|
|
8487
|
-
|
|
8982
|
+
writeFileSync10(path, `${JSON.stringify(session, null, 2)}
|
|
8488
8983
|
`, "utf-8");
|
|
8489
8984
|
}
|
|
8490
8985
|
addMessage(session, message) {
|
|
@@ -8496,7 +8991,7 @@ class SessionManager {
|
|
|
8496
8991
|
const sessions = [];
|
|
8497
8992
|
for (const file of files) {
|
|
8498
8993
|
try {
|
|
8499
|
-
const session = JSON.parse(
|
|
8994
|
+
const session = JSON.parse(readFileSync12(file, "utf-8"));
|
|
8500
8995
|
sessions.push({
|
|
8501
8996
|
id: session.id,
|
|
8502
8997
|
created: session.created,
|
|
@@ -8511,7 +9006,7 @@ class SessionManager {
|
|
|
8511
9006
|
}
|
|
8512
9007
|
delete(sessionId) {
|
|
8513
9008
|
const path = this.getSessionPath(sessionId);
|
|
8514
|
-
if (
|
|
9009
|
+
if (existsSync18(path)) {
|
|
8515
9010
|
unlinkSync3(path);
|
|
8516
9011
|
return true;
|
|
8517
9012
|
}
|
|
@@ -8523,7 +9018,7 @@ class SessionManager {
|
|
|
8523
9018
|
let pruned = 0;
|
|
8524
9019
|
const withStats = files.map((f) => {
|
|
8525
9020
|
try {
|
|
8526
|
-
const session = JSON.parse(
|
|
9021
|
+
const session = JSON.parse(readFileSync12(f, "utf-8"));
|
|
8527
9022
|
return { path: f, updated: new Date(session.updated).getTime() };
|
|
8528
9023
|
} catch {
|
|
8529
9024
|
return { path: f, updated: 0 };
|
|
@@ -8541,7 +9036,7 @@ class SessionManager {
|
|
|
8541
9036
|
const remaining = withStats.length - pruned;
|
|
8542
9037
|
if (remaining > MAX_SESSIONS) {
|
|
8543
9038
|
const toRemove = remaining - MAX_SESSIONS;
|
|
8544
|
-
const alive = withStats.filter((e) =>
|
|
9039
|
+
const alive = withStats.filter((e) => existsSync18(e.path));
|
|
8545
9040
|
for (let i = 0;i < toRemove && i < alive.length; i++) {
|
|
8546
9041
|
try {
|
|
8547
9042
|
unlinkSync3(alive[i].path);
|
|
@@ -8556,7 +9051,7 @@ class SessionManager {
|
|
|
8556
9051
|
}
|
|
8557
9052
|
listSessionFiles() {
|
|
8558
9053
|
try {
|
|
8559
|
-
return readdirSync6(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) =>
|
|
9054
|
+
return readdirSync6(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join19(this.sessionsDir, f));
|
|
8560
9055
|
} catch {
|
|
8561
9056
|
return [];
|
|
8562
9057
|
}
|
|
@@ -8565,7 +9060,7 @@ class SessionManager {
|
|
|
8565
9060
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
8566
9061
|
}
|
|
8567
9062
|
getSessionPath(sessionId) {
|
|
8568
|
-
return
|
|
9063
|
+
return join19(this.sessionsDir, `${sessionId}.json`);
|
|
8569
9064
|
}
|
|
8570
9065
|
}
|
|
8571
9066
|
var MAX_SESSIONS = 50, SESSION_MAX_AGE_MS;
|
|
@@ -8576,11 +9071,11 @@ var init_session_manager = __esm(() => {
|
|
|
8576
9071
|
|
|
8577
9072
|
// src/repl/voice.ts
|
|
8578
9073
|
import { execSync as execSync11, spawn as spawn6 } from "node:child_process";
|
|
8579
|
-
import { existsSync as
|
|
9074
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync14, unlinkSync as unlinkSync4 } from "node:fs";
|
|
8580
9075
|
import { cpus, homedir as homedir4, platform, tmpdir as tmpdir4 } from "node:os";
|
|
8581
|
-
import { join as
|
|
9076
|
+
import { join as join20 } from "node:path";
|
|
8582
9077
|
function getWhisperModelPath() {
|
|
8583
|
-
return
|
|
9078
|
+
return join20(WHISPER_MODELS_DIR, `ggml-${WHISPER_MODEL}.bin`);
|
|
8584
9079
|
}
|
|
8585
9080
|
function commandExists(cmd) {
|
|
8586
9081
|
try {
|
|
@@ -8598,16 +9093,16 @@ function findWhisperBinary() {
|
|
|
8598
9093
|
return name;
|
|
8599
9094
|
}
|
|
8600
9095
|
for (const name of candidates) {
|
|
8601
|
-
const fullPath =
|
|
8602
|
-
if (
|
|
9096
|
+
const fullPath = join20(LOCUS_BIN_DIR, name);
|
|
9097
|
+
if (existsSync19(fullPath))
|
|
8603
9098
|
return fullPath;
|
|
8604
9099
|
}
|
|
8605
9100
|
if (platform() === "darwin") {
|
|
8606
9101
|
const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
|
|
8607
9102
|
for (const dir of brewDirs) {
|
|
8608
9103
|
for (const name of candidates) {
|
|
8609
|
-
const fullPath =
|
|
8610
|
-
if (
|
|
9104
|
+
const fullPath = join20(dir, name);
|
|
9105
|
+
if (existsSync19(fullPath))
|
|
8611
9106
|
return fullPath;
|
|
8612
9107
|
}
|
|
8613
9108
|
}
|
|
@@ -8622,11 +9117,11 @@ function findSoxRecBinary() {
|
|
|
8622
9117
|
if (platform() === "darwin") {
|
|
8623
9118
|
const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
|
|
8624
9119
|
for (const dir of brewDirs) {
|
|
8625
|
-
const recPath =
|
|
8626
|
-
if (
|
|
9120
|
+
const recPath = join20(dir, "rec");
|
|
9121
|
+
if (existsSync19(recPath))
|
|
8627
9122
|
return recPath;
|
|
8628
|
-
const soxPath =
|
|
8629
|
-
if (
|
|
9123
|
+
const soxPath = join20(dir, "sox");
|
|
9124
|
+
if (existsSync19(soxPath))
|
|
8630
9125
|
return soxPath;
|
|
8631
9126
|
}
|
|
8632
9127
|
}
|
|
@@ -8635,7 +9130,7 @@ function findSoxRecBinary() {
|
|
|
8635
9130
|
function checkDependencies() {
|
|
8636
9131
|
const soxBinary = findSoxRecBinary();
|
|
8637
9132
|
const whisperBinary = findWhisperBinary();
|
|
8638
|
-
const modelDownloaded =
|
|
9133
|
+
const modelDownloaded = existsSync19(getWhisperModelPath());
|
|
8639
9134
|
return {
|
|
8640
9135
|
sox: soxBinary !== null,
|
|
8641
9136
|
whisper: whisperBinary !== null,
|
|
@@ -8789,19 +9284,19 @@ function ensureBuildDeps(pm) {
|
|
|
8789
9284
|
}
|
|
8790
9285
|
function buildWhisperFromSource(pm) {
|
|
8791
9286
|
const out = process.stderr;
|
|
8792
|
-
const buildDir =
|
|
9287
|
+
const buildDir = join20(tmpdir4(), `locus-whisper-build-${process.pid}`);
|
|
8793
9288
|
if (!ensureBuildDeps(pm)) {
|
|
8794
9289
|
out.write(` ${red2("✗")} Could not install build tools (cmake, g++, git).
|
|
8795
9290
|
`);
|
|
8796
9291
|
return false;
|
|
8797
9292
|
}
|
|
8798
9293
|
try {
|
|
8799
|
-
|
|
8800
|
-
|
|
9294
|
+
mkdirSync14(buildDir, { recursive: true });
|
|
9295
|
+
mkdirSync14(LOCUS_BIN_DIR, { recursive: true });
|
|
8801
9296
|
out.write(` ${dim2("Cloning whisper.cpp...")}
|
|
8802
9297
|
`);
|
|
8803
|
-
execSync11(`git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git "${
|
|
8804
|
-
const srcDir =
|
|
9298
|
+
execSync11(`git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git "${join20(buildDir, "whisper.cpp")}"`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
|
|
9299
|
+
const srcDir = join20(buildDir, "whisper.cpp");
|
|
8805
9300
|
const numCpus = cpus().length || 2;
|
|
8806
9301
|
out.write(` ${dim2("Building whisper.cpp (this may take a few minutes)...")}
|
|
8807
9302
|
`);
|
|
@@ -8815,13 +9310,13 @@ function buildWhisperFromSource(pm) {
|
|
|
8815
9310
|
stdio: ["pipe", "pipe", "pipe"],
|
|
8816
9311
|
timeout: 600000
|
|
8817
9312
|
});
|
|
8818
|
-
const destPath =
|
|
9313
|
+
const destPath = join20(LOCUS_BIN_DIR, "whisper-cli");
|
|
8819
9314
|
const binaryCandidates = [
|
|
8820
|
-
|
|
8821
|
-
|
|
9315
|
+
join20(srcDir, "build", "bin", "whisper-cli"),
|
|
9316
|
+
join20(srcDir, "build", "bin", "main")
|
|
8822
9317
|
];
|
|
8823
9318
|
for (const candidate of binaryCandidates) {
|
|
8824
|
-
if (
|
|
9319
|
+
if (existsSync19(candidate)) {
|
|
8825
9320
|
execSync11(`cp "${candidate}" "${destPath}" && chmod +x "${destPath}"`, {
|
|
8826
9321
|
stdio: "pipe"
|
|
8827
9322
|
});
|
|
@@ -8900,9 +9395,9 @@ ${bold2("Installing voice dependencies...")}
|
|
|
8900
9395
|
}
|
|
8901
9396
|
function downloadModel() {
|
|
8902
9397
|
const modelPath = getWhisperModelPath();
|
|
8903
|
-
if (
|
|
9398
|
+
if (existsSync19(modelPath))
|
|
8904
9399
|
return true;
|
|
8905
|
-
|
|
9400
|
+
mkdirSync14(WHISPER_MODELS_DIR, { recursive: true });
|
|
8906
9401
|
const url = `https://huggingface.co/ggerganov/whisper.cpp/resolve/main/${WHISPER_MODEL === "base.en" ? "ggml-base.en.bin" : `ggml-${WHISPER_MODEL}.bin`}`;
|
|
8907
9402
|
process.stderr.write(`${dim2("Downloading whisper model")} ${bold2(WHISPER_MODEL)} ${dim2("(~150MB)...")}
|
|
8908
9403
|
`);
|
|
@@ -8947,7 +9442,7 @@ class VoiceController {
|
|
|
8947
9442
|
onStateChange;
|
|
8948
9443
|
constructor(options) {
|
|
8949
9444
|
this.onStateChange = options.onStateChange;
|
|
8950
|
-
this.tempFile =
|
|
9445
|
+
this.tempFile = join20(tmpdir4(), `locus-voice-${process.pid}.wav`);
|
|
8951
9446
|
this.deps = checkDependencies();
|
|
8952
9447
|
}
|
|
8953
9448
|
getState() {
|
|
@@ -9015,7 +9510,7 @@ ${red2("✗")} Recording failed: ${err.message}\r
|
|
|
9015
9510
|
this.recordProcess = null;
|
|
9016
9511
|
this.setState("idle");
|
|
9017
9512
|
await sleep2(200);
|
|
9018
|
-
if (!
|
|
9513
|
+
if (!existsSync19(this.tempFile)) {
|
|
9019
9514
|
return null;
|
|
9020
9515
|
}
|
|
9021
9516
|
try {
|
|
@@ -9106,8 +9601,8 @@ function sleep2(ms) {
|
|
|
9106
9601
|
var WHISPER_MODEL = "base.en", WHISPER_MODELS_DIR, LOCUS_BIN_DIR;
|
|
9107
9602
|
var init_voice = __esm(() => {
|
|
9108
9603
|
init_terminal();
|
|
9109
|
-
WHISPER_MODELS_DIR =
|
|
9110
|
-
LOCUS_BIN_DIR =
|
|
9604
|
+
WHISPER_MODELS_DIR = join20(homedir4(), ".locus", "whisper-models");
|
|
9605
|
+
LOCUS_BIN_DIR = join20(homedir4(), ".locus", "bin");
|
|
9111
9606
|
});
|
|
9112
9607
|
|
|
9113
9608
|
// src/repl/repl.ts
|
|
@@ -9619,8 +10114,8 @@ var init_exec = __esm(() => {
|
|
|
9619
10114
|
|
|
9620
10115
|
// src/core/submodule.ts
|
|
9621
10116
|
import { execSync as execSync13 } from "node:child_process";
|
|
9622
|
-
import { existsSync as
|
|
9623
|
-
import { join as
|
|
10117
|
+
import { existsSync as existsSync20 } from "node:fs";
|
|
10118
|
+
import { join as join21 } from "node:path";
|
|
9624
10119
|
function git2(args, cwd) {
|
|
9625
10120
|
return execSync13(`git ${args}`, {
|
|
9626
10121
|
cwd,
|
|
@@ -9636,7 +10131,7 @@ function gitSafe(args, cwd) {
|
|
|
9636
10131
|
}
|
|
9637
10132
|
}
|
|
9638
10133
|
function hasSubmodules(cwd) {
|
|
9639
|
-
return
|
|
10134
|
+
return existsSync20(join21(cwd, ".gitmodules"));
|
|
9640
10135
|
}
|
|
9641
10136
|
function listSubmodules(cwd) {
|
|
9642
10137
|
if (!hasSubmodules(cwd))
|
|
@@ -9656,7 +10151,7 @@ function listSubmodules(cwd) {
|
|
|
9656
10151
|
continue;
|
|
9657
10152
|
submodules.push({
|
|
9658
10153
|
path,
|
|
9659
|
-
absolutePath:
|
|
10154
|
+
absolutePath: join21(cwd, path),
|
|
9660
10155
|
dirty
|
|
9661
10156
|
});
|
|
9662
10157
|
}
|
|
@@ -9669,7 +10164,7 @@ function getDirtySubmodules(cwd) {
|
|
|
9669
10164
|
const submodules = listSubmodules(cwd);
|
|
9670
10165
|
const dirty = [];
|
|
9671
10166
|
for (const sub of submodules) {
|
|
9672
|
-
if (!
|
|
10167
|
+
if (!existsSync20(sub.absolutePath))
|
|
9673
10168
|
continue;
|
|
9674
10169
|
const status = gitSafe("status --porcelain", sub.absolutePath);
|
|
9675
10170
|
if (status && status.trim().length > 0) {
|
|
@@ -9756,7 +10251,7 @@ function pushSubmoduleBranches(cwd) {
|
|
|
9756
10251
|
const log = getLogger();
|
|
9757
10252
|
const submodules = listSubmodules(cwd);
|
|
9758
10253
|
for (const sub of submodules) {
|
|
9759
|
-
if (!
|
|
10254
|
+
if (!existsSync20(sub.absolutePath))
|
|
9760
10255
|
continue;
|
|
9761
10256
|
const branch = gitSafe("rev-parse --abbrev-ref HEAD", sub.absolutePath)?.trim();
|
|
9762
10257
|
if (!branch || branch === "HEAD")
|
|
@@ -10005,9 +10500,32 @@ ${submoduleSummary}`;
|
|
|
10005
10500
|
|
|
10006
10501
|
\uD83E\uDD16 Automated by [Locus](https://github.com/asgarovf/locusai)`;
|
|
10007
10502
|
const prTitle = `${issue.title} (#${issue.number})`;
|
|
10008
|
-
|
|
10009
|
-
|
|
10503
|
+
let prNumber;
|
|
10504
|
+
try {
|
|
10505
|
+
const existing = execSync14(`gh pr list --head ${currentBranch} --base ${config.agent.baseBranch} --json number --limit 1`, { cwd: projectRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
10506
|
+
const parsed = JSON.parse(existing);
|
|
10507
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
10508
|
+
prNumber = parsed[0].number;
|
|
10509
|
+
}
|
|
10510
|
+
} catch {}
|
|
10511
|
+
if (prNumber) {
|
|
10512
|
+
try {
|
|
10513
|
+
execSync14(`gh pr edit ${prNumber} --title ${JSON.stringify(prTitle)} --body-file -`, {
|
|
10514
|
+
input: prBody,
|
|
10515
|
+
cwd: projectRoot,
|
|
10516
|
+
encoding: "utf-8",
|
|
10517
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
10518
|
+
});
|
|
10519
|
+
process.stderr.write(` ${green("✓")} Updated existing PR #${prNumber}
|
|
10520
|
+
`);
|
|
10521
|
+
} catch (editErr) {
|
|
10522
|
+
getLogger().warn(`Failed to update PR #${prNumber}: ${editErr}`);
|
|
10523
|
+
}
|
|
10524
|
+
} else {
|
|
10525
|
+
prNumber = createPR(prTitle, prBody, currentBranch, config.agent.baseBranch, { cwd: projectRoot });
|
|
10526
|
+
process.stderr.write(` ${green("✓")} Created PR #${prNumber}
|
|
10010
10527
|
`);
|
|
10528
|
+
}
|
|
10011
10529
|
return prNumber;
|
|
10012
10530
|
} catch (e) {
|
|
10013
10531
|
getLogger().warn(`Failed to create PR: ${e}`);
|
|
@@ -10173,15 +10691,15 @@ var init_conflict = __esm(() => {
|
|
|
10173
10691
|
|
|
10174
10692
|
// src/core/run-state.ts
|
|
10175
10693
|
import {
|
|
10176
|
-
existsSync as
|
|
10177
|
-
mkdirSync as
|
|
10178
|
-
readFileSync as
|
|
10694
|
+
existsSync as existsSync21,
|
|
10695
|
+
mkdirSync as mkdirSync15,
|
|
10696
|
+
readFileSync as readFileSync13,
|
|
10179
10697
|
unlinkSync as unlinkSync5,
|
|
10180
|
-
writeFileSync as
|
|
10698
|
+
writeFileSync as writeFileSync11
|
|
10181
10699
|
} from "node:fs";
|
|
10182
|
-
import { dirname as dirname6, join as
|
|
10700
|
+
import { dirname as dirname6, join as join22 } from "node:path";
|
|
10183
10701
|
function getRunStateDir(projectRoot) {
|
|
10184
|
-
return
|
|
10702
|
+
return join22(projectRoot, ".locus", "run-state");
|
|
10185
10703
|
}
|
|
10186
10704
|
function sprintSlug(name) {
|
|
10187
10705
|
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -10189,16 +10707,16 @@ function sprintSlug(name) {
|
|
|
10189
10707
|
function getRunStatePath(projectRoot, sprintName) {
|
|
10190
10708
|
const dir = getRunStateDir(projectRoot);
|
|
10191
10709
|
if (sprintName) {
|
|
10192
|
-
return
|
|
10710
|
+
return join22(dir, `${sprintSlug(sprintName)}.json`);
|
|
10193
10711
|
}
|
|
10194
|
-
return
|
|
10712
|
+
return join22(dir, "_parallel.json");
|
|
10195
10713
|
}
|
|
10196
10714
|
function loadRunState(projectRoot, sprintName) {
|
|
10197
10715
|
const path = getRunStatePath(projectRoot, sprintName);
|
|
10198
|
-
if (!
|
|
10716
|
+
if (!existsSync21(path))
|
|
10199
10717
|
return null;
|
|
10200
10718
|
try {
|
|
10201
|
-
return JSON.parse(
|
|
10719
|
+
return JSON.parse(readFileSync13(path, "utf-8"));
|
|
10202
10720
|
} catch {
|
|
10203
10721
|
getLogger().warn("Corrupted run state file, ignoring");
|
|
10204
10722
|
return null;
|
|
@@ -10207,15 +10725,15 @@ function loadRunState(projectRoot, sprintName) {
|
|
|
10207
10725
|
function saveRunState(projectRoot, state) {
|
|
10208
10726
|
const path = getRunStatePath(projectRoot, state.sprint);
|
|
10209
10727
|
const dir = dirname6(path);
|
|
10210
|
-
if (!
|
|
10211
|
-
|
|
10728
|
+
if (!existsSync21(dir)) {
|
|
10729
|
+
mkdirSync15(dir, { recursive: true });
|
|
10212
10730
|
}
|
|
10213
|
-
|
|
10731
|
+
writeFileSync11(path, `${JSON.stringify(state, null, 2)}
|
|
10214
10732
|
`, "utf-8");
|
|
10215
10733
|
}
|
|
10216
10734
|
function clearRunState(projectRoot, sprintName) {
|
|
10217
10735
|
const path = getRunStatePath(projectRoot, sprintName);
|
|
10218
|
-
if (
|
|
10736
|
+
if (existsSync21(path)) {
|
|
10219
10737
|
unlinkSync5(path);
|
|
10220
10738
|
}
|
|
10221
10739
|
}
|
|
@@ -10371,8 +10889,27 @@ __export(exports_worktree, {
|
|
|
10371
10889
|
cleanupStaleWorktrees: () => cleanupStaleWorktrees
|
|
10372
10890
|
});
|
|
10373
10891
|
import { execSync as execSync16 } from "node:child_process";
|
|
10374
|
-
import {
|
|
10375
|
-
|
|
10892
|
+
import {
|
|
10893
|
+
cpSync as cpSync2,
|
|
10894
|
+
existsSync as existsSync22,
|
|
10895
|
+
mkdirSync as mkdirSync16,
|
|
10896
|
+
readdirSync as readdirSync7,
|
|
10897
|
+
realpathSync,
|
|
10898
|
+
statSync as statSync4
|
|
10899
|
+
} from "node:fs";
|
|
10900
|
+
import { join as join23 } from "node:path";
|
|
10901
|
+
function copyLocusDir(projectRoot, worktreePath) {
|
|
10902
|
+
const srcLocus = join23(projectRoot, ".locus");
|
|
10903
|
+
if (!existsSync22(srcLocus))
|
|
10904
|
+
return;
|
|
10905
|
+
const destLocus = join23(worktreePath, ".locus");
|
|
10906
|
+
mkdirSync16(destLocus, { recursive: true });
|
|
10907
|
+
for (const entry of readdirSync7(srcLocus)) {
|
|
10908
|
+
if (entry === "worktrees")
|
|
10909
|
+
continue;
|
|
10910
|
+
cpSync2(join23(srcLocus, entry), join23(destLocus, entry), { recursive: true });
|
|
10911
|
+
}
|
|
10912
|
+
}
|
|
10376
10913
|
function git4(args, cwd) {
|
|
10377
10914
|
return execSync16(`git ${args}`, {
|
|
10378
10915
|
cwd,
|
|
@@ -10388,13 +10925,13 @@ function gitSafe3(args, cwd) {
|
|
|
10388
10925
|
}
|
|
10389
10926
|
}
|
|
10390
10927
|
function getWorktreeDir(projectRoot) {
|
|
10391
|
-
return
|
|
10928
|
+
return join23(projectRoot, ".locus", "worktrees");
|
|
10392
10929
|
}
|
|
10393
10930
|
function getWorktreePath(projectRoot, issueNumber) {
|
|
10394
|
-
return
|
|
10931
|
+
return join23(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
|
|
10395
10932
|
}
|
|
10396
10933
|
function getSprintWorktreePath(projectRoot, sprintSlug2) {
|
|
10397
|
-
return
|
|
10934
|
+
return join23(getWorktreeDir(projectRoot), `sprint-${sprintSlug2}`);
|
|
10398
10935
|
}
|
|
10399
10936
|
function generateBranchName(issueNumber) {
|
|
10400
10937
|
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
@@ -10407,7 +10944,7 @@ function createSprintWorktree(projectRoot, sprintName, baseBranch) {
|
|
|
10407
10944
|
const log = getLogger();
|
|
10408
10945
|
const slug = sprintSlug2(sprintName);
|
|
10409
10946
|
const worktreePath = getSprintWorktreePath(projectRoot, slug);
|
|
10410
|
-
if (
|
|
10947
|
+
if (existsSync22(worktreePath)) {
|
|
10411
10948
|
log.verbose(`Sprint worktree already exists for "${sprintName}"`);
|
|
10412
10949
|
const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/sprint-${slug}`;
|
|
10413
10950
|
return { path: worktreePath, branch: existingBranch };
|
|
@@ -10416,6 +10953,7 @@ function createSprintWorktree(projectRoot, sprintName, baseBranch) {
|
|
|
10416
10953
|
const branch = `locus/sprint-${slug}-${randomSuffix}`;
|
|
10417
10954
|
git4(`worktree add ${JSON.stringify(worktreePath)} -b ${branch} ${baseBranch}`, projectRoot);
|
|
10418
10955
|
initSubmodules(worktreePath);
|
|
10956
|
+
copyLocusDir(projectRoot, worktreePath);
|
|
10419
10957
|
log.info(`Created sprint worktree for "${sprintName}"`, {
|
|
10420
10958
|
path: worktreePath,
|
|
10421
10959
|
branch,
|
|
@@ -10427,7 +10965,7 @@ function removeSprintWorktree(projectRoot, sprintName) {
|
|
|
10427
10965
|
const log = getLogger();
|
|
10428
10966
|
const slug = sprintSlug2(sprintName);
|
|
10429
10967
|
const worktreePath = getSprintWorktreePath(projectRoot, slug);
|
|
10430
|
-
if (!
|
|
10968
|
+
if (!existsSync22(worktreePath)) {
|
|
10431
10969
|
log.verbose(`Sprint worktree for "${sprintName}" does not exist`);
|
|
10432
10970
|
return;
|
|
10433
10971
|
}
|
|
@@ -10457,7 +10995,7 @@ function getWorktreeBranch(worktreePath) {
|
|
|
10457
10995
|
function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
10458
10996
|
const log = getLogger();
|
|
10459
10997
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
10460
|
-
if (
|
|
10998
|
+
if (existsSync22(worktreePath)) {
|
|
10461
10999
|
log.verbose(`Worktree already exists for issue #${issueNumber}`);
|
|
10462
11000
|
const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
|
|
10463
11001
|
return {
|
|
@@ -10470,6 +11008,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
|
10470
11008
|
const branch = generateBranchName(issueNumber);
|
|
10471
11009
|
git4(`worktree add ${JSON.stringify(worktreePath)} -b ${branch} ${baseBranch}`, projectRoot);
|
|
10472
11010
|
initSubmodules(worktreePath);
|
|
11011
|
+
copyLocusDir(projectRoot, worktreePath);
|
|
10473
11012
|
log.info(`Created worktree for issue #${issueNumber}`, {
|
|
10474
11013
|
path: worktreePath,
|
|
10475
11014
|
branch,
|
|
@@ -10485,7 +11024,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
|
10485
11024
|
function removeWorktree(projectRoot, issueNumber) {
|
|
10486
11025
|
const log = getLogger();
|
|
10487
11026
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
10488
|
-
if (!
|
|
11027
|
+
if (!existsSync22(worktreePath)) {
|
|
10489
11028
|
log.verbose(`Worktree for issue #${issueNumber} does not exist`);
|
|
10490
11029
|
return;
|
|
10491
11030
|
}
|
|
@@ -10504,7 +11043,7 @@ function removeWorktree(projectRoot, issueNumber) {
|
|
|
10504
11043
|
function listWorktrees(projectRoot) {
|
|
10505
11044
|
const log = getLogger();
|
|
10506
11045
|
const worktreeDir = getWorktreeDir(projectRoot);
|
|
10507
|
-
if (!
|
|
11046
|
+
if (!existsSync22(worktreeDir)) {
|
|
10508
11047
|
return [];
|
|
10509
11048
|
}
|
|
10510
11049
|
const entries = readdirSync7(worktreeDir).filter((entry) => entry.startsWith("issue-"));
|
|
@@ -10524,7 +11063,7 @@ function listWorktrees(projectRoot) {
|
|
|
10524
11063
|
if (!match)
|
|
10525
11064
|
continue;
|
|
10526
11065
|
const issueNumber = Number.parseInt(match[1], 10);
|
|
10527
|
-
const path =
|
|
11066
|
+
const path = join23(worktreeDir, entry);
|
|
10528
11067
|
const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
|
|
10529
11068
|
let resolvedPath;
|
|
10530
11069
|
try {
|
|
@@ -10564,7 +11103,7 @@ function cleanupStaleWorktrees(projectRoot) {
|
|
|
10564
11103
|
}
|
|
10565
11104
|
function pushWorktreeBranch(projectRoot, issueNumber) {
|
|
10566
11105
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
10567
|
-
if (!
|
|
11106
|
+
if (!existsSync22(worktreePath)) {
|
|
10568
11107
|
throw new Error(`Worktree for issue #${issueNumber} does not exist`);
|
|
10569
11108
|
}
|
|
10570
11109
|
const branch = getWorktreeBranch(worktreePath);
|
|
@@ -10576,14 +11115,14 @@ function pushWorktreeBranch(projectRoot, issueNumber) {
|
|
|
10576
11115
|
}
|
|
10577
11116
|
function hasWorktreeChanges(projectRoot, issueNumber) {
|
|
10578
11117
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
10579
|
-
if (!
|
|
11118
|
+
if (!existsSync22(worktreePath))
|
|
10580
11119
|
return false;
|
|
10581
11120
|
const status = gitSafe3("status --porcelain", worktreePath);
|
|
10582
11121
|
return status !== null && status.trim().length > 0;
|
|
10583
11122
|
}
|
|
10584
11123
|
function getWorktreeAge(projectRoot, issueNumber) {
|
|
10585
11124
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
10586
|
-
if (!
|
|
11125
|
+
if (!existsSync22(worktreePath))
|
|
10587
11126
|
return 0;
|
|
10588
11127
|
try {
|
|
10589
11128
|
const stat = statSync4(worktreePath);
|
|
@@ -10603,8 +11142,8 @@ __export(exports_run, {
|
|
|
10603
11142
|
runCommand: () => runCommand
|
|
10604
11143
|
});
|
|
10605
11144
|
import { execSync as execSync17 } from "node:child_process";
|
|
10606
|
-
import { existsSync as
|
|
10607
|
-
import { join as
|
|
11145
|
+
import { existsSync as existsSync23 } from "node:fs";
|
|
11146
|
+
import { join as join24 } from "node:path";
|
|
10608
11147
|
function resolveExecutionContext(config, modelOverride) {
|
|
10609
11148
|
const model = modelOverride ?? config.ai.model;
|
|
10610
11149
|
const provider = inferProviderFromModel(model) ?? config.ai.provider;
|
|
@@ -11148,8 +11687,8 @@ async function handleResume(projectRoot, config, sandboxed, flags) {
|
|
|
11148
11687
|
const sprintsToResume = [];
|
|
11149
11688
|
try {
|
|
11150
11689
|
const { readdirSync: readdirSync8 } = await import("node:fs");
|
|
11151
|
-
const runStateDir =
|
|
11152
|
-
if (
|
|
11690
|
+
const runStateDir = join24(projectRoot, ".locus", "run-state");
|
|
11691
|
+
if (existsSync23(runStateDir)) {
|
|
11153
11692
|
const files = readdirSync8(runStateDir).filter((f) => f.endsWith(".json"));
|
|
11154
11693
|
for (const file of files) {
|
|
11155
11694
|
const sprintName = file === "_parallel.json" ? undefined : file.replace(/\.json$/, "");
|
|
@@ -11193,9 +11732,9 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}${state.sprint ? ` ($
|
|
|
11193
11732
|
let workDir = projectRoot;
|
|
11194
11733
|
if (state.type === "sprint" && state.sprint) {
|
|
11195
11734
|
const { getSprintWorktreePath: getSprintWorktreePath2, sprintSlug: sprintSlug3 } = await Promise.resolve().then(() => (init_worktree(), exports_worktree));
|
|
11196
|
-
const { existsSync:
|
|
11735
|
+
const { existsSync: existsSync24 } = await import("node:fs");
|
|
11197
11736
|
const wtPath = getSprintWorktreePath2(projectRoot, sprintSlug3(state.sprint));
|
|
11198
|
-
if (
|
|
11737
|
+
if (existsSync24(wtPath)) {
|
|
11199
11738
|
workDir = wtPath;
|
|
11200
11739
|
} else if (state.branch) {
|
|
11201
11740
|
try {
|
|
@@ -11365,9 +11904,35 @@ ${submoduleSummary}`;
|
|
|
11365
11904
|
---
|
|
11366
11905
|
|
|
11367
11906
|
\uD83E\uDD16 Automated by [Locus](https://github.com/asgarovf/locusai)`;
|
|
11368
|
-
const
|
|
11369
|
-
|
|
11907
|
+
const prTitle = `Sprint: ${sprintName}`;
|
|
11908
|
+
let prNumber;
|
|
11909
|
+
try {
|
|
11910
|
+
const existing = execSync17(`gh pr list --head ${branchName} --base ${config.agent.baseBranch} --json number --limit 1`, { cwd: workDir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
11911
|
+
const parsed = JSON.parse(existing);
|
|
11912
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
11913
|
+
prNumber = parsed[0].number;
|
|
11914
|
+
}
|
|
11915
|
+
} catch {}
|
|
11916
|
+
if (prNumber) {
|
|
11917
|
+
try {
|
|
11918
|
+
execSync17(`gh pr edit ${prNumber} --title ${JSON.stringify(prTitle)} --body-file -`, {
|
|
11919
|
+
input: prBody,
|
|
11920
|
+
cwd: workDir,
|
|
11921
|
+
encoding: "utf-8",
|
|
11922
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
11923
|
+
});
|
|
11924
|
+
process.stderr.write(` ${green("✓")} Updated existing sprint PR #${prNumber}
|
|
11925
|
+
`);
|
|
11926
|
+
} catch (editErr) {
|
|
11927
|
+
getLogger().warn(`Failed to update sprint PR #${prNumber}: ${editErr}`);
|
|
11928
|
+
process.stderr.write(` ${yellow2("⚠")} PR #${prNumber} exists but could not update: ${editErr}
|
|
11370
11929
|
`);
|
|
11930
|
+
}
|
|
11931
|
+
} else {
|
|
11932
|
+
prNumber = createPR(prTitle, prBody, branchName, config.agent.baseBranch, { cwd: workDir });
|
|
11933
|
+
process.stderr.write(` ${green("✓")} Created sprint PR #${prNumber}
|
|
11934
|
+
`);
|
|
11935
|
+
}
|
|
11371
11936
|
return prNumber;
|
|
11372
11937
|
} catch (e) {
|
|
11373
11938
|
getLogger().warn(`Failed to create sprint PR: ${e}`);
|
|
@@ -11399,8 +11964,8 @@ __export(exports_status, {
|
|
|
11399
11964
|
statusCommand: () => statusCommand
|
|
11400
11965
|
});
|
|
11401
11966
|
import { execSync as execSync18 } from "node:child_process";
|
|
11402
|
-
import { existsSync as
|
|
11403
|
-
import { dirname as dirname7, join as
|
|
11967
|
+
import { existsSync as existsSync24 } from "node:fs";
|
|
11968
|
+
import { dirname as dirname7, join as join25 } from "node:path";
|
|
11404
11969
|
async function statusCommand(projectRoot) {
|
|
11405
11970
|
const config = loadConfig(projectRoot);
|
|
11406
11971
|
const spinner = new Spinner;
|
|
@@ -11532,13 +12097,13 @@ ${drawBox(lines, { title: "Locus Status" })}
|
|
|
11532
12097
|
`);
|
|
11533
12098
|
}
|
|
11534
12099
|
function getPm2Bin() {
|
|
11535
|
-
const pkgsBin =
|
|
11536
|
-
if (
|
|
12100
|
+
const pkgsBin = join25(getPackagesDir(), "node_modules", ".bin", "pm2");
|
|
12101
|
+
if (existsSync24(pkgsBin))
|
|
11537
12102
|
return pkgsBin;
|
|
11538
12103
|
let dir = process.cwd();
|
|
11539
12104
|
while (dir !== dirname7(dir)) {
|
|
11540
|
-
const candidate =
|
|
11541
|
-
if (
|
|
12105
|
+
const candidate = join25(dir, "node_modules", ".bin", "pm2");
|
|
12106
|
+
if (existsSync24(candidate))
|
|
11542
12107
|
return candidate;
|
|
11543
12108
|
dir = dirname7(dir);
|
|
11544
12109
|
}
|
|
@@ -11734,13 +12299,13 @@ __export(exports_plan, {
|
|
|
11734
12299
|
parsePlanArgs: () => parsePlanArgs
|
|
11735
12300
|
});
|
|
11736
12301
|
import {
|
|
11737
|
-
existsSync as
|
|
11738
|
-
mkdirSync as
|
|
12302
|
+
existsSync as existsSync25,
|
|
12303
|
+
mkdirSync as mkdirSync17,
|
|
11739
12304
|
readdirSync as readdirSync8,
|
|
11740
|
-
readFileSync as
|
|
11741
|
-
writeFileSync as
|
|
12305
|
+
readFileSync as readFileSync14,
|
|
12306
|
+
writeFileSync as writeFileSync12
|
|
11742
12307
|
} from "node:fs";
|
|
11743
|
-
import { join as
|
|
12308
|
+
import { join as join26 } from "node:path";
|
|
11744
12309
|
function printHelp2() {
|
|
11745
12310
|
process.stderr.write(`
|
|
11746
12311
|
${bold2("locus plan")} — AI-powered sprint planning
|
|
@@ -11772,12 +12337,12 @@ function normalizeSprintName(name) {
|
|
|
11772
12337
|
return name.trim().toLowerCase();
|
|
11773
12338
|
}
|
|
11774
12339
|
function getPlansDir(projectRoot) {
|
|
11775
|
-
return
|
|
12340
|
+
return join26(projectRoot, ".locus", "plans");
|
|
11776
12341
|
}
|
|
11777
12342
|
function ensurePlansDir(projectRoot) {
|
|
11778
12343
|
const dir = getPlansDir(projectRoot);
|
|
11779
|
-
if (!
|
|
11780
|
-
|
|
12344
|
+
if (!existsSync25(dir)) {
|
|
12345
|
+
mkdirSync17(dir, { recursive: true });
|
|
11781
12346
|
}
|
|
11782
12347
|
return dir;
|
|
11783
12348
|
}
|
|
@@ -11786,14 +12351,14 @@ function generateId() {
|
|
|
11786
12351
|
}
|
|
11787
12352
|
function loadPlanFile(projectRoot, id) {
|
|
11788
12353
|
const dir = getPlansDir(projectRoot);
|
|
11789
|
-
if (!
|
|
12354
|
+
if (!existsSync25(dir))
|
|
11790
12355
|
return null;
|
|
11791
12356
|
const files = readdirSync8(dir).filter((f) => f.endsWith(".json"));
|
|
11792
12357
|
const match = files.find((f) => f.startsWith(id));
|
|
11793
12358
|
if (!match)
|
|
11794
12359
|
return null;
|
|
11795
12360
|
try {
|
|
11796
|
-
const content =
|
|
12361
|
+
const content = readFileSync14(join26(dir, match), "utf-8");
|
|
11797
12362
|
return JSON.parse(content);
|
|
11798
12363
|
} catch {
|
|
11799
12364
|
return null;
|
|
@@ -11860,7 +12425,7 @@ async function planCommand(projectRoot, args, flags = {}) {
|
|
|
11860
12425
|
}
|
|
11861
12426
|
function handleListPlans(projectRoot) {
|
|
11862
12427
|
const dir = getPlansDir(projectRoot);
|
|
11863
|
-
if (!
|
|
12428
|
+
if (!existsSync25(dir)) {
|
|
11864
12429
|
process.stderr.write(`${dim2("No saved plans yet.")}
|
|
11865
12430
|
`);
|
|
11866
12431
|
return;
|
|
@@ -11878,7 +12443,7 @@ ${bold2("Saved Plans:")}
|
|
|
11878
12443
|
for (const file of files) {
|
|
11879
12444
|
const id = file.replace(".json", "");
|
|
11880
12445
|
try {
|
|
11881
|
-
const content =
|
|
12446
|
+
const content = readFileSync14(join26(dir, file), "utf-8");
|
|
11882
12447
|
const plan = JSON.parse(content);
|
|
11883
12448
|
const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
|
|
11884
12449
|
const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
|
|
@@ -11946,7 +12511,7 @@ async function handleRefinePlan(projectRoot, id, feedback, flags) {
|
|
|
11946
12511
|
return;
|
|
11947
12512
|
}
|
|
11948
12513
|
const config = loadConfig(projectRoot);
|
|
11949
|
-
const planPath =
|
|
12514
|
+
const planPath = join26(getPlansDir(projectRoot), `${plan.id}.json`);
|
|
11950
12515
|
const planPathRelative = `.locus/plans/${plan.id}.json`;
|
|
11951
12516
|
process.stderr.write(`
|
|
11952
12517
|
${bold2("Refining plan:")} ${cyan2(plan.directive)}
|
|
@@ -11985,7 +12550,7 @@ ${red2("✗")} Refinement failed: ${aiResult.error}
|
|
|
11985
12550
|
`);
|
|
11986
12551
|
return;
|
|
11987
12552
|
}
|
|
11988
|
-
if (!
|
|
12553
|
+
if (!existsSync25(planPath)) {
|
|
11989
12554
|
process.stderr.write(`
|
|
11990
12555
|
${yellow2("⚠")} Plan file was not found at ${bold2(planPathRelative)}.
|
|
11991
12556
|
`);
|
|
@@ -11993,7 +12558,7 @@ ${yellow2("⚠")} Plan file was not found at ${bold2(planPathRelative)}.
|
|
|
11993
12558
|
}
|
|
11994
12559
|
let updatedPlan;
|
|
11995
12560
|
try {
|
|
11996
|
-
const content =
|
|
12561
|
+
const content = readFileSync14(planPath, "utf-8");
|
|
11997
12562
|
updatedPlan = JSON.parse(content);
|
|
11998
12563
|
} catch {
|
|
11999
12564
|
process.stderr.write(`
|
|
@@ -12011,7 +12576,7 @@ ${yellow2("⚠")} Refined plan has no issues.
|
|
|
12011
12576
|
updatedPlan.directive = plan.directive;
|
|
12012
12577
|
updatedPlan.sprint = updatedPlan.sprint ?? plan.sprint;
|
|
12013
12578
|
updatedPlan.createdAt = plan.createdAt;
|
|
12014
|
-
|
|
12579
|
+
writeFileSync12(planPath, JSON.stringify(updatedPlan, null, 2), "utf-8");
|
|
12015
12580
|
process.stderr.write(`
|
|
12016
12581
|
${bold2("Plan refined:")} ${cyan2(plan.id)}
|
|
12017
12582
|
|
|
@@ -12083,7 +12648,7 @@ ${bold2("Approving plan:")}
|
|
|
12083
12648
|
async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
|
|
12084
12649
|
const id = generateId();
|
|
12085
12650
|
const plansDir = ensurePlansDir(projectRoot);
|
|
12086
|
-
const planPath =
|
|
12651
|
+
const planPath = join26(plansDir, `${id}.json`);
|
|
12087
12652
|
const planPathRelative = `.locus/plans/${id}.json`;
|
|
12088
12653
|
const displayDirective = directive;
|
|
12089
12654
|
process.stderr.write(`
|
|
@@ -12126,7 +12691,7 @@ ${red2("✗")} Planning failed: ${aiResult.error}
|
|
|
12126
12691
|
`);
|
|
12127
12692
|
return;
|
|
12128
12693
|
}
|
|
12129
|
-
if (!
|
|
12694
|
+
if (!existsSync25(planPath)) {
|
|
12130
12695
|
process.stderr.write(`
|
|
12131
12696
|
${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
|
|
12132
12697
|
`);
|
|
@@ -12136,7 +12701,7 @@ ${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
|
|
|
12136
12701
|
}
|
|
12137
12702
|
let plan;
|
|
12138
12703
|
try {
|
|
12139
|
-
const content =
|
|
12704
|
+
const content = readFileSync14(planPath, "utf-8");
|
|
12140
12705
|
plan = JSON.parse(content);
|
|
12141
12706
|
} catch {
|
|
12142
12707
|
process.stderr.write(`
|
|
@@ -12158,7 +12723,7 @@ ${yellow2("⚠")} Plan file has no issues.
|
|
|
12158
12723
|
plan.sprint = sprintName;
|
|
12159
12724
|
if (!plan.createdAt)
|
|
12160
12725
|
plan.createdAt = new Date().toISOString();
|
|
12161
|
-
|
|
12726
|
+
writeFileSync12(planPath, JSON.stringify(plan, null, 2), "utf-8");
|
|
12162
12727
|
process.stderr.write(`
|
|
12163
12728
|
${bold2("Plan saved:")} ${cyan2(id)}
|
|
12164
12729
|
|
|
@@ -12308,16 +12873,16 @@ ${directive}${sprintName ? `
|
|
|
12308
12873
|
|
|
12309
12874
|
**Sprint:** ${sprintName}` : ""}
|
|
12310
12875
|
</directive>`);
|
|
12311
|
-
const locusPath =
|
|
12312
|
-
if (
|
|
12313
|
-
const content =
|
|
12876
|
+
const locusPath = join26(projectRoot, ".locus", "LOCUS.md");
|
|
12877
|
+
if (existsSync25(locusPath)) {
|
|
12878
|
+
const content = readFileSync14(locusPath, "utf-8");
|
|
12314
12879
|
parts.push(`<project-context>
|
|
12315
12880
|
${content.slice(0, 3000)}
|
|
12316
12881
|
</project-context>`);
|
|
12317
12882
|
}
|
|
12318
|
-
const learningsPath =
|
|
12319
|
-
if (
|
|
12320
|
-
const content =
|
|
12883
|
+
const learningsPath = join26(projectRoot, ".locus", "LEARNINGS.md");
|
|
12884
|
+
if (existsSync25(learningsPath)) {
|
|
12885
|
+
const content = readFileSync14(learningsPath, "utf-8");
|
|
12321
12886
|
parts.push(`<past-learnings>
|
|
12322
12887
|
${content.slice(0, 2000)}
|
|
12323
12888
|
</past-learnings>`);
|
|
@@ -12369,9 +12934,9 @@ ${JSON.stringify(plan, null, 2)}
|
|
|
12369
12934
|
parts.push(`<feedback>
|
|
12370
12935
|
${feedback}
|
|
12371
12936
|
</feedback>`);
|
|
12372
|
-
const locusPath =
|
|
12373
|
-
if (
|
|
12374
|
-
const content =
|
|
12937
|
+
const locusPath = join26(projectRoot, ".locus", "LOCUS.md");
|
|
12938
|
+
if (existsSync25(locusPath)) {
|
|
12939
|
+
const content = readFileSync14(locusPath, "utf-8");
|
|
12375
12940
|
parts.push(`<project-context>
|
|
12376
12941
|
${content.slice(0, 3000)}
|
|
12377
12942
|
</project-context>`);
|
|
@@ -12553,8 +13118,8 @@ __export(exports_review, {
|
|
|
12553
13118
|
reviewCommand: () => reviewCommand
|
|
12554
13119
|
});
|
|
12555
13120
|
import { execFileSync as execFileSync2, execSync as execSync19 } from "node:child_process";
|
|
12556
|
-
import { existsSync as
|
|
12557
|
-
import { join as
|
|
13121
|
+
import { existsSync as existsSync26, readFileSync as readFileSync15 } from "node:fs";
|
|
13122
|
+
import { join as join27 } from "node:path";
|
|
12558
13123
|
function printHelp3() {
|
|
12559
13124
|
process.stderr.write(`
|
|
12560
13125
|
${bold2("locus review")} — AI-powered code review
|
|
@@ -12723,9 +13288,9 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
|
|
|
12723
13288
|
parts.push(`<role>
|
|
12724
13289
|
You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.
|
|
12725
13290
|
</role>`);
|
|
12726
|
-
const locusPath =
|
|
12727
|
-
if (
|
|
12728
|
-
const content =
|
|
13291
|
+
const locusPath = join27(projectRoot, ".locus", "LOCUS.md");
|
|
13292
|
+
if (existsSync26(locusPath)) {
|
|
13293
|
+
const content = readFileSync15(locusPath, "utf-8");
|
|
12729
13294
|
parts.push(`<project-context>
|
|
12730
13295
|
${content.slice(0, 2000)}
|
|
12731
13296
|
</project-context>`);
|
|
@@ -13040,14 +13605,14 @@ __export(exports_discuss, {
|
|
|
13040
13605
|
discussCommand: () => discussCommand
|
|
13041
13606
|
});
|
|
13042
13607
|
import {
|
|
13043
|
-
existsSync as
|
|
13044
|
-
mkdirSync as
|
|
13608
|
+
existsSync as existsSync27,
|
|
13609
|
+
mkdirSync as mkdirSync18,
|
|
13045
13610
|
readdirSync as readdirSync9,
|
|
13046
|
-
readFileSync as
|
|
13611
|
+
readFileSync as readFileSync16,
|
|
13047
13612
|
unlinkSync as unlinkSync6,
|
|
13048
|
-
writeFileSync as
|
|
13613
|
+
writeFileSync as writeFileSync13
|
|
13049
13614
|
} from "node:fs";
|
|
13050
|
-
import { join as
|
|
13615
|
+
import { join as join28 } from "node:path";
|
|
13051
13616
|
function printHelp5() {
|
|
13052
13617
|
process.stderr.write(`
|
|
13053
13618
|
${bold2("locus discuss")} — AI-powered architectural discussions
|
|
@@ -13069,12 +13634,12 @@ ${bold2("Examples:")}
|
|
|
13069
13634
|
`);
|
|
13070
13635
|
}
|
|
13071
13636
|
function getDiscussionsDir(projectRoot) {
|
|
13072
|
-
return
|
|
13637
|
+
return join28(projectRoot, ".locus", "discussions");
|
|
13073
13638
|
}
|
|
13074
13639
|
function ensureDiscussionsDir(projectRoot) {
|
|
13075
13640
|
const dir = getDiscussionsDir(projectRoot);
|
|
13076
|
-
if (!
|
|
13077
|
-
|
|
13641
|
+
if (!existsSync27(dir)) {
|
|
13642
|
+
mkdirSync18(dir, { recursive: true });
|
|
13078
13643
|
}
|
|
13079
13644
|
return dir;
|
|
13080
13645
|
}
|
|
@@ -13108,7 +13673,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
|
|
|
13108
13673
|
}
|
|
13109
13674
|
function listDiscussions(projectRoot) {
|
|
13110
13675
|
const dir = getDiscussionsDir(projectRoot);
|
|
13111
|
-
if (!
|
|
13676
|
+
if (!existsSync27(dir)) {
|
|
13112
13677
|
process.stderr.write(`${dim2("No discussions yet.")}
|
|
13113
13678
|
`);
|
|
13114
13679
|
return;
|
|
@@ -13125,7 +13690,7 @@ ${bold2("Discussions:")}
|
|
|
13125
13690
|
`);
|
|
13126
13691
|
for (const file of files) {
|
|
13127
13692
|
const id = file.replace(".md", "");
|
|
13128
|
-
const content =
|
|
13693
|
+
const content = readFileSync16(join28(dir, file), "utf-8");
|
|
13129
13694
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
13130
13695
|
const title = titleMatch ? titleMatch[1] : id;
|
|
13131
13696
|
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
|
|
@@ -13143,7 +13708,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
13143
13708
|
return;
|
|
13144
13709
|
}
|
|
13145
13710
|
const dir = getDiscussionsDir(projectRoot);
|
|
13146
|
-
if (!
|
|
13711
|
+
if (!existsSync27(dir)) {
|
|
13147
13712
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
13148
13713
|
`);
|
|
13149
13714
|
return;
|
|
@@ -13155,7 +13720,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
13155
13720
|
`);
|
|
13156
13721
|
return;
|
|
13157
13722
|
}
|
|
13158
|
-
const content =
|
|
13723
|
+
const content = readFileSync16(join28(dir, match), "utf-8");
|
|
13159
13724
|
process.stdout.write(`${content}
|
|
13160
13725
|
`);
|
|
13161
13726
|
}
|
|
@@ -13166,7 +13731,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
13166
13731
|
return;
|
|
13167
13732
|
}
|
|
13168
13733
|
const dir = getDiscussionsDir(projectRoot);
|
|
13169
|
-
if (!
|
|
13734
|
+
if (!existsSync27(dir)) {
|
|
13170
13735
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
13171
13736
|
`);
|
|
13172
13737
|
return;
|
|
@@ -13178,7 +13743,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
13178
13743
|
`);
|
|
13179
13744
|
return;
|
|
13180
13745
|
}
|
|
13181
|
-
unlinkSync6(
|
|
13746
|
+
unlinkSync6(join28(dir, match));
|
|
13182
13747
|
process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
|
|
13183
13748
|
`);
|
|
13184
13749
|
}
|
|
@@ -13191,7 +13756,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
13191
13756
|
return;
|
|
13192
13757
|
}
|
|
13193
13758
|
const dir = getDiscussionsDir(projectRoot);
|
|
13194
|
-
if (!
|
|
13759
|
+
if (!existsSync27(dir)) {
|
|
13195
13760
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
13196
13761
|
`);
|
|
13197
13762
|
return;
|
|
@@ -13203,7 +13768,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
13203
13768
|
`);
|
|
13204
13769
|
return;
|
|
13205
13770
|
}
|
|
13206
|
-
const content =
|
|
13771
|
+
const content = readFileSync16(join28(dir, match), "utf-8");
|
|
13207
13772
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
13208
13773
|
const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
|
|
13209
13774
|
await planCommand(projectRoot, [
|
|
@@ -13329,7 +13894,7 @@ ${turn.content}`;
|
|
|
13329
13894
|
...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
|
|
13330
13895
|
].join(`
|
|
13331
13896
|
`);
|
|
13332
|
-
|
|
13897
|
+
writeFileSync13(join28(dir, `${id}.md`), markdown, "utf-8");
|
|
13333
13898
|
process.stderr.write(`
|
|
13334
13899
|
${green("✓")} Discussion saved: ${cyan2(id)} ${dim2(`(${timer.formatted()})`)}
|
|
13335
13900
|
`);
|
|
@@ -13344,16 +13909,16 @@ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFi
|
|
|
13344
13909
|
parts.push(`<role>
|
|
13345
13910
|
You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.
|
|
13346
13911
|
</role>`);
|
|
13347
|
-
const locusPath =
|
|
13348
|
-
if (
|
|
13349
|
-
const content =
|
|
13912
|
+
const locusPath = join28(projectRoot, ".locus", "LOCUS.md");
|
|
13913
|
+
if (existsSync27(locusPath)) {
|
|
13914
|
+
const content = readFileSync16(locusPath, "utf-8");
|
|
13350
13915
|
parts.push(`<project-context>
|
|
13351
13916
|
${content.slice(0, 3000)}
|
|
13352
13917
|
</project-context>`);
|
|
13353
13918
|
}
|
|
13354
|
-
const learningsPath =
|
|
13355
|
-
if (
|
|
13356
|
-
const content =
|
|
13919
|
+
const learningsPath = join28(projectRoot, ".locus", "LEARNINGS.md");
|
|
13920
|
+
if (existsSync27(learningsPath)) {
|
|
13921
|
+
const content = readFileSync16(learningsPath, "utf-8");
|
|
13357
13922
|
parts.push(`<past-learnings>
|
|
13358
13923
|
${content.slice(0, 2000)}
|
|
13359
13924
|
</past-learnings>`);
|
|
@@ -13424,8 +13989,8 @@ __export(exports_artifacts, {
|
|
|
13424
13989
|
formatDate: () => formatDate2,
|
|
13425
13990
|
artifactsCommand: () => artifactsCommand
|
|
13426
13991
|
});
|
|
13427
|
-
import { existsSync as
|
|
13428
|
-
import { join as
|
|
13992
|
+
import { existsSync as existsSync28, readdirSync as readdirSync10, readFileSync as readFileSync17, statSync as statSync5 } from "node:fs";
|
|
13993
|
+
import { join as join29 } from "node:path";
|
|
13429
13994
|
function printHelp6() {
|
|
13430
13995
|
process.stderr.write(`
|
|
13431
13996
|
${bold2("locus artifacts")} — View and manage AI-generated artifacts
|
|
@@ -13445,14 +14010,14 @@ ${dim2("Artifact names support partial matching.")}
|
|
|
13445
14010
|
`);
|
|
13446
14011
|
}
|
|
13447
14012
|
function getArtifactsDir(projectRoot) {
|
|
13448
|
-
return
|
|
14013
|
+
return join29(projectRoot, ".locus", "artifacts");
|
|
13449
14014
|
}
|
|
13450
14015
|
function listArtifacts(projectRoot) {
|
|
13451
14016
|
const dir = getArtifactsDir(projectRoot);
|
|
13452
|
-
if (!
|
|
14017
|
+
if (!existsSync28(dir))
|
|
13453
14018
|
return [];
|
|
13454
14019
|
return readdirSync10(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
|
|
13455
|
-
const filePath =
|
|
14020
|
+
const filePath = join29(dir, fileName);
|
|
13456
14021
|
const stat = statSync5(filePath);
|
|
13457
14022
|
return {
|
|
13458
14023
|
name: fileName.replace(/\.md$/, ""),
|
|
@@ -13465,12 +14030,12 @@ function listArtifacts(projectRoot) {
|
|
|
13465
14030
|
function readArtifact(projectRoot, name) {
|
|
13466
14031
|
const dir = getArtifactsDir(projectRoot);
|
|
13467
14032
|
const fileName = name.endsWith(".md") ? name : `${name}.md`;
|
|
13468
|
-
const filePath =
|
|
13469
|
-
if (!
|
|
14033
|
+
const filePath = join29(dir, fileName);
|
|
14034
|
+
if (!existsSync28(filePath))
|
|
13470
14035
|
return null;
|
|
13471
14036
|
const stat = statSync5(filePath);
|
|
13472
14037
|
return {
|
|
13473
|
-
content:
|
|
14038
|
+
content: readFileSync17(filePath, "utf-8"),
|
|
13474
14039
|
info: {
|
|
13475
14040
|
name: fileName.replace(/\.md$/, ""),
|
|
13476
14041
|
fileName,
|
|
@@ -13826,9 +14391,9 @@ __export(exports_sandbox2, {
|
|
|
13826
14391
|
parseSandboxInstallArgs: () => parseSandboxInstallArgs
|
|
13827
14392
|
});
|
|
13828
14393
|
import { execSync as execSync22, spawn as spawn7 } from "node:child_process";
|
|
13829
|
-
import { createHash } from "node:crypto";
|
|
13830
|
-
import { existsSync as
|
|
13831
|
-
import { basename as basename4, join as
|
|
14394
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
14395
|
+
import { existsSync as existsSync29, readFileSync as readFileSync18 } from "node:fs";
|
|
14396
|
+
import { basename as basename4, join as join30 } from "node:path";
|
|
13832
14397
|
import { createInterface as createInterface3 } from "node:readline";
|
|
13833
14398
|
function printSandboxHelp() {
|
|
13834
14399
|
process.stderr.write(`
|
|
@@ -14358,7 +14923,7 @@ async function handleLogs(projectRoot, args) {
|
|
|
14358
14923
|
}
|
|
14359
14924
|
function detectPackageManager2(projectRoot) {
|
|
14360
14925
|
try {
|
|
14361
|
-
const raw =
|
|
14926
|
+
const raw = readFileSync18(join30(projectRoot, "package.json"), "utf-8");
|
|
14362
14927
|
const pkgJson = JSON.parse(raw);
|
|
14363
14928
|
if (typeof pkgJson.packageManager === "string") {
|
|
14364
14929
|
const name = pkgJson.packageManager.split("@")[0];
|
|
@@ -14367,13 +14932,13 @@ function detectPackageManager2(projectRoot) {
|
|
|
14367
14932
|
}
|
|
14368
14933
|
}
|
|
14369
14934
|
} catch {}
|
|
14370
|
-
if (
|
|
14935
|
+
if (existsSync29(join30(projectRoot, "bun.lock")) || existsSync29(join30(projectRoot, "bun.lockb"))) {
|
|
14371
14936
|
return "bun";
|
|
14372
14937
|
}
|
|
14373
|
-
if (
|
|
14938
|
+
if (existsSync29(join30(projectRoot, "yarn.lock"))) {
|
|
14374
14939
|
return "yarn";
|
|
14375
14940
|
}
|
|
14376
|
-
if (
|
|
14941
|
+
if (existsSync29(join30(projectRoot, "pnpm-lock.yaml"))) {
|
|
14377
14942
|
return "pnpm";
|
|
14378
14943
|
}
|
|
14379
14944
|
return "npm";
|
|
@@ -14476,9 +15041,9 @@ Installing sandbox dependencies (${bold2(installCmd.join(" "))}) to container fi
|
|
|
14476
15041
|
${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
|
|
14477
15042
|
`);
|
|
14478
15043
|
}
|
|
14479
|
-
const setupScript =
|
|
14480
|
-
const containerSetupScript = containerWorkdir ?
|
|
14481
|
-
if (
|
|
15044
|
+
const setupScript = join30(projectRoot, ".locus", "sandbox-setup.sh");
|
|
15045
|
+
const containerSetupScript = containerWorkdir ? join30(containerWorkdir, ".locus", "sandbox-setup.sh") : setupScript;
|
|
15046
|
+
if (existsSync29(setupScript)) {
|
|
14482
15047
|
process.stderr.write(`Running ${bold2(".locus/sandbox-setup.sh")} in sandbox ${dim2(sandboxName)}...
|
|
14483
15048
|
`);
|
|
14484
15049
|
const hookOk = await runInteractiveCommand("docker", [
|
|
@@ -14525,7 +15090,7 @@ async function handleSetup(projectRoot) {
|
|
|
14525
15090
|
}
|
|
14526
15091
|
function buildProviderSandboxNames(projectRoot) {
|
|
14527
15092
|
const segment = sanitizeSegment(basename4(projectRoot));
|
|
14528
|
-
const hash =
|
|
15093
|
+
const hash = createHash2("sha1").update(projectRoot).digest("hex").slice(0, 8);
|
|
14529
15094
|
return {
|
|
14530
15095
|
claude: `locus-${segment}-claude-${hash}`,
|
|
14531
15096
|
codex: `locus-${segment}-codex-${hash}`
|
|
@@ -14619,16 +15184,21 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
14619
15184
|
}
|
|
14620
15185
|
}
|
|
14621
15186
|
function isSandboxAlive(name) {
|
|
14622
|
-
|
|
14623
|
-
|
|
14624
|
-
|
|
14625
|
-
|
|
14626
|
-
|
|
14627
|
-
|
|
14628
|
-
|
|
14629
|
-
|
|
14630
|
-
|
|
15187
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
15188
|
+
try {
|
|
15189
|
+
const output = execSync22("docker sandbox ls", {
|
|
15190
|
+
encoding: "utf-8",
|
|
15191
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
15192
|
+
timeout: 15000
|
|
15193
|
+
});
|
|
15194
|
+
return output.includes(name);
|
|
15195
|
+
} catch {
|
|
15196
|
+
if (attempt < 2) {
|
|
15197
|
+
execSync22("sleep 2", { stdio: "ignore" });
|
|
15198
|
+
}
|
|
15199
|
+
}
|
|
14631
15200
|
}
|
|
15201
|
+
return false;
|
|
14632
15202
|
}
|
|
14633
15203
|
var PROVIDERS;
|
|
14634
15204
|
var init_sandbox2 = __esm(() => {
|
|
@@ -14646,17 +15216,17 @@ init_context();
|
|
|
14646
15216
|
init_logger();
|
|
14647
15217
|
init_rate_limiter();
|
|
14648
15218
|
init_terminal();
|
|
14649
|
-
import { existsSync as
|
|
14650
|
-
import { join as
|
|
15219
|
+
import { existsSync as existsSync30, readFileSync as readFileSync19 } from "node:fs";
|
|
15220
|
+
import { join as join31 } from "node:path";
|
|
14651
15221
|
import { fileURLToPath } from "node:url";
|
|
14652
15222
|
function getCliVersion() {
|
|
14653
15223
|
const fallbackVersion = "0.0.0";
|
|
14654
|
-
const packageJsonPath =
|
|
14655
|
-
if (!
|
|
15224
|
+
const packageJsonPath = join31(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
|
|
15225
|
+
if (!existsSync30(packageJsonPath)) {
|
|
14656
15226
|
return fallbackVersion;
|
|
14657
15227
|
}
|
|
14658
15228
|
try {
|
|
14659
|
-
const parsed = JSON.parse(
|
|
15229
|
+
const parsed = JSON.parse(readFileSync19(packageJsonPath, "utf-8"));
|
|
14660
15230
|
return parsed.version ?? fallbackVersion;
|
|
14661
15231
|
} catch {
|
|
14662
15232
|
return fallbackVersion;
|
|
@@ -14686,6 +15256,7 @@ function parseArgs(argv) {
|
|
|
14686
15256
|
check: false,
|
|
14687
15257
|
upgrade: false,
|
|
14688
15258
|
list: false,
|
|
15259
|
+
installed: false,
|
|
14689
15260
|
noSandbox: false
|
|
14690
15261
|
};
|
|
14691
15262
|
const positional = [];
|
|
@@ -14763,6 +15334,9 @@ function parseArgs(argv) {
|
|
|
14763
15334
|
case "--target-version":
|
|
14764
15335
|
flags.targetVersion = rawArgs[++i];
|
|
14765
15336
|
break;
|
|
15337
|
+
case "--installed":
|
|
15338
|
+
flags.installed = true;
|
|
15339
|
+
break;
|
|
14766
15340
|
case "--no-sandbox":
|
|
14767
15341
|
flags.noSandbox = true;
|
|
14768
15342
|
break;
|
|
@@ -14841,6 +15415,7 @@ ${bold2("Commands:")}
|
|
|
14841
15415
|
${cyan2("uninstall")} Remove an installed package
|
|
14842
15416
|
${cyan2("packages")} Manage installed packages (list, outdated)
|
|
14843
15417
|
${cyan2("pkg")} ${dim2("<name> [cmd]")} Run a command from an installed package
|
|
15418
|
+
${cyan2("skills")} Discover and manage agent skills
|
|
14844
15419
|
${cyan2("sandbox")} Manage Docker sandbox lifecycle
|
|
14845
15420
|
${cyan2("upgrade")} Check for and install updates
|
|
14846
15421
|
|
|
@@ -14859,6 +15434,8 @@ ${bold2("Examples:")}
|
|
|
14859
15434
|
locus run 42 43 ${dim2("# Run issues in parallel")}
|
|
14860
15435
|
locus run 42 --no-sandbox ${dim2("# Run without sandbox")}
|
|
14861
15436
|
locus run 42 --sandbox=require ${dim2("# Require Docker sandbox")}
|
|
15437
|
+
locus skills list ${dim2("# Browse available skills")}
|
|
15438
|
+
locus skills install code-review ${dim2("# Install a skill")}
|
|
14862
15439
|
locus sandbox ${dim2("# Create Docker sandbox")}
|
|
14863
15440
|
locus sandbox claude ${dim2("# Login to Claude in sandbox")}
|
|
14864
15441
|
|
|
@@ -14932,7 +15509,7 @@ async function main() {
|
|
|
14932
15509
|
try {
|
|
14933
15510
|
const root = getGitRoot(cwd);
|
|
14934
15511
|
if (isInitialized(root)) {
|
|
14935
|
-
logDir =
|
|
15512
|
+
logDir = join31(root, ".locus", "logs");
|
|
14936
15513
|
getRateLimiter(root);
|
|
14937
15514
|
}
|
|
14938
15515
|
} catch {}
|
|
@@ -15000,6 +15577,17 @@ async function main() {
|
|
|
15000
15577
|
logger.destroy();
|
|
15001
15578
|
return;
|
|
15002
15579
|
}
|
|
15580
|
+
if (command === "skills") {
|
|
15581
|
+
const { skillsCommand: skillsCommand2 } = await Promise.resolve().then(() => (init_skills(), exports_skills));
|
|
15582
|
+
const skillsArgs = parsed.flags.help ? ["help"] : parsed.args;
|
|
15583
|
+
const skillsFlags = {};
|
|
15584
|
+
if (parsed.flags.installed) {
|
|
15585
|
+
skillsFlags.installed = "true";
|
|
15586
|
+
}
|
|
15587
|
+
await skillsCommand2(skillsArgs, skillsFlags);
|
|
15588
|
+
logger.destroy();
|
|
15589
|
+
return;
|
|
15590
|
+
}
|
|
15003
15591
|
if (command === "pkg") {
|
|
15004
15592
|
const { pkgCommand: pkgCommand2 } = await Promise.resolve().then(() => (init_pkg(), exports_pkg));
|
|
15005
15593
|
await pkgCommand2(parsed.args, {});
|