@basou/cli 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +529 -136
- package/dist/index.js.map +1 -1
- package/dist/program.js +529 -136
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1327,9 +1327,9 @@ async function assertWorkspaceInitialized4(basouRoot) {
|
|
|
1327
1327
|
|
|
1328
1328
|
// src/commands/import.ts
|
|
1329
1329
|
import { createReadStream } from "fs";
|
|
1330
|
-
import { readdir, readFile, rm } from "fs/promises";
|
|
1330
|
+
import { readdir, readFile, rm, stat } from "fs/promises";
|
|
1331
1331
|
import { homedir as homedir2 } from "os";
|
|
1332
|
-
import { basename, join as join3 } from "path";
|
|
1332
|
+
import { basename, join as join3, resolve } from "path";
|
|
1333
1333
|
import { createInterface } from "readline";
|
|
1334
1334
|
import {
|
|
1335
1335
|
assertBasouRootSafe as assertBasouRootSafe6,
|
|
@@ -1343,16 +1343,22 @@ import {
|
|
|
1343
1343
|
importSessionFromJson,
|
|
1344
1344
|
readManifest as readManifest3,
|
|
1345
1345
|
readSessionYaml,
|
|
1346
|
+
reimportPreservingId,
|
|
1346
1347
|
resolveRepositoryRoot as resolveRepositoryRoot6,
|
|
1347
1348
|
SessionImportPayloadSchema
|
|
1348
1349
|
} from "@basou/core";
|
|
1349
1350
|
var SES_PREFIX2 = "ses_";
|
|
1350
1351
|
var SHORT_ID_LEN2 = 6;
|
|
1352
|
+
function collectPath(value, previous) {
|
|
1353
|
+
return [...previous, value];
|
|
1354
|
+
}
|
|
1351
1355
|
function registerImportCommand(program2) {
|
|
1352
1356
|
const importCmd = program2.command("import").description("Import provenance from an external AI tool's native logs");
|
|
1353
1357
|
importCmd.command("claude-code").description("Derive Basou sessions from Claude Code native transcripts (~/.claude/projects)").option(
|
|
1354
1358
|
"--project <path>",
|
|
1355
|
-
"Source project path whose transcripts to import (defaults to the
|
|
1359
|
+
"Source project path whose transcripts to import (repeatable; defaults to the manifest source roots, then the repository root)",
|
|
1360
|
+
collectPath,
|
|
1361
|
+
[]
|
|
1356
1362
|
).option("--session <id>", "Import a single transcript by its Claude session id").option("--all", "Import every transcript found for the project").option(
|
|
1357
1363
|
"--force",
|
|
1358
1364
|
"Re-import sessions already imported: delete and replace them instead of skipping"
|
|
@@ -1361,7 +1367,9 @@ function registerImportCommand(program2) {
|
|
|
1361
1367
|
});
|
|
1362
1368
|
importCmd.command("codex").description("Derive Basou sessions from OpenAI Codex native rollout logs (~/.codex/sessions)").option(
|
|
1363
1369
|
"--project <path>",
|
|
1364
|
-
"Source project path whose rollouts to import (defaults to the
|
|
1370
|
+
"Source project path whose rollouts to import (repeatable; defaults to the manifest source roots, then the repository root)",
|
|
1371
|
+
collectPath,
|
|
1372
|
+
[]
|
|
1365
1373
|
).option("--session <id>", "Import a single rollout by its Codex session id").option("--all", "Import every rollout found for the project").option(
|
|
1366
1374
|
"--force",
|
|
1367
1375
|
"Re-import sessions already imported: delete and replace them instead of skipping"
|
|
@@ -1385,21 +1393,41 @@ async function runImportCodex(options, ctx = {}) {
|
|
|
1385
1393
|
process.exitCode = 1;
|
|
1386
1394
|
}
|
|
1387
1395
|
}
|
|
1396
|
+
function resolveSourceRoots(args) {
|
|
1397
|
+
const { projectFlags, manifest, repoRoot, cwd } = args;
|
|
1398
|
+
let resolved;
|
|
1399
|
+
if (projectFlags.length > 0) {
|
|
1400
|
+
resolved = projectFlags.map((p) => resolve(cwd, p));
|
|
1401
|
+
} else {
|
|
1402
|
+
const roots = manifest.import?.source_roots;
|
|
1403
|
+
resolved = roots !== void 0 && roots.length > 0 ? roots.map((r) => resolve(repoRoot, r)) : [repoRoot];
|
|
1404
|
+
}
|
|
1405
|
+
return [...new Set(resolved)];
|
|
1406
|
+
}
|
|
1388
1407
|
async function doRunImportClaudeCode(options, ctx) {
|
|
1389
1408
|
assertSelector(options);
|
|
1390
1409
|
const { repositoryRoot, paths, manifest } = await resolveImportTarget(ctx);
|
|
1391
|
-
const
|
|
1410
|
+
const projectPaths = resolveSourceRoots({
|
|
1411
|
+
projectFlags: options.project ?? [],
|
|
1412
|
+
manifest,
|
|
1413
|
+
repoRoot: repositoryRoot,
|
|
1414
|
+
cwd: ctx.cwd ?? process.cwd()
|
|
1415
|
+
});
|
|
1392
1416
|
const projectsRoot = ctx.claudeProjectsDir ?? join3(homedir2(), ".claude", "projects");
|
|
1393
|
-
const
|
|
1394
|
-
const files = await selectTranscriptFiles(transcriptDir, options);
|
|
1417
|
+
const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
|
|
1395
1418
|
const candidates = files.map((file) => {
|
|
1396
1419
|
const externalId = basename(file, ".jsonl");
|
|
1397
1420
|
return {
|
|
1398
1421
|
externalId,
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1422
|
+
sourcePath: file,
|
|
1423
|
+
toPayload: async () => {
|
|
1424
|
+
const { records, sizeBytes } = await readJsonlRecords(file);
|
|
1425
|
+
return claudeTranscriptToImportPayload(records, {
|
|
1426
|
+
workspaceId: manifest.workspace.id,
|
|
1427
|
+
externalId,
|
|
1428
|
+
sourceSizeBytes: sizeBytes
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1403
1431
|
};
|
|
1404
1432
|
});
|
|
1405
1433
|
await importDerivedSessions(paths, manifest, options, CLAUDE_IMPORT_SOURCE, candidates);
|
|
@@ -1407,19 +1435,32 @@ async function doRunImportClaudeCode(options, ctx) {
|
|
|
1407
1435
|
async function doRunImportCodex(options, ctx) {
|
|
1408
1436
|
assertSelector(options);
|
|
1409
1437
|
const { repositoryRoot, paths, manifest } = await resolveImportTarget(ctx);
|
|
1410
|
-
const
|
|
1438
|
+
const projectPaths = resolveSourceRoots({
|
|
1439
|
+
projectFlags: options.project ?? [],
|
|
1440
|
+
manifest,
|
|
1441
|
+
repoRoot: repositoryRoot,
|
|
1442
|
+
cwd: ctx.cwd ?? process.cwd()
|
|
1443
|
+
});
|
|
1411
1444
|
const sessionsRoot = ctx.codexSessionsDir ?? join3(homedir2(), ".codex", "sessions");
|
|
1412
|
-
const rollouts = await discoverCodexRollouts(sessionsRoot,
|
|
1445
|
+
const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
|
|
1413
1446
|
const candidates = rollouts.map(({ file, externalId }) => ({
|
|
1414
1447
|
externalId,
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1448
|
+
sourcePath: file,
|
|
1449
|
+
toPayload: async () => {
|
|
1450
|
+
const { records, sizeBytes } = await readJsonlRecords(file);
|
|
1451
|
+
return codexRolloutToImportPayload(records, {
|
|
1452
|
+
workspaceId: manifest.workspace.id,
|
|
1453
|
+
externalId,
|
|
1454
|
+
sourceSizeBytes: sizeBytes
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1419
1457
|
}));
|
|
1420
1458
|
await importDerivedSessions(paths, manifest, options, CODEX_IMPORT_SOURCE, candidates);
|
|
1421
1459
|
}
|
|
1422
1460
|
function assertSelector(options) {
|
|
1461
|
+
if (options.session !== void 0 && options.all === true) {
|
|
1462
|
+
throw new Error("Specify either --session <id> or --all, not both");
|
|
1463
|
+
}
|
|
1423
1464
|
if (options.session === void 0 && options.all !== true) {
|
|
1424
1465
|
throw new Error("Specify --session <id> or --all");
|
|
1425
1466
|
}
|
|
@@ -1436,41 +1477,76 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
1436
1477
|
const existingByExternalId = await loadExistingByExternalId(paths, sourceKind);
|
|
1437
1478
|
const seenThisRun = /* @__PURE__ */ new Set();
|
|
1438
1479
|
const results = [];
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1480
|
+
const counts = {
|
|
1481
|
+
skippedNoAction: 0,
|
|
1482
|
+
skippedExisting: 0,
|
|
1483
|
+
replaced: 0,
|
|
1484
|
+
reimported: 0,
|
|
1485
|
+
skippedLegacy: 0,
|
|
1486
|
+
skippedDecreased: 0,
|
|
1487
|
+
skippedDuplicate: 0
|
|
1488
|
+
};
|
|
1442
1489
|
let sanitizedPaths = 0;
|
|
1443
|
-
|
|
1490
|
+
const validate = (payload) => {
|
|
1491
|
+
if (payload === null) return null;
|
|
1492
|
+
const parsed = SessionImportPayloadSchema.safeParse(payload);
|
|
1493
|
+
if (!parsed.success) {
|
|
1494
|
+
throw new Error("Invalid import payload", { cause: parsed.error });
|
|
1495
|
+
}
|
|
1496
|
+
if (parsed.data.schema_version !== "0.1.0") {
|
|
1497
|
+
throw new Error(`Unsupported import schema_version: ${parsed.data.schema_version}`);
|
|
1498
|
+
}
|
|
1499
|
+
return parsed.data;
|
|
1500
|
+
};
|
|
1501
|
+
for (const { externalId, sourcePath, toPayload } of candidates) {
|
|
1444
1502
|
if (seenThisRun.has(externalId)) {
|
|
1445
|
-
skippedExisting++;
|
|
1503
|
+
counts.skippedExisting++;
|
|
1446
1504
|
continue;
|
|
1447
1505
|
}
|
|
1448
|
-
const
|
|
1449
|
-
if (
|
|
1450
|
-
|
|
1506
|
+
const priors = existingByExternalId.get(externalId) ?? [];
|
|
1507
|
+
if (priors.length > 0 && options.force !== true) {
|
|
1508
|
+
const prior = await classifyReimport(priors, sourcePath, externalId, counts);
|
|
1509
|
+
if (prior === null) continue;
|
|
1510
|
+
const payload2 = validate(await toPayload());
|
|
1511
|
+
if (payload2 === null) {
|
|
1512
|
+
counts.skippedNoAction++;
|
|
1513
|
+
continue;
|
|
1514
|
+
}
|
|
1515
|
+
const readSize = payload2.session.source.source_size_bytes;
|
|
1516
|
+
if (prior.sourceSizeBytes !== void 0 && readSize !== void 0 && readSize <= prior.sourceSizeBytes) {
|
|
1517
|
+
console.error(
|
|
1518
|
+
`Import: ${externalId} source changed during read (now ${readSize} <= ${prior.sourceSizeBytes} bytes); re-import skipped`
|
|
1519
|
+
);
|
|
1520
|
+
counts.skippedDecreased++;
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
const outcome = await reimportPreservingId(paths, manifest, prior.sessionId, payload2, {
|
|
1524
|
+
dryRun: options.dryRun === true
|
|
1525
|
+
});
|
|
1526
|
+
if (outcome.status === "skipped") {
|
|
1527
|
+
const detail = outcome.reason === "prior_events_unreadable" ? "prior events.jsonl has unreadable lines" : "source changed in a non-append way (derived events would be dropped)";
|
|
1528
|
+
console.error(`Import: ${externalId} ${detail}; re-import skipped`);
|
|
1529
|
+
counts.skippedNoAction++;
|
|
1530
|
+
continue;
|
|
1531
|
+
}
|
|
1532
|
+
counts.reimported++;
|
|
1533
|
+
seenThisRun.add(externalId);
|
|
1451
1534
|
continue;
|
|
1452
1535
|
}
|
|
1453
|
-
const payload = await toPayload();
|
|
1536
|
+
const payload = validate(await toPayload());
|
|
1454
1537
|
if (payload === null) {
|
|
1455
|
-
skippedNoAction++;
|
|
1538
|
+
counts.skippedNoAction++;
|
|
1456
1539
|
continue;
|
|
1457
1540
|
}
|
|
1458
|
-
|
|
1459
|
-
if (!parsed.success) {
|
|
1460
|
-
throw new Error("Invalid import payload", { cause: parsed.error });
|
|
1461
|
-
}
|
|
1462
|
-
if (parsed.data.schema_version !== "0.1.0") {
|
|
1463
|
-
throw new Error(`Unsupported import schema_version: ${parsed.data.schema_version}`);
|
|
1464
|
-
}
|
|
1465
|
-
if (priorSessionIds.length > 0 && options.force === true) {
|
|
1541
|
+
if (priors.length > 0 && options.force === true) {
|
|
1466
1542
|
if (options.dryRun !== true) {
|
|
1467
|
-
for (const
|
|
1468
|
-
await rm(join3(paths.sessions,
|
|
1543
|
+
for (const { sessionId } of priors) {
|
|
1544
|
+
await rm(join3(paths.sessions, sessionId), { recursive: true, force: true });
|
|
1469
1545
|
}
|
|
1470
1546
|
}
|
|
1471
|
-
replaced++;
|
|
1547
|
+
counts.replaced++;
|
|
1472
1548
|
}
|
|
1473
|
-
const result = await importSessionFromJson(paths, manifest,
|
|
1549
|
+
const result = await importSessionFromJson(paths, manifest, payload, {
|
|
1474
1550
|
dryRun: options.dryRun === true
|
|
1475
1551
|
});
|
|
1476
1552
|
results.push(result);
|
|
@@ -1480,17 +1556,52 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
1480
1556
|
if (sanitizedPaths > 0) {
|
|
1481
1557
|
console.error(`Imported sessions: ${sanitizedPaths} path(s) sanitized`);
|
|
1482
1558
|
}
|
|
1483
|
-
printImportResult(options, results,
|
|
1559
|
+
printImportResult(options, results, counts);
|
|
1560
|
+
}
|
|
1561
|
+
async function classifyReimport(priors, sourcePath, externalId, counts) {
|
|
1562
|
+
if (priors.length > 1) {
|
|
1563
|
+
console.error(
|
|
1564
|
+
`Import: ${externalId} has ${priors.length} prior sessions; re-import skipped (use --force)`
|
|
1565
|
+
);
|
|
1566
|
+
counts.skippedDuplicate++;
|
|
1567
|
+
return null;
|
|
1568
|
+
}
|
|
1569
|
+
const prior = priors[0];
|
|
1570
|
+
if (prior === void 0) {
|
|
1571
|
+
counts.skippedExisting++;
|
|
1572
|
+
return null;
|
|
1573
|
+
}
|
|
1574
|
+
const currentSize = await statSize(sourcePath);
|
|
1575
|
+
if (currentSize === void 0) {
|
|
1576
|
+
counts.skippedExisting++;
|
|
1577
|
+
return null;
|
|
1578
|
+
}
|
|
1579
|
+
if (prior.sourceSizeBytes === void 0) {
|
|
1580
|
+
counts.skippedLegacy++;
|
|
1581
|
+
return null;
|
|
1582
|
+
}
|
|
1583
|
+
if (currentSize === prior.sourceSizeBytes) {
|
|
1584
|
+
counts.skippedExisting++;
|
|
1585
|
+
return null;
|
|
1586
|
+
}
|
|
1587
|
+
if (currentSize < prior.sourceSizeBytes) {
|
|
1588
|
+
console.error(
|
|
1589
|
+
`Import: ${externalId} source shrank (${currentSize} < ${prior.sourceSizeBytes} bytes); re-import skipped (use --force to replace)`
|
|
1590
|
+
);
|
|
1591
|
+
counts.skippedDecreased++;
|
|
1592
|
+
return null;
|
|
1593
|
+
}
|
|
1594
|
+
return prior;
|
|
1484
1595
|
}
|
|
1485
1596
|
function encodeProjectDir(projectPath) {
|
|
1486
1597
|
return projectPath.replaceAll("/", "-");
|
|
1487
1598
|
}
|
|
1488
1599
|
async function loadExistingByExternalId(paths, sourceKind) {
|
|
1489
1600
|
const byExternalId = /* @__PURE__ */ new Map();
|
|
1490
|
-
const add = (externalId,
|
|
1601
|
+
const add = (externalId, prior) => {
|
|
1491
1602
|
const list = byExternalId.get(externalId);
|
|
1492
|
-
if (list === void 0) byExternalId.set(externalId, [
|
|
1493
|
-
else list.push(
|
|
1603
|
+
if (list === void 0) byExternalId.set(externalId, [prior]);
|
|
1604
|
+
else list.push(prior);
|
|
1494
1605
|
};
|
|
1495
1606
|
let sessionIds;
|
|
1496
1607
|
try {
|
|
@@ -1506,39 +1617,77 @@ async function loadExistingByExternalId(paths, sourceKind) {
|
|
|
1506
1617
|
continue;
|
|
1507
1618
|
}
|
|
1508
1619
|
if (session.session.source.kind !== sourceKind) continue;
|
|
1620
|
+
const sourceSizeBytes = session.session.source.source_size_bytes;
|
|
1621
|
+
const prior = sourceSizeBytes !== void 0 ? { sessionId, sourceSizeBytes } : { sessionId };
|
|
1509
1622
|
const ext = session.session.source.external_id;
|
|
1510
1623
|
if (typeof ext === "string" && ext.length > 0) {
|
|
1511
|
-
add(ext,
|
|
1624
|
+
add(ext, prior);
|
|
1512
1625
|
continue;
|
|
1513
1626
|
}
|
|
1514
1627
|
const label = session.session.label;
|
|
1515
1628
|
const match = typeof label === "string" ? label.match(/^claude-code import (\S+)$/) : null;
|
|
1516
|
-
if (match?.[1] !== void 0) add(match[1],
|
|
1629
|
+
if (match?.[1] !== void 0) add(match[1], prior);
|
|
1517
1630
|
}
|
|
1518
1631
|
return byExternalId;
|
|
1519
1632
|
}
|
|
1520
|
-
async function selectTranscriptFiles(
|
|
1633
|
+
async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
1521
1634
|
if (options.session !== void 0) {
|
|
1522
|
-
|
|
1635
|
+
const matches = [];
|
|
1636
|
+
for (const projectPath of projectPaths) {
|
|
1637
|
+
const file = join3(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
|
|
1638
|
+
if (await pathExists(file)) matches.push(file);
|
|
1639
|
+
}
|
|
1640
|
+
if (matches.length === 0) {
|
|
1641
|
+
throw new Error("Claude transcript not found for session id in project");
|
|
1642
|
+
}
|
|
1643
|
+
return [...new Set(matches)];
|
|
1644
|
+
}
|
|
1645
|
+
const files = [];
|
|
1646
|
+
let anyDirFound = false;
|
|
1647
|
+
for (const projectPath of projectPaths) {
|
|
1648
|
+
const transcriptDir = join3(projectsRoot, encodeProjectDir(projectPath));
|
|
1649
|
+
let entries;
|
|
1650
|
+
try {
|
|
1651
|
+
entries = await readdir(transcriptDir);
|
|
1652
|
+
} catch (error) {
|
|
1653
|
+
if (findErrorCode5(error, "ENOENT")) continue;
|
|
1654
|
+
throw new Error("Failed to read Claude transcript directory", { cause: error });
|
|
1655
|
+
}
|
|
1656
|
+
anyDirFound = true;
|
|
1657
|
+
for (const name of entries) {
|
|
1658
|
+
if (name.endsWith(".jsonl")) files.push(join3(transcriptDir, name));
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
if (!anyDirFound) {
|
|
1662
|
+
throw new Error("Claude transcript directory not found for project");
|
|
1523
1663
|
}
|
|
1524
|
-
|
|
1664
|
+
return [...new Set(files)].sort();
|
|
1665
|
+
}
|
|
1666
|
+
async function pathExists(file) {
|
|
1525
1667
|
try {
|
|
1526
|
-
|
|
1668
|
+
await stat(file);
|
|
1669
|
+
return true;
|
|
1527
1670
|
} catch (error) {
|
|
1528
|
-
if (findErrorCode5(error, "ENOENT"))
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1671
|
+
if (findErrorCode5(error, "ENOENT")) return false;
|
|
1672
|
+
throw error;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
async function statSize(file) {
|
|
1676
|
+
try {
|
|
1677
|
+
return (await stat(file)).size;
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
if (findErrorCode5(error, "ENOENT")) return void 0;
|
|
1680
|
+
throw error;
|
|
1532
1681
|
}
|
|
1533
|
-
return entries.filter((name) => name.endsWith(".jsonl")).sort().map((name) => join3(transcriptDir, name));
|
|
1534
1682
|
}
|
|
1535
|
-
async function discoverCodexRollouts(sessionsRoot,
|
|
1683
|
+
async function discoverCodexRollouts(sessionsRoot, projectPaths, options) {
|
|
1684
|
+
const projectSet = new Set(projectPaths);
|
|
1536
1685
|
const files = await findRolloutFiles(sessionsRoot);
|
|
1537
1686
|
const matched = [];
|
|
1538
1687
|
for (const file of files) {
|
|
1539
1688
|
const meta = await readRolloutMeta(file);
|
|
1540
1689
|
if (meta === void 0) continue;
|
|
1541
|
-
if (meta.cwd
|
|
1690
|
+
if (!projectSet.has(meta.cwd)) continue;
|
|
1542
1691
|
if (options.session !== void 0 && meta.id !== options.session) continue;
|
|
1543
1692
|
matched.push({ file, externalId: meta.id });
|
|
1544
1693
|
}
|
|
@@ -1609,9 +1758,9 @@ async function readFirstLine(file) {
|
|
|
1609
1758
|
}
|
|
1610
1759
|
}
|
|
1611
1760
|
async function readJsonlRecords(file) {
|
|
1612
|
-
let
|
|
1761
|
+
let buffer;
|
|
1613
1762
|
try {
|
|
1614
|
-
|
|
1763
|
+
buffer = await readFile(file);
|
|
1615
1764
|
} catch (error) {
|
|
1616
1765
|
if (findErrorCode5(error, "ENOENT")) {
|
|
1617
1766
|
throw new Error("Source log not found", { cause: error });
|
|
@@ -1622,7 +1771,7 @@ async function readJsonlRecords(file) {
|
|
|
1622
1771
|
throw new Error("Failed to read source log", { cause: error });
|
|
1623
1772
|
}
|
|
1624
1773
|
const records = [];
|
|
1625
|
-
for (const line of
|
|
1774
|
+
for (const line of buffer.toString("utf8").split("\n")) {
|
|
1626
1775
|
const trimmed = line.trim();
|
|
1627
1776
|
if (trimmed.length === 0) continue;
|
|
1628
1777
|
try {
|
|
@@ -1633,7 +1782,7 @@ async function readJsonlRecords(file) {
|
|
|
1633
1782
|
} catch {
|
|
1634
1783
|
}
|
|
1635
1784
|
}
|
|
1636
|
-
return records;
|
|
1785
|
+
return { records, sizeBytes: buffer.length };
|
|
1637
1786
|
}
|
|
1638
1787
|
function isObject(value) {
|
|
1639
1788
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -1641,7 +1790,15 @@ function isObject(value) {
|
|
|
1641
1790
|
function printImportResult(options, results, counts) {
|
|
1642
1791
|
const isDry = options.dryRun === true;
|
|
1643
1792
|
const eventTotal = results.reduce((sum, r) => sum + r.eventCount, 0);
|
|
1644
|
-
const {
|
|
1793
|
+
const {
|
|
1794
|
+
skippedNoAction,
|
|
1795
|
+
skippedExisting,
|
|
1796
|
+
replaced,
|
|
1797
|
+
reimported,
|
|
1798
|
+
skippedLegacy,
|
|
1799
|
+
skippedDecreased,
|
|
1800
|
+
skippedDuplicate
|
|
1801
|
+
} = counts;
|
|
1645
1802
|
if (options.json === true) {
|
|
1646
1803
|
console.log(
|
|
1647
1804
|
JSON.stringify({
|
|
@@ -1653,8 +1810,12 @@ function printImportResult(options, results, counts) {
|
|
|
1653
1810
|
})),
|
|
1654
1811
|
imported_count: results.length,
|
|
1655
1812
|
replaced_count: replaced,
|
|
1813
|
+
reimported_count: reimported,
|
|
1656
1814
|
skipped_no_action: skippedNoAction,
|
|
1657
1815
|
skipped_already_imported: skippedExisting,
|
|
1816
|
+
skipped_legacy_untracked: skippedLegacy,
|
|
1817
|
+
skipped_decreased: skippedDecreased,
|
|
1818
|
+
skipped_duplicate: skippedDuplicate,
|
|
1658
1819
|
event_total: eventTotal,
|
|
1659
1820
|
dry_run: isDry
|
|
1660
1821
|
})
|
|
@@ -1664,20 +1825,36 @@ function printImportResult(options, results, counts) {
|
|
|
1664
1825
|
const skipParts = [];
|
|
1665
1826
|
if (skippedNoAction > 0) skipParts.push(`${skippedNoAction} with no actions`);
|
|
1666
1827
|
if (skippedExisting > 0) skipParts.push(`${skippedExisting} already imported`);
|
|
1828
|
+
if (skippedLegacy > 0) skipParts.push(`${skippedLegacy} legacy (untracked size)`);
|
|
1829
|
+
if (skippedDecreased > 0) skipParts.push(`${skippedDecreased} shrank`);
|
|
1830
|
+
if (skippedDuplicate > 0) skipParts.push(`${skippedDuplicate} duplicated`);
|
|
1667
1831
|
const skipSuffix = skipParts.length > 0 ? `; skipped ${skipParts.join(", ")}` : "";
|
|
1668
1832
|
const eventsPart = replaced > 0 ? `${eventTotal} events, ${replaced} replaced` : `${eventTotal} events`;
|
|
1669
|
-
if (
|
|
1833
|
+
if (isDry) {
|
|
1834
|
+
const parts = [];
|
|
1835
|
+
if (results.length > 0) parts.push(`import ${results.length} session(s) (${eventsPart})`);
|
|
1836
|
+
if (reimported > 0) parts.push(`re-import ${reimported} changed session(s)`);
|
|
1837
|
+
const head = parts.length > 0 ? `Dry run: would ${parts.join(", ")}` : "Dry run: no changes";
|
|
1838
|
+
console.log(`${head}${skipSuffix}`);
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
if (results.length === 0 && reimported === 0) {
|
|
1670
1842
|
console.log(
|
|
1671
1843
|
skipParts.length > 0 ? `No new sessions imported (skipped ${skipParts.join(", ")})` : "No transcripts found to import"
|
|
1672
1844
|
);
|
|
1673
1845
|
return;
|
|
1674
1846
|
}
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1847
|
+
const segments = [];
|
|
1848
|
+
if (results.length > 0) {
|
|
1849
|
+
const single = results.length === 1 && results[0] !== void 0 ? ` (${shortId2(results[0].sessionId)})` : "";
|
|
1850
|
+
segments.push(`Imported ${results.length} session(s)${single} (${eventsPart})`);
|
|
1851
|
+
}
|
|
1852
|
+
if (reimported > 0) {
|
|
1853
|
+
segments.push(
|
|
1854
|
+
`${results.length > 0 ? "re-imported" : "Re-imported"} ${reimported} changed session(s)`
|
|
1855
|
+
);
|
|
1678
1856
|
}
|
|
1679
|
-
|
|
1680
|
-
console.log(`Imported ${results.length} session(s)${single} (${eventsPart})${skipSuffix}`);
|
|
1857
|
+
console.log(`${segments.join(", ")}${skipSuffix}`);
|
|
1681
1858
|
}
|
|
1682
1859
|
function shortId2(id) {
|
|
1683
1860
|
if (id.startsWith(SES_PREFIX2)) {
|
|
@@ -1709,7 +1886,7 @@ async function assertWorkspaceInitialized5(basouRoot) {
|
|
|
1709
1886
|
}
|
|
1710
1887
|
|
|
1711
1888
|
// src/commands/init.ts
|
|
1712
|
-
import { basename as basename2 } from "path";
|
|
1889
|
+
import { basename as basename2, relative, resolve as resolve2 } from "path";
|
|
1713
1890
|
import {
|
|
1714
1891
|
appendBasouGitignore,
|
|
1715
1892
|
createManifest,
|
|
@@ -1718,10 +1895,18 @@ import {
|
|
|
1718
1895
|
tryRemoteUrl,
|
|
1719
1896
|
writeManifest
|
|
1720
1897
|
} from "@basou/core";
|
|
1898
|
+
function collectValue(value, previous) {
|
|
1899
|
+
return [...previous, value];
|
|
1900
|
+
}
|
|
1721
1901
|
function registerInitCommand(program2) {
|
|
1722
1902
|
program2.command("init").description("Initialize a Basou workspace at the current Git repository root").option("--name <name>", "Workspace name (defaults to the repository directory name)").option("--project-name <name>", "Project display name").option("--project-description <description>", "Project description").option(
|
|
1723
1903
|
"--repo-url <url>",
|
|
1724
1904
|
"Repository URL (defaults to git remote.origin.url; pass empty string for null)"
|
|
1905
|
+
).option(
|
|
1906
|
+
"--source-root <path>",
|
|
1907
|
+
"Extra import source root, relative to the repo root (repeatable; aggregates sibling repos into this workspace)",
|
|
1908
|
+
collectValue,
|
|
1909
|
+
[]
|
|
1725
1910
|
).option("-f, --force", "Overwrite an existing manifest").option("-v, --verbose", "Show error causes").action(async (options) => {
|
|
1726
1911
|
await runInit(options);
|
|
1727
1912
|
});
|
|
@@ -1744,12 +1929,17 @@ async function doRunInit(options, ctx) {
|
|
|
1744
1929
|
} else {
|
|
1745
1930
|
repositoryUrl = await tryRemoteUrl(repositoryRoot);
|
|
1746
1931
|
}
|
|
1932
|
+
const sourceRoots = (options.sourceRoot ?? []).map((p) => {
|
|
1933
|
+
const rel = relative(repositoryRoot, resolve2(cwd, p));
|
|
1934
|
+
return rel === "" ? "." : rel;
|
|
1935
|
+
});
|
|
1747
1936
|
const paths = await ensureBasouDirectory(repositoryRoot);
|
|
1748
1937
|
const manifest = createManifest({
|
|
1749
1938
|
workspaceName,
|
|
1750
1939
|
...options.projectName !== void 0 ? { projectName: options.projectName } : {},
|
|
1751
1940
|
...options.projectDescription !== void 0 ? { projectDescription: options.projectDescription } : {},
|
|
1752
|
-
...repositoryUrl !== void 0 ? { repositoryUrl } : {}
|
|
1941
|
+
...repositoryUrl !== void 0 ? { repositoryUrl } : {},
|
|
1942
|
+
...sourceRoots.length > 0 ? { sourceRoots } : {}
|
|
1753
1943
|
});
|
|
1754
1944
|
await writeManifest(paths, manifest, { force: options.force === true });
|
|
1755
1945
|
try {
|
|
@@ -1783,7 +1973,8 @@ async function resolveRepositoryRootForInit(cwd) {
|
|
|
1783
1973
|
}
|
|
1784
1974
|
|
|
1785
1975
|
// src/commands/refresh.ts
|
|
1786
|
-
import { assertBasouRootSafe as assertBasouRootSafe7, basouPaths as basouPaths7, findErrorCode as
|
|
1976
|
+
import { assertBasouRootSafe as assertBasouRootSafe7, basouPaths as basouPaths7, findErrorCode as findErrorCode7, resolveRepositoryRoot as resolveRepositoryRoot8 } from "@basou/core";
|
|
1977
|
+
import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
1787
1978
|
|
|
1788
1979
|
// src/lib/provenance-actions.ts
|
|
1789
1980
|
import {
|
|
@@ -1836,8 +2027,10 @@ async function runImport(adapter, fn) {
|
|
|
1836
2027
|
status: "ran",
|
|
1837
2028
|
importedCount: readCount(json.imported_count),
|
|
1838
2029
|
replacedCount: readCount(json.replaced_count),
|
|
2030
|
+
reimportedCount: readCount(json.reimported_count),
|
|
1839
2031
|
skippedNoAction: readCount(json.skipped_no_action),
|
|
1840
2032
|
skippedAlreadyImported: readCount(json.skipped_already_imported),
|
|
2033
|
+
skippedLegacyUntracked: readCount(json.skipped_legacy_untracked),
|
|
1841
2034
|
eventTotal: readCount(json.event_total),
|
|
1842
2035
|
dryRun: json.dry_run === true
|
|
1843
2036
|
};
|
|
@@ -1906,25 +2099,215 @@ async function refreshAll(args) {
|
|
|
1906
2099
|
};
|
|
1907
2100
|
}
|
|
1908
2101
|
|
|
2102
|
+
// src/commands/refresh-watch.ts
|
|
2103
|
+
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
2104
|
+
import { homedir as homedir3 } from "os";
|
|
2105
|
+
import { join as join4 } from "path";
|
|
2106
|
+
import { findErrorCode as findErrorCode6 } from "@basou/core";
|
|
2107
|
+
var DEFAULT_WATCH_INTERVAL_SEC = 30;
|
|
2108
|
+
var MIN_WATCH_INTERVAL_SEC = 5;
|
|
2109
|
+
var MAX_WATCH_INTERVAL_SEC = 86400;
|
|
2110
|
+
function watchedRoots(ctx) {
|
|
2111
|
+
return [
|
|
2112
|
+
ctx.codexSessionsDir ?? join4(homedir3(), ".codex", "sessions"),
|
|
2113
|
+
ctx.claudeProjectsDir ?? join4(homedir3(), ".claude", "projects")
|
|
2114
|
+
];
|
|
2115
|
+
}
|
|
2116
|
+
async function scanSourceLogs(roots) {
|
|
2117
|
+
const out = /* @__PURE__ */ new Map();
|
|
2118
|
+
const walk = async (dir) => {
|
|
2119
|
+
let entries;
|
|
2120
|
+
try {
|
|
2121
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
2122
|
+
} catch (error) {
|
|
2123
|
+
if (findErrorCode6(error, "ENOENT") || findErrorCode6(error, "ENOTDIR")) return;
|
|
2124
|
+
throw new Error("Failed to read a source log directory", { cause: error });
|
|
2125
|
+
}
|
|
2126
|
+
for (const entry of entries) {
|
|
2127
|
+
const full = join4(dir, entry.name);
|
|
2128
|
+
if (entry.isDirectory()) {
|
|
2129
|
+
await walk(full);
|
|
2130
|
+
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
2131
|
+
try {
|
|
2132
|
+
const info = await stat2(full);
|
|
2133
|
+
out.set(full, { mtimeMs: info.mtimeMs, size: info.size });
|
|
2134
|
+
} catch (error) {
|
|
2135
|
+
if (findErrorCode6(error, "ENOENT")) continue;
|
|
2136
|
+
throw new Error("Failed to stat a source log file", { cause: error });
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
};
|
|
2141
|
+
for (const root of roots) await walk(root);
|
|
2142
|
+
return out;
|
|
2143
|
+
}
|
|
2144
|
+
function scansEqual(a, b) {
|
|
2145
|
+
if (a.size !== b.size) return false;
|
|
2146
|
+
for (const [path, sig] of a) {
|
|
2147
|
+
const other = b.get(path);
|
|
2148
|
+
if (other === void 0 || other.mtimeMs !== sig.mtimeMs || other.size !== sig.size) {
|
|
2149
|
+
return false;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
return true;
|
|
2153
|
+
}
|
|
2154
|
+
function changedCount(outcome) {
|
|
2155
|
+
return outcome.status === "ran" ? outcome.importedCount + outcome.reimportedCount + outcome.replacedCount : 0;
|
|
2156
|
+
}
|
|
2157
|
+
function describeOutcome(outcome) {
|
|
2158
|
+
if (outcome.status !== "ran") return `${outcome.adapter} skipped`;
|
|
2159
|
+
const reimported = outcome.reimportedCount > 0 ? ` ~${outcome.reimportedCount}` : "";
|
|
2160
|
+
return `${outcome.adapter} +${outcome.importedCount}${reimported}`;
|
|
2161
|
+
}
|
|
2162
|
+
function hms(date) {
|
|
2163
|
+
return date.toISOString().slice(11, 19);
|
|
2164
|
+
}
|
|
2165
|
+
async function runImports(deps) {
|
|
2166
|
+
const claude = await importClaudeCode(deps.importOptions, deps.ctx);
|
|
2167
|
+
const codex = await importCodex(deps.importOptions, deps.ctx);
|
|
2168
|
+
return { claude, codex, changed: changedCount(claude) + changedCount(codex) };
|
|
2169
|
+
}
|
|
2170
|
+
async function regenerate(deps) {
|
|
2171
|
+
const nowIso = deps.now().toISOString();
|
|
2172
|
+
const handoff = await regenerateHandoff(deps.paths, nowIso);
|
|
2173
|
+
await regenerateDecisions(deps.paths, nowIso);
|
|
2174
|
+
return handoff.sessionCount;
|
|
2175
|
+
}
|
|
2176
|
+
async function runRefreshWatch(deps) {
|
|
2177
|
+
const { intervalMs, ctx, signal, sleep, log } = deps;
|
|
2178
|
+
const roots = watchedRoots(ctx);
|
|
2179
|
+
log(
|
|
2180
|
+
`watching ${roots.join(", ")} every ${Math.round(intervalMs / 1e3)}s (imports on change; Ctrl-C to stop)`
|
|
2181
|
+
);
|
|
2182
|
+
let lastScan = await scanSourceLogs(roots);
|
|
2183
|
+
let importedScan = lastScan;
|
|
2184
|
+
const initial = await runImports(deps);
|
|
2185
|
+
const initialSessions = await regenerate(deps);
|
|
2186
|
+
log(
|
|
2187
|
+
`[${hms(deps.now())}] refreshed: ${describeOutcome(initial.codex)}, ${describeOutcome(initial.claude)} (sessions: ${initialSessions})`
|
|
2188
|
+
);
|
|
2189
|
+
if (signal.aborted) {
|
|
2190
|
+
log("watch stopped");
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
let pendingRegen = false;
|
|
2194
|
+
while (!signal.aborted) {
|
|
2195
|
+
await sleep(intervalMs, signal);
|
|
2196
|
+
if (signal.aborted) break;
|
|
2197
|
+
try {
|
|
2198
|
+
const current = await scanSourceLogs(roots);
|
|
2199
|
+
if (scansEqual(current, lastScan) && !scansEqual(current, importedScan)) {
|
|
2200
|
+
const { claude, codex, changed } = await runImports(deps);
|
|
2201
|
+
if (changed > 0) pendingRegen = true;
|
|
2202
|
+
if (pendingRegen) {
|
|
2203
|
+
const sessions = await regenerate(deps);
|
|
2204
|
+
pendingRegen = false;
|
|
2205
|
+
log(
|
|
2206
|
+
`[${hms(deps.now())}] refreshed: ${describeOutcome(codex)}, ${describeOutcome(claude)} (sessions: ${sessions})`
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2209
|
+
importedScan = current;
|
|
2210
|
+
}
|
|
2211
|
+
lastScan = current;
|
|
2212
|
+
} catch (error) {
|
|
2213
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2214
|
+
log(`[${hms(deps.now())}] refresh cycle skipped: ${message}`);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
log("watch stopped");
|
|
2218
|
+
}
|
|
2219
|
+
|
|
1909
2220
|
// src/commands/refresh.ts
|
|
2221
|
+
function collectPath2(value, previous) {
|
|
2222
|
+
return [...previous, value];
|
|
2223
|
+
}
|
|
2224
|
+
function parseInterval(value) {
|
|
2225
|
+
const seconds = Number(value);
|
|
2226
|
+
if (!Number.isInteger(seconds) || seconds < MIN_WATCH_INTERVAL_SEC || seconds > MAX_WATCH_INTERVAL_SEC) {
|
|
2227
|
+
throw new InvalidArgumentError2(
|
|
2228
|
+
`--interval must be an integer between ${MIN_WATCH_INTERVAL_SEC} and ${MAX_WATCH_INTERVAL_SEC} (seconds).`
|
|
2229
|
+
);
|
|
2230
|
+
}
|
|
2231
|
+
return seconds;
|
|
2232
|
+
}
|
|
2233
|
+
function abortableSleep(ms, signal) {
|
|
2234
|
+
return new Promise((resolve3) => {
|
|
2235
|
+
if (signal.aborted) {
|
|
2236
|
+
resolve3();
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
let timer;
|
|
2240
|
+
const onAbort = () => {
|
|
2241
|
+
clearTimeout(timer);
|
|
2242
|
+
resolve3();
|
|
2243
|
+
};
|
|
2244
|
+
timer = setTimeout(() => {
|
|
2245
|
+
signal.removeEventListener("abort", onAbort);
|
|
2246
|
+
resolve3();
|
|
2247
|
+
}, ms);
|
|
2248
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
1910
2251
|
function registerRefreshCommand(program2) {
|
|
1911
2252
|
program2.command("refresh").description(
|
|
1912
2253
|
"Import all adapters for the project and regenerate handoff + decisions in one step"
|
|
1913
2254
|
).option(
|
|
1914
2255
|
"--project <path>",
|
|
1915
|
-
"Source project path to import (defaults to the
|
|
1916
|
-
|
|
2256
|
+
"Source project path to import (repeatable; defaults to the manifest source roots, then the repository root)",
|
|
2257
|
+
collectPath2,
|
|
2258
|
+
[]
|
|
2259
|
+
).option("--force", "Re-import sessions already imported instead of skipping").option("--dry-run", "Preview imports and skip writing handoff / decisions").option("--json", "Output the result as JSON").option(
|
|
2260
|
+
"--watch",
|
|
2261
|
+
"Keep running: re-import + regenerate when the native logs change (Ctrl-C to stop)"
|
|
2262
|
+
).option(
|
|
2263
|
+
"--interval <seconds>",
|
|
2264
|
+
`Poll interval for --watch, in seconds (default ${DEFAULT_WATCH_INTERVAL_SEC}, min ${MIN_WATCH_INTERVAL_SEC})`,
|
|
2265
|
+
parseInterval
|
|
2266
|
+
).option("-v, --verbose", "Show error causes").action(async (options) => {
|
|
1917
2267
|
await runRefresh(options);
|
|
1918
2268
|
});
|
|
1919
2269
|
}
|
|
1920
2270
|
async function runRefresh(options, ctx = {}) {
|
|
1921
2271
|
try {
|
|
1922
|
-
|
|
2272
|
+
if (options.watch === true) {
|
|
2273
|
+
await doRunRefreshWatch(options, ctx);
|
|
2274
|
+
} else {
|
|
2275
|
+
await doRunRefresh(options, ctx);
|
|
2276
|
+
}
|
|
1923
2277
|
} catch (error) {
|
|
1924
2278
|
renderCliError(error, { verbose: isVerbose(options) });
|
|
1925
2279
|
process.exitCode = 1;
|
|
1926
2280
|
}
|
|
1927
2281
|
}
|
|
2282
|
+
async function doRunRefreshWatch(options, ctx) {
|
|
2283
|
+
if (options.dryRun === true) throw new Error("--watch cannot be combined with --dry-run.");
|
|
2284
|
+
if (options.json === true) throw new Error("--watch cannot be combined with --json.");
|
|
2285
|
+
if (options.force === true) throw new Error("--watch cannot be combined with --force.");
|
|
2286
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
2287
|
+
const repositoryRoot = await resolveRepositoryRootForRefresh(cwd);
|
|
2288
|
+
const paths = basouPaths7(repositoryRoot);
|
|
2289
|
+
await assertWorkspaceInitialized6(paths.root);
|
|
2290
|
+
const intervalMs = (options.interval ?? DEFAULT_WATCH_INTERVAL_SEC) * 1e3;
|
|
2291
|
+
const controller = new AbortController();
|
|
2292
|
+
const onSignal = () => controller.abort();
|
|
2293
|
+
process.on("SIGINT", onSignal);
|
|
2294
|
+
process.on("SIGTERM", onSignal);
|
|
2295
|
+
try {
|
|
2296
|
+
await runRefreshWatch({
|
|
2297
|
+
ctx,
|
|
2298
|
+
paths,
|
|
2299
|
+
intervalMs,
|
|
2300
|
+
importOptions: options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
|
|
2301
|
+
now: () => ctx.nowProvider?.() ?? /* @__PURE__ */ new Date(),
|
|
2302
|
+
signal: controller.signal,
|
|
2303
|
+
sleep: abortableSleep,
|
|
2304
|
+
log: (line) => console.log(line)
|
|
2305
|
+
});
|
|
2306
|
+
} finally {
|
|
2307
|
+
process.off("SIGINT", onSignal);
|
|
2308
|
+
process.off("SIGTERM", onSignal);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
1928
2311
|
async function doRunRefresh(options, ctx) {
|
|
1929
2312
|
const cwd = ctx.cwd ?? process.cwd();
|
|
1930
2313
|
const repositoryRoot = await resolveRepositoryRootForRefresh(cwd);
|
|
@@ -1933,7 +2316,7 @@ async function doRunRefresh(options, ctx) {
|
|
|
1933
2316
|
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1934
2317
|
const result = await refreshAll({
|
|
1935
2318
|
options: {
|
|
1936
|
-
...options.project !== void 0 ? { project: options.project } : {},
|
|
2319
|
+
...options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
|
|
1937
2320
|
...options.force === true ? { force: true } : {},
|
|
1938
2321
|
...options.dryRun === true ? { dryRun: true } : {}
|
|
1939
2322
|
},
|
|
@@ -1954,9 +2337,11 @@ function describeImport(outcome) {
|
|
|
1954
2337
|
}
|
|
1955
2338
|
const verb = outcome.dryRun ? "would import" : "imported";
|
|
1956
2339
|
const parts = [`${outcome.importedCount} session(s)`, `${outcome.eventTotal} events`];
|
|
2340
|
+
if (outcome.reimportedCount > 0) parts.push(`${outcome.reimportedCount} re-imported`);
|
|
1957
2341
|
if (outcome.replacedCount > 0) parts.push(`${outcome.replacedCount} replaced`);
|
|
1958
2342
|
if (outcome.skippedAlreadyImported > 0)
|
|
1959
2343
|
parts.push(`${outcome.skippedAlreadyImported} already imported`);
|
|
2344
|
+
if (outcome.skippedLegacyUntracked > 0) parts.push(`${outcome.skippedLegacyUntracked} legacy`);
|
|
1960
2345
|
return `${outcome.adapter}: ${verb} ${parts.join(", ")}`;
|
|
1961
2346
|
}
|
|
1962
2347
|
function printRefreshSummary(result) {
|
|
@@ -1991,7 +2376,7 @@ async function assertWorkspaceInitialized6(basouRoot) {
|
|
|
1991
2376
|
try {
|
|
1992
2377
|
await assertBasouRootSafe7(basouRoot);
|
|
1993
2378
|
} catch (error) {
|
|
1994
|
-
if (
|
|
2379
|
+
if (findErrorCode7(error, "ENOENT")) {
|
|
1995
2380
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
1996
2381
|
}
|
|
1997
2382
|
throw error;
|
|
@@ -2000,8 +2385,8 @@ async function assertWorkspaceInitialized6(basouRoot) {
|
|
|
2000
2385
|
|
|
2001
2386
|
// src/commands/run.ts
|
|
2002
2387
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
2003
|
-
import { homedir as
|
|
2004
|
-
import { join as
|
|
2388
|
+
import { homedir as homedir4 } from "os";
|
|
2389
|
+
import { join as join5 } from "path";
|
|
2005
2390
|
import {
|
|
2006
2391
|
assertBasouRootSafe as assertBasouRootSafe8,
|
|
2007
2392
|
basouPaths as basouPaths8,
|
|
@@ -2053,10 +2438,10 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2053
2438
|
await assertBasouRootSafe8(paths.root);
|
|
2054
2439
|
const manifest = await readManifest4(paths);
|
|
2055
2440
|
const sessionId = prefixedUlid4("ses");
|
|
2056
|
-
const sessionDir =
|
|
2441
|
+
const sessionDir = join5(paths.sessions, sessionId);
|
|
2057
2442
|
await mkdir2(sessionDir, { recursive: true });
|
|
2058
2443
|
const startedAt = now().toISOString();
|
|
2059
|
-
const sessionYamlPath =
|
|
2444
|
+
const sessionYamlPath = join5(sessionDir, "session.yaml");
|
|
2060
2445
|
const session = buildInitialSession2({
|
|
2061
2446
|
id: sessionId,
|
|
2062
2447
|
command,
|
|
@@ -2177,7 +2562,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2177
2562
|
const rawRelated = computeRelatedFiles(preSnapshot, postSnapshot, diff);
|
|
2178
2563
|
const relatedFiles = sanitizeRelatedFiles(rawRelated, {
|
|
2179
2564
|
workingDirectory: repoRoot,
|
|
2180
|
-
homedir:
|
|
2565
|
+
homedir: homedir4()
|
|
2181
2566
|
}).sanitized;
|
|
2182
2567
|
const finalStatus = decideFinalStatus2(result, signalReceived);
|
|
2183
2568
|
await appendEvent2(sessionDir, {
|
|
@@ -2321,7 +2706,7 @@ function buildInitialSession2(input) {
|
|
|
2321
2706
|
source: { ...claudeCodeAdapterMetadata },
|
|
2322
2707
|
started_at: input.startedAt,
|
|
2323
2708
|
status: "initialized",
|
|
2324
|
-
working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir:
|
|
2709
|
+
working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir4() }),
|
|
2325
2710
|
invocation: {
|
|
2326
2711
|
command: input.command,
|
|
2327
2712
|
args: [...input.args],
|
|
@@ -2394,13 +2779,13 @@ async function resolveRepositoryRootForRun(cwd) {
|
|
|
2394
2779
|
|
|
2395
2780
|
// src/commands/session.ts
|
|
2396
2781
|
import { readFile as readFile2 } from "fs/promises";
|
|
2397
|
-
import { basename as basename3, isAbsolute, join as
|
|
2782
|
+
import { basename as basename3, isAbsolute, join as join6, relative as relative2 } from "path";
|
|
2398
2783
|
import {
|
|
2399
2784
|
acquireLock as acquireLock2,
|
|
2400
2785
|
appendEventToExistingSession as appendEventToExistingSession2,
|
|
2401
2786
|
assertBasouRootSafe as assertBasouRootSafe9,
|
|
2402
2787
|
basouPaths as basouPaths9,
|
|
2403
|
-
findErrorCode as
|
|
2788
|
+
findErrorCode as findErrorCode8,
|
|
2404
2789
|
importSessionFromJson as importSessionFromJson2,
|
|
2405
2790
|
loadSessionEntries,
|
|
2406
2791
|
readAllEvents,
|
|
@@ -2414,7 +2799,7 @@ import {
|
|
|
2414
2799
|
SessionStatusSchema,
|
|
2415
2800
|
sessionWorkStatsFromEvents
|
|
2416
2801
|
} from "@basou/core";
|
|
2417
|
-
import { InvalidArgumentError as
|
|
2802
|
+
import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
|
|
2418
2803
|
|
|
2419
2804
|
// src/lib/format-duration.ts
|
|
2420
2805
|
function formatDurationMs(ms) {
|
|
@@ -2521,14 +2906,14 @@ async function doRunSessionShow(idInput, options, ctx) {
|
|
|
2521
2906
|
const paths = basouPaths9(repositoryRoot);
|
|
2522
2907
|
await assertWorkspaceInitialized7(paths.root);
|
|
2523
2908
|
const sessionId = await resolveSessionId2(paths, idInput);
|
|
2524
|
-
const sessionDir =
|
|
2525
|
-
const sessionYamlPath =
|
|
2909
|
+
const sessionDir = join6(paths.sessions, sessionId);
|
|
2910
|
+
const sessionYamlPath = join6(sessionDir, "session.yaml");
|
|
2526
2911
|
let session;
|
|
2527
2912
|
try {
|
|
2528
2913
|
const raw = await readYamlFile4(sessionYamlPath);
|
|
2529
2914
|
session = SessionSchema3.parse(raw);
|
|
2530
2915
|
} catch (error) {
|
|
2531
|
-
if (
|
|
2916
|
+
if (findErrorCode8(error, "ENOENT")) {
|
|
2532
2917
|
throw new Error(`Session not found: ${idInput}`);
|
|
2533
2918
|
}
|
|
2534
2919
|
throw new Error("Failed to read session", { cause: error });
|
|
@@ -2648,7 +3033,7 @@ function formatWorkingDir(workingDir, repositoryRoot, options) {
|
|
|
2648
3033
|
return workingDir;
|
|
2649
3034
|
}
|
|
2650
3035
|
if (workingDir === repositoryRoot) return "<repository_root>";
|
|
2651
|
-
const rel =
|
|
3036
|
+
const rel = relative2(repositoryRoot, workingDir);
|
|
2652
3037
|
if (rel.length === 0 || rel === ".") return "<repository_root>";
|
|
2653
3038
|
if (rel.startsWith("..")) return rel;
|
|
2654
3039
|
return `./${rel}`;
|
|
@@ -2778,7 +3163,7 @@ async function assertWorkspaceInitialized7(basouRoot) {
|
|
|
2778
3163
|
try {
|
|
2779
3164
|
await assertBasouRootSafe9(basouRoot);
|
|
2780
3165
|
} catch (error) {
|
|
2781
|
-
if (
|
|
3166
|
+
if (findErrorCode8(error, "ENOENT")) {
|
|
2782
3167
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
2783
3168
|
}
|
|
2784
3169
|
throw error;
|
|
@@ -2847,10 +3232,10 @@ async function readInputFile(path) {
|
|
|
2847
3232
|
try {
|
|
2848
3233
|
return await readFile2(path, "utf8");
|
|
2849
3234
|
} catch (error) {
|
|
2850
|
-
if (
|
|
3235
|
+
if (findErrorCode8(error, "ENOENT")) {
|
|
2851
3236
|
throw new Error("Import source not found", { cause: error });
|
|
2852
3237
|
}
|
|
2853
|
-
if (
|
|
3238
|
+
if (findErrorCode8(error, "EISDIR")) {
|
|
2854
3239
|
throw new Error("Import source is not a file", { cause: error });
|
|
2855
3240
|
}
|
|
2856
3241
|
throw new Error("Failed to read import source", { cause: error });
|
|
@@ -2865,19 +3250,19 @@ function parseJsonStrict(body) {
|
|
|
2865
3250
|
}
|
|
2866
3251
|
function parseImportFormat(raw) {
|
|
2867
3252
|
if (raw !== "json") {
|
|
2868
|
-
throw new
|
|
3253
|
+
throw new InvalidArgumentError3(`Unsupported format: ${raw}. Valid values: json`);
|
|
2869
3254
|
}
|
|
2870
3255
|
return "json";
|
|
2871
3256
|
}
|
|
2872
3257
|
function parseLabelOverride(raw) {
|
|
2873
3258
|
if (raw.length === 0) {
|
|
2874
|
-
throw new
|
|
3259
|
+
throw new InvalidArgumentError3("Label must not be empty");
|
|
2875
3260
|
}
|
|
2876
3261
|
return raw;
|
|
2877
3262
|
}
|
|
2878
3263
|
function parseTaskIdOverride(raw) {
|
|
2879
3264
|
if (raw.length === 0) {
|
|
2880
|
-
throw new
|
|
3265
|
+
throw new InvalidArgumentError3("Task id is empty");
|
|
2881
3266
|
}
|
|
2882
3267
|
return raw;
|
|
2883
3268
|
}
|
|
@@ -2964,10 +3349,10 @@ async function readNoteFile(path) {
|
|
|
2964
3349
|
try {
|
|
2965
3350
|
return await readFile2(path, "utf8");
|
|
2966
3351
|
} catch (error) {
|
|
2967
|
-
if (
|
|
3352
|
+
if (findErrorCode8(error, "ENOENT")) {
|
|
2968
3353
|
throw new Error("Note source not found", { cause: error });
|
|
2969
3354
|
}
|
|
2970
|
-
if (
|
|
3355
|
+
if (findErrorCode8(error, "EISDIR")) {
|
|
2971
3356
|
throw new Error("Note source is not a file", { cause: error });
|
|
2972
3357
|
}
|
|
2973
3358
|
throw new Error("Failed to read note source", { cause: error });
|
|
@@ -2975,7 +3360,7 @@ async function readNoteFile(path) {
|
|
|
2975
3360
|
}
|
|
2976
3361
|
function parseNoteBodyOption(raw) {
|
|
2977
3362
|
if (raw.length === 0) {
|
|
2978
|
-
throw new
|
|
3363
|
+
throw new InvalidArgumentError3("--body must not be empty");
|
|
2979
3364
|
}
|
|
2980
3365
|
return raw;
|
|
2981
3366
|
}
|
|
@@ -3001,7 +3386,7 @@ import {
|
|
|
3001
3386
|
assertBasouRootSafe as assertBasouRootSafe10,
|
|
3002
3387
|
basouPaths as basouPaths10,
|
|
3003
3388
|
computeWorkStats,
|
|
3004
|
-
findErrorCode as
|
|
3389
|
+
findErrorCode as findErrorCode9,
|
|
3005
3390
|
resolveRepositoryRoot as resolveRepositoryRoot11
|
|
3006
3391
|
} from "@basou/core";
|
|
3007
3392
|
function registerStatsCommand(program2) {
|
|
@@ -3119,7 +3504,7 @@ async function assertWorkspaceInitialized8(basouRoot) {
|
|
|
3119
3504
|
try {
|
|
3120
3505
|
await assertBasouRootSafe10(basouRoot);
|
|
3121
3506
|
} catch (error) {
|
|
3122
|
-
if (
|
|
3507
|
+
if (findErrorCode9(error, "ENOENT")) {
|
|
3123
3508
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3124
3509
|
}
|
|
3125
3510
|
throw error;
|
|
@@ -3131,7 +3516,7 @@ import {
|
|
|
3131
3516
|
assertBasouRootSafe as assertBasouRootSafe11,
|
|
3132
3517
|
basouPaths as basouPaths11,
|
|
3133
3518
|
buildStatusSnapshot,
|
|
3134
|
-
findErrorCode as
|
|
3519
|
+
findErrorCode as findErrorCode10,
|
|
3135
3520
|
readManifest as readManifest6,
|
|
3136
3521
|
resolveRepositoryRoot as resolveRepositoryRoot12,
|
|
3137
3522
|
writeStatus
|
|
@@ -3156,7 +3541,7 @@ async function doRunStatus(options, ctx) {
|
|
|
3156
3541
|
try {
|
|
3157
3542
|
await assertBasouRootSafe11(paths.root);
|
|
3158
3543
|
} catch (error) {
|
|
3159
|
-
if (
|
|
3544
|
+
if (findErrorCode10(error, "ENOENT")) {
|
|
3160
3545
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3161
3546
|
}
|
|
3162
3547
|
throw error;
|
|
@@ -3165,7 +3550,7 @@ async function doRunStatus(options, ctx) {
|
|
|
3165
3550
|
try {
|
|
3166
3551
|
manifest = await readManifest6(paths);
|
|
3167
3552
|
} catch (error) {
|
|
3168
|
-
if (
|
|
3553
|
+
if (findErrorCode10(error, "ENOENT")) {
|
|
3169
3554
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3170
3555
|
}
|
|
3171
3556
|
throw new Error("Failed to read workspace manifest", { cause: error });
|
|
@@ -3202,7 +3587,7 @@ async function resolveRepositoryRootForStatus(cwd) {
|
|
|
3202
3587
|
|
|
3203
3588
|
// src/commands/task.ts
|
|
3204
3589
|
import { readFile as readFile3 } from "fs/promises";
|
|
3205
|
-
import { join as
|
|
3590
|
+
import { join as join7 } from "path";
|
|
3206
3591
|
import {
|
|
3207
3592
|
archiveTask,
|
|
3208
3593
|
assertBasouRootSafe as assertBasouRootSafe12,
|
|
@@ -3211,7 +3596,7 @@ import {
|
|
|
3211
3596
|
deleteTask,
|
|
3212
3597
|
editTask,
|
|
3213
3598
|
enumerateArchivedTaskIds,
|
|
3214
|
-
findErrorCode as
|
|
3599
|
+
findErrorCode as findErrorCode11,
|
|
3215
3600
|
loadSessionEntries as loadSessionEntries2,
|
|
3216
3601
|
loadTaskEntries,
|
|
3217
3602
|
prefixedUlid as prefixedUlid5,
|
|
@@ -3229,7 +3614,7 @@ import {
|
|
|
3229
3614
|
TaskWriteAfterEventError,
|
|
3230
3615
|
updateTaskStatusWithEvent
|
|
3231
3616
|
} from "@basou/core";
|
|
3232
|
-
import { InvalidArgumentError as
|
|
3617
|
+
import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
|
|
3233
3618
|
var STATUS_VALUES3 = TaskStatusSchema.options;
|
|
3234
3619
|
function registerTaskCommand(program2) {
|
|
3235
3620
|
const task = program2.command("task").description("Manage Basou tasks (purpose units that span sessions)");
|
|
@@ -3529,7 +3914,7 @@ async function doRunTaskShow(idInput, options, ctx) {
|
|
|
3529
3914
|
const events = [];
|
|
3530
3915
|
const linkedSessionIds = new Set(doc.task.task.linked_sessions);
|
|
3531
3916
|
for (const s of sessions) {
|
|
3532
|
-
const sessionDir =
|
|
3917
|
+
const sessionDir = join7(paths.sessions, s.sessionId);
|
|
3533
3918
|
try {
|
|
3534
3919
|
for await (const ev of replayEvents2(sessionDir, {
|
|
3535
3920
|
onWarning: (w) => printReplayWarning(w, s.sessionId)
|
|
@@ -4158,20 +4543,20 @@ async function readSingleLineFromStdin() {
|
|
|
4158
4543
|
}
|
|
4159
4544
|
function parseTitle2(raw) {
|
|
4160
4545
|
if (raw.length === 0) {
|
|
4161
|
-
throw new
|
|
4546
|
+
throw new InvalidArgumentError4("Title must not be empty");
|
|
4162
4547
|
}
|
|
4163
4548
|
return raw;
|
|
4164
4549
|
}
|
|
4165
4550
|
function parseLabel(raw) {
|
|
4166
4551
|
if (raw.length === 0) {
|
|
4167
|
-
throw new
|
|
4552
|
+
throw new InvalidArgumentError4("Label must not be empty");
|
|
4168
4553
|
}
|
|
4169
4554
|
return raw;
|
|
4170
4555
|
}
|
|
4171
4556
|
function parseInitialTaskStatus(raw) {
|
|
4172
4557
|
const result = TaskStatusSchema.safeParse(raw);
|
|
4173
4558
|
if (!result.success) {
|
|
4174
|
-
throw new
|
|
4559
|
+
throw new InvalidArgumentError4(
|
|
4175
4560
|
`Initial task status must be one of: ${STATUS_VALUES3.join(", ")}`
|
|
4176
4561
|
);
|
|
4177
4562
|
}
|
|
@@ -4180,7 +4565,7 @@ function parseInitialTaskStatus(raw) {
|
|
|
4180
4565
|
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
4181
4566
|
function parseIsoTimestampOption(raw) {
|
|
4182
4567
|
if (!ISO_DATE_RE.test(raw) || Number.isNaN(Date.parse(raw))) {
|
|
4183
|
-
throw new
|
|
4568
|
+
throw new InvalidArgumentError4(
|
|
4184
4569
|
"Invalid --completed-at value; expected ISO-8601 timestamp like 2026-05-10T12:34:56+09:00"
|
|
4185
4570
|
);
|
|
4186
4571
|
}
|
|
@@ -4189,7 +4574,7 @@ function parseIsoTimestampOption(raw) {
|
|
|
4189
4574
|
function parseTaskStatusFilter(raw) {
|
|
4190
4575
|
const result = TaskStatusSchema.safeParse(raw);
|
|
4191
4576
|
if (!result.success) {
|
|
4192
|
-
throw new
|
|
4577
|
+
throw new InvalidArgumentError4(
|
|
4193
4578
|
`Invalid task status: ${raw}. Valid values: ${STATUS_VALUES3.join(", ")}`
|
|
4194
4579
|
);
|
|
4195
4580
|
}
|
|
@@ -4204,14 +4589,14 @@ function parseTaskStatusPositional(raw) {
|
|
|
4204
4589
|
}
|
|
4205
4590
|
function parseDescriptionOption(raw) {
|
|
4206
4591
|
if (raw.length === 0) {
|
|
4207
|
-
throw new
|
|
4592
|
+
throw new InvalidArgumentError4("Description must not be empty");
|
|
4208
4593
|
}
|
|
4209
4594
|
return raw;
|
|
4210
4595
|
}
|
|
4211
4596
|
function parsePositiveInt2(raw) {
|
|
4212
4597
|
const n = Number.parseInt(raw, 10);
|
|
4213
4598
|
if (!Number.isInteger(n) || n < 1 || raw.trim() !== String(n)) {
|
|
4214
|
-
throw new
|
|
4599
|
+
throw new InvalidArgumentError4(`Invalid number: ${raw}`);
|
|
4215
4600
|
}
|
|
4216
4601
|
return n;
|
|
4217
4602
|
}
|
|
@@ -4219,10 +4604,10 @@ async function readDescriptionFile(path) {
|
|
|
4219
4604
|
try {
|
|
4220
4605
|
return await readFile3(path, "utf8");
|
|
4221
4606
|
} catch (error) {
|
|
4222
|
-
if (
|
|
4607
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
4223
4608
|
throw new Error("Description source not found", { cause: error });
|
|
4224
4609
|
}
|
|
4225
|
-
if (
|
|
4610
|
+
if (findErrorCode11(error, "EISDIR")) {
|
|
4226
4611
|
throw new Error("Description source is not a file", { cause: error });
|
|
4227
4612
|
}
|
|
4228
4613
|
throw new Error("Failed to read description source", { cause: error });
|
|
@@ -4245,7 +4630,7 @@ async function assertWorkspaceInitialized9(basouRoot) {
|
|
|
4245
4630
|
try {
|
|
4246
4631
|
await assertBasouRootSafe12(basouRoot);
|
|
4247
4632
|
} catch (error) {
|
|
4248
|
-
if (
|
|
4633
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
4249
4634
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
4250
4635
|
}
|
|
4251
4636
|
throw error;
|
|
@@ -4333,16 +4718,16 @@ function maxLen3(values, floor) {
|
|
|
4333
4718
|
|
|
4334
4719
|
// src/commands/view.ts
|
|
4335
4720
|
import { spawn } from "child_process";
|
|
4336
|
-
import { assertBasouRootSafe as assertBasouRootSafe13, basouPaths as basouPaths13, findErrorCode as
|
|
4337
|
-
import { InvalidArgumentError as
|
|
4721
|
+
import { assertBasouRootSafe as assertBasouRootSafe13, basouPaths as basouPaths13, findErrorCode as findErrorCode13, resolveRepositoryRoot as resolveRepositoryRoot14 } from "@basou/core";
|
|
4722
|
+
import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
|
|
4338
4723
|
|
|
4339
4724
|
// src/lib/view-server.ts
|
|
4340
4725
|
import { createServer } from "http";
|
|
4341
|
-
import { join as
|
|
4726
|
+
import { join as join8 } from "path";
|
|
4342
4727
|
import {
|
|
4343
4728
|
computeWorkStats as computeWorkStats2,
|
|
4344
4729
|
enumerateApprovals as enumerateApprovals2,
|
|
4345
|
-
findErrorCode as
|
|
4730
|
+
findErrorCode as findErrorCode12,
|
|
4346
4731
|
isLazyExpired as isLazyExpired2,
|
|
4347
4732
|
loadApproval as loadApproval2,
|
|
4348
4733
|
loadSessionEntries as loadSessionEntries3,
|
|
@@ -4407,7 +4792,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
4407
4792
|
<body>
|
|
4408
4793
|
<header>
|
|
4409
4794
|
<h1>basou view</h1>
|
|
4410
|
-
<input type="text" id="project" placeholder="
|
|
4795
|
+
<input type="text" id="project" placeholder="source root (optional override)" />
|
|
4411
4796
|
<button class="primary" id="btn-refresh">Refresh all</button>
|
|
4412
4797
|
<button id="btn-import-claude">Import claude-code</button>
|
|
4413
4798
|
<button id="btn-import-codex">Import codex</button>
|
|
@@ -4561,7 +4946,10 @@ var VIEW_HTML = `<!doctype html>
|
|
|
4561
4946
|
detail.appendChild(el('p', { class: 'muted', text: 'Workspace not initialized.' }));
|
|
4562
4947
|
return;
|
|
4563
4948
|
}
|
|
4564
|
-
|
|
4949
|
+
// Leave the project field empty by default so refresh / import use the
|
|
4950
|
+
// manifest's import.source_roots (then the repo root) -- pre-filling the
|
|
4951
|
+
// repo root here would send it as an explicit --project and silently
|
|
4952
|
+
// override multi-root source roots. The field is an optional override.
|
|
4565
4953
|
state.repoRoot = d.repoRoot || '';
|
|
4566
4954
|
detail.appendChild(el('p', {}, [
|
|
4567
4955
|
el('strong', { text: d.workspace.name }), ' ',
|
|
@@ -4812,7 +5200,7 @@ function startViewServer(opts) {
|
|
|
4812
5200
|
};
|
|
4813
5201
|
let boundPort = port;
|
|
4814
5202
|
const getPort = () => boundPort;
|
|
4815
|
-
return new Promise((
|
|
5203
|
+
return new Promise((resolve3, reject) => {
|
|
4816
5204
|
const server = createServer((req, res) => {
|
|
4817
5205
|
handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
|
|
4818
5206
|
sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
|
|
@@ -4823,7 +5211,7 @@ function startViewServer(opts) {
|
|
|
4823
5211
|
const address = server.address();
|
|
4824
5212
|
boundPort = isAddressInfo(address) ? address.port : port;
|
|
4825
5213
|
server.off("error", reject);
|
|
4826
|
-
|
|
5214
|
+
resolve3({
|
|
4827
5215
|
url: `http://${host}:${boundPort}`,
|
|
4828
5216
|
port: boundPort,
|
|
4829
5217
|
close: () => closeServer(server)
|
|
@@ -4835,8 +5223,8 @@ function isAddressInfo(value) {
|
|
|
4835
5223
|
return value !== null && typeof value === "object";
|
|
4836
5224
|
}
|
|
4837
5225
|
function closeServer(server) {
|
|
4838
|
-
return new Promise((
|
|
4839
|
-
server.close(() =>
|
|
5226
|
+
return new Promise((resolve3) => {
|
|
5227
|
+
server.close(() => resolve3());
|
|
4840
5228
|
server.closeAllConnections();
|
|
4841
5229
|
});
|
|
4842
5230
|
}
|
|
@@ -4941,7 +5329,7 @@ async function overview(deps) {
|
|
|
4941
5329
|
try {
|
|
4942
5330
|
manifest = await readManifest8(deps.paths);
|
|
4943
5331
|
} catch (error) {
|
|
4944
|
-
if (
|
|
5332
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
4945
5333
|
return { initialized: false, repoRoot: deps.repoRoot };
|
|
4946
5334
|
}
|
|
4947
5335
|
throw error;
|
|
@@ -4996,7 +5384,7 @@ async function sessionDetail(deps, sessionId) {
|
|
|
4996
5384
|
throw error;
|
|
4997
5385
|
}
|
|
4998
5386
|
try {
|
|
4999
|
-
const events = await readAllEvents2(
|
|
5387
|
+
const events = await readAllEvents2(join8(deps.paths.sessions, sessionId));
|
|
5000
5388
|
return { session, events };
|
|
5001
5389
|
} catch {
|
|
5002
5390
|
return { session, events: [], degraded: true };
|
|
@@ -5051,11 +5439,16 @@ async function handoffView(deps) {
|
|
|
5051
5439
|
}
|
|
5052
5440
|
function readActionOptions(body) {
|
|
5053
5441
|
const options = {};
|
|
5054
|
-
|
|
5442
|
+
const project = normalizeProject(body.project);
|
|
5443
|
+
if (project.length > 0) options.project = project;
|
|
5055
5444
|
if (body.force === true) options.force = true;
|
|
5056
5445
|
if (body.dryRun === true) options.dryRun = true;
|
|
5057
5446
|
return options;
|
|
5058
5447
|
}
|
|
5448
|
+
function normalizeProject(value) {
|
|
5449
|
+
const raw = Array.isArray(value) ? value : [value];
|
|
5450
|
+
return raw.filter((p) => typeof p === "string" && p.length > 0);
|
|
5451
|
+
}
|
|
5059
5452
|
function hostAllowed(req, port) {
|
|
5060
5453
|
const host = req.headers.host;
|
|
5061
5454
|
return host === `127.0.0.1:${port}` || host === `localhost:${port}`;
|
|
@@ -5131,7 +5524,7 @@ var DEFAULT_PORT = 4319;
|
|
|
5131
5524
|
function parsePort(value) {
|
|
5132
5525
|
const port = Number.parseInt(value, 10);
|
|
5133
5526
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
5134
|
-
throw new
|
|
5527
|
+
throw new InvalidArgumentError5("Port must be an integer between 1 and 65535.");
|
|
5135
5528
|
}
|
|
5136
5529
|
return port;
|
|
5137
5530
|
}
|
|
@@ -5183,7 +5576,7 @@ async function startListening(port, deps) {
|
|
|
5183
5576
|
try {
|
|
5184
5577
|
return await startViewServer({ port, deps });
|
|
5185
5578
|
} catch (error) {
|
|
5186
|
-
if (
|
|
5579
|
+
if (findErrorCode13(error, "EADDRINUSE")) {
|
|
5187
5580
|
throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
|
|
5188
5581
|
cause: error
|
|
5189
5582
|
});
|
|
@@ -5206,7 +5599,7 @@ function openInBrowser(url, override) {
|
|
|
5206
5599
|
}
|
|
5207
5600
|
}
|
|
5208
5601
|
function waitForShutdown(signal) {
|
|
5209
|
-
return new Promise((
|
|
5602
|
+
return new Promise((resolve3) => {
|
|
5210
5603
|
const cleanup = () => {
|
|
5211
5604
|
process.off("SIGINT", onSignal);
|
|
5212
5605
|
process.off("SIGTERM", onSignal);
|
|
@@ -5214,18 +5607,18 @@ function waitForShutdown(signal) {
|
|
|
5214
5607
|
};
|
|
5215
5608
|
const onSignal = () => {
|
|
5216
5609
|
cleanup();
|
|
5217
|
-
|
|
5610
|
+
resolve3();
|
|
5218
5611
|
};
|
|
5219
5612
|
const onAbort = () => {
|
|
5220
5613
|
cleanup();
|
|
5221
|
-
|
|
5614
|
+
resolve3();
|
|
5222
5615
|
};
|
|
5223
5616
|
process.on("SIGINT", onSignal);
|
|
5224
5617
|
process.on("SIGTERM", onSignal);
|
|
5225
5618
|
if (signal !== void 0) {
|
|
5226
5619
|
if (signal.aborted) {
|
|
5227
5620
|
cleanup();
|
|
5228
|
-
|
|
5621
|
+
resolve3();
|
|
5229
5622
|
return;
|
|
5230
5623
|
}
|
|
5231
5624
|
signal.addEventListener("abort", onAbort);
|
|
@@ -5248,7 +5641,7 @@ async function assertWorkspaceInitialized10(basouRoot) {
|
|
|
5248
5641
|
try {
|
|
5249
5642
|
await assertBasouRootSafe13(basouRoot);
|
|
5250
5643
|
} catch (error) {
|
|
5251
|
-
if (
|
|
5644
|
+
if (findErrorCode13(error, "ENOENT")) {
|
|
5252
5645
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
5253
5646
|
}
|
|
5254
5647
|
throw error;
|