@basou/cli 0.11.0 → 0.12.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 +1046 -281
- package/dist/index.js.map +1 -1
- package/dist/program.js +1046 -281
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/program.js
CHANGED
|
@@ -1512,7 +1512,8 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
1512
1512
|
reimported: 0,
|
|
1513
1513
|
skippedLegacy: 0,
|
|
1514
1514
|
skippedDecreased: 0,
|
|
1515
|
-
skippedDuplicate: 0
|
|
1515
|
+
skippedDuplicate: 0,
|
|
1516
|
+
skippedUnverifiable: 0
|
|
1516
1517
|
};
|
|
1517
1518
|
let sanitizedPaths = 0;
|
|
1518
1519
|
const validate = (payload) => {
|
|
@@ -1554,7 +1555,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
1554
1555
|
if (outcome.status === "skipped") {
|
|
1555
1556
|
const detail = outcome.reason === "prior_events_unreadable" ? "prior events.jsonl has unreadable lines" : outcome.reason === "prior_chain_broken" ? "prior events.jsonl failed hash-chain verification (run 'basou verify')" : "source changed in a non-append way (derived events would be dropped)";
|
|
1556
1557
|
console.error(`Import: ${externalId} ${detail}; re-import skipped`);
|
|
1557
|
-
counts.
|
|
1558
|
+
counts.skippedUnverifiable++;
|
|
1558
1559
|
continue;
|
|
1559
1560
|
}
|
|
1560
1561
|
counts.reimported++;
|
|
@@ -1825,7 +1826,8 @@ function printImportResult(options, results, counts) {
|
|
|
1825
1826
|
reimported,
|
|
1826
1827
|
skippedLegacy,
|
|
1827
1828
|
skippedDecreased,
|
|
1828
|
-
skippedDuplicate
|
|
1829
|
+
skippedDuplicate,
|
|
1830
|
+
skippedUnverifiable
|
|
1829
1831
|
} = counts;
|
|
1830
1832
|
if (options.json === true) {
|
|
1831
1833
|
console.log(
|
|
@@ -1844,6 +1846,7 @@ function printImportResult(options, results, counts) {
|
|
|
1844
1846
|
skipped_legacy_untracked: skippedLegacy,
|
|
1845
1847
|
skipped_decreased: skippedDecreased,
|
|
1846
1848
|
skipped_duplicate: skippedDuplicate,
|
|
1849
|
+
skipped_unverifiable: skippedUnverifiable,
|
|
1847
1850
|
event_total: eventTotal,
|
|
1848
1851
|
dry_run: isDry
|
|
1849
1852
|
})
|
|
@@ -1856,6 +1859,8 @@ function printImportResult(options, results, counts) {
|
|
|
1856
1859
|
if (skippedLegacy > 0) skipParts.push(`${skippedLegacy} legacy (untracked size)`);
|
|
1857
1860
|
if (skippedDecreased > 0) skipParts.push(`${skippedDecreased} shrank`);
|
|
1858
1861
|
if (skippedDuplicate > 0) skipParts.push(`${skippedDuplicate} duplicated`);
|
|
1862
|
+
if (skippedUnverifiable > 0)
|
|
1863
|
+
skipParts.push(`${skippedUnverifiable} unverifiable (run 'basou verify')`);
|
|
1859
1864
|
const skipSuffix = skipParts.length > 0 ? `; skipped ${skipParts.join(", ")}` : "";
|
|
1860
1865
|
const eventsPart = replaced > 0 ? `${eventTotal} events, ${replaced} replaced` : `${eventTotal} events`;
|
|
1861
1866
|
if (isDry) {
|
|
@@ -1935,6 +1940,9 @@ function registerInitCommand(program) {
|
|
|
1935
1940
|
"Extra import source root, relative to the repo root (repeatable; aggregates sibling repos into this workspace)",
|
|
1936
1941
|
collectValue,
|
|
1937
1942
|
[]
|
|
1943
|
+
).option(
|
|
1944
|
+
"--local-only",
|
|
1945
|
+
"Write a .basou/ full-exclude .gitignore block (keep the trail out of version control) instead of the default ignore+commit block"
|
|
1938
1946
|
).option("-f, --force", "Overwrite an existing manifest").option("-v, --verbose", "Show error causes").action(async (options) => {
|
|
1939
1947
|
await runInit(options);
|
|
1940
1948
|
});
|
|
@@ -1971,7 +1979,7 @@ async function doRunInit(options, ctx) {
|
|
|
1971
1979
|
});
|
|
1972
1980
|
await writeManifest(paths, manifest, { force: options.force === true });
|
|
1973
1981
|
try {
|
|
1974
|
-
await appendBasouGitignore(repositoryRoot);
|
|
1982
|
+
await appendBasouGitignore(repositoryRoot, { localOnly: options.localOnly === true });
|
|
1975
1983
|
} catch (error) {
|
|
1976
1984
|
renderGitignoreWarning(error, isVerbose(options));
|
|
1977
1985
|
}
|
|
@@ -2000,15 +2008,21 @@ async function resolveRepositoryRootForInit(cwd) {
|
|
|
2000
2008
|
}
|
|
2001
2009
|
}
|
|
2002
2010
|
|
|
2003
|
-
// src/commands/
|
|
2004
|
-
import {
|
|
2005
|
-
|
|
2011
|
+
// src/commands/orient.ts
|
|
2012
|
+
import {
|
|
2013
|
+
assertBasouRootSafe as assertBasouRootSafe7,
|
|
2014
|
+
basouPaths as basouPaths7,
|
|
2015
|
+
findErrorCode as findErrorCode6,
|
|
2016
|
+
renderOrientation as renderOrientation2,
|
|
2017
|
+
writeMarkdownFile as writeMarkdownFile4
|
|
2018
|
+
} from "@basou/core";
|
|
2006
2019
|
|
|
2007
2020
|
// src/lib/provenance-actions.ts
|
|
2008
2021
|
import {
|
|
2009
2022
|
readMarkdownFile as readMarkdownFile3,
|
|
2010
2023
|
renderDecisions as renderDecisions2,
|
|
2011
2024
|
renderHandoff as renderHandoff2,
|
|
2025
|
+
renderOrientation,
|
|
2012
2026
|
renderWithMarkers as renderWithMarkers3,
|
|
2013
2027
|
writeMarkdownFile as writeMarkdownFile3
|
|
2014
2028
|
} from "@basou/core";
|
|
@@ -2059,6 +2073,7 @@ async function runImport(adapter, fn) {
|
|
|
2059
2073
|
skippedNoAction: readCount(json.skipped_no_action),
|
|
2060
2074
|
skippedAlreadyImported: readCount(json.skipped_already_imported),
|
|
2061
2075
|
skippedLegacyUntracked: readCount(json.skipped_legacy_untracked),
|
|
2076
|
+
skippedUnverifiable: readCount(json.skipped_unverifiable),
|
|
2062
2077
|
eventTotal: readCount(json.event_total),
|
|
2063
2078
|
dryRun: json.dry_run === true
|
|
2064
2079
|
};
|
|
@@ -2107,6 +2122,17 @@ async function regenerateDecisions(paths, nowIso, callbacks) {
|
|
|
2107
2122
|
);
|
|
2108
2123
|
return { decisionCount: result.decisionCount };
|
|
2109
2124
|
}
|
|
2125
|
+
async function regenerateOrientation(paths, nowIso, callbacks) {
|
|
2126
|
+
const result = await renderOrientation({ paths, nowIso, ...callbacks });
|
|
2127
|
+
await writeMarkdownFile3(paths.files.orientation, `${result.body}
|
|
2128
|
+
`);
|
|
2129
|
+
return {
|
|
2130
|
+
sessionCount: result.sessionCount,
|
|
2131
|
+
inFlightTaskCount: result.inFlightTaskCount,
|
|
2132
|
+
pendingApprovalsCount: result.pendingApprovalsCount,
|
|
2133
|
+
suspectCount: result.suspectCount
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2110
2136
|
async function refreshAll(args) {
|
|
2111
2137
|
const { options, ctx, paths, nowIso } = args;
|
|
2112
2138
|
const dryRun = options.dryRun === true;
|
|
@@ -2114,31 +2140,210 @@ async function refreshAll(args) {
|
|
|
2114
2140
|
const codex = await importCodex(options, ctx);
|
|
2115
2141
|
if (dryRun) {
|
|
2116
2142
|
const skipped = { status: "skipped", reason: "dry-run" };
|
|
2117
|
-
return {
|
|
2143
|
+
return {
|
|
2144
|
+
claudeCode,
|
|
2145
|
+
codex,
|
|
2146
|
+
handoff: skipped,
|
|
2147
|
+
decisions: skipped,
|
|
2148
|
+
orientation: skipped,
|
|
2149
|
+
dryRun
|
|
2150
|
+
};
|
|
2118
2151
|
}
|
|
2119
2152
|
const handoffCounts = await regenerateHandoff(paths, nowIso);
|
|
2120
2153
|
const decisionCounts = await regenerateDecisions(paths, nowIso);
|
|
2154
|
+
const scoped = options.project !== void 0 && options.project.length > 0;
|
|
2155
|
+
const orientationCounts = await regenerateOrientation(
|
|
2156
|
+
paths,
|
|
2157
|
+
nowIso,
|
|
2158
|
+
scoped ? {} : {
|
|
2159
|
+
staleness: {
|
|
2160
|
+
newSessions: 0,
|
|
2161
|
+
updatedSessions: 0,
|
|
2162
|
+
unverifiableSessions: wouldBlock(claudeCode) + wouldBlock(codex)
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
);
|
|
2121
2166
|
return {
|
|
2122
2167
|
claudeCode,
|
|
2123
2168
|
codex,
|
|
2124
2169
|
handoff: { status: "generated", ...handoffCounts },
|
|
2125
2170
|
decisions: { status: "generated", ...decisionCounts },
|
|
2171
|
+
orientation: { status: "generated", ...orientationCounts },
|
|
2126
2172
|
dryRun
|
|
2127
2173
|
};
|
|
2128
2174
|
}
|
|
2175
|
+
function wouldImport(outcome) {
|
|
2176
|
+
return outcome.status === "ran" ? outcome.importedCount : 0;
|
|
2177
|
+
}
|
|
2178
|
+
function wouldUpdate(outcome) {
|
|
2179
|
+
return outcome.status === "ran" ? outcome.reimportedCount + outcome.replacedCount : 0;
|
|
2180
|
+
}
|
|
2181
|
+
function wouldBlock(outcome) {
|
|
2182
|
+
return outcome.status === "ran" ? outcome.skippedUnverifiable : 0;
|
|
2183
|
+
}
|
|
2184
|
+
async function probeStaleness(args) {
|
|
2185
|
+
try {
|
|
2186
|
+
const dry = await refreshAll({
|
|
2187
|
+
options: { dryRun: true },
|
|
2188
|
+
ctx: args.ctx,
|
|
2189
|
+
paths: args.paths,
|
|
2190
|
+
nowIso: args.nowIso
|
|
2191
|
+
});
|
|
2192
|
+
return {
|
|
2193
|
+
newSessions: wouldImport(dry.claudeCode) + wouldImport(dry.codex),
|
|
2194
|
+
updatedSessions: wouldUpdate(dry.claudeCode) + wouldUpdate(dry.codex),
|
|
2195
|
+
unverifiableSessions: wouldBlock(dry.claudeCode) + wouldBlock(dry.codex)
|
|
2196
|
+
};
|
|
2197
|
+
} catch {
|
|
2198
|
+
return null;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
// src/lib/repo-root.ts
|
|
2203
|
+
import { resolveBasouRepositoryRoot } from "@basou/core";
|
|
2204
|
+
async function resolveBasouRootForCommand(cwd, commandName) {
|
|
2205
|
+
try {
|
|
2206
|
+
return await resolveBasouRepositoryRoot(cwd, {
|
|
2207
|
+
onRedirect: ({ via, root }) => console.error(`Resolved workspace view to ${root} (via ${via}).`)
|
|
2208
|
+
});
|
|
2209
|
+
} catch (error) {
|
|
2210
|
+
if (error instanceof Error && error.message === "Not a git repository") {
|
|
2211
|
+
throw new Error(
|
|
2212
|
+
`Not a git repository. Run 'git init' first, then re-run 'basou ${commandName}'.`,
|
|
2213
|
+
{ cause: error }
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
throw error;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
// src/commands/orient.ts
|
|
2221
|
+
function registerOrientCommand(program) {
|
|
2222
|
+
program.command("orient").description("Show the workspace's current position (also writes .basou/orientation.md)").option("-q, --quiet", "Write the file without printing the body").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
2223
|
+
await runOrient(opts);
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
async function runOrient(options, ctx = {}) {
|
|
2227
|
+
try {
|
|
2228
|
+
await doRunOrient(options, ctx);
|
|
2229
|
+
} catch (error) {
|
|
2230
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
2231
|
+
process.exitCode = 1;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
async function doRunOrient(options, ctx) {
|
|
2235
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
2236
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "orient");
|
|
2237
|
+
const paths = basouPaths7(repositoryRoot);
|
|
2238
|
+
await assertWorkspaceInitialized6(paths.root);
|
|
2239
|
+
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2240
|
+
const probeCtx = { cwd: repositoryRoot };
|
|
2241
|
+
if (ctx.claudeProjectsDir !== void 0) probeCtx.claudeProjectsDir = ctx.claudeProjectsDir;
|
|
2242
|
+
if (ctx.codexSessionsDir !== void 0) probeCtx.codexSessionsDir = ctx.codexSessionsDir;
|
|
2243
|
+
const staleness = await probeStaleness({ ctx: probeCtx, paths, nowIso });
|
|
2244
|
+
const result = await renderOrientation2({
|
|
2245
|
+
paths,
|
|
2246
|
+
nowIso,
|
|
2247
|
+
staleness,
|
|
2248
|
+
verbose: options.verbose === true,
|
|
2249
|
+
onWarning: (w, sid) => printReplayWarning(w, sid),
|
|
2250
|
+
onSessionSkip: (sid, reason) => printSessionSkip(sid, reason),
|
|
2251
|
+
onTaskSkip: (taskId, reason) => printTaskSkip(taskId, reason)
|
|
2252
|
+
});
|
|
2253
|
+
await writeMarkdownFile4(paths.files.orientation, `${result.body}
|
|
2254
|
+
`);
|
|
2255
|
+
if (options.quiet === true) {
|
|
2256
|
+
console.log(
|
|
2257
|
+
`Generated .basou/orientation.md (sessions: ${result.sessionCount}, in-flight tasks: ${result.inFlightTaskCount}, pending approvals: ${result.pendingApprovalsCount}, suspect: ${result.suspectCount})`
|
|
2258
|
+
);
|
|
2259
|
+
} else {
|
|
2260
|
+
console.log(result.body);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
async function assertWorkspaceInitialized6(basouRoot) {
|
|
2264
|
+
try {
|
|
2265
|
+
await assertBasouRootSafe7(basouRoot);
|
|
2266
|
+
} catch (error) {
|
|
2267
|
+
if (findErrorCode6(error, "ENOENT")) {
|
|
2268
|
+
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
2269
|
+
}
|
|
2270
|
+
throw error;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// src/commands/refresh.ts
|
|
2275
|
+
import { assertBasouRootSafe as assertBasouRootSafe8, basouPaths as basouPaths8, findErrorCode as findErrorCode8 } from "@basou/core";
|
|
2276
|
+
import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
2277
|
+
|
|
2278
|
+
// src/lib/portfolio-config.ts
|
|
2279
|
+
import { homedir as homedir3 } from "os";
|
|
2280
|
+
import { isAbsolute, join as join4, resolve as resolve3 } from "path";
|
|
2281
|
+
import { readYamlFile as readYamlFile3 } from "@basou/core";
|
|
2282
|
+
var DEFAULT_PORTFOLIO_CONFIG_PATH = join4(homedir3(), ".basou", "portfolio.yaml");
|
|
2283
|
+
function expandTilde(p) {
|
|
2284
|
+
if (p === "~") return homedir3();
|
|
2285
|
+
if (p.startsWith("~/")) return join4(homedir3(), p.slice(2));
|
|
2286
|
+
return p;
|
|
2287
|
+
}
|
|
2288
|
+
function isRecord(value) {
|
|
2289
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2290
|
+
}
|
|
2291
|
+
async function loadPortfolioConfig(configPath = DEFAULT_PORTFOLIO_CONFIG_PATH) {
|
|
2292
|
+
let raw;
|
|
2293
|
+
try {
|
|
2294
|
+
raw = await readYamlFile3(configPath);
|
|
2295
|
+
} catch (error) {
|
|
2296
|
+
if (error instanceof Error && error.message === "YAML file not found") {
|
|
2297
|
+
throw new Error(
|
|
2298
|
+
"No portfolio config at ~/.basou/portfolio.yaml. Create one (a 'workspaces:' list of repo paths) or pass --workspace <path>."
|
|
2299
|
+
);
|
|
2300
|
+
}
|
|
2301
|
+
if (error instanceof Error && error.message === "Failed to parse YAML content") {
|
|
2302
|
+
throw new Error("~/.basou/portfolio.yaml is not valid YAML.");
|
|
2303
|
+
}
|
|
2304
|
+
throw error;
|
|
2305
|
+
}
|
|
2306
|
+
if (!isRecord(raw) || !Array.isArray(raw.workspaces)) {
|
|
2307
|
+
throw new Error("~/.basou/portfolio.yaml must contain a 'workspaces:' list.");
|
|
2308
|
+
}
|
|
2309
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2310
|
+
const result = [];
|
|
2311
|
+
for (const entry of raw.workspaces) {
|
|
2312
|
+
if (!isRecord(entry) || typeof entry.path !== "string" || entry.path.trim().length === 0) {
|
|
2313
|
+
throw new Error("Each portfolio workspace needs a non-empty string 'path'.");
|
|
2314
|
+
}
|
|
2315
|
+
if (entry.label !== void 0 && typeof entry.label !== "string") {
|
|
2316
|
+
throw new Error("A portfolio workspace 'label' must be a string when present.");
|
|
2317
|
+
}
|
|
2318
|
+
const expanded = expandTilde(entry.path.trim());
|
|
2319
|
+
if (!isAbsolute(expanded)) {
|
|
2320
|
+
throw new Error(
|
|
2321
|
+
"Portfolio workspace paths must be absolute (or start with '~'); use --workspace for relative ad-hoc paths."
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
const abs = resolve3(expanded);
|
|
2325
|
+
if (seen.has(abs)) continue;
|
|
2326
|
+
seen.add(abs);
|
|
2327
|
+
result.push(entry.label !== void 0 ? { path: abs, label: entry.label } : { path: abs });
|
|
2328
|
+
}
|
|
2329
|
+
if (result.length === 0) {
|
|
2330
|
+
throw new Error("~/.basou/portfolio.yaml has no workspaces.");
|
|
2331
|
+
}
|
|
2332
|
+
return result;
|
|
2333
|
+
}
|
|
2129
2334
|
|
|
2130
2335
|
// src/commands/refresh-watch.ts
|
|
2131
2336
|
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
2132
|
-
import { homedir as
|
|
2133
|
-
import { join as
|
|
2134
|
-
import { findErrorCode as
|
|
2337
|
+
import { homedir as homedir4 } from "os";
|
|
2338
|
+
import { join as join5 } from "path";
|
|
2339
|
+
import { findErrorCode as findErrorCode7 } from "@basou/core";
|
|
2135
2340
|
var DEFAULT_WATCH_INTERVAL_SEC = 30;
|
|
2136
2341
|
var MIN_WATCH_INTERVAL_SEC = 5;
|
|
2137
2342
|
var MAX_WATCH_INTERVAL_SEC = 86400;
|
|
2138
2343
|
function watchedRoots(ctx) {
|
|
2139
2344
|
return [
|
|
2140
|
-
ctx.codexSessionsDir ??
|
|
2141
|
-
ctx.claudeProjectsDir ??
|
|
2345
|
+
ctx.codexSessionsDir ?? join5(homedir4(), ".codex", "sessions"),
|
|
2346
|
+
ctx.claudeProjectsDir ?? join5(homedir4(), ".claude", "projects")
|
|
2142
2347
|
];
|
|
2143
2348
|
}
|
|
2144
2349
|
async function scanSourceLogs(roots) {
|
|
@@ -2148,11 +2353,11 @@ async function scanSourceLogs(roots) {
|
|
|
2148
2353
|
try {
|
|
2149
2354
|
entries = await readdir2(dir, { withFileTypes: true });
|
|
2150
2355
|
} catch (error) {
|
|
2151
|
-
if (
|
|
2356
|
+
if (findErrorCode7(error, "ENOENT") || findErrorCode7(error, "ENOTDIR")) return;
|
|
2152
2357
|
throw new Error("Failed to read a source log directory", { cause: error });
|
|
2153
2358
|
}
|
|
2154
2359
|
for (const entry of entries) {
|
|
2155
|
-
const full =
|
|
2360
|
+
const full = join5(dir, entry.name);
|
|
2156
2361
|
if (entry.isDirectory()) {
|
|
2157
2362
|
await walk(full);
|
|
2158
2363
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -2160,7 +2365,7 @@ async function scanSourceLogs(roots) {
|
|
|
2160
2365
|
const info = await stat2(full);
|
|
2161
2366
|
out.set(full, { mtimeMs: info.mtimeMs, size: info.size });
|
|
2162
2367
|
} catch (error) {
|
|
2163
|
-
if (
|
|
2368
|
+
if (findErrorCode7(error, "ENOENT")) continue;
|
|
2164
2369
|
throw new Error("Failed to stat a source log file", { cause: error });
|
|
2165
2370
|
}
|
|
2166
2371
|
}
|
|
@@ -2259,19 +2464,19 @@ function parseInterval(value) {
|
|
|
2259
2464
|
return seconds;
|
|
2260
2465
|
}
|
|
2261
2466
|
function abortableSleep(ms, signal) {
|
|
2262
|
-
return new Promise((
|
|
2467
|
+
return new Promise((resolve7) => {
|
|
2263
2468
|
if (signal.aborted) {
|
|
2264
|
-
|
|
2469
|
+
resolve7();
|
|
2265
2470
|
return;
|
|
2266
2471
|
}
|
|
2267
2472
|
let timer;
|
|
2268
2473
|
const onAbort = () => {
|
|
2269
2474
|
clearTimeout(timer);
|
|
2270
|
-
|
|
2475
|
+
resolve7();
|
|
2271
2476
|
};
|
|
2272
2477
|
timer = setTimeout(() => {
|
|
2273
2478
|
signal.removeEventListener("abort", onAbort);
|
|
2274
|
-
|
|
2479
|
+
resolve7();
|
|
2275
2480
|
}, ms);
|
|
2276
2481
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
2277
2482
|
});
|
|
@@ -2285,6 +2490,9 @@ function registerRefreshCommand(program) {
|
|
|
2285
2490
|
collectPath2,
|
|
2286
2491
|
[]
|
|
2287
2492
|
).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(
|
|
2493
|
+
"--portfolio",
|
|
2494
|
+
"Refresh every workspace listed in ~/.basou/portfolio.yaml (each with its own source roots)"
|
|
2495
|
+
).option(
|
|
2288
2496
|
"--watch",
|
|
2289
2497
|
"Keep running: re-import + regenerate when the native logs change (Ctrl-C to stop)"
|
|
2290
2498
|
).option(
|
|
@@ -2297,7 +2505,9 @@ function registerRefreshCommand(program) {
|
|
|
2297
2505
|
}
|
|
2298
2506
|
async function runRefresh(options, ctx = {}) {
|
|
2299
2507
|
try {
|
|
2300
|
-
if (options.
|
|
2508
|
+
if (options.portfolio === true) {
|
|
2509
|
+
await doRunRefreshPortfolio(options, ctx);
|
|
2510
|
+
} else if (options.watch === true) {
|
|
2301
2511
|
await doRunRefreshWatch(options, ctx);
|
|
2302
2512
|
} else {
|
|
2303
2513
|
await doRunRefresh(options, ctx);
|
|
@@ -2307,14 +2517,58 @@ async function runRefresh(options, ctx = {}) {
|
|
|
2307
2517
|
process.exitCode = 1;
|
|
2308
2518
|
}
|
|
2309
2519
|
}
|
|
2520
|
+
async function doRunRefreshPortfolio(options, ctx) {
|
|
2521
|
+
if (options.watch === true) throw new Error("--portfolio cannot be combined with --watch.");
|
|
2522
|
+
if (options.project !== void 0 && options.project.length > 0) {
|
|
2523
|
+
throw new Error(
|
|
2524
|
+
"--portfolio refreshes each workspace with its own source roots; remove --project."
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2527
|
+
const workspaces = await loadPortfolioConfig(ctx.portfolioConfigPath);
|
|
2528
|
+
const rollup = [];
|
|
2529
|
+
for (const ws of workspaces) {
|
|
2530
|
+
const label = ws.label ?? ws.path;
|
|
2531
|
+
try {
|
|
2532
|
+
const result = await computeRefresh(
|
|
2533
|
+
{ ...options, portfolio: false },
|
|
2534
|
+
{ ...ctx, cwd: ws.path }
|
|
2535
|
+
);
|
|
2536
|
+
rollup.push({ label, path: ws.path, status: "ok", result });
|
|
2537
|
+
if (options.json !== true) {
|
|
2538
|
+
console.log(`
|
|
2539
|
+
## ${label} (${ws.path})`);
|
|
2540
|
+
printRefreshSummary(result);
|
|
2541
|
+
}
|
|
2542
|
+
} catch (error) {
|
|
2543
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2544
|
+
rollup.push({ label, path: ws.path, status: "failed", error: message });
|
|
2545
|
+
if (options.json !== true) {
|
|
2546
|
+
console.log(`
|
|
2547
|
+
## ${label} (${ws.path})`);
|
|
2548
|
+
console.log(` failed: ${message}`);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
if (options.json === true) {
|
|
2553
|
+
console.log(JSON.stringify({ portfolio: true, workspaces: rollup }));
|
|
2554
|
+
} else {
|
|
2555
|
+
const failed = rollup.filter((r) => r.status === "failed").length;
|
|
2556
|
+
const ok = rollup.length - failed;
|
|
2557
|
+
console.log(
|
|
2558
|
+
`
|
|
2559
|
+
portfolio: ${ok}/${rollup.length} refreshed${failed > 0 ? `, ${failed} failed` : ""}.`
|
|
2560
|
+
);
|
|
2561
|
+
}
|
|
2562
|
+
if (rollup.some((r) => r.status === "failed")) process.exitCode = 1;
|
|
2563
|
+
}
|
|
2310
2564
|
async function doRunRefreshWatch(options, ctx) {
|
|
2311
2565
|
if (options.dryRun === true) throw new Error("--watch cannot be combined with --dry-run.");
|
|
2312
2566
|
if (options.json === true) throw new Error("--watch cannot be combined with --json.");
|
|
2313
2567
|
if (options.force === true) throw new Error("--watch cannot be combined with --force.");
|
|
2314
2568
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2315
|
-
const repositoryRoot = await
|
|
2316
|
-
const paths =
|
|
2317
|
-
await
|
|
2569
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "refresh");
|
|
2570
|
+
const paths = basouPaths8(repositoryRoot);
|
|
2571
|
+
await assertWorkspaceInitialized7(paths.root);
|
|
2318
2572
|
const intervalMs = (options.interval ?? DEFAULT_WATCH_INTERVAL_SEC) * 1e3;
|
|
2319
2573
|
const controller = new AbortController();
|
|
2320
2574
|
const onSignal = () => controller.abort();
|
|
@@ -2322,7 +2576,9 @@ async function doRunRefreshWatch(options, ctx) {
|
|
|
2322
2576
|
process.on("SIGTERM", onSignal);
|
|
2323
2577
|
try {
|
|
2324
2578
|
await runRefreshWatch({
|
|
2325
|
-
|
|
2579
|
+
// Watch from a workspace view: import from the resolved planning repo, not
|
|
2580
|
+
// the raw (non-git) view cwd — mirrors the redirect in computeRefresh.
|
|
2581
|
+
ctx: { ...ctx, cwd: repositoryRoot },
|
|
2326
2582
|
paths,
|
|
2327
2583
|
intervalMs,
|
|
2328
2584
|
importOptions: options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
|
|
@@ -2336,22 +2592,27 @@ async function doRunRefreshWatch(options, ctx) {
|
|
|
2336
2592
|
process.off("SIGTERM", onSignal);
|
|
2337
2593
|
}
|
|
2338
2594
|
}
|
|
2339
|
-
async function
|
|
2595
|
+
async function computeRefresh(options, ctx) {
|
|
2340
2596
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2341
|
-
const repositoryRoot = await
|
|
2342
|
-
const paths =
|
|
2343
|
-
await
|
|
2597
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "refresh");
|
|
2598
|
+
const paths = basouPaths8(repositoryRoot);
|
|
2599
|
+
await assertWorkspaceInitialized7(paths.root);
|
|
2344
2600
|
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2345
|
-
|
|
2601
|
+
return refreshAll({
|
|
2346
2602
|
options: {
|
|
2347
2603
|
...options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
|
|
2348
2604
|
...options.force === true ? { force: true } : {},
|
|
2349
2605
|
...options.dryRun === true ? { dryRun: true } : {}
|
|
2350
2606
|
},
|
|
2351
|
-
|
|
2607
|
+
// Import from the resolved repo root, not the raw cwd: a workspace-view cwd
|
|
2608
|
+
// redirects to its planning repo, and the import must run there too.
|
|
2609
|
+
ctx: { ...ctx, cwd: repositoryRoot },
|
|
2352
2610
|
paths,
|
|
2353
2611
|
nowIso
|
|
2354
2612
|
});
|
|
2613
|
+
}
|
|
2614
|
+
async function doRunRefresh(options, ctx) {
|
|
2615
|
+
const result = await computeRefresh(options, ctx);
|
|
2355
2616
|
if (options.json === true) {
|
|
2356
2617
|
console.log(JSON.stringify(result));
|
|
2357
2618
|
} else {
|
|
@@ -2387,24 +2648,19 @@ function printRefreshSummary(result) {
|
|
|
2387
2648
|
} else {
|
|
2388
2649
|
console.log(`decisions: skipped (${result.decisions.reason})`);
|
|
2389
2650
|
}
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
|
-
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou refresh'.", {
|
|
2397
|
-
cause: error
|
|
2398
|
-
});
|
|
2399
|
-
}
|
|
2400
|
-
throw error;
|
|
2651
|
+
if (result.orientation.status === "generated") {
|
|
2652
|
+
console.log(
|
|
2653
|
+
`orientation: regenerated (in-flight: ${result.orientation.inFlightTaskCount}, pending approvals: ${result.orientation.pendingApprovalsCount}, suspect: ${result.orientation.suspectCount})`
|
|
2654
|
+
);
|
|
2655
|
+
} else {
|
|
2656
|
+
console.log(`orientation: skipped (${result.orientation.reason})`);
|
|
2401
2657
|
}
|
|
2402
2658
|
}
|
|
2403
|
-
async function
|
|
2659
|
+
async function assertWorkspaceInitialized7(basouRoot) {
|
|
2404
2660
|
try {
|
|
2405
|
-
await
|
|
2661
|
+
await assertBasouRootSafe8(basouRoot);
|
|
2406
2662
|
} catch (error) {
|
|
2407
|
-
if (
|
|
2663
|
+
if (findErrorCode8(error, "ENOENT")) {
|
|
2408
2664
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
2409
2665
|
}
|
|
2410
2666
|
throw error;
|
|
@@ -2412,14 +2668,14 @@ async function assertWorkspaceInitialized6(basouRoot) {
|
|
|
2412
2668
|
}
|
|
2413
2669
|
|
|
2414
2670
|
// src/commands/report.ts
|
|
2415
|
-
import { isAbsolute, resolve as
|
|
2671
|
+
import { isAbsolute as isAbsolute2, resolve as resolve4 } from "path";
|
|
2416
2672
|
import {
|
|
2417
|
-
assertBasouRootSafe as
|
|
2418
|
-
basouPaths as
|
|
2419
|
-
findErrorCode as
|
|
2673
|
+
assertBasouRootSafe as assertBasouRootSafe9,
|
|
2674
|
+
basouPaths as basouPaths9,
|
|
2675
|
+
findErrorCode as findErrorCode9,
|
|
2420
2676
|
renderReport,
|
|
2421
|
-
resolveRepositoryRoot as
|
|
2422
|
-
writeMarkdownFile as
|
|
2677
|
+
resolveRepositoryRoot as resolveRepositoryRoot8,
|
|
2678
|
+
writeMarkdownFile as writeMarkdownFile5
|
|
2423
2679
|
} from "@basou/core";
|
|
2424
2680
|
function registerReportCommand(program) {
|
|
2425
2681
|
const report = program.command("report").description(
|
|
@@ -2440,8 +2696,8 @@ async function runReportGenerate(options, ctx = {}) {
|
|
|
2440
2696
|
async function doRunReportGenerate(options, ctx) {
|
|
2441
2697
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2442
2698
|
const repositoryRoot = await resolveRepositoryRootForReport(cwd);
|
|
2443
|
-
const paths =
|
|
2444
|
-
await
|
|
2699
|
+
const paths = basouPaths9(repositoryRoot);
|
|
2700
|
+
await assertWorkspaceInitialized8(paths.root);
|
|
2445
2701
|
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2446
2702
|
const result = await renderReport({
|
|
2447
2703
|
paths,
|
|
@@ -2452,8 +2708,8 @@ async function doRunReportGenerate(options, ctx) {
|
|
|
2452
2708
|
onTaskSkip: (taskId, reason) => printTaskSkip(taskId, reason)
|
|
2453
2709
|
});
|
|
2454
2710
|
if (options.out !== void 0) {
|
|
2455
|
-
const outPath =
|
|
2456
|
-
await
|
|
2711
|
+
const outPath = isAbsolute2(options.out) ? options.out : resolve4(cwd, options.out);
|
|
2712
|
+
await writeMarkdownFile5(outPath, result.body);
|
|
2457
2713
|
const { sessions, decisions, tasks } = result.data;
|
|
2458
2714
|
console.error(
|
|
2459
2715
|
`Wrote report to ${options.out} (sessions: ${sessions.total}, decisions: ${decisions.count}, tasks: ${tasks.total})`
|
|
@@ -2467,7 +2723,7 @@ async function doRunReportGenerate(options, ctx) {
|
|
|
2467
2723
|
}
|
|
2468
2724
|
async function resolveRepositoryRootForReport(cwd) {
|
|
2469
2725
|
try {
|
|
2470
|
-
return await
|
|
2726
|
+
return await resolveRepositoryRoot8(cwd);
|
|
2471
2727
|
} catch (error) {
|
|
2472
2728
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
2473
2729
|
throw new Error(
|
|
@@ -2478,11 +2734,11 @@ async function resolveRepositoryRootForReport(cwd) {
|
|
|
2478
2734
|
throw error;
|
|
2479
2735
|
}
|
|
2480
2736
|
}
|
|
2481
|
-
async function
|
|
2737
|
+
async function assertWorkspaceInitialized8(basouRoot) {
|
|
2482
2738
|
try {
|
|
2483
|
-
await
|
|
2739
|
+
await assertBasouRootSafe9(basouRoot);
|
|
2484
2740
|
} catch (error) {
|
|
2485
|
-
if (
|
|
2741
|
+
if (findErrorCode9(error, "ENOENT")) {
|
|
2486
2742
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
2487
2743
|
}
|
|
2488
2744
|
throw error;
|
|
@@ -2491,12 +2747,12 @@ async function assertWorkspaceInitialized7(basouRoot) {
|
|
|
2491
2747
|
|
|
2492
2748
|
// src/commands/run.ts
|
|
2493
2749
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
2494
|
-
import { homedir as
|
|
2495
|
-
import { join as
|
|
2750
|
+
import { homedir as homedir5 } from "os";
|
|
2751
|
+
import { join as join6 } from "path";
|
|
2496
2752
|
import {
|
|
2497
2753
|
acquireLock as acquireLock4,
|
|
2498
|
-
assertBasouRootSafe as
|
|
2499
|
-
basouPaths as
|
|
2754
|
+
assertBasouRootSafe as assertBasouRootSafe10,
|
|
2755
|
+
basouPaths as basouPaths10,
|
|
2500
2756
|
ChildProcessRunner as ChildProcessRunner2,
|
|
2501
2757
|
claudeCodeAdapterMetadata,
|
|
2502
2758
|
appendChainedEvent as coreAppendChainedEvent2,
|
|
@@ -2506,9 +2762,9 @@ import {
|
|
|
2506
2762
|
overwriteYamlFile as overwriteYamlFile2,
|
|
2507
2763
|
prefixedUlid as prefixedUlid4,
|
|
2508
2764
|
readManifest as readManifest4,
|
|
2509
|
-
readYamlFile as
|
|
2765
|
+
readYamlFile as readYamlFile4,
|
|
2510
2766
|
resolveClaudeCodeCommand,
|
|
2511
|
-
resolveRepositoryRoot as
|
|
2767
|
+
resolveRepositoryRoot as resolveRepositoryRoot9,
|
|
2512
2768
|
SessionSchema as SessionSchema2,
|
|
2513
2769
|
sanitizeRelatedFiles,
|
|
2514
2770
|
sanitizeWorkingDirectory as sanitizeWorkingDirectory2,
|
|
@@ -2541,17 +2797,17 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2541
2797
|
const { command } = await resolveCommand();
|
|
2542
2798
|
const cwd = options.cwd ?? process.cwd();
|
|
2543
2799
|
const repoRoot = await resolveRepositoryRootForRun(cwd);
|
|
2544
|
-
const paths =
|
|
2545
|
-
await
|
|
2800
|
+
const paths = basouPaths10(repoRoot);
|
|
2801
|
+
await assertBasouRootSafe10(paths.root);
|
|
2546
2802
|
const manifest = await readManifest4(paths);
|
|
2547
2803
|
const sessionId = prefixedUlid4("ses");
|
|
2548
|
-
const sessionDir =
|
|
2804
|
+
const sessionDir = join6(paths.sessions, sessionId);
|
|
2549
2805
|
await mkdir2(sessionDir, { recursive: true });
|
|
2550
2806
|
const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
|
|
2551
2807
|
await coreAppendChainedEvent2(paths, sessionId, event);
|
|
2552
2808
|
});
|
|
2553
2809
|
const startedAt = now().toISOString();
|
|
2554
|
-
const sessionYamlPath =
|
|
2810
|
+
const sessionYamlPath = join6(sessionDir, "session.yaml");
|
|
2555
2811
|
const session = buildInitialSession2({
|
|
2556
2812
|
id: sessionId,
|
|
2557
2813
|
command,
|
|
@@ -2677,7 +2933,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2677
2933
|
const rawRelated = computeRelatedFiles(preSnapshot, postSnapshot, diff);
|
|
2678
2934
|
const relatedFiles = sanitizeRelatedFiles(rawRelated, {
|
|
2679
2935
|
workingDirectory: repoRoot,
|
|
2680
|
-
homedir:
|
|
2936
|
+
homedir: homedir5()
|
|
2681
2937
|
}).sanitized;
|
|
2682
2938
|
const finalStatus = decideFinalStatus2(result, signalReceived);
|
|
2683
2939
|
await appendEvent(sessionDir, {
|
|
@@ -2821,7 +3077,7 @@ function buildInitialSession2(input) {
|
|
|
2821
3077
|
source: { ...claudeCodeAdapterMetadata },
|
|
2822
3078
|
started_at: input.startedAt,
|
|
2823
3079
|
status: "initialized",
|
|
2824
|
-
working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir:
|
|
3080
|
+
working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir5() }),
|
|
2825
3081
|
invocation: {
|
|
2826
3082
|
command: input.command,
|
|
2827
3083
|
args: [...input.args],
|
|
@@ -2833,7 +3089,7 @@ function buildInitialSession2(input) {
|
|
|
2833
3089
|
};
|
|
2834
3090
|
}
|
|
2835
3091
|
async function mutateSessionYaml2(filePath, mutator) {
|
|
2836
|
-
const raw = await
|
|
3092
|
+
const raw = await readYamlFile4(filePath);
|
|
2837
3093
|
const parsed = SessionSchema2.parse(raw);
|
|
2838
3094
|
mutator(parsed);
|
|
2839
3095
|
const validated = SessionSchema2.parse(parsed);
|
|
@@ -2881,7 +3137,7 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
|
|
|
2881
3137
|
}
|
|
2882
3138
|
async function resolveRepositoryRootForRun(cwd) {
|
|
2883
3139
|
try {
|
|
2884
|
-
return await
|
|
3140
|
+
return await resolveRepositoryRoot9(cwd);
|
|
2885
3141
|
} catch (error) {
|
|
2886
3142
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
2887
3143
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou run'.", {
|
|
@@ -2894,21 +3150,21 @@ async function resolveRepositoryRootForRun(cwd) {
|
|
|
2894
3150
|
|
|
2895
3151
|
// src/commands/session.ts
|
|
2896
3152
|
import { readFile as readFile2 } from "fs/promises";
|
|
2897
|
-
import { basename as basename3, isAbsolute as
|
|
3153
|
+
import { basename as basename3, isAbsolute as isAbsolute3, join as join7, relative as relative2 } from "path";
|
|
2898
3154
|
import {
|
|
2899
3155
|
acquireLock as acquireLock5,
|
|
2900
3156
|
appendEventToExistingSession as appendEventToExistingSession2,
|
|
2901
|
-
assertBasouRootSafe as
|
|
2902
|
-
basouPaths as
|
|
3157
|
+
assertBasouRootSafe as assertBasouRootSafe11,
|
|
3158
|
+
basouPaths as basouPaths11,
|
|
2903
3159
|
enumerateSessionDirs as enumerateSessionDirs2,
|
|
2904
|
-
findErrorCode as
|
|
3160
|
+
findErrorCode as findErrorCode10,
|
|
2905
3161
|
importSessionFromJson as importSessionFromJson2,
|
|
2906
3162
|
loadSessionEntries,
|
|
2907
3163
|
readAllEvents,
|
|
2908
3164
|
readManifest as readManifest5,
|
|
2909
|
-
readYamlFile as
|
|
3165
|
+
readYamlFile as readYamlFile5,
|
|
2910
3166
|
rechainSessionInPlace,
|
|
2911
|
-
resolveRepositoryRoot as
|
|
3167
|
+
resolveRepositoryRoot as resolveRepositoryRoot10,
|
|
2912
3168
|
resolveSessionId as resolveSessionId2,
|
|
2913
3169
|
resolveTaskId,
|
|
2914
3170
|
SessionImportPayloadSchema as SessionImportPayloadSchema2,
|
|
@@ -2965,8 +3221,8 @@ async function runSessionList(options, ctx = {}) {
|
|
|
2965
3221
|
async function doRunSessionList(options, ctx) {
|
|
2966
3222
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2967
3223
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "list");
|
|
2968
|
-
const paths =
|
|
2969
|
-
await
|
|
3224
|
+
const paths = basouPaths11(repositoryRoot);
|
|
3225
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
2970
3226
|
const now = /* @__PURE__ */ new Date();
|
|
2971
3227
|
const records = (await loadSessionEntries(paths, {
|
|
2972
3228
|
now,
|
|
@@ -3017,17 +3273,17 @@ async function runSessionShow(idInput, options, ctx = {}) {
|
|
|
3017
3273
|
async function doRunSessionShow(idInput, options, ctx) {
|
|
3018
3274
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3019
3275
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "show");
|
|
3020
|
-
const paths =
|
|
3021
|
-
await
|
|
3276
|
+
const paths = basouPaths11(repositoryRoot);
|
|
3277
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
3022
3278
|
const sessionId = await resolveSessionId2(paths, idInput);
|
|
3023
|
-
const sessionDir =
|
|
3024
|
-
const sessionYamlPath =
|
|
3279
|
+
const sessionDir = join7(paths.sessions, sessionId);
|
|
3280
|
+
const sessionYamlPath = join7(sessionDir, "session.yaml");
|
|
3025
3281
|
let session;
|
|
3026
3282
|
try {
|
|
3027
|
-
const raw = await
|
|
3283
|
+
const raw = await readYamlFile5(sessionYamlPath);
|
|
3028
3284
|
session = SessionSchema3.parse(raw);
|
|
3029
3285
|
} catch (error) {
|
|
3030
|
-
if (
|
|
3286
|
+
if (findErrorCode10(error, "ENOENT")) {
|
|
3031
3287
|
throw new Error(`Session not found: ${idInput}`);
|
|
3032
3288
|
}
|
|
3033
3289
|
throw new Error("Failed to read session", { cause: error });
|
|
@@ -3142,7 +3398,7 @@ function formatSessionWork(session, events, now) {
|
|
|
3142
3398
|
}
|
|
3143
3399
|
function formatWorkingDir(workingDir, repositoryRoot, options) {
|
|
3144
3400
|
if (options.fullPath === true) return workingDir;
|
|
3145
|
-
if (!
|
|
3401
|
+
if (!isAbsolute3(workingDir)) {
|
|
3146
3402
|
if (workingDir === ".") return "<repository_root>";
|
|
3147
3403
|
return workingDir;
|
|
3148
3404
|
}
|
|
@@ -3262,7 +3518,7 @@ function maxLen2(values, floor) {
|
|
|
3262
3518
|
}
|
|
3263
3519
|
async function resolveRepositoryRootForSession(cwd, subcmd) {
|
|
3264
3520
|
try {
|
|
3265
|
-
return await
|
|
3521
|
+
return await resolveRepositoryRoot10(cwd);
|
|
3266
3522
|
} catch (error) {
|
|
3267
3523
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
3268
3524
|
throw new Error(
|
|
@@ -3273,11 +3529,11 @@ async function resolveRepositoryRootForSession(cwd, subcmd) {
|
|
|
3273
3529
|
throw error;
|
|
3274
3530
|
}
|
|
3275
3531
|
}
|
|
3276
|
-
async function
|
|
3532
|
+
async function assertWorkspaceInitialized9(basouRoot) {
|
|
3277
3533
|
try {
|
|
3278
|
-
await
|
|
3534
|
+
await assertBasouRootSafe11(basouRoot);
|
|
3279
3535
|
} catch (error) {
|
|
3280
|
-
if (
|
|
3536
|
+
if (findErrorCode10(error, "ENOENT")) {
|
|
3281
3537
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3282
3538
|
}
|
|
3283
3539
|
throw error;
|
|
@@ -3315,8 +3571,8 @@ async function runSessionImport(options, ctx = {}) {
|
|
|
3315
3571
|
async function doRunSessionImport(options, ctx) {
|
|
3316
3572
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3317
3573
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "import");
|
|
3318
|
-
const paths =
|
|
3319
|
-
await
|
|
3574
|
+
const paths = basouPaths11(repositoryRoot);
|
|
3575
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
3320
3576
|
const manifest = await readManifest5(paths);
|
|
3321
3577
|
const rawBody = await readInputFile(options.from);
|
|
3322
3578
|
const json = parseJsonStrict(rawBody);
|
|
@@ -3346,10 +3602,10 @@ async function readInputFile(path) {
|
|
|
3346
3602
|
try {
|
|
3347
3603
|
return await readFile2(path, "utf8");
|
|
3348
3604
|
} catch (error) {
|
|
3349
|
-
if (
|
|
3605
|
+
if (findErrorCode10(error, "ENOENT")) {
|
|
3350
3606
|
throw new Error("Import source not found", { cause: error });
|
|
3351
3607
|
}
|
|
3352
|
-
if (
|
|
3608
|
+
if (findErrorCode10(error, "EISDIR")) {
|
|
3353
3609
|
throw new Error("Import source is not a file", { cause: error });
|
|
3354
3610
|
}
|
|
3355
3611
|
throw new Error("Failed to read import source", { cause: error });
|
|
@@ -3429,8 +3685,8 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
|
|
|
3429
3685
|
}
|
|
3430
3686
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3431
3687
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "note");
|
|
3432
|
-
const paths =
|
|
3433
|
-
await
|
|
3688
|
+
const paths = basouPaths11(repositoryRoot);
|
|
3689
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
3434
3690
|
const sessionId = await resolveSessionId2(paths, sessionIdInput);
|
|
3435
3691
|
const body = hasBody ? options.body : await readNoteFile(options.fromFile);
|
|
3436
3692
|
if (body.length === 0) {
|
|
@@ -3463,10 +3719,10 @@ async function readNoteFile(path) {
|
|
|
3463
3719
|
try {
|
|
3464
3720
|
return await readFile2(path, "utf8");
|
|
3465
3721
|
} catch (error) {
|
|
3466
|
-
if (
|
|
3722
|
+
if (findErrorCode10(error, "ENOENT")) {
|
|
3467
3723
|
throw new Error("Note source not found", { cause: error });
|
|
3468
3724
|
}
|
|
3469
|
-
if (
|
|
3725
|
+
if (findErrorCode10(error, "EISDIR")) {
|
|
3470
3726
|
throw new Error("Note source is not a file", { cause: error });
|
|
3471
3727
|
}
|
|
3472
3728
|
throw new Error("Failed to read note source", { cause: error });
|
|
@@ -3511,8 +3767,8 @@ async function doRunSessionRechain(options, ctx) {
|
|
|
3511
3767
|
}
|
|
3512
3768
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3513
3769
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "rechain");
|
|
3514
|
-
const paths =
|
|
3515
|
-
await
|
|
3770
|
+
const paths = basouPaths11(repositoryRoot);
|
|
3771
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
3516
3772
|
const sessionIds = options.session !== void 0 ? [await resolveSessionId2(paths, options.session)] : await enumerateSessionDirs2(paths);
|
|
3517
3773
|
const dryRun = options.dryRun === true;
|
|
3518
3774
|
const rows = [];
|
|
@@ -3565,11 +3821,11 @@ function renderRechainRow(row, dryRun) {
|
|
|
3565
3821
|
|
|
3566
3822
|
// src/commands/stats.ts
|
|
3567
3823
|
import {
|
|
3568
|
-
assertBasouRootSafe as
|
|
3569
|
-
basouPaths as
|
|
3824
|
+
assertBasouRootSafe as assertBasouRootSafe12,
|
|
3825
|
+
basouPaths as basouPaths12,
|
|
3570
3826
|
computeWorkStats,
|
|
3571
|
-
findErrorCode as
|
|
3572
|
-
resolveRepositoryRoot as
|
|
3827
|
+
findErrorCode as findErrorCode11,
|
|
3828
|
+
resolveRepositoryRoot as resolveRepositoryRoot11
|
|
3573
3829
|
} from "@basou/core";
|
|
3574
3830
|
function registerStatsCommand(program) {
|
|
3575
3831
|
program.command("stats").description("Report how much the AI worked (output volume + time proxies) across sessions").option("--by-source", "Break the totals down by session source kind").option("--by-day", "Break billable time and volume down by calendar day").option("--json", "Output the full stats as JSON").option("-v, --verbose", "Show error causes").action(async (options) => {
|
|
@@ -3587,8 +3843,8 @@ async function runStats(options, ctx = {}) {
|
|
|
3587
3843
|
async function doRunStats(options, ctx) {
|
|
3588
3844
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3589
3845
|
const repositoryRoot = await resolveRepositoryRootForStats(cwd);
|
|
3590
|
-
const paths =
|
|
3591
|
-
await
|
|
3846
|
+
const paths = basouPaths12(repositoryRoot);
|
|
3847
|
+
await assertWorkspaceInitialized10(paths.root);
|
|
3592
3848
|
const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
|
|
3593
3849
|
const result = await computeWorkStats({
|
|
3594
3850
|
paths,
|
|
@@ -3672,7 +3928,7 @@ function formatInt(n) {
|
|
|
3672
3928
|
}
|
|
3673
3929
|
async function resolveRepositoryRootForStats(cwd) {
|
|
3674
3930
|
try {
|
|
3675
|
-
return await
|
|
3931
|
+
return await resolveRepositoryRoot11(cwd);
|
|
3676
3932
|
} catch (error) {
|
|
3677
3933
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
3678
3934
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou stats'.", {
|
|
@@ -3682,11 +3938,11 @@ async function resolveRepositoryRootForStats(cwd) {
|
|
|
3682
3938
|
throw error;
|
|
3683
3939
|
}
|
|
3684
3940
|
}
|
|
3685
|
-
async function
|
|
3941
|
+
async function assertWorkspaceInitialized10(basouRoot) {
|
|
3686
3942
|
try {
|
|
3687
|
-
await
|
|
3943
|
+
await assertBasouRootSafe12(basouRoot);
|
|
3688
3944
|
} catch (error) {
|
|
3689
|
-
if (
|
|
3945
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
3690
3946
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3691
3947
|
}
|
|
3692
3948
|
throw error;
|
|
@@ -3695,12 +3951,12 @@ async function assertWorkspaceInitialized9(basouRoot) {
|
|
|
3695
3951
|
|
|
3696
3952
|
// src/commands/status.ts
|
|
3697
3953
|
import {
|
|
3698
|
-
assertBasouRootSafe as
|
|
3699
|
-
basouPaths as
|
|
3954
|
+
assertBasouRootSafe as assertBasouRootSafe13,
|
|
3955
|
+
basouPaths as basouPaths13,
|
|
3700
3956
|
buildStatusSnapshot,
|
|
3701
|
-
findErrorCode as
|
|
3957
|
+
findErrorCode as findErrorCode12,
|
|
3702
3958
|
readManifest as readManifest6,
|
|
3703
|
-
resolveRepositoryRoot as
|
|
3959
|
+
resolveRepositoryRoot as resolveRepositoryRoot12,
|
|
3704
3960
|
writeStatus
|
|
3705
3961
|
} from "@basou/core";
|
|
3706
3962
|
function registerStatusCommand(program) {
|
|
@@ -3719,11 +3975,11 @@ async function runStatus(options, ctx = {}) {
|
|
|
3719
3975
|
async function doRunStatus(options, ctx) {
|
|
3720
3976
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3721
3977
|
const repositoryRoot = await resolveRepositoryRootForStatus(cwd);
|
|
3722
|
-
const paths =
|
|
3978
|
+
const paths = basouPaths13(repositoryRoot);
|
|
3723
3979
|
try {
|
|
3724
|
-
await
|
|
3980
|
+
await assertBasouRootSafe13(paths.root);
|
|
3725
3981
|
} catch (error) {
|
|
3726
|
-
if (
|
|
3982
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
3727
3983
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3728
3984
|
}
|
|
3729
3985
|
throw error;
|
|
@@ -3732,7 +3988,7 @@ async function doRunStatus(options, ctx) {
|
|
|
3732
3988
|
try {
|
|
3733
3989
|
manifest = await readManifest6(paths);
|
|
3734
3990
|
} catch (error) {
|
|
3735
|
-
if (
|
|
3991
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
3736
3992
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3737
3993
|
}
|
|
3738
3994
|
throw new Error("Failed to read workspace manifest", { cause: error });
|
|
@@ -3756,7 +4012,7 @@ function renderTextStatus(s) {
|
|
|
3756
4012
|
}
|
|
3757
4013
|
async function resolveRepositoryRootForStatus(cwd) {
|
|
3758
4014
|
try {
|
|
3759
|
-
return await
|
|
4015
|
+
return await resolveRepositoryRoot12(cwd);
|
|
3760
4016
|
} catch (error) {
|
|
3761
4017
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
3762
4018
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou status'.", {
|
|
@@ -3769,16 +4025,16 @@ async function resolveRepositoryRootForStatus(cwd) {
|
|
|
3769
4025
|
|
|
3770
4026
|
// src/commands/task.ts
|
|
3771
4027
|
import { readFile as readFile3 } from "fs/promises";
|
|
3772
|
-
import { join as
|
|
4028
|
+
import { join as join8 } from "path";
|
|
3773
4029
|
import {
|
|
3774
4030
|
archiveTask,
|
|
3775
|
-
assertBasouRootSafe as
|
|
3776
|
-
basouPaths as
|
|
4031
|
+
assertBasouRootSafe as assertBasouRootSafe14,
|
|
4032
|
+
basouPaths as basouPaths14,
|
|
3777
4033
|
createTaskWithEvent,
|
|
3778
4034
|
deleteTask,
|
|
3779
4035
|
editTask,
|
|
3780
4036
|
enumerateArchivedTaskIds,
|
|
3781
|
-
findErrorCode as
|
|
4037
|
+
findErrorCode as findErrorCode13,
|
|
3782
4038
|
loadSessionEntries as loadSessionEntries2,
|
|
3783
4039
|
loadTaskEntries,
|
|
3784
4040
|
prefixedUlid as prefixedUlid5,
|
|
@@ -3789,7 +4045,7 @@ import {
|
|
|
3789
4045
|
reconcileTask,
|
|
3790
4046
|
refreshTaskLinkedSessions,
|
|
3791
4047
|
replayEvents as replayEvents2,
|
|
3792
|
-
resolveRepositoryRoot as
|
|
4048
|
+
resolveRepositoryRoot as resolveRepositoryRoot13,
|
|
3793
4049
|
resolveSessionId as resolveSessionId3,
|
|
3794
4050
|
resolveTaskId as resolveTaskId2,
|
|
3795
4051
|
TaskStatusSchema,
|
|
@@ -3875,8 +4131,8 @@ async function doRunTaskNew(options, ctx) {
|
|
|
3875
4131
|
}
|
|
3876
4132
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3877
4133
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "new");
|
|
3878
|
-
const paths =
|
|
3879
|
-
await
|
|
4134
|
+
const paths = basouPaths14(repositoryRoot);
|
|
4135
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
3880
4136
|
const description = options.description !== void 0 ? options.description : options.fromFile !== void 0 ? await readDescriptionFile(options.fromFile) : "";
|
|
3881
4137
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
3882
4138
|
const occurredAt = now.toISOString();
|
|
@@ -3984,8 +4240,8 @@ async function runTaskList(options, ctx = {}) {
|
|
|
3984
4240
|
async function doRunTaskList(options, ctx) {
|
|
3985
4241
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3986
4242
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "list");
|
|
3987
|
-
const paths =
|
|
3988
|
-
await
|
|
4243
|
+
const paths = basouPaths14(repositoryRoot);
|
|
4244
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
3989
4245
|
const entries = await loadTaskEntries(paths, {
|
|
3990
4246
|
onSkip: (id, reason) => printTaskSkip(id, reason)
|
|
3991
4247
|
});
|
|
@@ -4088,15 +4344,15 @@ async function runTaskShow(idInput, options, ctx = {}) {
|
|
|
4088
4344
|
async function doRunTaskShow(idInput, options, ctx) {
|
|
4089
4345
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4090
4346
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "show");
|
|
4091
|
-
const paths =
|
|
4092
|
-
await
|
|
4347
|
+
const paths = basouPaths14(repositoryRoot);
|
|
4348
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
4093
4349
|
const taskId = await resolveTaskId2(paths, idInput, { includeArchived: true });
|
|
4094
4350
|
const { doc, archived } = await readTaskFileWithArchiveFallback(paths, taskId);
|
|
4095
4351
|
const sessions = await loadSessionEntries2(paths, { now: /* @__PURE__ */ new Date() });
|
|
4096
4352
|
const events = [];
|
|
4097
4353
|
const linkedSessionIds = new Set(doc.task.task.linked_sessions);
|
|
4098
4354
|
for (const s of sessions) {
|
|
4099
|
-
const sessionDir =
|
|
4355
|
+
const sessionDir = join8(paths.sessions, s.sessionId);
|
|
4100
4356
|
try {
|
|
4101
4357
|
for await (const ev of replayEvents2(sessionDir, {
|
|
4102
4358
|
onWarning: (w) => printReplayWarning(w, s.sessionId)
|
|
@@ -4232,8 +4488,8 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
|
|
|
4232
4488
|
const newStatus = parseTaskStatusPositional(newStatusInput);
|
|
4233
4489
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4234
4490
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "status");
|
|
4235
|
-
const paths =
|
|
4236
|
-
await
|
|
4491
|
+
const paths = basouPaths14(repositoryRoot);
|
|
4492
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
4237
4493
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4238
4494
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
4239
4495
|
const occurredAt = now.toISOString();
|
|
@@ -4309,8 +4565,8 @@ async function runTaskReconcile(options, ctx = {}) {
|
|
|
4309
4565
|
async function doRunTaskReconcile(options, ctx) {
|
|
4310
4566
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4311
4567
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "reconcile");
|
|
4312
|
-
const paths =
|
|
4313
|
-
await
|
|
4568
|
+
const paths = basouPaths14(repositoryRoot);
|
|
4569
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
4314
4570
|
const manifest = await readManifest7(paths);
|
|
4315
4571
|
const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
|
|
4316
4572
|
const write = options.write === true;
|
|
@@ -4489,8 +4745,8 @@ async function doRunTaskRefreshLinkage(taskIdInput, options, ctx) {
|
|
|
4489
4745
|
}
|
|
4490
4746
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4491
4747
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "refresh-linkage");
|
|
4492
|
-
const paths =
|
|
4493
|
-
await
|
|
4748
|
+
const paths = basouPaths14(repositoryRoot);
|
|
4749
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
4494
4750
|
const manifest = await readManifest7(paths);
|
|
4495
4751
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4496
4752
|
const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
|
|
@@ -4569,8 +4825,8 @@ async function doRunTaskEdit(taskIdInput, options, ctx) {
|
|
|
4569
4825
|
}
|
|
4570
4826
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4571
4827
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "edit");
|
|
4572
|
-
const paths =
|
|
4573
|
-
await
|
|
4828
|
+
const paths = basouPaths14(repositoryRoot);
|
|
4829
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
4574
4830
|
const manifest = await readManifest7(paths);
|
|
4575
4831
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4576
4832
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
@@ -4625,8 +4881,8 @@ async function doRunTaskDelete(taskIdInput, options, ctx) {
|
|
|
4625
4881
|
}
|
|
4626
4882
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4627
4883
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "delete");
|
|
4628
|
-
const paths =
|
|
4629
|
-
await
|
|
4884
|
+
const paths = basouPaths14(repositoryRoot);
|
|
4885
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
4630
4886
|
const manifest = await readManifest7(paths);
|
|
4631
4887
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4632
4888
|
if (options.yes !== true) {
|
|
@@ -4670,8 +4926,8 @@ async function doRunTaskArchive(taskIdInput, options, ctx) {
|
|
|
4670
4926
|
}
|
|
4671
4927
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4672
4928
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "archive");
|
|
4673
|
-
const paths =
|
|
4674
|
-
await
|
|
4929
|
+
const paths = basouPaths14(repositoryRoot);
|
|
4930
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
4675
4931
|
const manifest = await readManifest7(paths);
|
|
4676
4932
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4677
4933
|
if (options.yes !== true) {
|
|
@@ -4786,10 +5042,10 @@ async function readDescriptionFile(path) {
|
|
|
4786
5042
|
try {
|
|
4787
5043
|
return await readFile3(path, "utf8");
|
|
4788
5044
|
} catch (error) {
|
|
4789
|
-
if (
|
|
5045
|
+
if (findErrorCode13(error, "ENOENT")) {
|
|
4790
5046
|
throw new Error("Description source not found", { cause: error });
|
|
4791
5047
|
}
|
|
4792
|
-
if (
|
|
5048
|
+
if (findErrorCode13(error, "EISDIR")) {
|
|
4793
5049
|
throw new Error("Description source is not a file", { cause: error });
|
|
4794
5050
|
}
|
|
4795
5051
|
throw new Error("Failed to read description source", { cause: error });
|
|
@@ -4797,7 +5053,7 @@ async function readDescriptionFile(path) {
|
|
|
4797
5053
|
}
|
|
4798
5054
|
async function resolveRepositoryRootForTask(cwd, subcmd) {
|
|
4799
5055
|
try {
|
|
4800
|
-
return await
|
|
5056
|
+
return await resolveRepositoryRoot13(cwd);
|
|
4801
5057
|
} catch (error) {
|
|
4802
5058
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
4803
5059
|
throw new Error(
|
|
@@ -4808,11 +5064,11 @@ async function resolveRepositoryRootForTask(cwd, subcmd) {
|
|
|
4808
5064
|
throw error;
|
|
4809
5065
|
}
|
|
4810
5066
|
}
|
|
4811
|
-
async function
|
|
5067
|
+
async function assertWorkspaceInitialized11(basouRoot) {
|
|
4812
5068
|
try {
|
|
4813
|
-
await
|
|
5069
|
+
await assertBasouRootSafe14(basouRoot);
|
|
4814
5070
|
} catch (error) {
|
|
4815
|
-
if (
|
|
5071
|
+
if (findErrorCode13(error, "ENOENT")) {
|
|
4816
5072
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
4817
5073
|
}
|
|
4818
5074
|
throw error;
|
|
@@ -4900,11 +5156,11 @@ function maxLen3(values, floor) {
|
|
|
4900
5156
|
|
|
4901
5157
|
// src/commands/verify.ts
|
|
4902
5158
|
import {
|
|
4903
|
-
assertBasouRootSafe as
|
|
4904
|
-
basouPaths as
|
|
5159
|
+
assertBasouRootSafe as assertBasouRootSafe15,
|
|
5160
|
+
basouPaths as basouPaths15,
|
|
4905
5161
|
enumerateSessionDirs as enumerateSessionDirs3,
|
|
4906
|
-
findErrorCode as
|
|
4907
|
-
resolveRepositoryRoot as
|
|
5162
|
+
findErrorCode as findErrorCode14,
|
|
5163
|
+
resolveRepositoryRoot as resolveRepositoryRoot14,
|
|
4908
5164
|
resolveSessionId as resolveSessionId4,
|
|
4909
5165
|
verifyEventsChain
|
|
4910
5166
|
} from "@basou/core";
|
|
@@ -4927,8 +5183,8 @@ async function doRunVerify(options, ctx) {
|
|
|
4927
5183
|
}
|
|
4928
5184
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4929
5185
|
const repositoryRoot = await resolveRepositoryRootForVerify(cwd);
|
|
4930
|
-
const paths =
|
|
4931
|
-
await
|
|
5186
|
+
const paths = basouPaths15(repositoryRoot);
|
|
5187
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
4932
5188
|
const sessionIds = options.session !== void 0 ? [await resolveSessionId4(paths, options.session)] : await enumerateSessionDirs3(paths);
|
|
4933
5189
|
const rows = [];
|
|
4934
5190
|
for (const sessionId of sessionIds) {
|
|
@@ -4975,7 +5231,7 @@ function renderVerdict(row) {
|
|
|
4975
5231
|
}
|
|
4976
5232
|
async function resolveRepositoryRootForVerify(cwd) {
|
|
4977
5233
|
try {
|
|
4978
|
-
return await
|
|
5234
|
+
return await resolveRepositoryRoot14(cwd);
|
|
4979
5235
|
} catch (error) {
|
|
4980
5236
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
4981
5237
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou verify'.", {
|
|
@@ -4985,11 +5241,11 @@ async function resolveRepositoryRootForVerify(cwd) {
|
|
|
4985
5241
|
throw error;
|
|
4986
5242
|
}
|
|
4987
5243
|
}
|
|
4988
|
-
async function
|
|
5244
|
+
async function assertWorkspaceInitialized12(basouRoot) {
|
|
4989
5245
|
try {
|
|
4990
|
-
await
|
|
5246
|
+
await assertBasouRootSafe15(basouRoot);
|
|
4991
5247
|
} catch (error) {
|
|
4992
|
-
if (
|
|
5248
|
+
if (findErrorCode14(error, "ENOENT")) {
|
|
4993
5249
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
4994
5250
|
}
|
|
4995
5251
|
throw error;
|
|
@@ -4998,27 +5254,162 @@ async function assertWorkspaceInitialized11(basouRoot) {
|
|
|
4998
5254
|
|
|
4999
5255
|
// src/commands/view.ts
|
|
5000
5256
|
import { spawn } from "child_process";
|
|
5001
|
-
import {
|
|
5257
|
+
import { createHash } from "crypto";
|
|
5258
|
+
import { basename as basename4, resolve as resolve6 } from "path";
|
|
5259
|
+
import {
|
|
5260
|
+
assertBasouRootSafe as assertBasouRootSafe16,
|
|
5261
|
+
basouPaths as basouPaths16,
|
|
5262
|
+
findErrorCode as findErrorCode16,
|
|
5263
|
+
readManifest as readManifest10,
|
|
5264
|
+
resolveRepositoryRoot as resolveRepositoryRoot15
|
|
5265
|
+
} from "@basou/core";
|
|
5002
5266
|
import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
|
|
5003
5267
|
|
|
5268
|
+
// src/lib/portfolio-safety.ts
|
|
5269
|
+
import { execFile } from "child_process";
|
|
5270
|
+
import { lstat, realpath } from "fs/promises";
|
|
5271
|
+
import { isAbsolute as isAbsolute4, join as join9, relative as relative3, resolve as resolve5 } from "path";
|
|
5272
|
+
import { promisify } from "util";
|
|
5273
|
+
import { readManifest as readManifest8 } from "@basou/core";
|
|
5274
|
+
var execFileAsync = promisify(execFile);
|
|
5275
|
+
function errorCode(error) {
|
|
5276
|
+
return error instanceof Error ? error.code : void 0;
|
|
5277
|
+
}
|
|
5278
|
+
async function canonical(p) {
|
|
5279
|
+
try {
|
|
5280
|
+
return await realpath(p);
|
|
5281
|
+
} catch {
|
|
5282
|
+
return resolve5(p);
|
|
5283
|
+
}
|
|
5284
|
+
}
|
|
5285
|
+
function isInside(child, parent) {
|
|
5286
|
+
const rel = relative3(parent, child);
|
|
5287
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
|
|
5288
|
+
}
|
|
5289
|
+
function isBasouPath(p) {
|
|
5290
|
+
return p === ".basou" || p.startsWith(".basou/") || p.includes("/.basou/") || p.endsWith("/.basou");
|
|
5291
|
+
}
|
|
5292
|
+
async function inspectRepo(repoPath) {
|
|
5293
|
+
let hasEntry = false;
|
|
5294
|
+
try {
|
|
5295
|
+
await lstat(join9(repoPath, ".basou"));
|
|
5296
|
+
hasEntry = true;
|
|
5297
|
+
} catch (error) {
|
|
5298
|
+
if (errorCode(error) !== "ENOENT") {
|
|
5299
|
+
return {
|
|
5300
|
+
kind: "unverifiable",
|
|
5301
|
+
detail: `could not check for a .basou here (${errorCode(error) ?? "unknown error"}) \u2014 treat as unsafe`
|
|
5302
|
+
};
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
try {
|
|
5306
|
+
const { stdout } = await execFileAsync("git", ["-C", repoPath, "ls-files", "-z"]);
|
|
5307
|
+
const tracked = stdout.split("\0").some((f) => f.length > 0 && isBasouPath(f));
|
|
5308
|
+
if (tracked) {
|
|
5309
|
+
return {
|
|
5310
|
+
kind: "footprint",
|
|
5311
|
+
detail: "a .basou/ entry is tracked by git here and would be pushed"
|
|
5312
|
+
};
|
|
5313
|
+
}
|
|
5314
|
+
} catch {
|
|
5315
|
+
}
|
|
5316
|
+
if (hasEntry) return { kind: "footprint", detail: "a .basou/ entry exists here" };
|
|
5317
|
+
return null;
|
|
5318
|
+
}
|
|
5319
|
+
async function checkPortfolioSafety(workspaces) {
|
|
5320
|
+
const findings = [];
|
|
5321
|
+
let monitoredReposChecked = 0;
|
|
5322
|
+
for (const ws of workspaces) {
|
|
5323
|
+
const wsReal = await canonical(ws.repoRoot);
|
|
5324
|
+
let sourceRoots = [];
|
|
5325
|
+
try {
|
|
5326
|
+
const manifest = await readManifest8(ws.paths);
|
|
5327
|
+
sourceRoots = manifest.import?.source_roots ?? [];
|
|
5328
|
+
} catch (error) {
|
|
5329
|
+
if (error instanceof Error && error.message === "YAML file not found") {
|
|
5330
|
+
sourceRoots = [];
|
|
5331
|
+
} else {
|
|
5332
|
+
findings.push({
|
|
5333
|
+
workspaceLabel: ws.label,
|
|
5334
|
+
workspaceRoot: ws.repoRoot,
|
|
5335
|
+
monitoredRepo: ws.repoRoot,
|
|
5336
|
+
kind: "unverifiable",
|
|
5337
|
+
detail: "the workspace manifest is present but unreadable \u2014 cannot determine which repos it monitors; treat as unsafe"
|
|
5338
|
+
});
|
|
5339
|
+
continue;
|
|
5340
|
+
}
|
|
5341
|
+
}
|
|
5342
|
+
const monitored = /* @__PURE__ */ new Map();
|
|
5343
|
+
for (const root of sourceRoots) {
|
|
5344
|
+
const display = resolve5(ws.repoRoot, root);
|
|
5345
|
+
const real = await canonical(display);
|
|
5346
|
+
if (real !== wsReal) monitored.set(real, display);
|
|
5347
|
+
}
|
|
5348
|
+
for (const [real, display] of monitored) {
|
|
5349
|
+
monitoredReposChecked++;
|
|
5350
|
+
if (isInside(wsReal, real)) {
|
|
5351
|
+
findings.push({
|
|
5352
|
+
workspaceLabel: ws.label,
|
|
5353
|
+
workspaceRoot: ws.repoRoot,
|
|
5354
|
+
monitoredRepo: display,
|
|
5355
|
+
kind: "overlap",
|
|
5356
|
+
detail: "the workspace (where .basou/ is written) is inside this monitored repo"
|
|
5357
|
+
});
|
|
5358
|
+
}
|
|
5359
|
+
const inspection = await inspectRepo(real);
|
|
5360
|
+
if (inspection !== null) {
|
|
5361
|
+
findings.push({
|
|
5362
|
+
workspaceLabel: ws.label,
|
|
5363
|
+
workspaceRoot: ws.repoRoot,
|
|
5364
|
+
monitoredRepo: display,
|
|
5365
|
+
kind: inspection.kind,
|
|
5366
|
+
detail: inspection.detail
|
|
5367
|
+
});
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
return { findings, workspacesChecked: workspaces.length, monitoredReposChecked };
|
|
5372
|
+
}
|
|
5373
|
+
function formatSafetyReport(result) {
|
|
5374
|
+
if (result.findings.length === 0) {
|
|
5375
|
+
if (result.monitoredReposChecked === 0) {
|
|
5376
|
+
return [
|
|
5377
|
+
`Portfolio safety: OK. ${result.workspacesChecked} workspace(s) checked \u2014 no monitored repos configured (portfolio safety applies when a workspace imports from sibling repos via source_roots).`
|
|
5378
|
+
];
|
|
5379
|
+
}
|
|
5380
|
+
return [
|
|
5381
|
+
`Portfolio safety: OK. ${result.workspacesChecked} workspace(s), ${result.monitoredReposChecked} monitored repo(s) checked \u2014 no .basou footprint, no overlap.`
|
|
5382
|
+
];
|
|
5383
|
+
}
|
|
5384
|
+
const lines = [`Portfolio safety: DANGER \u2014 ${result.findings.length} finding(s):`];
|
|
5385
|
+
for (const f of result.findings) {
|
|
5386
|
+
lines.push(` [${f.kind}] ${f.monitoredRepo} (workspace "${f.workspaceLabel}"): ${f.detail}`);
|
|
5387
|
+
}
|
|
5388
|
+
lines.push(
|
|
5389
|
+
"A monitored repo must have no basou footprint. Use a separate workspace repo whose source_roots point at the monitored repo as a sibling; never 'basou init' / 'run' / 'exec' inside a monitored repo."
|
|
5390
|
+
);
|
|
5391
|
+
return lines;
|
|
5392
|
+
}
|
|
5393
|
+
|
|
5004
5394
|
// src/lib/view-server.ts
|
|
5005
5395
|
import { createServer } from "http";
|
|
5006
|
-
import { join as
|
|
5396
|
+
import { join as join10 } from "path";
|
|
5007
5397
|
import {
|
|
5008
5398
|
computeWorkStats as computeWorkStats2,
|
|
5009
5399
|
enumerateApprovals as enumerateApprovals2,
|
|
5010
|
-
findErrorCode as
|
|
5400
|
+
findErrorCode as findErrorCode15,
|
|
5011
5401
|
isLazyExpired as isLazyExpired2,
|
|
5012
5402
|
loadApproval as loadApproval2,
|
|
5013
5403
|
loadSessionEntries as loadSessionEntries3,
|
|
5014
5404
|
loadTaskEntries as loadTaskEntries2,
|
|
5015
5405
|
readAllEvents as readAllEvents2,
|
|
5016
|
-
readManifest as
|
|
5406
|
+
readManifest as readManifest9,
|
|
5017
5407
|
readMarkdownFile as readMarkdownFile4,
|
|
5018
5408
|
readSessionYaml as readSessionYaml3,
|
|
5019
5409
|
readTaskFile as readTaskFile2,
|
|
5020
5410
|
renderDecisions as renderDecisions3,
|
|
5021
|
-
renderHandoff as renderHandoff3
|
|
5411
|
+
renderHandoff as renderHandoff3,
|
|
5412
|
+
summarizeOrientation
|
|
5022
5413
|
} from "@basou/core";
|
|
5023
5414
|
|
|
5024
5415
|
// src/lib/view-ui.ts
|
|
@@ -5040,8 +5431,13 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5040
5431
|
button.primary { background: #2563eb; color: #fff; border-color: #2563eb; }
|
|
5041
5432
|
button:disabled { opacity: .5; cursor: default; }
|
|
5042
5433
|
label.chk { font-size: 13px; opacity: .85; }
|
|
5434
|
+
/* On the portfolio landing there is no selected workspace, so the per-workspace action bar is hidden. */
|
|
5435
|
+
body.landing #project, body.landing label.chk,
|
|
5436
|
+
body.landing #btn-refresh, body.landing #btn-import-claude, body.landing #btn-import-codex,
|
|
5437
|
+
body.landing #btn-gen-handoff, body.landing #btn-gen-decisions { display: none; }
|
|
5043
5438
|
#status { padding: 6px 16px; font-size: 13px; min-height: 20px; border-bottom: 1px solid #8884; white-space: pre-wrap; }
|
|
5044
5439
|
#status.err { color: #dc2626; }
|
|
5440
|
+
.err { color: #dc2626; }
|
|
5045
5441
|
nav { display: flex; gap: 2px; padding: 6px 12px; border-bottom: 1px solid #8884; flex-wrap: wrap; }
|
|
5046
5442
|
nav button { border: none; border-radius: 6px; background: transparent; }
|
|
5047
5443
|
nav button.active { background: #2563eb22; font-weight: 600; }
|
|
@@ -5055,6 +5451,8 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5055
5451
|
#detail { padding: 12px 16px; overflow: auto; max-height: 80vh; }
|
|
5056
5452
|
.badge { display: inline-block; padding: 0 6px; border-radius: 6px; background: #8882; font-size: 12px; }
|
|
5057
5453
|
.badge.warn { background: #f59e0b33; }
|
|
5454
|
+
.badge.danger { background: #ef444433; }
|
|
5455
|
+
.badge.ok { background: #22c55e33; }
|
|
5058
5456
|
pre { background: #8881; padding: 12px; border-radius: 8px; overflow: auto; white-space: pre-wrap; word-break: break-word; }
|
|
5059
5457
|
table.kv { border-collapse: collapse; }
|
|
5060
5458
|
table.kv td { padding: 2px 10px 2px 0; vertical-align: top; }
|
|
@@ -5063,6 +5461,11 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5063
5461
|
.card { border: 1px solid #8884; border-radius: 8px; padding: 10px 14px; min-width: 120px; }
|
|
5064
5462
|
.card .n { font-size: 22px; font-weight: 700; }
|
|
5065
5463
|
.card .l { font-size: 12px; opacity: .7; }
|
|
5464
|
+
.pcard { min-width: 240px; max-width: 340px; }
|
|
5465
|
+
.pcard.open { cursor: pointer; }
|
|
5466
|
+
.pcard.open:hover { background: #8881; }
|
|
5467
|
+
.pcard .l { font-size: 14px; font-weight: 700; opacity: 1; margin-bottom: 4px; }
|
|
5468
|
+
.pcard .f { font-size: 13px; }
|
|
5066
5469
|
.tl { border-left: 2px solid #8885; margin-left: 6px; padding-left: 12px; }
|
|
5067
5470
|
.tl .ev { margin-bottom: 8px; }
|
|
5068
5471
|
.tl .ev .t { font-size: 12px; opacity: .65; }
|
|
@@ -5072,6 +5475,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5072
5475
|
<body>
|
|
5073
5476
|
<header>
|
|
5074
5477
|
<h1>basou view</h1>
|
|
5478
|
+
<button id="btn-back" style="display:none">← portfolio</button>
|
|
5075
5479
|
<input type="text" id="project" placeholder="source root (optional override)" />
|
|
5076
5480
|
<button class="primary" id="btn-refresh">Refresh all</button>
|
|
5077
5481
|
<button id="btn-import-claude">Import claude-code</button>
|
|
@@ -5091,7 +5495,12 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5091
5495
|
<script>
|
|
5092
5496
|
(function () {
|
|
5093
5497
|
var TABS = ['overview', 'stats', 'sessions', 'tasks', 'decisions', 'approvals', 'handoff'];
|
|
5094
|
-
|
|
5498
|
+
// base is the API prefix for the active workspace: '/api' in single mode,
|
|
5499
|
+
// '/api/ws/<key>' once a portfolio card is opened.
|
|
5500
|
+
// canAct gates the mutating action bar: true only when a concrete workspace
|
|
5501
|
+
// is active (single mode, or a portfolio card opened). It is the real safety
|
|
5502
|
+
// guard \u2014 body.landing also hides the buttons, but that is cosmetic.
|
|
5503
|
+
var state = { tab: 'overview', repoRoot: '', base: '/api', mode: 'single', wsKey: null, canAct: false };
|
|
5095
5504
|
|
|
5096
5505
|
function $(id) { return document.getElementById(id); }
|
|
5097
5506
|
function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }
|
|
@@ -5155,7 +5564,15 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5155
5564
|
for (var i = 0; i < ids.length; i++) $(ids[i]).disabled = busy;
|
|
5156
5565
|
}
|
|
5157
5566
|
|
|
5567
|
+
// Enable the action bar only when a workspace is active; disabled buttons
|
|
5568
|
+
// cannot post to a stale/wrong workspace even if a CSS regression un-hides them.
|
|
5569
|
+
function updateActionBar() {
|
|
5570
|
+
var ids = ['btn-refresh', 'btn-import-claude', 'btn-import-codex', 'btn-gen-handoff', 'btn-gen-decisions'];
|
|
5571
|
+
for (var i = 0; i < ids.length; i++) $(ids[i]).disabled = !state.canAct;
|
|
5572
|
+
}
|
|
5573
|
+
|
|
5158
5574
|
function post(path, label) {
|
|
5575
|
+
if (!state.canAct) { setStatus('Open a workspace first.', true); return; }
|
|
5159
5576
|
setBusy(true);
|
|
5160
5577
|
setStatus(label + '...', false);
|
|
5161
5578
|
fetchJson(path, {
|
|
@@ -5188,6 +5605,155 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5188
5605
|
return (o.dryRun ? 'would import ' : 'imported ') + o.importedCount + ' (' + o.eventTotal + ' events)';
|
|
5189
5606
|
}
|
|
5190
5607
|
|
|
5608
|
+
// --- portfolio landing --------------------------------------------------
|
|
5609
|
+
|
|
5610
|
+
function boot() {
|
|
5611
|
+
fetchJson('/api/portfolio').then(function (d) {
|
|
5612
|
+
if (d && d.mode === 'portfolio') { state.mode = 'portfolio'; showLanding(d); }
|
|
5613
|
+
else { enterSingle(); }
|
|
5614
|
+
}).catch(function () {
|
|
5615
|
+
// First-load bootstrap failure: the single-workspace view is the safe default.
|
|
5616
|
+
enterSingle();
|
|
5617
|
+
});
|
|
5618
|
+
}
|
|
5619
|
+
|
|
5620
|
+
// Re-render the portfolio landing (the back button). Unlike boot(), a fetch
|
|
5621
|
+
// failure here keeps the inert landing and shows an error rather than silently
|
|
5622
|
+
// dropping into single mode pointed at the first workspace.
|
|
5623
|
+
function backToPortfolio() {
|
|
5624
|
+
enterLandingChrome();
|
|
5625
|
+
fetchJson('/api/portfolio').then(function (d) {
|
|
5626
|
+
if (d && d.workspaces) renderCards(d);
|
|
5627
|
+
else portfolioError('Portfolio unavailable.');
|
|
5628
|
+
}).catch(function (err) { portfolioError('Could not load portfolio: ' + err.message); });
|
|
5629
|
+
}
|
|
5630
|
+
|
|
5631
|
+
function enterSingle() {
|
|
5632
|
+
state.mode = 'single';
|
|
5633
|
+
state.base = '/api';
|
|
5634
|
+
state.wsKey = null;
|
|
5635
|
+
state.canAct = true;
|
|
5636
|
+
document.body.classList.remove('landing');
|
|
5637
|
+
$('btn-back').style.display = 'none';
|
|
5638
|
+
updateActionBar();
|
|
5639
|
+
buildTabs();
|
|
5640
|
+
loadTab('overview');
|
|
5641
|
+
}
|
|
5642
|
+
|
|
5643
|
+
// Landing chrome: no workspace is active, so actions are disabled (and hidden
|
|
5644
|
+
// by body.landing). The disable is the safety guard; the hide is cosmetic.
|
|
5645
|
+
function enterLandingChrome() {
|
|
5646
|
+
state.wsKey = null;
|
|
5647
|
+
state.canAct = false;
|
|
5648
|
+
document.body.classList.add('landing');
|
|
5649
|
+
$('btn-back').style.display = 'none';
|
|
5650
|
+
setStatus('', false);
|
|
5651
|
+
clear($('tabs'));
|
|
5652
|
+
updateActionBar();
|
|
5653
|
+
single(true);
|
|
5654
|
+
}
|
|
5655
|
+
|
|
5656
|
+
function showLanding(d) { enterLandingChrome(); renderCards(d); }
|
|
5657
|
+
|
|
5658
|
+
function renderCards(d) {
|
|
5659
|
+
var detail = $('detail');
|
|
5660
|
+
clear(detail);
|
|
5661
|
+
var ws = d.workspaces || [];
|
|
5662
|
+
detail.appendChild(el('p', { class: 'muted', text: 'Portfolio \u2014 ' + ws.length + ' workspace(s). Click a card to open it.' }));
|
|
5663
|
+
var cards = el('div', { class: 'cards' }, []);
|
|
5664
|
+
ws.forEach(function (w) { cards.appendChild(portfolioCard(w, d.generatedAt)); });
|
|
5665
|
+
detail.appendChild(cards);
|
|
5666
|
+
}
|
|
5667
|
+
|
|
5668
|
+
function portfolioError(msg) {
|
|
5669
|
+
var detail = $('detail');
|
|
5670
|
+
clear(detail);
|
|
5671
|
+
detail.appendChild(el('p', { class: 'err', text: msg }));
|
|
5672
|
+
detail.appendChild(el('button', { text: 'Retry', onclick: backToPortfolio }));
|
|
5673
|
+
}
|
|
5674
|
+
|
|
5675
|
+
function highestRisk(approvals) {
|
|
5676
|
+
var order = ['critical', 'high', 'medium', 'low'];
|
|
5677
|
+
for (var i = 0; i < order.length; i++) {
|
|
5678
|
+
for (var j = 0; j < approvals.length; j++) {
|
|
5679
|
+
if (approvals[j].risk === order[i]) return order[i];
|
|
5680
|
+
}
|
|
5681
|
+
}
|
|
5682
|
+
return approvals.length ? approvals[0].risk : '';
|
|
5683
|
+
}
|
|
5684
|
+
|
|
5685
|
+
// Human-readable age of an ISO timestamp relative to the portfolio's
|
|
5686
|
+
// generatedAt ("now"), so a stale capture reads as "3d ago" not a raw ISO.
|
|
5687
|
+
function relAge(iso, nowIso) {
|
|
5688
|
+
if (!iso) return '(none)';
|
|
5689
|
+
var ms = Date.parse(nowIso) - Date.parse(iso);
|
|
5690
|
+
if (!isFinite(ms)) return iso;
|
|
5691
|
+
if (ms < 60000) return 'just now';
|
|
5692
|
+
var m = Math.floor(ms / 60000); if (m < 60) return m + 'm ago';
|
|
5693
|
+
var h = Math.floor(m / 60); if (h < 48) return h + 'h ago';
|
|
5694
|
+
return Math.floor(h / 24) + 'd ago';
|
|
5695
|
+
}
|
|
5696
|
+
|
|
5697
|
+
// A "run refresh" badge when a dry-run found uncaptured/changed native sessions,
|
|
5698
|
+
// an "up to date" badge when the capture is current, and nothing loud when the
|
|
5699
|
+
// staleness probe could not run (degrades to a quiet note).
|
|
5700
|
+
function stalenessBadge(st) {
|
|
5701
|
+
if (!st) return null;
|
|
5702
|
+
if (!st.checked) return el('span', { class: 'badge', text: 'freshness unknown' });
|
|
5703
|
+
if (st.unverifiableSessions > 0)
|
|
5704
|
+
return el('span', { class: 'badge danger', text: '\u26A0 ' + st.unverifiableSessions + ' unverifiable \u2014 run verify' });
|
|
5705
|
+
if (st.newSessions > 0)
|
|
5706
|
+
return el('span', { class: 'badge danger', text: '\u26A0 ' + st.newSessions + ' uncaptured \u2014 run refresh' });
|
|
5707
|
+
if (st.updatedSessions > 0)
|
|
5708
|
+
return el('span', { class: 'badge warn', text: st.updatedSessions + ' updated \u2014 run refresh' });
|
|
5709
|
+
return el('span', { class: 'badge ok', text: 'up to date' });
|
|
5710
|
+
}
|
|
5711
|
+
|
|
5712
|
+
function portfolioCard(w, generatedAt) {
|
|
5713
|
+
if (!w.initialized) {
|
|
5714
|
+
return el('div', { class: 'card pcard muted' }, [
|
|
5715
|
+
el('div', { class: 'l', text: w.label }),
|
|
5716
|
+
el('div', { class: 'f', text: w.error ? ('unreadable: ' + w.error) : 'not initialized' })
|
|
5717
|
+
]);
|
|
5718
|
+
}
|
|
5719
|
+
if (w.error) {
|
|
5720
|
+
return el('div', { class: 'card pcard' }, [
|
|
5721
|
+
el('div', { class: 'l', text: w.label }),
|
|
5722
|
+
el('div', { class: 'f' }, [el('span', { class: 'badge warn', text: 'unreadable: ' + w.error })])
|
|
5723
|
+
]);
|
|
5724
|
+
}
|
|
5725
|
+
var pend = w.pendingApprovals || [];
|
|
5726
|
+
var pendText = 'pending ' + pend.length + (pend.length ? ' (' + highestRisk(pend) + ')' : '');
|
|
5727
|
+
var now = w.latestSession ? ((w.latestSession.label || '(session)') + ' [' + w.latestSession.status + ']') : '(no live sessions)';
|
|
5728
|
+
var dec = w.latestDecision ? w.latestDecision.title : '(no decisions yet)';
|
|
5729
|
+
var newest = (w.freshness && w.freshness.newestStartedAt) ? w.freshness.newestStartedAt : null;
|
|
5730
|
+
var badge = stalenessBadge(w.staleness);
|
|
5731
|
+
return el('div', { class: 'card pcard open', onclick: function () { openWorkspace(w.key, w.label); } }, [
|
|
5732
|
+
el('div', { class: 'l' }, [
|
|
5733
|
+
el('span', { text: w.label }),
|
|
5734
|
+
badge ? el('span', { text: ' ' }) : null,
|
|
5735
|
+
badge
|
|
5736
|
+
]),
|
|
5737
|
+
el('div', { class: 'f', text: 'now: ' + now }),
|
|
5738
|
+
el('div', { class: 'f', text: 'latest: ' + dec }),
|
|
5739
|
+
el('div', { class: 'f', text: 'in-flight ' + w.inFlightCount + ' | ' + pendText + ' | suspect ' + w.suspectCount }),
|
|
5740
|
+
el('div', { class: 'f muted', text: 'sessions ' + w.sessionCount + ' | newest ' + relAge(newest, generatedAt) })
|
|
5741
|
+
]);
|
|
5742
|
+
}
|
|
5743
|
+
|
|
5744
|
+
function openWorkspace(key, label) {
|
|
5745
|
+
state.mode = 'portfolio';
|
|
5746
|
+
state.wsKey = key;
|
|
5747
|
+
state.base = '/api/ws/' + encodeURIComponent(key);
|
|
5748
|
+
state.canAct = true;
|
|
5749
|
+
document.body.classList.remove('landing');
|
|
5750
|
+
$('btn-back').style.display = '';
|
|
5751
|
+
updateActionBar();
|
|
5752
|
+
setStatus('workspace: ' + label, false);
|
|
5753
|
+
buildTabs();
|
|
5754
|
+
loadTab('overview');
|
|
5755
|
+
}
|
|
5756
|
+
|
|
5191
5757
|
// --- tabs ---------------------------------------------------------------
|
|
5192
5758
|
|
|
5193
5759
|
function buildTabs() {
|
|
@@ -5211,16 +5777,16 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5211
5777
|
if (name === 'stats') return loadStats();
|
|
5212
5778
|
if (name === 'sessions') return loadSessions();
|
|
5213
5779
|
if (name === 'tasks') return loadTasks();
|
|
5214
|
-
if (name === 'decisions') return loadMarkdown('/
|
|
5780
|
+
if (name === 'decisions') return loadMarkdown(state.base + '/decisions', 'decisions');
|
|
5215
5781
|
if (name === 'approvals') return loadApprovals();
|
|
5216
|
-
if (name === 'handoff') return loadMarkdown('/
|
|
5782
|
+
if (name === 'handoff') return loadMarkdown(state.base + '/handoff', 'handoff');
|
|
5217
5783
|
}
|
|
5218
5784
|
|
|
5219
5785
|
function fail(err) { setStatus(err.message, true); }
|
|
5220
5786
|
|
|
5221
5787
|
function loadOverview() {
|
|
5222
5788
|
single(true);
|
|
5223
|
-
fetchJson('/
|
|
5789
|
+
fetchJson(state.base + '/overview').then(function (d) {
|
|
5224
5790
|
var detail = $('detail');
|
|
5225
5791
|
if (!d || d.initialized === false) {
|
|
5226
5792
|
detail.appendChild(el('p', { class: 'muted', text: 'Workspace not initialized.' }));
|
|
@@ -5269,7 +5835,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5269
5835
|
|
|
5270
5836
|
function loadStats() {
|
|
5271
5837
|
single(true);
|
|
5272
|
-
fetchJson('/
|
|
5838
|
+
fetchJson(state.base + '/stats').then(function (d) {
|
|
5273
5839
|
var detail = $('detail');
|
|
5274
5840
|
var t = d.totals;
|
|
5275
5841
|
detail.appendChild(el('p', { text: 'Sessions: ' + t.sessionCount }));
|
|
@@ -5330,7 +5896,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5330
5896
|
|
|
5331
5897
|
function loadSessions() {
|
|
5332
5898
|
single(false);
|
|
5333
|
-
fetchJson('/
|
|
5899
|
+
fetchJson(state.base + '/sessions').then(function (d) {
|
|
5334
5900
|
var list = $('list');
|
|
5335
5901
|
var rows = (d && d.sessions) || [];
|
|
5336
5902
|
if (rows.length === 0) { list.appendChild(el('div', { class: 'row muted', text: 'no sessions' })); return; }
|
|
@@ -5349,7 +5915,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5349
5915
|
row.classList.add('active');
|
|
5350
5916
|
var detail = $('detail');
|
|
5351
5917
|
clear(detail);
|
|
5352
|
-
fetchJson('/
|
|
5918
|
+
fetchJson(state.base + '/sessions/' + encodeURIComponent(id)).then(function (d) {
|
|
5353
5919
|
var s = d.session.session;
|
|
5354
5920
|
detail.appendChild(el('h3', { text: s.label || id }));
|
|
5355
5921
|
detail.appendChild(kv([
|
|
@@ -5382,7 +5948,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5382
5948
|
|
|
5383
5949
|
function loadTasks() {
|
|
5384
5950
|
single(false);
|
|
5385
|
-
fetchJson('/
|
|
5951
|
+
fetchJson(state.base + '/tasks').then(function (d) {
|
|
5386
5952
|
var list = $('list');
|
|
5387
5953
|
var rows = (d && d.tasks) || [];
|
|
5388
5954
|
if (rows.length === 0) { list.appendChild(el('div', { class: 'row muted', text: 'no tasks' })); return; }
|
|
@@ -5401,7 +5967,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5401
5967
|
row.classList.add('active');
|
|
5402
5968
|
var detail = $('detail');
|
|
5403
5969
|
clear(detail);
|
|
5404
|
-
fetchJson('/
|
|
5970
|
+
fetchJson(state.base + '/tasks/' + encodeURIComponent(id)).then(function (d) {
|
|
5405
5971
|
detail.appendChild(el('h3', { text: (d.task && (d.task.title || d.task.label)) || id }));
|
|
5406
5972
|
detail.appendChild(el('pre', { text: JSON.stringify(d.task, null, 2) }));
|
|
5407
5973
|
if (d.body) detail.appendChild(el('pre', { text: d.body }));
|
|
@@ -5420,7 +5986,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5420
5986
|
|
|
5421
5987
|
function loadApprovals() {
|
|
5422
5988
|
single(true);
|
|
5423
|
-
fetchJson('/
|
|
5989
|
+
fetchJson(state.base + '/approvals').then(function (d) {
|
|
5424
5990
|
var detail = $('detail');
|
|
5425
5991
|
var groups = [['pending', d.pending || []], ['resolved', d.resolved || []]];
|
|
5426
5992
|
groups.forEach(function (g) {
|
|
@@ -5445,14 +6011,14 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5445
6011
|
|
|
5446
6012
|
// --- wire up ------------------------------------------------------------
|
|
5447
6013
|
|
|
5448
|
-
$('btn-
|
|
5449
|
-
$('btn-
|
|
5450
|
-
$('btn-import-
|
|
5451
|
-
$('btn-
|
|
5452
|
-
$('btn-gen-
|
|
6014
|
+
$('btn-back').addEventListener('click', function () { backToPortfolio(); });
|
|
6015
|
+
$('btn-refresh').addEventListener('click', function () { post(state.base + '/refresh', 'Refresh all'); });
|
|
6016
|
+
$('btn-import-claude').addEventListener('click', function () { post(state.base + '/import/claude-code', 'Import claude-code'); });
|
|
6017
|
+
$('btn-import-codex').addEventListener('click', function () { post(state.base + '/import/codex', 'Import codex'); });
|
|
6018
|
+
$('btn-gen-handoff').addEventListener('click', function () { post(state.base + '/handoff/generate', 'Regenerate handoff'); });
|
|
6019
|
+
$('btn-gen-decisions').addEventListener('click', function () { post(state.base + '/decisions/generate', 'Regenerate decisions'); });
|
|
5453
6020
|
|
|
5454
|
-
|
|
5455
|
-
loadTab('overview');
|
|
6021
|
+
boot();
|
|
5456
6022
|
})();
|
|
5457
6023
|
</script>
|
|
5458
6024
|
</body>
|
|
@@ -5467,6 +6033,8 @@ var HttpError = class extends Error {
|
|
|
5467
6033
|
status;
|
|
5468
6034
|
};
|
|
5469
6035
|
var MAX_BODY_BYTES = 64 * 1024;
|
|
6036
|
+
var API_PREFIX = "/api/";
|
|
6037
|
+
var WS_PREFIX = "/api/ws/";
|
|
5470
6038
|
function startViewServer(opts) {
|
|
5471
6039
|
const { port, host = "127.0.0.1", deps } = opts;
|
|
5472
6040
|
let actionQueue = Promise.resolve();
|
|
@@ -5480,7 +6048,7 @@ function startViewServer(opts) {
|
|
|
5480
6048
|
};
|
|
5481
6049
|
let boundPort = port;
|
|
5482
6050
|
const getPort = () => boundPort;
|
|
5483
|
-
return new Promise((
|
|
6051
|
+
return new Promise((resolve7, reject) => {
|
|
5484
6052
|
const server = createServer((req, res) => {
|
|
5485
6053
|
handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
|
|
5486
6054
|
sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
|
|
@@ -5491,7 +6059,7 @@ function startViewServer(opts) {
|
|
|
5491
6059
|
const address = server.address();
|
|
5492
6060
|
boundPort = isAddressInfo(address) ? address.port : port;
|
|
5493
6061
|
server.off("error", reject);
|
|
5494
|
-
|
|
6062
|
+
resolve7({
|
|
5495
6063
|
url: `http://${host}:${boundPort}`,
|
|
5496
6064
|
port: boundPort,
|
|
5497
6065
|
close: () => closeServer(server)
|
|
@@ -5503,8 +6071,8 @@ function isAddressInfo(value) {
|
|
|
5503
6071
|
return value !== null && typeof value === "object";
|
|
5504
6072
|
}
|
|
5505
6073
|
function closeServer(server) {
|
|
5506
|
-
return new Promise((
|
|
5507
|
-
server.close(() =>
|
|
6074
|
+
return new Promise((resolve7) => {
|
|
6075
|
+
server.close(() => resolve7());
|
|
5508
6076
|
server.closeAllConnections();
|
|
5509
6077
|
});
|
|
5510
6078
|
}
|
|
@@ -5536,90 +6104,204 @@ async function handleGet(res, pathname, deps) {
|
|
|
5536
6104
|
sendHtml(res, VIEW_HTML);
|
|
5537
6105
|
return;
|
|
5538
6106
|
}
|
|
5539
|
-
if (pathname === "/api/
|
|
5540
|
-
sendJson(res, 200, await
|
|
6107
|
+
if (pathname === "/api/portfolio") {
|
|
6108
|
+
sendJson(res, 200, await portfolio(deps));
|
|
5541
6109
|
return;
|
|
5542
6110
|
}
|
|
5543
|
-
|
|
5544
|
-
|
|
6111
|
+
const scoped = matchWsRoute(pathname);
|
|
6112
|
+
if (scoped !== null) {
|
|
6113
|
+
const ws = findWorkspace(deps, scoped.key);
|
|
6114
|
+
if (ws === null) {
|
|
6115
|
+
sendError(res, 404, "Unknown workspace");
|
|
6116
|
+
return;
|
|
6117
|
+
}
|
|
6118
|
+
if (!await handleWorkspaceGet(res, scoped.sub, ws, deps.nowProvider)) {
|
|
6119
|
+
sendError(res, 404, "Not found");
|
|
6120
|
+
}
|
|
5545
6121
|
return;
|
|
5546
6122
|
}
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
6123
|
+
if (pathname.startsWith(API_PREFIX)) {
|
|
6124
|
+
const sub = pathname.slice(API_PREFIX.length);
|
|
6125
|
+
if (!await handleWorkspaceGet(res, sub, primaryWorkspace(deps), deps.nowProvider)) {
|
|
6126
|
+
sendError(res, 404, "Not found");
|
|
6127
|
+
}
|
|
5550
6128
|
return;
|
|
5551
6129
|
}
|
|
5552
|
-
|
|
5553
|
-
|
|
6130
|
+
sendError(res, 404, "Not found");
|
|
6131
|
+
}
|
|
6132
|
+
async function handlePost(res, pathname, body, deps, runExclusive) {
|
|
6133
|
+
const scoped = matchWsRoute(pathname);
|
|
6134
|
+
if (scoped !== null) {
|
|
6135
|
+
const ws = findWorkspace(deps, scoped.key);
|
|
6136
|
+
if (ws === null) {
|
|
6137
|
+
sendError(res, 404, "Unknown workspace");
|
|
6138
|
+
return;
|
|
6139
|
+
}
|
|
6140
|
+
if (!await handleWorkspacePost(res, scoped.sub, ws, body, deps, runExclusive)) {
|
|
6141
|
+
sendError(res, 404, "Not found");
|
|
6142
|
+
}
|
|
5554
6143
|
return;
|
|
5555
6144
|
}
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
6145
|
+
if (pathname.startsWith(API_PREFIX)) {
|
|
6146
|
+
const sub = pathname.slice(API_PREFIX.length);
|
|
6147
|
+
if (!await handleWorkspacePost(res, sub, primaryWorkspace(deps), body, deps, runExclusive)) {
|
|
6148
|
+
sendError(res, 404, "Not found");
|
|
6149
|
+
}
|
|
5559
6150
|
return;
|
|
5560
6151
|
}
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
6152
|
+
sendError(res, 404, "Not found");
|
|
6153
|
+
}
|
|
6154
|
+
async function handleWorkspaceGet(res, sub, ws, nowProvider) {
|
|
6155
|
+
if (sub === "overview") {
|
|
6156
|
+
sendJson(res, 200, await overview(ws, nowProvider));
|
|
6157
|
+
return true;
|
|
5564
6158
|
}
|
|
5565
|
-
if (
|
|
5566
|
-
sendJson(res, 200, await
|
|
5567
|
-
return;
|
|
6159
|
+
if (sub === "sessions") {
|
|
6160
|
+
sendJson(res, 200, await sessionsList(ws, nowProvider));
|
|
6161
|
+
return true;
|
|
5568
6162
|
}
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
6163
|
+
const sessionId = matchId(sub, "sessions/");
|
|
6164
|
+
if (sessionId !== null) {
|
|
6165
|
+
sendJson(res, 200, await sessionDetail(ws, sessionId));
|
|
6166
|
+
return true;
|
|
5572
6167
|
}
|
|
5573
|
-
if (
|
|
5574
|
-
sendJson(res, 200, await
|
|
5575
|
-
return;
|
|
6168
|
+
if (sub === "tasks") {
|
|
6169
|
+
sendJson(res, 200, await tasksList(ws));
|
|
6170
|
+
return true;
|
|
5576
6171
|
}
|
|
5577
|
-
|
|
6172
|
+
const taskId = matchId(sub, "tasks/");
|
|
6173
|
+
if (taskId !== null) {
|
|
6174
|
+
sendJson(res, 200, await taskDetail(ws, taskId));
|
|
6175
|
+
return true;
|
|
6176
|
+
}
|
|
6177
|
+
if (sub === "decisions") {
|
|
6178
|
+
sendJson(res, 200, await decisionsView(ws, nowProvider));
|
|
6179
|
+
return true;
|
|
6180
|
+
}
|
|
6181
|
+
if (sub === "approvals") {
|
|
6182
|
+
sendJson(res, 200, await approvalsView(ws, nowProvider));
|
|
6183
|
+
return true;
|
|
6184
|
+
}
|
|
6185
|
+
if (sub === "handoff") {
|
|
6186
|
+
sendJson(res, 200, await handoffView(ws, nowProvider));
|
|
6187
|
+
return true;
|
|
6188
|
+
}
|
|
6189
|
+
if (sub === "stats") {
|
|
6190
|
+
sendJson(res, 200, await computeWorkStats2({ paths: ws.paths, now: nowProvider() }));
|
|
6191
|
+
return true;
|
|
6192
|
+
}
|
|
6193
|
+
return false;
|
|
5578
6194
|
}
|
|
5579
|
-
async function
|
|
6195
|
+
async function handleWorkspacePost(res, sub, ws, body, deps, runExclusive) {
|
|
5580
6196
|
const nowIso = deps.nowProvider().toISOString();
|
|
5581
6197
|
const actionOptions = readActionOptions(body);
|
|
5582
|
-
if (
|
|
6198
|
+
if (sub === "refresh") {
|
|
5583
6199
|
const result = await runExclusive(
|
|
5584
|
-
() => refreshAll({ options: actionOptions, ctx:
|
|
6200
|
+
() => refreshAll({ options: actionOptions, ctx: ws.importCtx, paths: ws.paths, nowIso })
|
|
5585
6201
|
);
|
|
5586
6202
|
sendJson(res, 200, result);
|
|
5587
|
-
return;
|
|
6203
|
+
return true;
|
|
5588
6204
|
}
|
|
5589
|
-
if (
|
|
5590
|
-
sendJson(res, 200, await runExclusive(() => importClaudeCode(actionOptions,
|
|
5591
|
-
return;
|
|
6205
|
+
if (sub === "import/claude-code") {
|
|
6206
|
+
sendJson(res, 200, await runExclusive(() => importClaudeCode(actionOptions, ws.importCtx)));
|
|
6207
|
+
return true;
|
|
5592
6208
|
}
|
|
5593
|
-
if (
|
|
5594
|
-
sendJson(res, 200, await runExclusive(() => importCodex(actionOptions,
|
|
5595
|
-
return;
|
|
6209
|
+
if (sub === "import/codex") {
|
|
6210
|
+
sendJson(res, 200, await runExclusive(() => importCodex(actionOptions, ws.importCtx)));
|
|
6211
|
+
return true;
|
|
5596
6212
|
}
|
|
5597
|
-
if (
|
|
5598
|
-
sendJson(res, 200, await runExclusive(() => regenerateHandoff(
|
|
5599
|
-
return;
|
|
6213
|
+
if (sub === "handoff/generate") {
|
|
6214
|
+
sendJson(res, 200, await runExclusive(() => regenerateHandoff(ws.paths, nowIso)));
|
|
6215
|
+
return true;
|
|
5600
6216
|
}
|
|
5601
|
-
if (
|
|
5602
|
-
sendJson(res, 200, await runExclusive(() => regenerateDecisions(
|
|
5603
|
-
return;
|
|
6217
|
+
if (sub === "decisions/generate") {
|
|
6218
|
+
sendJson(res, 200, await runExclusive(() => regenerateDecisions(ws.paths, nowIso)));
|
|
6219
|
+
return true;
|
|
5604
6220
|
}
|
|
5605
|
-
|
|
6221
|
+
return false;
|
|
6222
|
+
}
|
|
6223
|
+
function primaryWorkspace(deps) {
|
|
6224
|
+
const first = deps.workspaces[0];
|
|
6225
|
+
if (first === void 0) throw new HttpError(500, "No workspace configured");
|
|
6226
|
+
return first;
|
|
6227
|
+
}
|
|
6228
|
+
function findWorkspace(deps, key) {
|
|
6229
|
+
return deps.workspaces.find((w) => w.key === key) ?? null;
|
|
5606
6230
|
}
|
|
5607
|
-
|
|
6231
|
+
function matchWsRoute(pathname) {
|
|
6232
|
+
if (!pathname.startsWith(WS_PREFIX)) return null;
|
|
6233
|
+
const rest = pathname.slice(WS_PREFIX.length);
|
|
6234
|
+
const slash = rest.indexOf("/");
|
|
6235
|
+
if (slash <= 0) return null;
|
|
6236
|
+
const sub = rest.slice(slash + 1);
|
|
6237
|
+
if (sub.length === 0) return null;
|
|
6238
|
+
let key;
|
|
6239
|
+
try {
|
|
6240
|
+
key = decodeURIComponent(rest.slice(0, slash));
|
|
6241
|
+
} catch {
|
|
6242
|
+
return null;
|
|
6243
|
+
}
|
|
6244
|
+
if (key.length === 0 || key.includes("/") || key.includes("\0")) return null;
|
|
6245
|
+
return { key, sub };
|
|
6246
|
+
}
|
|
6247
|
+
async function portfolio(deps) {
|
|
6248
|
+
const nowIso = deps.nowProvider().toISOString();
|
|
6249
|
+
const workspaces = await Promise.all(deps.workspaces.map((ws) => portfolioCard(ws, nowIso)));
|
|
6250
|
+
for (let i = 0; i < deps.workspaces.length; i++) {
|
|
6251
|
+
const card = workspaces[i];
|
|
6252
|
+
const ws = deps.workspaces[i];
|
|
6253
|
+
if (ws !== void 0 && card !== void 0 && card.initialized === true && card.error === void 0) {
|
|
6254
|
+
card.staleness = await captureStaleness(ws, nowIso);
|
|
6255
|
+
}
|
|
6256
|
+
}
|
|
6257
|
+
return { mode: deps.mode, generatedAt: nowIso, workspaces };
|
|
6258
|
+
}
|
|
6259
|
+
async function portfolioCard(ws, nowIso) {
|
|
6260
|
+
const base = { key: ws.key, label: ws.label, repoRoot: ws.repoRoot };
|
|
6261
|
+
if (!ws.initialized) {
|
|
6262
|
+
return ws.manifestError !== void 0 ? { ...base, initialized: false, error: ws.manifestError } : { ...base, initialized: false };
|
|
6263
|
+
}
|
|
6264
|
+
try {
|
|
6265
|
+
const s = await summarizeOrientation({ paths: ws.paths, nowIso });
|
|
6266
|
+
return {
|
|
6267
|
+
...base,
|
|
6268
|
+
initialized: true,
|
|
6269
|
+
sessionCount: s.sessionCount,
|
|
6270
|
+
suspectCount: s.suspects.length,
|
|
6271
|
+
inFlightCount: s.inFlightTasks.length,
|
|
6272
|
+
pendingApprovals: s.pendingApprovals.map((a) => ({
|
|
6273
|
+
risk: a.risk,
|
|
6274
|
+
kind: a.kind,
|
|
6275
|
+
expired: a.expired
|
|
6276
|
+
})),
|
|
6277
|
+
latestDecision: s.latestDecision !== null ? { title: s.latestDecision.title } : null,
|
|
6278
|
+
latestSession: s.latestSession !== null ? { label: s.latestSession.label, status: s.latestSession.status } : null,
|
|
6279
|
+
freshness: { newestStartedAt: s.freshness.newestStartedAt, bySource: s.freshness.bySource }
|
|
6280
|
+
};
|
|
6281
|
+
} catch (error) {
|
|
6282
|
+
return { ...base, initialized: true, error: pathlessMessage(error) };
|
|
6283
|
+
}
|
|
6284
|
+
}
|
|
6285
|
+
async function captureStaleness(ws, nowIso) {
|
|
6286
|
+
const probe = await probeStaleness({ ctx: ws.importCtx, paths: ws.paths, nowIso });
|
|
6287
|
+
return probe === null ? { checked: false } : { checked: true, ...probe };
|
|
6288
|
+
}
|
|
6289
|
+
async function overview(ws, nowProvider) {
|
|
5608
6290
|
let manifest;
|
|
5609
6291
|
try {
|
|
5610
|
-
manifest = await
|
|
6292
|
+
manifest = await readManifest9(ws.paths);
|
|
5611
6293
|
} catch (error) {
|
|
5612
|
-
if (
|
|
5613
|
-
return { initialized: false, repoRoot:
|
|
6294
|
+
if (findErrorCode15(error, "ENOENT")) {
|
|
6295
|
+
return { initialized: false, repoRoot: ws.repoRoot };
|
|
5614
6296
|
}
|
|
5615
6297
|
throw error;
|
|
5616
6298
|
}
|
|
5617
|
-
const nowIso =
|
|
5618
|
-
const handoff = await renderHandoff3({ paths:
|
|
5619
|
-
const approvals = await enumerateApprovals2(
|
|
6299
|
+
const nowIso = nowProvider().toISOString();
|
|
6300
|
+
const handoff = await renderHandoff3({ paths: ws.paths, nowIso });
|
|
6301
|
+
const approvals = await enumerateApprovals2(ws.paths);
|
|
5620
6302
|
return {
|
|
5621
6303
|
initialized: true,
|
|
5622
|
-
repoRoot:
|
|
6304
|
+
repoRoot: ws.repoRoot,
|
|
5623
6305
|
workspace: {
|
|
5624
6306
|
id: manifest.workspace.id,
|
|
5625
6307
|
name: manifest.workspace.name,
|
|
@@ -5637,8 +6319,8 @@ async function overview(deps) {
|
|
|
5637
6319
|
generatedAt: nowIso
|
|
5638
6320
|
};
|
|
5639
6321
|
}
|
|
5640
|
-
async function sessionsList(
|
|
5641
|
-
const entries = await loadSessionEntries3(
|
|
6322
|
+
async function sessionsList(ws, nowProvider) {
|
|
6323
|
+
const entries = await loadSessionEntries3(ws.paths, { now: nowProvider() });
|
|
5642
6324
|
const sessions = entries.map((entry) => ({
|
|
5643
6325
|
sessionId: entry.sessionId,
|
|
5644
6326
|
label: entry.session.session.label ?? null,
|
|
@@ -5653,10 +6335,10 @@ async function sessionsList(deps) {
|
|
|
5653
6335
|
})).reverse();
|
|
5654
6336
|
return { sessions };
|
|
5655
6337
|
}
|
|
5656
|
-
async function sessionDetail(
|
|
6338
|
+
async function sessionDetail(ws, sessionId) {
|
|
5657
6339
|
let session;
|
|
5658
6340
|
try {
|
|
5659
|
-
session = await readSessionYaml3(
|
|
6341
|
+
session = await readSessionYaml3(ws.paths, sessionId);
|
|
5660
6342
|
} catch (error) {
|
|
5661
6343
|
if (error instanceof Error && error.message === "YAML file not found") {
|
|
5662
6344
|
throw new HttpError(404, "Session not found");
|
|
@@ -5664,19 +6346,19 @@ async function sessionDetail(deps, sessionId) {
|
|
|
5664
6346
|
throw error;
|
|
5665
6347
|
}
|
|
5666
6348
|
try {
|
|
5667
|
-
const events = await readAllEvents2(
|
|
6349
|
+
const events = await readAllEvents2(join10(ws.paths.sessions, sessionId));
|
|
5668
6350
|
return { session, events };
|
|
5669
6351
|
} catch {
|
|
5670
6352
|
return { session, events: [], degraded: true };
|
|
5671
6353
|
}
|
|
5672
6354
|
}
|
|
5673
|
-
async function tasksList(
|
|
5674
|
-
const entries = await loadTaskEntries2(
|
|
6355
|
+
async function tasksList(ws) {
|
|
6356
|
+
const entries = await loadTaskEntries2(ws.paths);
|
|
5675
6357
|
return { tasks: entries.map((entry) => entry.task).reverse() };
|
|
5676
6358
|
}
|
|
5677
|
-
async function taskDetail(
|
|
6359
|
+
async function taskDetail(ws, taskId) {
|
|
5678
6360
|
try {
|
|
5679
|
-
const doc = await readTaskFile2(
|
|
6361
|
+
const doc = await readTaskFile2(ws.paths, taskId);
|
|
5680
6362
|
return { task: doc.task, body: doc.body };
|
|
5681
6363
|
} catch (error) {
|
|
5682
6364
|
if (error instanceof Error && error.message === "Task file not found") {
|
|
@@ -5685,22 +6367,22 @@ async function taskDetail(deps, taskId) {
|
|
|
5685
6367
|
throw error;
|
|
5686
6368
|
}
|
|
5687
6369
|
}
|
|
5688
|
-
async function decisionsView(
|
|
5689
|
-
const fromDisk = await readMarkdownFile4(
|
|
6370
|
+
async function decisionsView(ws, nowProvider) {
|
|
6371
|
+
const fromDisk = await readMarkdownFile4(ws.paths.files.decisions);
|
|
5690
6372
|
if (fromDisk !== null) {
|
|
5691
6373
|
return { body: fromDisk, fromDisk: true };
|
|
5692
6374
|
}
|
|
5693
|
-
const nowIso =
|
|
5694
|
-
const result = await renderDecisions3({ paths:
|
|
6375
|
+
const nowIso = nowProvider().toISOString();
|
|
6376
|
+
const result = await renderDecisions3({ paths: ws.paths, nowIso });
|
|
5695
6377
|
return { body: result.body, decisionCount: result.decisionCount, fromDisk: false };
|
|
5696
6378
|
}
|
|
5697
|
-
async function approvalsView(
|
|
5698
|
-
const now =
|
|
5699
|
-
const ids = await enumerateApprovals2(
|
|
6379
|
+
async function approvalsView(ws, nowProvider) {
|
|
6380
|
+
const now = nowProvider();
|
|
6381
|
+
const ids = await enumerateApprovals2(ws.paths);
|
|
5700
6382
|
const toViews = async (list) => {
|
|
5701
6383
|
const views = [];
|
|
5702
6384
|
for (const id of list) {
|
|
5703
|
-
const loaded = await loadApproval2(
|
|
6385
|
+
const loaded = await loadApproval2(ws.paths, id);
|
|
5704
6386
|
if (loaded === null) continue;
|
|
5705
6387
|
views.push({ id, expired: isLazyExpired2(loaded.approval, now), approval: loaded.approval });
|
|
5706
6388
|
}
|
|
@@ -5708,13 +6390,13 @@ async function approvalsView(deps) {
|
|
|
5708
6390
|
};
|
|
5709
6391
|
return { pending: await toViews(ids.pending), resolved: await toViews(ids.resolved) };
|
|
5710
6392
|
}
|
|
5711
|
-
async function handoffView(
|
|
5712
|
-
const fromDisk = await readMarkdownFile4(
|
|
6393
|
+
async function handoffView(ws, nowProvider) {
|
|
6394
|
+
const fromDisk = await readMarkdownFile4(ws.paths.files.handoff);
|
|
5713
6395
|
if (fromDisk !== null) {
|
|
5714
6396
|
return { body: fromDisk, fromDisk: true };
|
|
5715
6397
|
}
|
|
5716
|
-
const nowIso =
|
|
5717
|
-
const result = await renderHandoff3({ paths:
|
|
6398
|
+
const nowIso = nowProvider().toISOString();
|
|
6399
|
+
const result = await renderHandoff3({ paths: ws.paths, nowIso });
|
|
5718
6400
|
return { body: result.body, fromDisk: false };
|
|
5719
6401
|
}
|
|
5720
6402
|
function readActionOptions(body) {
|
|
@@ -5808,8 +6490,18 @@ function parsePort(value) {
|
|
|
5808
6490
|
}
|
|
5809
6491
|
return port;
|
|
5810
6492
|
}
|
|
6493
|
+
function collectPath3(value, previous = []) {
|
|
6494
|
+
return [...previous, value];
|
|
6495
|
+
}
|
|
5811
6496
|
function registerViewCommand(program) {
|
|
5812
|
-
program.command("view").description("Open a local web UI to browse provenance and run imports (localhost only)").option("--port <number>", "Port to listen on (default 4319)", parsePort).option("--no-open", "Do not open the browser automatically").option(
|
|
6497
|
+
program.command("view").description("Open a local web UI to browse provenance and run imports (localhost only)").option("--port <number>", "Port to listen on (default 4319)", parsePort).option("--no-open", "Do not open the browser automatically").option(
|
|
6498
|
+
"--portfolio",
|
|
6499
|
+
"Serve every workspace listed in ~/.basou/portfolio.yaml (cross-repo orientation)"
|
|
6500
|
+
).option(
|
|
6501
|
+
"--workspace <path>",
|
|
6502
|
+
"Workspace repo path to include (repeatable; implies portfolio mode; resolved against the cwd)",
|
|
6503
|
+
collectPath3
|
|
6504
|
+
).option("--check", "Run the portfolio safety preflight and exit (no server)").option("--skip-safety-check", "Skip the portfolio safety preflight on start (not recommended)").option("-v, --verbose", "Show error causes").action(async (options) => {
|
|
5813
6505
|
await runView(options);
|
|
5814
6506
|
});
|
|
5815
6507
|
}
|
|
@@ -5823,23 +6515,37 @@ async function runView(options, ctx = {}) {
|
|
|
5823
6515
|
}
|
|
5824
6516
|
async function doRunView(options, ctx) {
|
|
5825
6517
|
const cwd = ctx.cwd ?? process.cwd();
|
|
5826
|
-
const
|
|
5827
|
-
const
|
|
5828
|
-
await
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
6518
|
+
const workspaceFlags = options.workspace ?? [];
|
|
6519
|
+
const isPortfolio = workspaceFlags.length > 0 || options.portfolio === true;
|
|
6520
|
+
const deps = isPortfolio ? await buildPortfolioDeps(workspaceFlags, ctx, cwd) : await buildSingleDeps(ctx, cwd);
|
|
6521
|
+
if (options.check === true) {
|
|
6522
|
+
const result = await checkPortfolioSafety(deps.workspaces);
|
|
6523
|
+
for (const line of formatSafetyReport(result)) console.log(line);
|
|
6524
|
+
if (result.findings.length > 0) process.exitCode = 1;
|
|
6525
|
+
return;
|
|
6526
|
+
}
|
|
6527
|
+
if (deps.mode === "portfolio" && options.skipSafetyCheck !== true) {
|
|
6528
|
+
const result = await checkPortfolioSafety(deps.workspaces);
|
|
6529
|
+
const blocking = result.findings.filter((f) => f.kind === "footprint" || f.kind === "overlap");
|
|
6530
|
+
if (blocking.length > 0) {
|
|
6531
|
+
for (const line of formatSafetyReport(result)) console.error(line);
|
|
6532
|
+
throw new Error(
|
|
6533
|
+
"Portfolio safety preflight failed (see findings above). Fix the monitored repos, or re-run with --skip-safety-check to override."
|
|
6534
|
+
);
|
|
6535
|
+
}
|
|
6536
|
+
if (result.findings.length > 0) {
|
|
6537
|
+
console.error(
|
|
6538
|
+
`Portfolio safety: ${result.findings.length} unverifiable item(s) \u2014 the read-only view will still open; run 'basou view --check' for detail.`
|
|
6539
|
+
);
|
|
6540
|
+
}
|
|
6541
|
+
}
|
|
5839
6542
|
const port = options.port ?? DEFAULT_PORT;
|
|
5840
6543
|
const handle = await startListening(port, deps);
|
|
5841
6544
|
try {
|
|
5842
6545
|
console.log(`basou view running at ${handle.url}`);
|
|
6546
|
+
if (deps.mode === "portfolio") {
|
|
6547
|
+
console.log(`Portfolio mode: ${deps.workspaces.length} workspace(s).`);
|
|
6548
|
+
}
|
|
5843
6549
|
console.log(
|
|
5844
6550
|
"Localhost only, no authentication. Do not expose this port beyond your machine. Press Ctrl+C to stop."
|
|
5845
6551
|
);
|
|
@@ -5852,11 +6558,69 @@ async function doRunView(options, ctx) {
|
|
|
5852
6558
|
await handle.close();
|
|
5853
6559
|
}
|
|
5854
6560
|
}
|
|
6561
|
+
async function buildSingleDeps(ctx, cwd) {
|
|
6562
|
+
const repositoryRoot = await resolveRepositoryRootForView(cwd);
|
|
6563
|
+
const paths = basouPaths16(repositoryRoot);
|
|
6564
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
6565
|
+
const entry = await buildWorkspaceEntry(repositoryRoot, ctx);
|
|
6566
|
+
return { workspaces: [entry], mode: "single", nowProvider: nowProviderOf(ctx) };
|
|
6567
|
+
}
|
|
6568
|
+
async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
|
|
6569
|
+
const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve6(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
|
|
6570
|
+
const entries = [];
|
|
6571
|
+
const seenPath = /* @__PURE__ */ new Set();
|
|
6572
|
+
const seenKey = /* @__PURE__ */ new Set();
|
|
6573
|
+
for (const spec of specs) {
|
|
6574
|
+
const repoRoot = resolve6(spec.path);
|
|
6575
|
+
if (seenPath.has(repoRoot)) continue;
|
|
6576
|
+
seenPath.add(repoRoot);
|
|
6577
|
+
const entry = await buildWorkspaceEntry(repoRoot, ctx, spec.label);
|
|
6578
|
+
let key = entry.key;
|
|
6579
|
+
for (let n = 1; seenKey.has(key); n++) key = `${entry.key}-${n}`;
|
|
6580
|
+
seenKey.add(key);
|
|
6581
|
+
entries.push({ ...entry, key });
|
|
6582
|
+
}
|
|
6583
|
+
if (entries.length === 0) throw new Error("No workspaces to show.");
|
|
6584
|
+
return { workspaces: entries, mode: "portfolio", nowProvider: nowProviderOf(ctx) };
|
|
6585
|
+
}
|
|
6586
|
+
async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
|
|
6587
|
+
const paths = basouPaths16(repoRoot);
|
|
6588
|
+
const importCtx = {
|
|
6589
|
+
cwd: repoRoot,
|
|
6590
|
+
...ctx.claudeProjectsDir !== void 0 ? { claudeProjectsDir: ctx.claudeProjectsDir } : {},
|
|
6591
|
+
...ctx.codexSessionsDir !== void 0 ? { codexSessionsDir: ctx.codexSessionsDir } : {}
|
|
6592
|
+
};
|
|
6593
|
+
try {
|
|
6594
|
+
const manifest = await readManifest10(paths);
|
|
6595
|
+
return {
|
|
6596
|
+
key: manifest.workspace.id,
|
|
6597
|
+
label: labelOverride ?? manifest.workspace.name,
|
|
6598
|
+
paths,
|
|
6599
|
+
repoRoot,
|
|
6600
|
+
importCtx,
|
|
6601
|
+
initialized: true
|
|
6602
|
+
};
|
|
6603
|
+
} catch (error) {
|
|
6604
|
+
const notFound = error instanceof Error && error.message === "YAML file not found";
|
|
6605
|
+
return {
|
|
6606
|
+
key: `ws-${createHash("sha1").update(repoRoot).digest("hex").slice(0, 12)}`,
|
|
6607
|
+
label: labelOverride ?? basename4(repoRoot),
|
|
6608
|
+
paths,
|
|
6609
|
+
repoRoot,
|
|
6610
|
+
importCtx,
|
|
6611
|
+
initialized: false,
|
|
6612
|
+
...notFound ? {} : { manifestError: "manifest unreadable or invalid" }
|
|
6613
|
+
};
|
|
6614
|
+
}
|
|
6615
|
+
}
|
|
6616
|
+
function nowProviderOf(ctx) {
|
|
6617
|
+
return ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
|
|
6618
|
+
}
|
|
5855
6619
|
async function startListening(port, deps) {
|
|
5856
6620
|
try {
|
|
5857
6621
|
return await startViewServer({ port, deps });
|
|
5858
6622
|
} catch (error) {
|
|
5859
|
-
if (
|
|
6623
|
+
if (findErrorCode16(error, "EADDRINUSE")) {
|
|
5860
6624
|
throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
|
|
5861
6625
|
cause: error
|
|
5862
6626
|
});
|
|
@@ -5879,7 +6643,7 @@ function openInBrowser(url, override) {
|
|
|
5879
6643
|
}
|
|
5880
6644
|
}
|
|
5881
6645
|
function waitForShutdown(signal) {
|
|
5882
|
-
return new Promise((
|
|
6646
|
+
return new Promise((resolve7) => {
|
|
5883
6647
|
const cleanup = () => {
|
|
5884
6648
|
process.off("SIGINT", onSignal);
|
|
5885
6649
|
process.off("SIGTERM", onSignal);
|
|
@@ -5887,18 +6651,18 @@ function waitForShutdown(signal) {
|
|
|
5887
6651
|
};
|
|
5888
6652
|
const onSignal = () => {
|
|
5889
6653
|
cleanup();
|
|
5890
|
-
|
|
6654
|
+
resolve7();
|
|
5891
6655
|
};
|
|
5892
6656
|
const onAbort = () => {
|
|
5893
6657
|
cleanup();
|
|
5894
|
-
|
|
6658
|
+
resolve7();
|
|
5895
6659
|
};
|
|
5896
6660
|
process.on("SIGINT", onSignal);
|
|
5897
6661
|
process.on("SIGTERM", onSignal);
|
|
5898
6662
|
if (signal !== void 0) {
|
|
5899
6663
|
if (signal.aborted) {
|
|
5900
6664
|
cleanup();
|
|
5901
|
-
|
|
6665
|
+
resolve7();
|
|
5902
6666
|
return;
|
|
5903
6667
|
}
|
|
5904
6668
|
signal.addEventListener("abort", onAbort);
|
|
@@ -5907,7 +6671,7 @@ function waitForShutdown(signal) {
|
|
|
5907
6671
|
}
|
|
5908
6672
|
async function resolveRepositoryRootForView(cwd) {
|
|
5909
6673
|
try {
|
|
5910
|
-
return await
|
|
6674
|
+
return await resolveRepositoryRoot15(cwd);
|
|
5911
6675
|
} catch (error) {
|
|
5912
6676
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
5913
6677
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {
|
|
@@ -5917,11 +6681,11 @@ async function resolveRepositoryRootForView(cwd) {
|
|
|
5917
6681
|
throw error;
|
|
5918
6682
|
}
|
|
5919
6683
|
}
|
|
5920
|
-
async function
|
|
6684
|
+
async function assertWorkspaceInitialized13(basouRoot) {
|
|
5921
6685
|
try {
|
|
5922
|
-
await
|
|
6686
|
+
await assertBasouRootSafe16(basouRoot);
|
|
5923
6687
|
} catch (error) {
|
|
5924
|
-
if (
|
|
6688
|
+
if (findErrorCode16(error, "ENOENT")) {
|
|
5925
6689
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
5926
6690
|
}
|
|
5927
6691
|
throw error;
|
|
@@ -5951,6 +6715,7 @@ function buildProgram() {
|
|
|
5951
6715
|
registerHandoffCommand(program);
|
|
5952
6716
|
registerDecisionsCommand(program);
|
|
5953
6717
|
registerReportCommand(program);
|
|
6718
|
+
registerOrientCommand(program);
|
|
5954
6719
|
return program;
|
|
5955
6720
|
}
|
|
5956
6721
|
export {
|