@basou/cli 0.6.0 → 0.7.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 +357 -89
- package/dist/index.js.map +1 -1
- package/dist/program.js +357 -89
- 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,
|
|
@@ -1348,11 +1348,16 @@ import {
|
|
|
1348
1348
|
} from "@basou/core";
|
|
1349
1349
|
var SES_PREFIX2 = "ses_";
|
|
1350
1350
|
var SHORT_ID_LEN2 = 6;
|
|
1351
|
+
function collectPath(value, previous) {
|
|
1352
|
+
return [...previous, value];
|
|
1353
|
+
}
|
|
1351
1354
|
function registerImportCommand(program2) {
|
|
1352
1355
|
const importCmd = program2.command("import").description("Import provenance from an external AI tool's native logs");
|
|
1353
1356
|
importCmd.command("claude-code").description("Derive Basou sessions from Claude Code native transcripts (~/.claude/projects)").option(
|
|
1354
1357
|
"--project <path>",
|
|
1355
|
-
"Source project path whose transcripts to import (defaults to the
|
|
1358
|
+
"Source project path whose transcripts to import (repeatable; defaults to the manifest source roots, then the repository root)",
|
|
1359
|
+
collectPath,
|
|
1360
|
+
[]
|
|
1356
1361
|
).option("--session <id>", "Import a single transcript by its Claude session id").option("--all", "Import every transcript found for the project").option(
|
|
1357
1362
|
"--force",
|
|
1358
1363
|
"Re-import sessions already imported: delete and replace them instead of skipping"
|
|
@@ -1361,7 +1366,9 @@ function registerImportCommand(program2) {
|
|
|
1361
1366
|
});
|
|
1362
1367
|
importCmd.command("codex").description("Derive Basou sessions from OpenAI Codex native rollout logs (~/.codex/sessions)").option(
|
|
1363
1368
|
"--project <path>",
|
|
1364
|
-
"Source project path whose rollouts to import (defaults to the
|
|
1369
|
+
"Source project path whose rollouts to import (repeatable; defaults to the manifest source roots, then the repository root)",
|
|
1370
|
+
collectPath,
|
|
1371
|
+
[]
|
|
1365
1372
|
).option("--session <id>", "Import a single rollout by its Codex session id").option("--all", "Import every rollout found for the project").option(
|
|
1366
1373
|
"--force",
|
|
1367
1374
|
"Re-import sessions already imported: delete and replace them instead of skipping"
|
|
@@ -1385,13 +1392,28 @@ async function runImportCodex(options, ctx = {}) {
|
|
|
1385
1392
|
process.exitCode = 1;
|
|
1386
1393
|
}
|
|
1387
1394
|
}
|
|
1395
|
+
function resolveSourceRoots(args) {
|
|
1396
|
+
const { projectFlags, manifest, repoRoot, cwd } = args;
|
|
1397
|
+
let resolved;
|
|
1398
|
+
if (projectFlags.length > 0) {
|
|
1399
|
+
resolved = projectFlags.map((p) => resolve(cwd, p));
|
|
1400
|
+
} else {
|
|
1401
|
+
const roots = manifest.import?.source_roots;
|
|
1402
|
+
resolved = roots !== void 0 && roots.length > 0 ? roots.map((r) => resolve(repoRoot, r)) : [repoRoot];
|
|
1403
|
+
}
|
|
1404
|
+
return [...new Set(resolved)];
|
|
1405
|
+
}
|
|
1388
1406
|
async function doRunImportClaudeCode(options, ctx) {
|
|
1389
1407
|
assertSelector(options);
|
|
1390
1408
|
const { repositoryRoot, paths, manifest } = await resolveImportTarget(ctx);
|
|
1391
|
-
const
|
|
1409
|
+
const projectPaths = resolveSourceRoots({
|
|
1410
|
+
projectFlags: options.project ?? [],
|
|
1411
|
+
manifest,
|
|
1412
|
+
repoRoot: repositoryRoot,
|
|
1413
|
+
cwd: ctx.cwd ?? process.cwd()
|
|
1414
|
+
});
|
|
1392
1415
|
const projectsRoot = ctx.claudeProjectsDir ?? join3(homedir2(), ".claude", "projects");
|
|
1393
|
-
const
|
|
1394
|
-
const files = await selectTranscriptFiles(transcriptDir, options);
|
|
1416
|
+
const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
|
|
1395
1417
|
const candidates = files.map((file) => {
|
|
1396
1418
|
const externalId = basename(file, ".jsonl");
|
|
1397
1419
|
return {
|
|
@@ -1407,9 +1429,14 @@ async function doRunImportClaudeCode(options, ctx) {
|
|
|
1407
1429
|
async function doRunImportCodex(options, ctx) {
|
|
1408
1430
|
assertSelector(options);
|
|
1409
1431
|
const { repositoryRoot, paths, manifest } = await resolveImportTarget(ctx);
|
|
1410
|
-
const
|
|
1432
|
+
const projectPaths = resolveSourceRoots({
|
|
1433
|
+
projectFlags: options.project ?? [],
|
|
1434
|
+
manifest,
|
|
1435
|
+
repoRoot: repositoryRoot,
|
|
1436
|
+
cwd: ctx.cwd ?? process.cwd()
|
|
1437
|
+
});
|
|
1411
1438
|
const sessionsRoot = ctx.codexSessionsDir ?? join3(homedir2(), ".codex", "sessions");
|
|
1412
|
-
const rollouts = await discoverCodexRollouts(sessionsRoot,
|
|
1439
|
+
const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
|
|
1413
1440
|
const candidates = rollouts.map(({ file, externalId }) => ({
|
|
1414
1441
|
externalId,
|
|
1415
1442
|
toPayload: async () => codexRolloutToImportPayload(await readJsonlRecords(file), {
|
|
@@ -1420,6 +1447,9 @@ async function doRunImportCodex(options, ctx) {
|
|
|
1420
1447
|
await importDerivedSessions(paths, manifest, options, CODEX_IMPORT_SOURCE, candidates);
|
|
1421
1448
|
}
|
|
1422
1449
|
function assertSelector(options) {
|
|
1450
|
+
if (options.session !== void 0 && options.all === true) {
|
|
1451
|
+
throw new Error("Specify either --session <id> or --all, not both");
|
|
1452
|
+
}
|
|
1423
1453
|
if (options.session === void 0 && options.all !== true) {
|
|
1424
1454
|
throw new Error("Specify --session <id> or --all");
|
|
1425
1455
|
}
|
|
@@ -1517,28 +1547,56 @@ async function loadExistingByExternalId(paths, sourceKind) {
|
|
|
1517
1547
|
}
|
|
1518
1548
|
return byExternalId;
|
|
1519
1549
|
}
|
|
1520
|
-
async function selectTranscriptFiles(
|
|
1550
|
+
async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
1521
1551
|
if (options.session !== void 0) {
|
|
1522
|
-
|
|
1552
|
+
const matches = [];
|
|
1553
|
+
for (const projectPath of projectPaths) {
|
|
1554
|
+
const file = join3(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
|
|
1555
|
+
if (await pathExists(file)) matches.push(file);
|
|
1556
|
+
}
|
|
1557
|
+
if (matches.length === 0) {
|
|
1558
|
+
throw new Error("Claude transcript not found for session id in project");
|
|
1559
|
+
}
|
|
1560
|
+
return [...new Set(matches)];
|
|
1561
|
+
}
|
|
1562
|
+
const files = [];
|
|
1563
|
+
let anyDirFound = false;
|
|
1564
|
+
for (const projectPath of projectPaths) {
|
|
1565
|
+
const transcriptDir = join3(projectsRoot, encodeProjectDir(projectPath));
|
|
1566
|
+
let entries;
|
|
1567
|
+
try {
|
|
1568
|
+
entries = await readdir(transcriptDir);
|
|
1569
|
+
} catch (error) {
|
|
1570
|
+
if (findErrorCode5(error, "ENOENT")) continue;
|
|
1571
|
+
throw new Error("Failed to read Claude transcript directory", { cause: error });
|
|
1572
|
+
}
|
|
1573
|
+
anyDirFound = true;
|
|
1574
|
+
for (const name of entries) {
|
|
1575
|
+
if (name.endsWith(".jsonl")) files.push(join3(transcriptDir, name));
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
if (!anyDirFound) {
|
|
1579
|
+
throw new Error("Claude transcript directory not found for project");
|
|
1523
1580
|
}
|
|
1524
|
-
|
|
1581
|
+
return [...new Set(files)].sort();
|
|
1582
|
+
}
|
|
1583
|
+
async function pathExists(file) {
|
|
1525
1584
|
try {
|
|
1526
|
-
|
|
1585
|
+
await stat(file);
|
|
1586
|
+
return true;
|
|
1527
1587
|
} catch (error) {
|
|
1528
|
-
if (findErrorCode5(error, "ENOENT"))
|
|
1529
|
-
|
|
1530
|
-
}
|
|
1531
|
-
throw new Error("Failed to read Claude transcript directory", { cause: error });
|
|
1588
|
+
if (findErrorCode5(error, "ENOENT")) return false;
|
|
1589
|
+
throw error;
|
|
1532
1590
|
}
|
|
1533
|
-
return entries.filter((name) => name.endsWith(".jsonl")).sort().map((name) => join3(transcriptDir, name));
|
|
1534
1591
|
}
|
|
1535
|
-
async function discoverCodexRollouts(sessionsRoot,
|
|
1592
|
+
async function discoverCodexRollouts(sessionsRoot, projectPaths, options) {
|
|
1593
|
+
const projectSet = new Set(projectPaths);
|
|
1536
1594
|
const files = await findRolloutFiles(sessionsRoot);
|
|
1537
1595
|
const matched = [];
|
|
1538
1596
|
for (const file of files) {
|
|
1539
1597
|
const meta = await readRolloutMeta(file);
|
|
1540
1598
|
if (meta === void 0) continue;
|
|
1541
|
-
if (meta.cwd
|
|
1599
|
+
if (!projectSet.has(meta.cwd)) continue;
|
|
1542
1600
|
if (options.session !== void 0 && meta.id !== options.session) continue;
|
|
1543
1601
|
matched.push({ file, externalId: meta.id });
|
|
1544
1602
|
}
|
|
@@ -1709,7 +1767,7 @@ async function assertWorkspaceInitialized5(basouRoot) {
|
|
|
1709
1767
|
}
|
|
1710
1768
|
|
|
1711
1769
|
// src/commands/init.ts
|
|
1712
|
-
import { basename as basename2 } from "path";
|
|
1770
|
+
import { basename as basename2, relative, resolve as resolve2 } from "path";
|
|
1713
1771
|
import {
|
|
1714
1772
|
appendBasouGitignore,
|
|
1715
1773
|
createManifest,
|
|
@@ -1718,10 +1776,18 @@ import {
|
|
|
1718
1776
|
tryRemoteUrl,
|
|
1719
1777
|
writeManifest
|
|
1720
1778
|
} from "@basou/core";
|
|
1779
|
+
function collectValue(value, previous) {
|
|
1780
|
+
return [...previous, value];
|
|
1781
|
+
}
|
|
1721
1782
|
function registerInitCommand(program2) {
|
|
1722
1783
|
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
1784
|
"--repo-url <url>",
|
|
1724
1785
|
"Repository URL (defaults to git remote.origin.url; pass empty string for null)"
|
|
1786
|
+
).option(
|
|
1787
|
+
"--source-root <path>",
|
|
1788
|
+
"Extra import source root, relative to the repo root (repeatable; aggregates sibling repos into this workspace)",
|
|
1789
|
+
collectValue,
|
|
1790
|
+
[]
|
|
1725
1791
|
).option("-f, --force", "Overwrite an existing manifest").option("-v, --verbose", "Show error causes").action(async (options) => {
|
|
1726
1792
|
await runInit(options);
|
|
1727
1793
|
});
|
|
@@ -1744,12 +1810,17 @@ async function doRunInit(options, ctx) {
|
|
|
1744
1810
|
} else {
|
|
1745
1811
|
repositoryUrl = await tryRemoteUrl(repositoryRoot);
|
|
1746
1812
|
}
|
|
1813
|
+
const sourceRoots = (options.sourceRoot ?? []).map((p) => {
|
|
1814
|
+
const rel = relative(repositoryRoot, resolve2(cwd, p));
|
|
1815
|
+
return rel === "" ? "." : rel;
|
|
1816
|
+
});
|
|
1747
1817
|
const paths = await ensureBasouDirectory(repositoryRoot);
|
|
1748
1818
|
const manifest = createManifest({
|
|
1749
1819
|
workspaceName,
|
|
1750
1820
|
...options.projectName !== void 0 ? { projectName: options.projectName } : {},
|
|
1751
1821
|
...options.projectDescription !== void 0 ? { projectDescription: options.projectDescription } : {},
|
|
1752
|
-
...repositoryUrl !== void 0 ? { repositoryUrl } : {}
|
|
1822
|
+
...repositoryUrl !== void 0 ? { repositoryUrl } : {},
|
|
1823
|
+
...sourceRoots.length > 0 ? { sourceRoots } : {}
|
|
1753
1824
|
});
|
|
1754
1825
|
await writeManifest(paths, manifest, { force: options.force === true });
|
|
1755
1826
|
try {
|
|
@@ -1783,7 +1854,8 @@ async function resolveRepositoryRootForInit(cwd) {
|
|
|
1783
1854
|
}
|
|
1784
1855
|
|
|
1785
1856
|
// src/commands/refresh.ts
|
|
1786
|
-
import { assertBasouRootSafe as assertBasouRootSafe7, basouPaths as basouPaths7, findErrorCode as
|
|
1857
|
+
import { assertBasouRootSafe as assertBasouRootSafe7, basouPaths as basouPaths7, findErrorCode as findErrorCode7, resolveRepositoryRoot as resolveRepositoryRoot8 } from "@basou/core";
|
|
1858
|
+
import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
1787
1859
|
|
|
1788
1860
|
// src/lib/provenance-actions.ts
|
|
1789
1861
|
import {
|
|
@@ -1906,25 +1978,213 @@ async function refreshAll(args) {
|
|
|
1906
1978
|
};
|
|
1907
1979
|
}
|
|
1908
1980
|
|
|
1981
|
+
// src/commands/refresh-watch.ts
|
|
1982
|
+
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
1983
|
+
import { homedir as homedir3 } from "os";
|
|
1984
|
+
import { join as join4 } from "path";
|
|
1985
|
+
import { findErrorCode as findErrorCode6 } from "@basou/core";
|
|
1986
|
+
var DEFAULT_WATCH_INTERVAL_SEC = 30;
|
|
1987
|
+
var MIN_WATCH_INTERVAL_SEC = 5;
|
|
1988
|
+
var MAX_WATCH_INTERVAL_SEC = 86400;
|
|
1989
|
+
function watchedRoots(ctx) {
|
|
1990
|
+
return [
|
|
1991
|
+
ctx.codexSessionsDir ?? join4(homedir3(), ".codex", "sessions"),
|
|
1992
|
+
ctx.claudeProjectsDir ?? join4(homedir3(), ".claude", "projects")
|
|
1993
|
+
];
|
|
1994
|
+
}
|
|
1995
|
+
async function scanSourceLogs(roots) {
|
|
1996
|
+
const out = /* @__PURE__ */ new Map();
|
|
1997
|
+
const walk = async (dir) => {
|
|
1998
|
+
let entries;
|
|
1999
|
+
try {
|
|
2000
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
2001
|
+
} catch (error) {
|
|
2002
|
+
if (findErrorCode6(error, "ENOENT") || findErrorCode6(error, "ENOTDIR")) return;
|
|
2003
|
+
throw new Error("Failed to read a source log directory", { cause: error });
|
|
2004
|
+
}
|
|
2005
|
+
for (const entry of entries) {
|
|
2006
|
+
const full = join4(dir, entry.name);
|
|
2007
|
+
if (entry.isDirectory()) {
|
|
2008
|
+
await walk(full);
|
|
2009
|
+
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
2010
|
+
try {
|
|
2011
|
+
const info = await stat2(full);
|
|
2012
|
+
out.set(full, { mtimeMs: info.mtimeMs, size: info.size });
|
|
2013
|
+
} catch (error) {
|
|
2014
|
+
if (findErrorCode6(error, "ENOENT")) continue;
|
|
2015
|
+
throw new Error("Failed to stat a source log file", { cause: error });
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
};
|
|
2020
|
+
for (const root of roots) await walk(root);
|
|
2021
|
+
return out;
|
|
2022
|
+
}
|
|
2023
|
+
function scansEqual(a, b) {
|
|
2024
|
+
if (a.size !== b.size) return false;
|
|
2025
|
+
for (const [path, sig] of a) {
|
|
2026
|
+
const other = b.get(path);
|
|
2027
|
+
if (other === void 0 || other.mtimeMs !== sig.mtimeMs || other.size !== sig.size) {
|
|
2028
|
+
return false;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
return true;
|
|
2032
|
+
}
|
|
2033
|
+
function importedCount(outcome) {
|
|
2034
|
+
return outcome.status === "ran" ? outcome.importedCount : 0;
|
|
2035
|
+
}
|
|
2036
|
+
function describeOutcome(outcome) {
|
|
2037
|
+
return outcome.status === "ran" ? `${outcome.adapter} +${outcome.importedCount}` : `${outcome.adapter} skipped`;
|
|
2038
|
+
}
|
|
2039
|
+
function hms(date) {
|
|
2040
|
+
return date.toISOString().slice(11, 19);
|
|
2041
|
+
}
|
|
2042
|
+
async function runImports(deps) {
|
|
2043
|
+
const claude = await importClaudeCode(deps.importOptions, deps.ctx);
|
|
2044
|
+
const codex = await importCodex(deps.importOptions, deps.ctx);
|
|
2045
|
+
return { claude, codex, imported: importedCount(claude) + importedCount(codex) };
|
|
2046
|
+
}
|
|
2047
|
+
async function regenerate(deps) {
|
|
2048
|
+
const nowIso = deps.now().toISOString();
|
|
2049
|
+
const handoff = await regenerateHandoff(deps.paths, nowIso);
|
|
2050
|
+
await regenerateDecisions(deps.paths, nowIso);
|
|
2051
|
+
return handoff.sessionCount;
|
|
2052
|
+
}
|
|
2053
|
+
async function runRefreshWatch(deps) {
|
|
2054
|
+
const { intervalMs, ctx, signal, sleep, log } = deps;
|
|
2055
|
+
const roots = watchedRoots(ctx);
|
|
2056
|
+
log(
|
|
2057
|
+
`watching ${roots.join(", ")} every ${Math.round(intervalMs / 1e3)}s (imports on change; Ctrl-C to stop)`
|
|
2058
|
+
);
|
|
2059
|
+
let lastScan = await scanSourceLogs(roots);
|
|
2060
|
+
let importedScan = lastScan;
|
|
2061
|
+
const initial = await runImports(deps);
|
|
2062
|
+
const initialSessions = await regenerate(deps);
|
|
2063
|
+
log(
|
|
2064
|
+
`[${hms(deps.now())}] refreshed: ${describeOutcome(initial.codex)}, ${describeOutcome(initial.claude)} (sessions: ${initialSessions})`
|
|
2065
|
+
);
|
|
2066
|
+
if (signal.aborted) {
|
|
2067
|
+
log("watch stopped");
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
let pendingRegen = false;
|
|
2071
|
+
while (!signal.aborted) {
|
|
2072
|
+
await sleep(intervalMs, signal);
|
|
2073
|
+
if (signal.aborted) break;
|
|
2074
|
+
try {
|
|
2075
|
+
const current = await scanSourceLogs(roots);
|
|
2076
|
+
if (scansEqual(current, lastScan) && !scansEqual(current, importedScan)) {
|
|
2077
|
+
const { claude, codex, imported } = await runImports(deps);
|
|
2078
|
+
if (imported > 0) pendingRegen = true;
|
|
2079
|
+
if (pendingRegen) {
|
|
2080
|
+
const sessions = await regenerate(deps);
|
|
2081
|
+
pendingRegen = false;
|
|
2082
|
+
log(
|
|
2083
|
+
`[${hms(deps.now())}] refreshed: ${describeOutcome(codex)}, ${describeOutcome(claude)} (sessions: ${sessions})`
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
importedScan = current;
|
|
2087
|
+
}
|
|
2088
|
+
lastScan = current;
|
|
2089
|
+
} catch (error) {
|
|
2090
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2091
|
+
log(`[${hms(deps.now())}] refresh cycle skipped: ${message}`);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
log("watch stopped");
|
|
2095
|
+
}
|
|
2096
|
+
|
|
1909
2097
|
// src/commands/refresh.ts
|
|
2098
|
+
function collectPath2(value, previous) {
|
|
2099
|
+
return [...previous, value];
|
|
2100
|
+
}
|
|
2101
|
+
function parseInterval(value) {
|
|
2102
|
+
const seconds = Number(value);
|
|
2103
|
+
if (!Number.isInteger(seconds) || seconds < MIN_WATCH_INTERVAL_SEC || seconds > MAX_WATCH_INTERVAL_SEC) {
|
|
2104
|
+
throw new InvalidArgumentError2(
|
|
2105
|
+
`--interval must be an integer between ${MIN_WATCH_INTERVAL_SEC} and ${MAX_WATCH_INTERVAL_SEC} (seconds).`
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
return seconds;
|
|
2109
|
+
}
|
|
2110
|
+
function abortableSleep(ms, signal) {
|
|
2111
|
+
return new Promise((resolve3) => {
|
|
2112
|
+
if (signal.aborted) {
|
|
2113
|
+
resolve3();
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
let timer;
|
|
2117
|
+
const onAbort = () => {
|
|
2118
|
+
clearTimeout(timer);
|
|
2119
|
+
resolve3();
|
|
2120
|
+
};
|
|
2121
|
+
timer = setTimeout(() => {
|
|
2122
|
+
signal.removeEventListener("abort", onAbort);
|
|
2123
|
+
resolve3();
|
|
2124
|
+
}, ms);
|
|
2125
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
1910
2128
|
function registerRefreshCommand(program2) {
|
|
1911
2129
|
program2.command("refresh").description(
|
|
1912
2130
|
"Import all adapters for the project and regenerate handoff + decisions in one step"
|
|
1913
2131
|
).option(
|
|
1914
2132
|
"--project <path>",
|
|
1915
|
-
"Source project path to import (defaults to the
|
|
1916
|
-
|
|
2133
|
+
"Source project path to import (repeatable; defaults to the manifest source roots, then the repository root)",
|
|
2134
|
+
collectPath2,
|
|
2135
|
+
[]
|
|
2136
|
+
).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(
|
|
2137
|
+
"--watch",
|
|
2138
|
+
"Keep running: re-import + regenerate when the native logs change (Ctrl-C to stop)"
|
|
2139
|
+
).option(
|
|
2140
|
+
"--interval <seconds>",
|
|
2141
|
+
`Poll interval for --watch, in seconds (default ${DEFAULT_WATCH_INTERVAL_SEC}, min ${MIN_WATCH_INTERVAL_SEC})`,
|
|
2142
|
+
parseInterval
|
|
2143
|
+
).option("-v, --verbose", "Show error causes").action(async (options) => {
|
|
1917
2144
|
await runRefresh(options);
|
|
1918
2145
|
});
|
|
1919
2146
|
}
|
|
1920
2147
|
async function runRefresh(options, ctx = {}) {
|
|
1921
2148
|
try {
|
|
1922
|
-
|
|
2149
|
+
if (options.watch === true) {
|
|
2150
|
+
await doRunRefreshWatch(options, ctx);
|
|
2151
|
+
} else {
|
|
2152
|
+
await doRunRefresh(options, ctx);
|
|
2153
|
+
}
|
|
1923
2154
|
} catch (error) {
|
|
1924
2155
|
renderCliError(error, { verbose: isVerbose(options) });
|
|
1925
2156
|
process.exitCode = 1;
|
|
1926
2157
|
}
|
|
1927
2158
|
}
|
|
2159
|
+
async function doRunRefreshWatch(options, ctx) {
|
|
2160
|
+
if (options.dryRun === true) throw new Error("--watch cannot be combined with --dry-run.");
|
|
2161
|
+
if (options.json === true) throw new Error("--watch cannot be combined with --json.");
|
|
2162
|
+
if (options.force === true) throw new Error("--watch cannot be combined with --force.");
|
|
2163
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
2164
|
+
const repositoryRoot = await resolveRepositoryRootForRefresh(cwd);
|
|
2165
|
+
const paths = basouPaths7(repositoryRoot);
|
|
2166
|
+
await assertWorkspaceInitialized6(paths.root);
|
|
2167
|
+
const intervalMs = (options.interval ?? DEFAULT_WATCH_INTERVAL_SEC) * 1e3;
|
|
2168
|
+
const controller = new AbortController();
|
|
2169
|
+
const onSignal = () => controller.abort();
|
|
2170
|
+
process.on("SIGINT", onSignal);
|
|
2171
|
+
process.on("SIGTERM", onSignal);
|
|
2172
|
+
try {
|
|
2173
|
+
await runRefreshWatch({
|
|
2174
|
+
ctx,
|
|
2175
|
+
paths,
|
|
2176
|
+
intervalMs,
|
|
2177
|
+
importOptions: options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
|
|
2178
|
+
now: () => ctx.nowProvider?.() ?? /* @__PURE__ */ new Date(),
|
|
2179
|
+
signal: controller.signal,
|
|
2180
|
+
sleep: abortableSleep,
|
|
2181
|
+
log: (line) => console.log(line)
|
|
2182
|
+
});
|
|
2183
|
+
} finally {
|
|
2184
|
+
process.off("SIGINT", onSignal);
|
|
2185
|
+
process.off("SIGTERM", onSignal);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
1928
2188
|
async function doRunRefresh(options, ctx) {
|
|
1929
2189
|
const cwd = ctx.cwd ?? process.cwd();
|
|
1930
2190
|
const repositoryRoot = await resolveRepositoryRootForRefresh(cwd);
|
|
@@ -1933,7 +2193,7 @@ async function doRunRefresh(options, ctx) {
|
|
|
1933
2193
|
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1934
2194
|
const result = await refreshAll({
|
|
1935
2195
|
options: {
|
|
1936
|
-
...options.project !== void 0 ? { project: options.project } : {},
|
|
2196
|
+
...options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
|
|
1937
2197
|
...options.force === true ? { force: true } : {},
|
|
1938
2198
|
...options.dryRun === true ? { dryRun: true } : {}
|
|
1939
2199
|
},
|
|
@@ -1991,7 +2251,7 @@ async function assertWorkspaceInitialized6(basouRoot) {
|
|
|
1991
2251
|
try {
|
|
1992
2252
|
await assertBasouRootSafe7(basouRoot);
|
|
1993
2253
|
} catch (error) {
|
|
1994
|
-
if (
|
|
2254
|
+
if (findErrorCode7(error, "ENOENT")) {
|
|
1995
2255
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
1996
2256
|
}
|
|
1997
2257
|
throw error;
|
|
@@ -2000,8 +2260,8 @@ async function assertWorkspaceInitialized6(basouRoot) {
|
|
|
2000
2260
|
|
|
2001
2261
|
// src/commands/run.ts
|
|
2002
2262
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
2003
|
-
import { homedir as
|
|
2004
|
-
import { join as
|
|
2263
|
+
import { homedir as homedir4 } from "os";
|
|
2264
|
+
import { join as join5 } from "path";
|
|
2005
2265
|
import {
|
|
2006
2266
|
assertBasouRootSafe as assertBasouRootSafe8,
|
|
2007
2267
|
basouPaths as basouPaths8,
|
|
@@ -2053,10 +2313,10 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2053
2313
|
await assertBasouRootSafe8(paths.root);
|
|
2054
2314
|
const manifest = await readManifest4(paths);
|
|
2055
2315
|
const sessionId = prefixedUlid4("ses");
|
|
2056
|
-
const sessionDir =
|
|
2316
|
+
const sessionDir = join5(paths.sessions, sessionId);
|
|
2057
2317
|
await mkdir2(sessionDir, { recursive: true });
|
|
2058
2318
|
const startedAt = now().toISOString();
|
|
2059
|
-
const sessionYamlPath =
|
|
2319
|
+
const sessionYamlPath = join5(sessionDir, "session.yaml");
|
|
2060
2320
|
const session = buildInitialSession2({
|
|
2061
2321
|
id: sessionId,
|
|
2062
2322
|
command,
|
|
@@ -2177,7 +2437,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2177
2437
|
const rawRelated = computeRelatedFiles(preSnapshot, postSnapshot, diff);
|
|
2178
2438
|
const relatedFiles = sanitizeRelatedFiles(rawRelated, {
|
|
2179
2439
|
workingDirectory: repoRoot,
|
|
2180
|
-
homedir:
|
|
2440
|
+
homedir: homedir4()
|
|
2181
2441
|
}).sanitized;
|
|
2182
2442
|
const finalStatus = decideFinalStatus2(result, signalReceived);
|
|
2183
2443
|
await appendEvent2(sessionDir, {
|
|
@@ -2321,7 +2581,7 @@ function buildInitialSession2(input) {
|
|
|
2321
2581
|
source: { ...claudeCodeAdapterMetadata },
|
|
2322
2582
|
started_at: input.startedAt,
|
|
2323
2583
|
status: "initialized",
|
|
2324
|
-
working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir:
|
|
2584
|
+
working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir4() }),
|
|
2325
2585
|
invocation: {
|
|
2326
2586
|
command: input.command,
|
|
2327
2587
|
args: [...input.args],
|
|
@@ -2394,13 +2654,13 @@ async function resolveRepositoryRootForRun(cwd) {
|
|
|
2394
2654
|
|
|
2395
2655
|
// src/commands/session.ts
|
|
2396
2656
|
import { readFile as readFile2 } from "fs/promises";
|
|
2397
|
-
import { basename as basename3, isAbsolute, join as
|
|
2657
|
+
import { basename as basename3, isAbsolute, join as join6, relative as relative2 } from "path";
|
|
2398
2658
|
import {
|
|
2399
2659
|
acquireLock as acquireLock2,
|
|
2400
2660
|
appendEventToExistingSession as appendEventToExistingSession2,
|
|
2401
2661
|
assertBasouRootSafe as assertBasouRootSafe9,
|
|
2402
2662
|
basouPaths as basouPaths9,
|
|
2403
|
-
findErrorCode as
|
|
2663
|
+
findErrorCode as findErrorCode8,
|
|
2404
2664
|
importSessionFromJson as importSessionFromJson2,
|
|
2405
2665
|
loadSessionEntries,
|
|
2406
2666
|
readAllEvents,
|
|
@@ -2414,7 +2674,7 @@ import {
|
|
|
2414
2674
|
SessionStatusSchema,
|
|
2415
2675
|
sessionWorkStatsFromEvents
|
|
2416
2676
|
} from "@basou/core";
|
|
2417
|
-
import { InvalidArgumentError as
|
|
2677
|
+
import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
|
|
2418
2678
|
|
|
2419
2679
|
// src/lib/format-duration.ts
|
|
2420
2680
|
function formatDurationMs(ms) {
|
|
@@ -2521,14 +2781,14 @@ async function doRunSessionShow(idInput, options, ctx) {
|
|
|
2521
2781
|
const paths = basouPaths9(repositoryRoot);
|
|
2522
2782
|
await assertWorkspaceInitialized7(paths.root);
|
|
2523
2783
|
const sessionId = await resolveSessionId2(paths, idInput);
|
|
2524
|
-
const sessionDir =
|
|
2525
|
-
const sessionYamlPath =
|
|
2784
|
+
const sessionDir = join6(paths.sessions, sessionId);
|
|
2785
|
+
const sessionYamlPath = join6(sessionDir, "session.yaml");
|
|
2526
2786
|
let session;
|
|
2527
2787
|
try {
|
|
2528
2788
|
const raw = await readYamlFile4(sessionYamlPath);
|
|
2529
2789
|
session = SessionSchema3.parse(raw);
|
|
2530
2790
|
} catch (error) {
|
|
2531
|
-
if (
|
|
2791
|
+
if (findErrorCode8(error, "ENOENT")) {
|
|
2532
2792
|
throw new Error(`Session not found: ${idInput}`);
|
|
2533
2793
|
}
|
|
2534
2794
|
throw new Error("Failed to read session", { cause: error });
|
|
@@ -2648,7 +2908,7 @@ function formatWorkingDir(workingDir, repositoryRoot, options) {
|
|
|
2648
2908
|
return workingDir;
|
|
2649
2909
|
}
|
|
2650
2910
|
if (workingDir === repositoryRoot) return "<repository_root>";
|
|
2651
|
-
const rel =
|
|
2911
|
+
const rel = relative2(repositoryRoot, workingDir);
|
|
2652
2912
|
if (rel.length === 0 || rel === ".") return "<repository_root>";
|
|
2653
2913
|
if (rel.startsWith("..")) return rel;
|
|
2654
2914
|
return `./${rel}`;
|
|
@@ -2778,7 +3038,7 @@ async function assertWorkspaceInitialized7(basouRoot) {
|
|
|
2778
3038
|
try {
|
|
2779
3039
|
await assertBasouRootSafe9(basouRoot);
|
|
2780
3040
|
} catch (error) {
|
|
2781
|
-
if (
|
|
3041
|
+
if (findErrorCode8(error, "ENOENT")) {
|
|
2782
3042
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
2783
3043
|
}
|
|
2784
3044
|
throw error;
|
|
@@ -2847,10 +3107,10 @@ async function readInputFile(path) {
|
|
|
2847
3107
|
try {
|
|
2848
3108
|
return await readFile2(path, "utf8");
|
|
2849
3109
|
} catch (error) {
|
|
2850
|
-
if (
|
|
3110
|
+
if (findErrorCode8(error, "ENOENT")) {
|
|
2851
3111
|
throw new Error("Import source not found", { cause: error });
|
|
2852
3112
|
}
|
|
2853
|
-
if (
|
|
3113
|
+
if (findErrorCode8(error, "EISDIR")) {
|
|
2854
3114
|
throw new Error("Import source is not a file", { cause: error });
|
|
2855
3115
|
}
|
|
2856
3116
|
throw new Error("Failed to read import source", { cause: error });
|
|
@@ -2865,19 +3125,19 @@ function parseJsonStrict(body) {
|
|
|
2865
3125
|
}
|
|
2866
3126
|
function parseImportFormat(raw) {
|
|
2867
3127
|
if (raw !== "json") {
|
|
2868
|
-
throw new
|
|
3128
|
+
throw new InvalidArgumentError3(`Unsupported format: ${raw}. Valid values: json`);
|
|
2869
3129
|
}
|
|
2870
3130
|
return "json";
|
|
2871
3131
|
}
|
|
2872
3132
|
function parseLabelOverride(raw) {
|
|
2873
3133
|
if (raw.length === 0) {
|
|
2874
|
-
throw new
|
|
3134
|
+
throw new InvalidArgumentError3("Label must not be empty");
|
|
2875
3135
|
}
|
|
2876
3136
|
return raw;
|
|
2877
3137
|
}
|
|
2878
3138
|
function parseTaskIdOverride(raw) {
|
|
2879
3139
|
if (raw.length === 0) {
|
|
2880
|
-
throw new
|
|
3140
|
+
throw new InvalidArgumentError3("Task id is empty");
|
|
2881
3141
|
}
|
|
2882
3142
|
return raw;
|
|
2883
3143
|
}
|
|
@@ -2964,10 +3224,10 @@ async function readNoteFile(path) {
|
|
|
2964
3224
|
try {
|
|
2965
3225
|
return await readFile2(path, "utf8");
|
|
2966
3226
|
} catch (error) {
|
|
2967
|
-
if (
|
|
3227
|
+
if (findErrorCode8(error, "ENOENT")) {
|
|
2968
3228
|
throw new Error("Note source not found", { cause: error });
|
|
2969
3229
|
}
|
|
2970
|
-
if (
|
|
3230
|
+
if (findErrorCode8(error, "EISDIR")) {
|
|
2971
3231
|
throw new Error("Note source is not a file", { cause: error });
|
|
2972
3232
|
}
|
|
2973
3233
|
throw new Error("Failed to read note source", { cause: error });
|
|
@@ -2975,7 +3235,7 @@ async function readNoteFile(path) {
|
|
|
2975
3235
|
}
|
|
2976
3236
|
function parseNoteBodyOption(raw) {
|
|
2977
3237
|
if (raw.length === 0) {
|
|
2978
|
-
throw new
|
|
3238
|
+
throw new InvalidArgumentError3("--body must not be empty");
|
|
2979
3239
|
}
|
|
2980
3240
|
return raw;
|
|
2981
3241
|
}
|
|
@@ -3001,7 +3261,7 @@ import {
|
|
|
3001
3261
|
assertBasouRootSafe as assertBasouRootSafe10,
|
|
3002
3262
|
basouPaths as basouPaths10,
|
|
3003
3263
|
computeWorkStats,
|
|
3004
|
-
findErrorCode as
|
|
3264
|
+
findErrorCode as findErrorCode9,
|
|
3005
3265
|
resolveRepositoryRoot as resolveRepositoryRoot11
|
|
3006
3266
|
} from "@basou/core";
|
|
3007
3267
|
function registerStatsCommand(program2) {
|
|
@@ -3119,7 +3379,7 @@ async function assertWorkspaceInitialized8(basouRoot) {
|
|
|
3119
3379
|
try {
|
|
3120
3380
|
await assertBasouRootSafe10(basouRoot);
|
|
3121
3381
|
} catch (error) {
|
|
3122
|
-
if (
|
|
3382
|
+
if (findErrorCode9(error, "ENOENT")) {
|
|
3123
3383
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3124
3384
|
}
|
|
3125
3385
|
throw error;
|
|
@@ -3131,7 +3391,7 @@ import {
|
|
|
3131
3391
|
assertBasouRootSafe as assertBasouRootSafe11,
|
|
3132
3392
|
basouPaths as basouPaths11,
|
|
3133
3393
|
buildStatusSnapshot,
|
|
3134
|
-
findErrorCode as
|
|
3394
|
+
findErrorCode as findErrorCode10,
|
|
3135
3395
|
readManifest as readManifest6,
|
|
3136
3396
|
resolveRepositoryRoot as resolveRepositoryRoot12,
|
|
3137
3397
|
writeStatus
|
|
@@ -3156,7 +3416,7 @@ async function doRunStatus(options, ctx) {
|
|
|
3156
3416
|
try {
|
|
3157
3417
|
await assertBasouRootSafe11(paths.root);
|
|
3158
3418
|
} catch (error) {
|
|
3159
|
-
if (
|
|
3419
|
+
if (findErrorCode10(error, "ENOENT")) {
|
|
3160
3420
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3161
3421
|
}
|
|
3162
3422
|
throw error;
|
|
@@ -3165,7 +3425,7 @@ async function doRunStatus(options, ctx) {
|
|
|
3165
3425
|
try {
|
|
3166
3426
|
manifest = await readManifest6(paths);
|
|
3167
3427
|
} catch (error) {
|
|
3168
|
-
if (
|
|
3428
|
+
if (findErrorCode10(error, "ENOENT")) {
|
|
3169
3429
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3170
3430
|
}
|
|
3171
3431
|
throw new Error("Failed to read workspace manifest", { cause: error });
|
|
@@ -3202,7 +3462,7 @@ async function resolveRepositoryRootForStatus(cwd) {
|
|
|
3202
3462
|
|
|
3203
3463
|
// src/commands/task.ts
|
|
3204
3464
|
import { readFile as readFile3 } from "fs/promises";
|
|
3205
|
-
import { join as
|
|
3465
|
+
import { join as join7 } from "path";
|
|
3206
3466
|
import {
|
|
3207
3467
|
archiveTask,
|
|
3208
3468
|
assertBasouRootSafe as assertBasouRootSafe12,
|
|
@@ -3211,7 +3471,7 @@ import {
|
|
|
3211
3471
|
deleteTask,
|
|
3212
3472
|
editTask,
|
|
3213
3473
|
enumerateArchivedTaskIds,
|
|
3214
|
-
findErrorCode as
|
|
3474
|
+
findErrorCode as findErrorCode11,
|
|
3215
3475
|
loadSessionEntries as loadSessionEntries2,
|
|
3216
3476
|
loadTaskEntries,
|
|
3217
3477
|
prefixedUlid as prefixedUlid5,
|
|
@@ -3229,7 +3489,7 @@ import {
|
|
|
3229
3489
|
TaskWriteAfterEventError,
|
|
3230
3490
|
updateTaskStatusWithEvent
|
|
3231
3491
|
} from "@basou/core";
|
|
3232
|
-
import { InvalidArgumentError as
|
|
3492
|
+
import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
|
|
3233
3493
|
var STATUS_VALUES3 = TaskStatusSchema.options;
|
|
3234
3494
|
function registerTaskCommand(program2) {
|
|
3235
3495
|
const task = program2.command("task").description("Manage Basou tasks (purpose units that span sessions)");
|
|
@@ -3529,7 +3789,7 @@ async function doRunTaskShow(idInput, options, ctx) {
|
|
|
3529
3789
|
const events = [];
|
|
3530
3790
|
const linkedSessionIds = new Set(doc.task.task.linked_sessions);
|
|
3531
3791
|
for (const s of sessions) {
|
|
3532
|
-
const sessionDir =
|
|
3792
|
+
const sessionDir = join7(paths.sessions, s.sessionId);
|
|
3533
3793
|
try {
|
|
3534
3794
|
for await (const ev of replayEvents2(sessionDir, {
|
|
3535
3795
|
onWarning: (w) => printReplayWarning(w, s.sessionId)
|
|
@@ -4158,20 +4418,20 @@ async function readSingleLineFromStdin() {
|
|
|
4158
4418
|
}
|
|
4159
4419
|
function parseTitle2(raw) {
|
|
4160
4420
|
if (raw.length === 0) {
|
|
4161
|
-
throw new
|
|
4421
|
+
throw new InvalidArgumentError4("Title must not be empty");
|
|
4162
4422
|
}
|
|
4163
4423
|
return raw;
|
|
4164
4424
|
}
|
|
4165
4425
|
function parseLabel(raw) {
|
|
4166
4426
|
if (raw.length === 0) {
|
|
4167
|
-
throw new
|
|
4427
|
+
throw new InvalidArgumentError4("Label must not be empty");
|
|
4168
4428
|
}
|
|
4169
4429
|
return raw;
|
|
4170
4430
|
}
|
|
4171
4431
|
function parseInitialTaskStatus(raw) {
|
|
4172
4432
|
const result = TaskStatusSchema.safeParse(raw);
|
|
4173
4433
|
if (!result.success) {
|
|
4174
|
-
throw new
|
|
4434
|
+
throw new InvalidArgumentError4(
|
|
4175
4435
|
`Initial task status must be one of: ${STATUS_VALUES3.join(", ")}`
|
|
4176
4436
|
);
|
|
4177
4437
|
}
|
|
@@ -4180,7 +4440,7 @@ function parseInitialTaskStatus(raw) {
|
|
|
4180
4440
|
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
4181
4441
|
function parseIsoTimestampOption(raw) {
|
|
4182
4442
|
if (!ISO_DATE_RE.test(raw) || Number.isNaN(Date.parse(raw))) {
|
|
4183
|
-
throw new
|
|
4443
|
+
throw new InvalidArgumentError4(
|
|
4184
4444
|
"Invalid --completed-at value; expected ISO-8601 timestamp like 2026-05-10T12:34:56+09:00"
|
|
4185
4445
|
);
|
|
4186
4446
|
}
|
|
@@ -4189,7 +4449,7 @@ function parseIsoTimestampOption(raw) {
|
|
|
4189
4449
|
function parseTaskStatusFilter(raw) {
|
|
4190
4450
|
const result = TaskStatusSchema.safeParse(raw);
|
|
4191
4451
|
if (!result.success) {
|
|
4192
|
-
throw new
|
|
4452
|
+
throw new InvalidArgumentError4(
|
|
4193
4453
|
`Invalid task status: ${raw}. Valid values: ${STATUS_VALUES3.join(", ")}`
|
|
4194
4454
|
);
|
|
4195
4455
|
}
|
|
@@ -4204,14 +4464,14 @@ function parseTaskStatusPositional(raw) {
|
|
|
4204
4464
|
}
|
|
4205
4465
|
function parseDescriptionOption(raw) {
|
|
4206
4466
|
if (raw.length === 0) {
|
|
4207
|
-
throw new
|
|
4467
|
+
throw new InvalidArgumentError4("Description must not be empty");
|
|
4208
4468
|
}
|
|
4209
4469
|
return raw;
|
|
4210
4470
|
}
|
|
4211
4471
|
function parsePositiveInt2(raw) {
|
|
4212
4472
|
const n = Number.parseInt(raw, 10);
|
|
4213
4473
|
if (!Number.isInteger(n) || n < 1 || raw.trim() !== String(n)) {
|
|
4214
|
-
throw new
|
|
4474
|
+
throw new InvalidArgumentError4(`Invalid number: ${raw}`);
|
|
4215
4475
|
}
|
|
4216
4476
|
return n;
|
|
4217
4477
|
}
|
|
@@ -4219,10 +4479,10 @@ async function readDescriptionFile(path) {
|
|
|
4219
4479
|
try {
|
|
4220
4480
|
return await readFile3(path, "utf8");
|
|
4221
4481
|
} catch (error) {
|
|
4222
|
-
if (
|
|
4482
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
4223
4483
|
throw new Error("Description source not found", { cause: error });
|
|
4224
4484
|
}
|
|
4225
|
-
if (
|
|
4485
|
+
if (findErrorCode11(error, "EISDIR")) {
|
|
4226
4486
|
throw new Error("Description source is not a file", { cause: error });
|
|
4227
4487
|
}
|
|
4228
4488
|
throw new Error("Failed to read description source", { cause: error });
|
|
@@ -4245,7 +4505,7 @@ async function assertWorkspaceInitialized9(basouRoot) {
|
|
|
4245
4505
|
try {
|
|
4246
4506
|
await assertBasouRootSafe12(basouRoot);
|
|
4247
4507
|
} catch (error) {
|
|
4248
|
-
if (
|
|
4508
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
4249
4509
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
4250
4510
|
}
|
|
4251
4511
|
throw error;
|
|
@@ -4333,16 +4593,16 @@ function maxLen3(values, floor) {
|
|
|
4333
4593
|
|
|
4334
4594
|
// src/commands/view.ts
|
|
4335
4595
|
import { spawn } from "child_process";
|
|
4336
|
-
import { assertBasouRootSafe as assertBasouRootSafe13, basouPaths as basouPaths13, findErrorCode as
|
|
4337
|
-
import { InvalidArgumentError as
|
|
4596
|
+
import { assertBasouRootSafe as assertBasouRootSafe13, basouPaths as basouPaths13, findErrorCode as findErrorCode13, resolveRepositoryRoot as resolveRepositoryRoot14 } from "@basou/core";
|
|
4597
|
+
import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
|
|
4338
4598
|
|
|
4339
4599
|
// src/lib/view-server.ts
|
|
4340
4600
|
import { createServer } from "http";
|
|
4341
|
-
import { join as
|
|
4601
|
+
import { join as join8 } from "path";
|
|
4342
4602
|
import {
|
|
4343
4603
|
computeWorkStats as computeWorkStats2,
|
|
4344
4604
|
enumerateApprovals as enumerateApprovals2,
|
|
4345
|
-
findErrorCode as
|
|
4605
|
+
findErrorCode as findErrorCode12,
|
|
4346
4606
|
isLazyExpired as isLazyExpired2,
|
|
4347
4607
|
loadApproval as loadApproval2,
|
|
4348
4608
|
loadSessionEntries as loadSessionEntries3,
|
|
@@ -4407,7 +4667,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
4407
4667
|
<body>
|
|
4408
4668
|
<header>
|
|
4409
4669
|
<h1>basou view</h1>
|
|
4410
|
-
<input type="text" id="project" placeholder="
|
|
4670
|
+
<input type="text" id="project" placeholder="source root (optional override)" />
|
|
4411
4671
|
<button class="primary" id="btn-refresh">Refresh all</button>
|
|
4412
4672
|
<button id="btn-import-claude">Import claude-code</button>
|
|
4413
4673
|
<button id="btn-import-codex">Import codex</button>
|
|
@@ -4561,7 +4821,10 @@ var VIEW_HTML = `<!doctype html>
|
|
|
4561
4821
|
detail.appendChild(el('p', { class: 'muted', text: 'Workspace not initialized.' }));
|
|
4562
4822
|
return;
|
|
4563
4823
|
}
|
|
4564
|
-
|
|
4824
|
+
// Leave the project field empty by default so refresh / import use the
|
|
4825
|
+
// manifest's import.source_roots (then the repo root) -- pre-filling the
|
|
4826
|
+
// repo root here would send it as an explicit --project and silently
|
|
4827
|
+
// override multi-root source roots. The field is an optional override.
|
|
4565
4828
|
state.repoRoot = d.repoRoot || '';
|
|
4566
4829
|
detail.appendChild(el('p', {}, [
|
|
4567
4830
|
el('strong', { text: d.workspace.name }), ' ',
|
|
@@ -4812,7 +5075,7 @@ function startViewServer(opts) {
|
|
|
4812
5075
|
};
|
|
4813
5076
|
let boundPort = port;
|
|
4814
5077
|
const getPort = () => boundPort;
|
|
4815
|
-
return new Promise((
|
|
5078
|
+
return new Promise((resolve3, reject) => {
|
|
4816
5079
|
const server = createServer((req, res) => {
|
|
4817
5080
|
handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
|
|
4818
5081
|
sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
|
|
@@ -4823,7 +5086,7 @@ function startViewServer(opts) {
|
|
|
4823
5086
|
const address = server.address();
|
|
4824
5087
|
boundPort = isAddressInfo(address) ? address.port : port;
|
|
4825
5088
|
server.off("error", reject);
|
|
4826
|
-
|
|
5089
|
+
resolve3({
|
|
4827
5090
|
url: `http://${host}:${boundPort}`,
|
|
4828
5091
|
port: boundPort,
|
|
4829
5092
|
close: () => closeServer(server)
|
|
@@ -4835,8 +5098,8 @@ function isAddressInfo(value) {
|
|
|
4835
5098
|
return value !== null && typeof value === "object";
|
|
4836
5099
|
}
|
|
4837
5100
|
function closeServer(server) {
|
|
4838
|
-
return new Promise((
|
|
4839
|
-
server.close(() =>
|
|
5101
|
+
return new Promise((resolve3) => {
|
|
5102
|
+
server.close(() => resolve3());
|
|
4840
5103
|
server.closeAllConnections();
|
|
4841
5104
|
});
|
|
4842
5105
|
}
|
|
@@ -4941,7 +5204,7 @@ async function overview(deps) {
|
|
|
4941
5204
|
try {
|
|
4942
5205
|
manifest = await readManifest8(deps.paths);
|
|
4943
5206
|
} catch (error) {
|
|
4944
|
-
if (
|
|
5207
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
4945
5208
|
return { initialized: false, repoRoot: deps.repoRoot };
|
|
4946
5209
|
}
|
|
4947
5210
|
throw error;
|
|
@@ -4996,7 +5259,7 @@ async function sessionDetail(deps, sessionId) {
|
|
|
4996
5259
|
throw error;
|
|
4997
5260
|
}
|
|
4998
5261
|
try {
|
|
4999
|
-
const events = await readAllEvents2(
|
|
5262
|
+
const events = await readAllEvents2(join8(deps.paths.sessions, sessionId));
|
|
5000
5263
|
return { session, events };
|
|
5001
5264
|
} catch {
|
|
5002
5265
|
return { session, events: [], degraded: true };
|
|
@@ -5051,11 +5314,16 @@ async function handoffView(deps) {
|
|
|
5051
5314
|
}
|
|
5052
5315
|
function readActionOptions(body) {
|
|
5053
5316
|
const options = {};
|
|
5054
|
-
|
|
5317
|
+
const project = normalizeProject(body.project);
|
|
5318
|
+
if (project.length > 0) options.project = project;
|
|
5055
5319
|
if (body.force === true) options.force = true;
|
|
5056
5320
|
if (body.dryRun === true) options.dryRun = true;
|
|
5057
5321
|
return options;
|
|
5058
5322
|
}
|
|
5323
|
+
function normalizeProject(value) {
|
|
5324
|
+
const raw = Array.isArray(value) ? value : [value];
|
|
5325
|
+
return raw.filter((p) => typeof p === "string" && p.length > 0);
|
|
5326
|
+
}
|
|
5059
5327
|
function hostAllowed(req, port) {
|
|
5060
5328
|
const host = req.headers.host;
|
|
5061
5329
|
return host === `127.0.0.1:${port}` || host === `localhost:${port}`;
|
|
@@ -5131,7 +5399,7 @@ var DEFAULT_PORT = 4319;
|
|
|
5131
5399
|
function parsePort(value) {
|
|
5132
5400
|
const port = Number.parseInt(value, 10);
|
|
5133
5401
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
5134
|
-
throw new
|
|
5402
|
+
throw new InvalidArgumentError5("Port must be an integer between 1 and 65535.");
|
|
5135
5403
|
}
|
|
5136
5404
|
return port;
|
|
5137
5405
|
}
|
|
@@ -5183,7 +5451,7 @@ async function startListening(port, deps) {
|
|
|
5183
5451
|
try {
|
|
5184
5452
|
return await startViewServer({ port, deps });
|
|
5185
5453
|
} catch (error) {
|
|
5186
|
-
if (
|
|
5454
|
+
if (findErrorCode13(error, "EADDRINUSE")) {
|
|
5187
5455
|
throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
|
|
5188
5456
|
cause: error
|
|
5189
5457
|
});
|
|
@@ -5206,7 +5474,7 @@ function openInBrowser(url, override) {
|
|
|
5206
5474
|
}
|
|
5207
5475
|
}
|
|
5208
5476
|
function waitForShutdown(signal) {
|
|
5209
|
-
return new Promise((
|
|
5477
|
+
return new Promise((resolve3) => {
|
|
5210
5478
|
const cleanup = () => {
|
|
5211
5479
|
process.off("SIGINT", onSignal);
|
|
5212
5480
|
process.off("SIGTERM", onSignal);
|
|
@@ -5214,18 +5482,18 @@ function waitForShutdown(signal) {
|
|
|
5214
5482
|
};
|
|
5215
5483
|
const onSignal = () => {
|
|
5216
5484
|
cleanup();
|
|
5217
|
-
|
|
5485
|
+
resolve3();
|
|
5218
5486
|
};
|
|
5219
5487
|
const onAbort = () => {
|
|
5220
5488
|
cleanup();
|
|
5221
|
-
|
|
5489
|
+
resolve3();
|
|
5222
5490
|
};
|
|
5223
5491
|
process.on("SIGINT", onSignal);
|
|
5224
5492
|
process.on("SIGTERM", onSignal);
|
|
5225
5493
|
if (signal !== void 0) {
|
|
5226
5494
|
if (signal.aborted) {
|
|
5227
5495
|
cleanup();
|
|
5228
|
-
|
|
5496
|
+
resolve3();
|
|
5229
5497
|
return;
|
|
5230
5498
|
}
|
|
5231
5499
|
signal.addEventListener("abort", onAbort);
|
|
@@ -5248,7 +5516,7 @@ async function assertWorkspaceInitialized10(basouRoot) {
|
|
|
5248
5516
|
try {
|
|
5249
5517
|
await assertBasouRootSafe13(basouRoot);
|
|
5250
5518
|
} catch (error) {
|
|
5251
|
-
if (
|
|
5519
|
+
if (findErrorCode13(error, "ENOENT")) {
|
|
5252
5520
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
5253
5521
|
}
|
|
5254
5522
|
throw error;
|