@basou/cli 0.10.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 +1092 -256
- package/dist/index.js.map +1 -1
- package/dist/program.js +1092 -256
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/index.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(program2) {
|
|
|
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(program2) {
|
|
2222
|
+
program2.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(program2) {
|
|
|
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(program2) {
|
|
|
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,97 @@ function printRefreshSummary(result) {
|
|
|
2387
2648
|
} else {
|
|
2388
2649
|
console.log(`decisions: skipped (${result.decisions.reason})`);
|
|
2389
2650
|
}
|
|
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})`);
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
async function assertWorkspaceInitialized7(basouRoot) {
|
|
2660
|
+
try {
|
|
2661
|
+
await assertBasouRootSafe8(basouRoot);
|
|
2662
|
+
} catch (error) {
|
|
2663
|
+
if (findErrorCode8(error, "ENOENT")) {
|
|
2664
|
+
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
2665
|
+
}
|
|
2666
|
+
throw error;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
// src/commands/report.ts
|
|
2671
|
+
import { isAbsolute as isAbsolute2, resolve as resolve4 } from "path";
|
|
2672
|
+
import {
|
|
2673
|
+
assertBasouRootSafe as assertBasouRootSafe9,
|
|
2674
|
+
basouPaths as basouPaths9,
|
|
2675
|
+
findErrorCode as findErrorCode9,
|
|
2676
|
+
renderReport,
|
|
2677
|
+
resolveRepositoryRoot as resolveRepositoryRoot8,
|
|
2678
|
+
writeMarkdownFile as writeMarkdownFile5
|
|
2679
|
+
} from "@basou/core";
|
|
2680
|
+
function registerReportCommand(program2) {
|
|
2681
|
+
const report = program2.command("report").description(
|
|
2682
|
+
"Generate a work report \u2014 a shareable export explaining the work in this workspace"
|
|
2683
|
+
);
|
|
2684
|
+
report.command("generate").description("Generate a work report from the current workspace state").option("--out <path>", "Write the markdown report to a file instead of stdout").option("--json", "Emit the structured report data as JSON to stdout").option("--title <text>", "Subject line shown in the report header").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
2685
|
+
await runReportGenerate(opts);
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2688
|
+
async function runReportGenerate(options, ctx = {}) {
|
|
2689
|
+
try {
|
|
2690
|
+
await doRunReportGenerate(options, ctx);
|
|
2691
|
+
} catch (error) {
|
|
2692
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
2693
|
+
process.exitCode = 1;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
async function doRunReportGenerate(options, ctx) {
|
|
2697
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
2698
|
+
const repositoryRoot = await resolveRepositoryRootForReport(cwd);
|
|
2699
|
+
const paths = basouPaths9(repositoryRoot);
|
|
2700
|
+
await assertWorkspaceInitialized8(paths.root);
|
|
2701
|
+
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2702
|
+
const result = await renderReport({
|
|
2703
|
+
paths,
|
|
2704
|
+
nowIso,
|
|
2705
|
+
...options.title !== void 0 ? { title: options.title } : {},
|
|
2706
|
+
onWarning: (w, sid) => printReplayWarning(w, sid),
|
|
2707
|
+
onSessionSkip: (sid, reason) => printSessionSkip(sid, reason),
|
|
2708
|
+
onTaskSkip: (taskId, reason) => printTaskSkip(taskId, reason)
|
|
2709
|
+
});
|
|
2710
|
+
if (options.out !== void 0) {
|
|
2711
|
+
const outPath = isAbsolute2(options.out) ? options.out : resolve4(cwd, options.out);
|
|
2712
|
+
await writeMarkdownFile5(outPath, result.body);
|
|
2713
|
+
const { sessions, decisions, tasks } = result.data;
|
|
2714
|
+
console.error(
|
|
2715
|
+
`Wrote report to ${options.out} (sessions: ${sessions.total}, decisions: ${decisions.count}, tasks: ${tasks.total})`
|
|
2716
|
+
);
|
|
2717
|
+
}
|
|
2718
|
+
if (options.json === true) {
|
|
2719
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
2720
|
+
} else if (options.out === void 0) {
|
|
2721
|
+
console.log(result.body);
|
|
2722
|
+
}
|
|
2390
2723
|
}
|
|
2391
|
-
async function
|
|
2724
|
+
async function resolveRepositoryRootForReport(cwd) {
|
|
2392
2725
|
try {
|
|
2393
2726
|
return await resolveRepositoryRoot8(cwd);
|
|
2394
2727
|
} catch (error) {
|
|
2395
2728
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
2396
|
-
throw new Error(
|
|
2397
|
-
|
|
2398
|
-
|
|
2729
|
+
throw new Error(
|
|
2730
|
+
"Not a git repository. Run 'git init' first, then re-run 'basou report generate'.",
|
|
2731
|
+
{ cause: error }
|
|
2732
|
+
);
|
|
2399
2733
|
}
|
|
2400
2734
|
throw error;
|
|
2401
2735
|
}
|
|
2402
2736
|
}
|
|
2403
|
-
async function
|
|
2737
|
+
async function assertWorkspaceInitialized8(basouRoot) {
|
|
2404
2738
|
try {
|
|
2405
|
-
await
|
|
2739
|
+
await assertBasouRootSafe9(basouRoot);
|
|
2406
2740
|
} catch (error) {
|
|
2407
|
-
if (
|
|
2741
|
+
if (findErrorCode9(error, "ENOENT")) {
|
|
2408
2742
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
2409
2743
|
}
|
|
2410
2744
|
throw error;
|
|
@@ -2413,12 +2747,12 @@ async function assertWorkspaceInitialized6(basouRoot) {
|
|
|
2413
2747
|
|
|
2414
2748
|
// src/commands/run.ts
|
|
2415
2749
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
2416
|
-
import { homedir as
|
|
2417
|
-
import { join as
|
|
2750
|
+
import { homedir as homedir5 } from "os";
|
|
2751
|
+
import { join as join6 } from "path";
|
|
2418
2752
|
import {
|
|
2419
2753
|
acquireLock as acquireLock4,
|
|
2420
|
-
assertBasouRootSafe as
|
|
2421
|
-
basouPaths as
|
|
2754
|
+
assertBasouRootSafe as assertBasouRootSafe10,
|
|
2755
|
+
basouPaths as basouPaths10,
|
|
2422
2756
|
ChildProcessRunner as ChildProcessRunner2,
|
|
2423
2757
|
claudeCodeAdapterMetadata,
|
|
2424
2758
|
appendChainedEvent as coreAppendChainedEvent2,
|
|
@@ -2428,7 +2762,7 @@ import {
|
|
|
2428
2762
|
overwriteYamlFile as overwriteYamlFile2,
|
|
2429
2763
|
prefixedUlid as prefixedUlid4,
|
|
2430
2764
|
readManifest as readManifest4,
|
|
2431
|
-
readYamlFile as
|
|
2765
|
+
readYamlFile as readYamlFile4,
|
|
2432
2766
|
resolveClaudeCodeCommand,
|
|
2433
2767
|
resolveRepositoryRoot as resolveRepositoryRoot9,
|
|
2434
2768
|
SessionSchema as SessionSchema2,
|
|
@@ -2463,17 +2797,17 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2463
2797
|
const { command } = await resolveCommand();
|
|
2464
2798
|
const cwd = options.cwd ?? process.cwd();
|
|
2465
2799
|
const repoRoot = await resolveRepositoryRootForRun(cwd);
|
|
2466
|
-
const paths =
|
|
2467
|
-
await
|
|
2800
|
+
const paths = basouPaths10(repoRoot);
|
|
2801
|
+
await assertBasouRootSafe10(paths.root);
|
|
2468
2802
|
const manifest = await readManifest4(paths);
|
|
2469
2803
|
const sessionId = prefixedUlid4("ses");
|
|
2470
|
-
const sessionDir =
|
|
2804
|
+
const sessionDir = join6(paths.sessions, sessionId);
|
|
2471
2805
|
await mkdir2(sessionDir, { recursive: true });
|
|
2472
2806
|
const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
|
|
2473
2807
|
await coreAppendChainedEvent2(paths, sessionId, event);
|
|
2474
2808
|
});
|
|
2475
2809
|
const startedAt = now().toISOString();
|
|
2476
|
-
const sessionYamlPath =
|
|
2810
|
+
const sessionYamlPath = join6(sessionDir, "session.yaml");
|
|
2477
2811
|
const session = buildInitialSession2({
|
|
2478
2812
|
id: sessionId,
|
|
2479
2813
|
command,
|
|
@@ -2599,7 +2933,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2599
2933
|
const rawRelated = computeRelatedFiles(preSnapshot, postSnapshot, diff);
|
|
2600
2934
|
const relatedFiles = sanitizeRelatedFiles(rawRelated, {
|
|
2601
2935
|
workingDirectory: repoRoot,
|
|
2602
|
-
homedir:
|
|
2936
|
+
homedir: homedir5()
|
|
2603
2937
|
}).sanitized;
|
|
2604
2938
|
const finalStatus = decideFinalStatus2(result, signalReceived);
|
|
2605
2939
|
await appendEvent(sessionDir, {
|
|
@@ -2743,7 +3077,7 @@ function buildInitialSession2(input) {
|
|
|
2743
3077
|
source: { ...claudeCodeAdapterMetadata },
|
|
2744
3078
|
started_at: input.startedAt,
|
|
2745
3079
|
status: "initialized",
|
|
2746
|
-
working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir:
|
|
3080
|
+
working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir5() }),
|
|
2747
3081
|
invocation: {
|
|
2748
3082
|
command: input.command,
|
|
2749
3083
|
args: [...input.args],
|
|
@@ -2755,7 +3089,7 @@ function buildInitialSession2(input) {
|
|
|
2755
3089
|
};
|
|
2756
3090
|
}
|
|
2757
3091
|
async function mutateSessionYaml2(filePath, mutator) {
|
|
2758
|
-
const raw = await
|
|
3092
|
+
const raw = await readYamlFile4(filePath);
|
|
2759
3093
|
const parsed = SessionSchema2.parse(raw);
|
|
2760
3094
|
mutator(parsed);
|
|
2761
3095
|
const validated = SessionSchema2.parse(parsed);
|
|
@@ -2816,19 +3150,19 @@ async function resolveRepositoryRootForRun(cwd) {
|
|
|
2816
3150
|
|
|
2817
3151
|
// src/commands/session.ts
|
|
2818
3152
|
import { readFile as readFile2 } from "fs/promises";
|
|
2819
|
-
import { basename as basename3, isAbsolute, join as
|
|
3153
|
+
import { basename as basename3, isAbsolute as isAbsolute3, join as join7, relative as relative2 } from "path";
|
|
2820
3154
|
import {
|
|
2821
3155
|
acquireLock as acquireLock5,
|
|
2822
3156
|
appendEventToExistingSession as appendEventToExistingSession2,
|
|
2823
|
-
assertBasouRootSafe as
|
|
2824
|
-
basouPaths as
|
|
3157
|
+
assertBasouRootSafe as assertBasouRootSafe11,
|
|
3158
|
+
basouPaths as basouPaths11,
|
|
2825
3159
|
enumerateSessionDirs as enumerateSessionDirs2,
|
|
2826
|
-
findErrorCode as
|
|
3160
|
+
findErrorCode as findErrorCode10,
|
|
2827
3161
|
importSessionFromJson as importSessionFromJson2,
|
|
2828
3162
|
loadSessionEntries,
|
|
2829
3163
|
readAllEvents,
|
|
2830
3164
|
readManifest as readManifest5,
|
|
2831
|
-
readYamlFile as
|
|
3165
|
+
readYamlFile as readYamlFile5,
|
|
2832
3166
|
rechainSessionInPlace,
|
|
2833
3167
|
resolveRepositoryRoot as resolveRepositoryRoot10,
|
|
2834
3168
|
resolveSessionId as resolveSessionId2,
|
|
@@ -2841,15 +3175,7 @@ import {
|
|
|
2841
3175
|
import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
|
|
2842
3176
|
|
|
2843
3177
|
// src/lib/format-duration.ts
|
|
2844
|
-
|
|
2845
|
-
const totalSeconds = Math.round(ms / 1e3);
|
|
2846
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
2847
|
-
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
2848
|
-
const seconds = totalSeconds % 60;
|
|
2849
|
-
if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m`;
|
|
2850
|
-
if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
2851
|
-
return `${seconds}s`;
|
|
2852
|
-
}
|
|
3178
|
+
import { formatDurationMs } from "@basou/core";
|
|
2853
3179
|
|
|
2854
3180
|
// src/commands/session.ts
|
|
2855
3181
|
var SES_PREFIX3 = "ses_";
|
|
@@ -2895,8 +3221,8 @@ async function runSessionList(options, ctx = {}) {
|
|
|
2895
3221
|
async function doRunSessionList(options, ctx) {
|
|
2896
3222
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2897
3223
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "list");
|
|
2898
|
-
const paths =
|
|
2899
|
-
await
|
|
3224
|
+
const paths = basouPaths11(repositoryRoot);
|
|
3225
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
2900
3226
|
const now = /* @__PURE__ */ new Date();
|
|
2901
3227
|
const records = (await loadSessionEntries(paths, {
|
|
2902
3228
|
now,
|
|
@@ -2947,17 +3273,17 @@ async function runSessionShow(idInput, options, ctx = {}) {
|
|
|
2947
3273
|
async function doRunSessionShow(idInput, options, ctx) {
|
|
2948
3274
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2949
3275
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "show");
|
|
2950
|
-
const paths =
|
|
2951
|
-
await
|
|
3276
|
+
const paths = basouPaths11(repositoryRoot);
|
|
3277
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
2952
3278
|
const sessionId = await resolveSessionId2(paths, idInput);
|
|
2953
|
-
const sessionDir =
|
|
2954
|
-
const sessionYamlPath =
|
|
3279
|
+
const sessionDir = join7(paths.sessions, sessionId);
|
|
3280
|
+
const sessionYamlPath = join7(sessionDir, "session.yaml");
|
|
2955
3281
|
let session;
|
|
2956
3282
|
try {
|
|
2957
|
-
const raw = await
|
|
3283
|
+
const raw = await readYamlFile5(sessionYamlPath);
|
|
2958
3284
|
session = SessionSchema3.parse(raw);
|
|
2959
3285
|
} catch (error) {
|
|
2960
|
-
if (
|
|
3286
|
+
if (findErrorCode10(error, "ENOENT")) {
|
|
2961
3287
|
throw new Error(`Session not found: ${idInput}`);
|
|
2962
3288
|
}
|
|
2963
3289
|
throw new Error("Failed to read session", { cause: error });
|
|
@@ -3072,7 +3398,7 @@ function formatSessionWork(session, events, now) {
|
|
|
3072
3398
|
}
|
|
3073
3399
|
function formatWorkingDir(workingDir, repositoryRoot, options) {
|
|
3074
3400
|
if (options.fullPath === true) return workingDir;
|
|
3075
|
-
if (!
|
|
3401
|
+
if (!isAbsolute3(workingDir)) {
|
|
3076
3402
|
if (workingDir === ".") return "<repository_root>";
|
|
3077
3403
|
return workingDir;
|
|
3078
3404
|
}
|
|
@@ -3203,11 +3529,11 @@ async function resolveRepositoryRootForSession(cwd, subcmd) {
|
|
|
3203
3529
|
throw error;
|
|
3204
3530
|
}
|
|
3205
3531
|
}
|
|
3206
|
-
async function
|
|
3532
|
+
async function assertWorkspaceInitialized9(basouRoot) {
|
|
3207
3533
|
try {
|
|
3208
|
-
await
|
|
3534
|
+
await assertBasouRootSafe11(basouRoot);
|
|
3209
3535
|
} catch (error) {
|
|
3210
|
-
if (
|
|
3536
|
+
if (findErrorCode10(error, "ENOENT")) {
|
|
3211
3537
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3212
3538
|
}
|
|
3213
3539
|
throw error;
|
|
@@ -3245,8 +3571,8 @@ async function runSessionImport(options, ctx = {}) {
|
|
|
3245
3571
|
async function doRunSessionImport(options, ctx) {
|
|
3246
3572
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3247
3573
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "import");
|
|
3248
|
-
const paths =
|
|
3249
|
-
await
|
|
3574
|
+
const paths = basouPaths11(repositoryRoot);
|
|
3575
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
3250
3576
|
const manifest = await readManifest5(paths);
|
|
3251
3577
|
const rawBody = await readInputFile(options.from);
|
|
3252
3578
|
const json = parseJsonStrict(rawBody);
|
|
@@ -3276,10 +3602,10 @@ async function readInputFile(path) {
|
|
|
3276
3602
|
try {
|
|
3277
3603
|
return await readFile2(path, "utf8");
|
|
3278
3604
|
} catch (error) {
|
|
3279
|
-
if (
|
|
3605
|
+
if (findErrorCode10(error, "ENOENT")) {
|
|
3280
3606
|
throw new Error("Import source not found", { cause: error });
|
|
3281
3607
|
}
|
|
3282
|
-
if (
|
|
3608
|
+
if (findErrorCode10(error, "EISDIR")) {
|
|
3283
3609
|
throw new Error("Import source is not a file", { cause: error });
|
|
3284
3610
|
}
|
|
3285
3611
|
throw new Error("Failed to read import source", { cause: error });
|
|
@@ -3359,8 +3685,8 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
|
|
|
3359
3685
|
}
|
|
3360
3686
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3361
3687
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "note");
|
|
3362
|
-
const paths =
|
|
3363
|
-
await
|
|
3688
|
+
const paths = basouPaths11(repositoryRoot);
|
|
3689
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
3364
3690
|
const sessionId = await resolveSessionId2(paths, sessionIdInput);
|
|
3365
3691
|
const body = hasBody ? options.body : await readNoteFile(options.fromFile);
|
|
3366
3692
|
if (body.length === 0) {
|
|
@@ -3393,10 +3719,10 @@ async function readNoteFile(path) {
|
|
|
3393
3719
|
try {
|
|
3394
3720
|
return await readFile2(path, "utf8");
|
|
3395
3721
|
} catch (error) {
|
|
3396
|
-
if (
|
|
3722
|
+
if (findErrorCode10(error, "ENOENT")) {
|
|
3397
3723
|
throw new Error("Note source not found", { cause: error });
|
|
3398
3724
|
}
|
|
3399
|
-
if (
|
|
3725
|
+
if (findErrorCode10(error, "EISDIR")) {
|
|
3400
3726
|
throw new Error("Note source is not a file", { cause: error });
|
|
3401
3727
|
}
|
|
3402
3728
|
throw new Error("Failed to read note source", { cause: error });
|
|
@@ -3441,8 +3767,8 @@ async function doRunSessionRechain(options, ctx) {
|
|
|
3441
3767
|
}
|
|
3442
3768
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3443
3769
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "rechain");
|
|
3444
|
-
const paths =
|
|
3445
|
-
await
|
|
3770
|
+
const paths = basouPaths11(repositoryRoot);
|
|
3771
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
3446
3772
|
const sessionIds = options.session !== void 0 ? [await resolveSessionId2(paths, options.session)] : await enumerateSessionDirs2(paths);
|
|
3447
3773
|
const dryRun = options.dryRun === true;
|
|
3448
3774
|
const rows = [];
|
|
@@ -3495,10 +3821,10 @@ function renderRechainRow(row, dryRun) {
|
|
|
3495
3821
|
|
|
3496
3822
|
// src/commands/stats.ts
|
|
3497
3823
|
import {
|
|
3498
|
-
assertBasouRootSafe as
|
|
3499
|
-
basouPaths as
|
|
3824
|
+
assertBasouRootSafe as assertBasouRootSafe12,
|
|
3825
|
+
basouPaths as basouPaths12,
|
|
3500
3826
|
computeWorkStats,
|
|
3501
|
-
findErrorCode as
|
|
3827
|
+
findErrorCode as findErrorCode11,
|
|
3502
3828
|
resolveRepositoryRoot as resolveRepositoryRoot11
|
|
3503
3829
|
} from "@basou/core";
|
|
3504
3830
|
function registerStatsCommand(program2) {
|
|
@@ -3517,8 +3843,8 @@ async function runStats(options, ctx = {}) {
|
|
|
3517
3843
|
async function doRunStats(options, ctx) {
|
|
3518
3844
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3519
3845
|
const repositoryRoot = await resolveRepositoryRootForStats(cwd);
|
|
3520
|
-
const paths =
|
|
3521
|
-
await
|
|
3846
|
+
const paths = basouPaths12(repositoryRoot);
|
|
3847
|
+
await assertWorkspaceInitialized10(paths.root);
|
|
3522
3848
|
const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
|
|
3523
3849
|
const result = await computeWorkStats({
|
|
3524
3850
|
paths,
|
|
@@ -3612,11 +3938,11 @@ async function resolveRepositoryRootForStats(cwd) {
|
|
|
3612
3938
|
throw error;
|
|
3613
3939
|
}
|
|
3614
3940
|
}
|
|
3615
|
-
async function
|
|
3941
|
+
async function assertWorkspaceInitialized10(basouRoot) {
|
|
3616
3942
|
try {
|
|
3617
|
-
await
|
|
3943
|
+
await assertBasouRootSafe12(basouRoot);
|
|
3618
3944
|
} catch (error) {
|
|
3619
|
-
if (
|
|
3945
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
3620
3946
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3621
3947
|
}
|
|
3622
3948
|
throw error;
|
|
@@ -3625,10 +3951,10 @@ async function assertWorkspaceInitialized8(basouRoot) {
|
|
|
3625
3951
|
|
|
3626
3952
|
// src/commands/status.ts
|
|
3627
3953
|
import {
|
|
3628
|
-
assertBasouRootSafe as
|
|
3629
|
-
basouPaths as
|
|
3954
|
+
assertBasouRootSafe as assertBasouRootSafe13,
|
|
3955
|
+
basouPaths as basouPaths13,
|
|
3630
3956
|
buildStatusSnapshot,
|
|
3631
|
-
findErrorCode as
|
|
3957
|
+
findErrorCode as findErrorCode12,
|
|
3632
3958
|
readManifest as readManifest6,
|
|
3633
3959
|
resolveRepositoryRoot as resolveRepositoryRoot12,
|
|
3634
3960
|
writeStatus
|
|
@@ -3649,11 +3975,11 @@ async function runStatus(options, ctx = {}) {
|
|
|
3649
3975
|
async function doRunStatus(options, ctx) {
|
|
3650
3976
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3651
3977
|
const repositoryRoot = await resolveRepositoryRootForStatus(cwd);
|
|
3652
|
-
const paths =
|
|
3978
|
+
const paths = basouPaths13(repositoryRoot);
|
|
3653
3979
|
try {
|
|
3654
|
-
await
|
|
3980
|
+
await assertBasouRootSafe13(paths.root);
|
|
3655
3981
|
} catch (error) {
|
|
3656
|
-
if (
|
|
3982
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
3657
3983
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3658
3984
|
}
|
|
3659
3985
|
throw error;
|
|
@@ -3662,7 +3988,7 @@ async function doRunStatus(options, ctx) {
|
|
|
3662
3988
|
try {
|
|
3663
3989
|
manifest = await readManifest6(paths);
|
|
3664
3990
|
} catch (error) {
|
|
3665
|
-
if (
|
|
3991
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
3666
3992
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3667
3993
|
}
|
|
3668
3994
|
throw new Error("Failed to read workspace manifest", { cause: error });
|
|
@@ -3699,16 +4025,16 @@ async function resolveRepositoryRootForStatus(cwd) {
|
|
|
3699
4025
|
|
|
3700
4026
|
// src/commands/task.ts
|
|
3701
4027
|
import { readFile as readFile3 } from "fs/promises";
|
|
3702
|
-
import { join as
|
|
4028
|
+
import { join as join8 } from "path";
|
|
3703
4029
|
import {
|
|
3704
4030
|
archiveTask,
|
|
3705
|
-
assertBasouRootSafe as
|
|
3706
|
-
basouPaths as
|
|
4031
|
+
assertBasouRootSafe as assertBasouRootSafe14,
|
|
4032
|
+
basouPaths as basouPaths14,
|
|
3707
4033
|
createTaskWithEvent,
|
|
3708
4034
|
deleteTask,
|
|
3709
4035
|
editTask,
|
|
3710
4036
|
enumerateArchivedTaskIds,
|
|
3711
|
-
findErrorCode as
|
|
4037
|
+
findErrorCode as findErrorCode13,
|
|
3712
4038
|
loadSessionEntries as loadSessionEntries2,
|
|
3713
4039
|
loadTaskEntries,
|
|
3714
4040
|
prefixedUlid as prefixedUlid5,
|
|
@@ -3805,8 +4131,8 @@ async function doRunTaskNew(options, ctx) {
|
|
|
3805
4131
|
}
|
|
3806
4132
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3807
4133
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "new");
|
|
3808
|
-
const paths =
|
|
3809
|
-
await
|
|
4134
|
+
const paths = basouPaths14(repositoryRoot);
|
|
4135
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
3810
4136
|
const description = options.description !== void 0 ? options.description : options.fromFile !== void 0 ? await readDescriptionFile(options.fromFile) : "";
|
|
3811
4137
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
3812
4138
|
const occurredAt = now.toISOString();
|
|
@@ -3914,8 +4240,8 @@ async function runTaskList(options, ctx = {}) {
|
|
|
3914
4240
|
async function doRunTaskList(options, ctx) {
|
|
3915
4241
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3916
4242
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "list");
|
|
3917
|
-
const paths =
|
|
3918
|
-
await
|
|
4243
|
+
const paths = basouPaths14(repositoryRoot);
|
|
4244
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
3919
4245
|
const entries = await loadTaskEntries(paths, {
|
|
3920
4246
|
onSkip: (id, reason) => printTaskSkip(id, reason)
|
|
3921
4247
|
});
|
|
@@ -4018,15 +4344,15 @@ async function runTaskShow(idInput, options, ctx = {}) {
|
|
|
4018
4344
|
async function doRunTaskShow(idInput, options, ctx) {
|
|
4019
4345
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4020
4346
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "show");
|
|
4021
|
-
const paths =
|
|
4022
|
-
await
|
|
4347
|
+
const paths = basouPaths14(repositoryRoot);
|
|
4348
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
4023
4349
|
const taskId = await resolveTaskId2(paths, idInput, { includeArchived: true });
|
|
4024
4350
|
const { doc, archived } = await readTaskFileWithArchiveFallback(paths, taskId);
|
|
4025
4351
|
const sessions = await loadSessionEntries2(paths, { now: /* @__PURE__ */ new Date() });
|
|
4026
4352
|
const events = [];
|
|
4027
4353
|
const linkedSessionIds = new Set(doc.task.task.linked_sessions);
|
|
4028
4354
|
for (const s of sessions) {
|
|
4029
|
-
const sessionDir =
|
|
4355
|
+
const sessionDir = join8(paths.sessions, s.sessionId);
|
|
4030
4356
|
try {
|
|
4031
4357
|
for await (const ev of replayEvents2(sessionDir, {
|
|
4032
4358
|
onWarning: (w) => printReplayWarning(w, s.sessionId)
|
|
@@ -4162,8 +4488,8 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
|
|
|
4162
4488
|
const newStatus = parseTaskStatusPositional(newStatusInput);
|
|
4163
4489
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4164
4490
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "status");
|
|
4165
|
-
const paths =
|
|
4166
|
-
await
|
|
4491
|
+
const paths = basouPaths14(repositoryRoot);
|
|
4492
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
4167
4493
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4168
4494
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
4169
4495
|
const occurredAt = now.toISOString();
|
|
@@ -4239,8 +4565,8 @@ async function runTaskReconcile(options, ctx = {}) {
|
|
|
4239
4565
|
async function doRunTaskReconcile(options, ctx) {
|
|
4240
4566
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4241
4567
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "reconcile");
|
|
4242
|
-
const paths =
|
|
4243
|
-
await
|
|
4568
|
+
const paths = basouPaths14(repositoryRoot);
|
|
4569
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
4244
4570
|
const manifest = await readManifest7(paths);
|
|
4245
4571
|
const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
|
|
4246
4572
|
const write = options.write === true;
|
|
@@ -4419,8 +4745,8 @@ async function doRunTaskRefreshLinkage(taskIdInput, options, ctx) {
|
|
|
4419
4745
|
}
|
|
4420
4746
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4421
4747
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "refresh-linkage");
|
|
4422
|
-
const paths =
|
|
4423
|
-
await
|
|
4748
|
+
const paths = basouPaths14(repositoryRoot);
|
|
4749
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
4424
4750
|
const manifest = await readManifest7(paths);
|
|
4425
4751
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4426
4752
|
const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
|
|
@@ -4499,8 +4825,8 @@ async function doRunTaskEdit(taskIdInput, options, ctx) {
|
|
|
4499
4825
|
}
|
|
4500
4826
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4501
4827
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "edit");
|
|
4502
|
-
const paths =
|
|
4503
|
-
await
|
|
4828
|
+
const paths = basouPaths14(repositoryRoot);
|
|
4829
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
4504
4830
|
const manifest = await readManifest7(paths);
|
|
4505
4831
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4506
4832
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
@@ -4555,8 +4881,8 @@ async function doRunTaskDelete(taskIdInput, options, ctx) {
|
|
|
4555
4881
|
}
|
|
4556
4882
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4557
4883
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "delete");
|
|
4558
|
-
const paths =
|
|
4559
|
-
await
|
|
4884
|
+
const paths = basouPaths14(repositoryRoot);
|
|
4885
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
4560
4886
|
const manifest = await readManifest7(paths);
|
|
4561
4887
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4562
4888
|
if (options.yes !== true) {
|
|
@@ -4600,8 +4926,8 @@ async function doRunTaskArchive(taskIdInput, options, ctx) {
|
|
|
4600
4926
|
}
|
|
4601
4927
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4602
4928
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "archive");
|
|
4603
|
-
const paths =
|
|
4604
|
-
await
|
|
4929
|
+
const paths = basouPaths14(repositoryRoot);
|
|
4930
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
4605
4931
|
const manifest = await readManifest7(paths);
|
|
4606
4932
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4607
4933
|
if (options.yes !== true) {
|
|
@@ -4716,10 +5042,10 @@ async function readDescriptionFile(path) {
|
|
|
4716
5042
|
try {
|
|
4717
5043
|
return await readFile3(path, "utf8");
|
|
4718
5044
|
} catch (error) {
|
|
4719
|
-
if (
|
|
5045
|
+
if (findErrorCode13(error, "ENOENT")) {
|
|
4720
5046
|
throw new Error("Description source not found", { cause: error });
|
|
4721
5047
|
}
|
|
4722
|
-
if (
|
|
5048
|
+
if (findErrorCode13(error, "EISDIR")) {
|
|
4723
5049
|
throw new Error("Description source is not a file", { cause: error });
|
|
4724
5050
|
}
|
|
4725
5051
|
throw new Error("Failed to read description source", { cause: error });
|
|
@@ -4738,11 +5064,11 @@ async function resolveRepositoryRootForTask(cwd, subcmd) {
|
|
|
4738
5064
|
throw error;
|
|
4739
5065
|
}
|
|
4740
5066
|
}
|
|
4741
|
-
async function
|
|
5067
|
+
async function assertWorkspaceInitialized11(basouRoot) {
|
|
4742
5068
|
try {
|
|
4743
|
-
await
|
|
5069
|
+
await assertBasouRootSafe14(basouRoot);
|
|
4744
5070
|
} catch (error) {
|
|
4745
|
-
if (
|
|
5071
|
+
if (findErrorCode13(error, "ENOENT")) {
|
|
4746
5072
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
4747
5073
|
}
|
|
4748
5074
|
throw error;
|
|
@@ -4830,10 +5156,10 @@ function maxLen3(values, floor) {
|
|
|
4830
5156
|
|
|
4831
5157
|
// src/commands/verify.ts
|
|
4832
5158
|
import {
|
|
4833
|
-
assertBasouRootSafe as
|
|
4834
|
-
basouPaths as
|
|
5159
|
+
assertBasouRootSafe as assertBasouRootSafe15,
|
|
5160
|
+
basouPaths as basouPaths15,
|
|
4835
5161
|
enumerateSessionDirs as enumerateSessionDirs3,
|
|
4836
|
-
findErrorCode as
|
|
5162
|
+
findErrorCode as findErrorCode14,
|
|
4837
5163
|
resolveRepositoryRoot as resolveRepositoryRoot14,
|
|
4838
5164
|
resolveSessionId as resolveSessionId4,
|
|
4839
5165
|
verifyEventsChain
|
|
@@ -4857,8 +5183,8 @@ async function doRunVerify(options, ctx) {
|
|
|
4857
5183
|
}
|
|
4858
5184
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4859
5185
|
const repositoryRoot = await resolveRepositoryRootForVerify(cwd);
|
|
4860
|
-
const paths =
|
|
4861
|
-
await
|
|
5186
|
+
const paths = basouPaths15(repositoryRoot);
|
|
5187
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
4862
5188
|
const sessionIds = options.session !== void 0 ? [await resolveSessionId4(paths, options.session)] : await enumerateSessionDirs3(paths);
|
|
4863
5189
|
const rows = [];
|
|
4864
5190
|
for (const sessionId of sessionIds) {
|
|
@@ -4915,11 +5241,11 @@ async function resolveRepositoryRootForVerify(cwd) {
|
|
|
4915
5241
|
throw error;
|
|
4916
5242
|
}
|
|
4917
5243
|
}
|
|
4918
|
-
async function
|
|
5244
|
+
async function assertWorkspaceInitialized12(basouRoot) {
|
|
4919
5245
|
try {
|
|
4920
|
-
await
|
|
5246
|
+
await assertBasouRootSafe15(basouRoot);
|
|
4921
5247
|
} catch (error) {
|
|
4922
|
-
if (
|
|
5248
|
+
if (findErrorCode14(error, "ENOENT")) {
|
|
4923
5249
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
4924
5250
|
}
|
|
4925
5251
|
throw error;
|
|
@@ -4928,27 +5254,162 @@ async function assertWorkspaceInitialized10(basouRoot) {
|
|
|
4928
5254
|
|
|
4929
5255
|
// src/commands/view.ts
|
|
4930
5256
|
import { spawn } from "child_process";
|
|
4931
|
-
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";
|
|
4932
5266
|
import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
|
|
4933
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
|
+
|
|
4934
5394
|
// src/lib/view-server.ts
|
|
4935
5395
|
import { createServer } from "http";
|
|
4936
|
-
import { join as
|
|
5396
|
+
import { join as join10 } from "path";
|
|
4937
5397
|
import {
|
|
4938
5398
|
computeWorkStats as computeWorkStats2,
|
|
4939
5399
|
enumerateApprovals as enumerateApprovals2,
|
|
4940
|
-
findErrorCode as
|
|
5400
|
+
findErrorCode as findErrorCode15,
|
|
4941
5401
|
isLazyExpired as isLazyExpired2,
|
|
4942
5402
|
loadApproval as loadApproval2,
|
|
4943
5403
|
loadSessionEntries as loadSessionEntries3,
|
|
4944
5404
|
loadTaskEntries as loadTaskEntries2,
|
|
4945
5405
|
readAllEvents as readAllEvents2,
|
|
4946
|
-
readManifest as
|
|
5406
|
+
readManifest as readManifest9,
|
|
4947
5407
|
readMarkdownFile as readMarkdownFile4,
|
|
4948
5408
|
readSessionYaml as readSessionYaml3,
|
|
4949
5409
|
readTaskFile as readTaskFile2,
|
|
4950
5410
|
renderDecisions as renderDecisions3,
|
|
4951
|
-
renderHandoff as renderHandoff3
|
|
5411
|
+
renderHandoff as renderHandoff3,
|
|
5412
|
+
summarizeOrientation
|
|
4952
5413
|
} from "@basou/core";
|
|
4953
5414
|
|
|
4954
5415
|
// src/lib/view-ui.ts
|
|
@@ -4970,8 +5431,13 @@ var VIEW_HTML = `<!doctype html>
|
|
|
4970
5431
|
button.primary { background: #2563eb; color: #fff; border-color: #2563eb; }
|
|
4971
5432
|
button:disabled { opacity: .5; cursor: default; }
|
|
4972
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; }
|
|
4973
5438
|
#status { padding: 6px 16px; font-size: 13px; min-height: 20px; border-bottom: 1px solid #8884; white-space: pre-wrap; }
|
|
4974
5439
|
#status.err { color: #dc2626; }
|
|
5440
|
+
.err { color: #dc2626; }
|
|
4975
5441
|
nav { display: flex; gap: 2px; padding: 6px 12px; border-bottom: 1px solid #8884; flex-wrap: wrap; }
|
|
4976
5442
|
nav button { border: none; border-radius: 6px; background: transparent; }
|
|
4977
5443
|
nav button.active { background: #2563eb22; font-weight: 600; }
|
|
@@ -4985,6 +5451,8 @@ var VIEW_HTML = `<!doctype html>
|
|
|
4985
5451
|
#detail { padding: 12px 16px; overflow: auto; max-height: 80vh; }
|
|
4986
5452
|
.badge { display: inline-block; padding: 0 6px; border-radius: 6px; background: #8882; font-size: 12px; }
|
|
4987
5453
|
.badge.warn { background: #f59e0b33; }
|
|
5454
|
+
.badge.danger { background: #ef444433; }
|
|
5455
|
+
.badge.ok { background: #22c55e33; }
|
|
4988
5456
|
pre { background: #8881; padding: 12px; border-radius: 8px; overflow: auto; white-space: pre-wrap; word-break: break-word; }
|
|
4989
5457
|
table.kv { border-collapse: collapse; }
|
|
4990
5458
|
table.kv td { padding: 2px 10px 2px 0; vertical-align: top; }
|
|
@@ -4993,6 +5461,11 @@ var VIEW_HTML = `<!doctype html>
|
|
|
4993
5461
|
.card { border: 1px solid #8884; border-radius: 8px; padding: 10px 14px; min-width: 120px; }
|
|
4994
5462
|
.card .n { font-size: 22px; font-weight: 700; }
|
|
4995
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; }
|
|
4996
5469
|
.tl { border-left: 2px solid #8885; margin-left: 6px; padding-left: 12px; }
|
|
4997
5470
|
.tl .ev { margin-bottom: 8px; }
|
|
4998
5471
|
.tl .ev .t { font-size: 12px; opacity: .65; }
|
|
@@ -5002,6 +5475,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5002
5475
|
<body>
|
|
5003
5476
|
<header>
|
|
5004
5477
|
<h1>basou view</h1>
|
|
5478
|
+
<button id="btn-back" style="display:none">← portfolio</button>
|
|
5005
5479
|
<input type="text" id="project" placeholder="source root (optional override)" />
|
|
5006
5480
|
<button class="primary" id="btn-refresh">Refresh all</button>
|
|
5007
5481
|
<button id="btn-import-claude">Import claude-code</button>
|
|
@@ -5021,7 +5495,12 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5021
5495
|
<script>
|
|
5022
5496
|
(function () {
|
|
5023
5497
|
var TABS = ['overview', 'stats', 'sessions', 'tasks', 'decisions', 'approvals', 'handoff'];
|
|
5024
|
-
|
|
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 };
|
|
5025
5504
|
|
|
5026
5505
|
function $(id) { return document.getElementById(id); }
|
|
5027
5506
|
function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }
|
|
@@ -5085,7 +5564,15 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5085
5564
|
for (var i = 0; i < ids.length; i++) $(ids[i]).disabled = busy;
|
|
5086
5565
|
}
|
|
5087
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
|
+
|
|
5088
5574
|
function post(path, label) {
|
|
5575
|
+
if (!state.canAct) { setStatus('Open a workspace first.', true); return; }
|
|
5089
5576
|
setBusy(true);
|
|
5090
5577
|
setStatus(label + '...', false);
|
|
5091
5578
|
fetchJson(path, {
|
|
@@ -5118,6 +5605,155 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5118
5605
|
return (o.dryRun ? 'would import ' : 'imported ') + o.importedCount + ' (' + o.eventTotal + ' events)';
|
|
5119
5606
|
}
|
|
5120
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
|
+
|
|
5121
5757
|
// --- tabs ---------------------------------------------------------------
|
|
5122
5758
|
|
|
5123
5759
|
function buildTabs() {
|
|
@@ -5141,16 +5777,16 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5141
5777
|
if (name === 'stats') return loadStats();
|
|
5142
5778
|
if (name === 'sessions') return loadSessions();
|
|
5143
5779
|
if (name === 'tasks') return loadTasks();
|
|
5144
|
-
if (name === 'decisions') return loadMarkdown('/
|
|
5780
|
+
if (name === 'decisions') return loadMarkdown(state.base + '/decisions', 'decisions');
|
|
5145
5781
|
if (name === 'approvals') return loadApprovals();
|
|
5146
|
-
if (name === 'handoff') return loadMarkdown('/
|
|
5782
|
+
if (name === 'handoff') return loadMarkdown(state.base + '/handoff', 'handoff');
|
|
5147
5783
|
}
|
|
5148
5784
|
|
|
5149
5785
|
function fail(err) { setStatus(err.message, true); }
|
|
5150
5786
|
|
|
5151
5787
|
function loadOverview() {
|
|
5152
5788
|
single(true);
|
|
5153
|
-
fetchJson('/
|
|
5789
|
+
fetchJson(state.base + '/overview').then(function (d) {
|
|
5154
5790
|
var detail = $('detail');
|
|
5155
5791
|
if (!d || d.initialized === false) {
|
|
5156
5792
|
detail.appendChild(el('p', { class: 'muted', text: 'Workspace not initialized.' }));
|
|
@@ -5199,7 +5835,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5199
5835
|
|
|
5200
5836
|
function loadStats() {
|
|
5201
5837
|
single(true);
|
|
5202
|
-
fetchJson('/
|
|
5838
|
+
fetchJson(state.base + '/stats').then(function (d) {
|
|
5203
5839
|
var detail = $('detail');
|
|
5204
5840
|
var t = d.totals;
|
|
5205
5841
|
detail.appendChild(el('p', { text: 'Sessions: ' + t.sessionCount }));
|
|
@@ -5260,7 +5896,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5260
5896
|
|
|
5261
5897
|
function loadSessions() {
|
|
5262
5898
|
single(false);
|
|
5263
|
-
fetchJson('/
|
|
5899
|
+
fetchJson(state.base + '/sessions').then(function (d) {
|
|
5264
5900
|
var list = $('list');
|
|
5265
5901
|
var rows = (d && d.sessions) || [];
|
|
5266
5902
|
if (rows.length === 0) { list.appendChild(el('div', { class: 'row muted', text: 'no sessions' })); return; }
|
|
@@ -5279,7 +5915,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5279
5915
|
row.classList.add('active');
|
|
5280
5916
|
var detail = $('detail');
|
|
5281
5917
|
clear(detail);
|
|
5282
|
-
fetchJson('/
|
|
5918
|
+
fetchJson(state.base + '/sessions/' + encodeURIComponent(id)).then(function (d) {
|
|
5283
5919
|
var s = d.session.session;
|
|
5284
5920
|
detail.appendChild(el('h3', { text: s.label || id }));
|
|
5285
5921
|
detail.appendChild(kv([
|
|
@@ -5312,7 +5948,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5312
5948
|
|
|
5313
5949
|
function loadTasks() {
|
|
5314
5950
|
single(false);
|
|
5315
|
-
fetchJson('/
|
|
5951
|
+
fetchJson(state.base + '/tasks').then(function (d) {
|
|
5316
5952
|
var list = $('list');
|
|
5317
5953
|
var rows = (d && d.tasks) || [];
|
|
5318
5954
|
if (rows.length === 0) { list.appendChild(el('div', { class: 'row muted', text: 'no tasks' })); return; }
|
|
@@ -5331,7 +5967,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5331
5967
|
row.classList.add('active');
|
|
5332
5968
|
var detail = $('detail');
|
|
5333
5969
|
clear(detail);
|
|
5334
|
-
fetchJson('/
|
|
5970
|
+
fetchJson(state.base + '/tasks/' + encodeURIComponent(id)).then(function (d) {
|
|
5335
5971
|
detail.appendChild(el('h3', { text: (d.task && (d.task.title || d.task.label)) || id }));
|
|
5336
5972
|
detail.appendChild(el('pre', { text: JSON.stringify(d.task, null, 2) }));
|
|
5337
5973
|
if (d.body) detail.appendChild(el('pre', { text: d.body }));
|
|
@@ -5350,7 +5986,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5350
5986
|
|
|
5351
5987
|
function loadApprovals() {
|
|
5352
5988
|
single(true);
|
|
5353
|
-
fetchJson('/
|
|
5989
|
+
fetchJson(state.base + '/approvals').then(function (d) {
|
|
5354
5990
|
var detail = $('detail');
|
|
5355
5991
|
var groups = [['pending', d.pending || []], ['resolved', d.resolved || []]];
|
|
5356
5992
|
groups.forEach(function (g) {
|
|
@@ -5375,14 +6011,14 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5375
6011
|
|
|
5376
6012
|
// --- wire up ------------------------------------------------------------
|
|
5377
6013
|
|
|
5378
|
-
$('btn-
|
|
5379
|
-
$('btn-
|
|
5380
|
-
$('btn-import-
|
|
5381
|
-
$('btn-
|
|
5382
|
-
$('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'); });
|
|
5383
6020
|
|
|
5384
|
-
|
|
5385
|
-
loadTab('overview');
|
|
6021
|
+
boot();
|
|
5386
6022
|
})();
|
|
5387
6023
|
</script>
|
|
5388
6024
|
</body>
|
|
@@ -5397,6 +6033,8 @@ var HttpError = class extends Error {
|
|
|
5397
6033
|
status;
|
|
5398
6034
|
};
|
|
5399
6035
|
var MAX_BODY_BYTES = 64 * 1024;
|
|
6036
|
+
var API_PREFIX = "/api/";
|
|
6037
|
+
var WS_PREFIX = "/api/ws/";
|
|
5400
6038
|
function startViewServer(opts) {
|
|
5401
6039
|
const { port, host = "127.0.0.1", deps } = opts;
|
|
5402
6040
|
let actionQueue = Promise.resolve();
|
|
@@ -5410,7 +6048,7 @@ function startViewServer(opts) {
|
|
|
5410
6048
|
};
|
|
5411
6049
|
let boundPort = port;
|
|
5412
6050
|
const getPort = () => boundPort;
|
|
5413
|
-
return new Promise((
|
|
6051
|
+
return new Promise((resolve7, reject) => {
|
|
5414
6052
|
const server = createServer((req, res) => {
|
|
5415
6053
|
handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
|
|
5416
6054
|
sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
|
|
@@ -5421,7 +6059,7 @@ function startViewServer(opts) {
|
|
|
5421
6059
|
const address = server.address();
|
|
5422
6060
|
boundPort = isAddressInfo(address) ? address.port : port;
|
|
5423
6061
|
server.off("error", reject);
|
|
5424
|
-
|
|
6062
|
+
resolve7({
|
|
5425
6063
|
url: `http://${host}:${boundPort}`,
|
|
5426
6064
|
port: boundPort,
|
|
5427
6065
|
close: () => closeServer(server)
|
|
@@ -5433,8 +6071,8 @@ function isAddressInfo(value) {
|
|
|
5433
6071
|
return value !== null && typeof value === "object";
|
|
5434
6072
|
}
|
|
5435
6073
|
function closeServer(server) {
|
|
5436
|
-
return new Promise((
|
|
5437
|
-
server.close(() =>
|
|
6074
|
+
return new Promise((resolve7) => {
|
|
6075
|
+
server.close(() => resolve7());
|
|
5438
6076
|
server.closeAllConnections();
|
|
5439
6077
|
});
|
|
5440
6078
|
}
|
|
@@ -5466,90 +6104,204 @@ async function handleGet(res, pathname, deps) {
|
|
|
5466
6104
|
sendHtml(res, VIEW_HTML);
|
|
5467
6105
|
return;
|
|
5468
6106
|
}
|
|
5469
|
-
if (pathname === "/api/
|
|
5470
|
-
sendJson(res, 200, await
|
|
6107
|
+
if (pathname === "/api/portfolio") {
|
|
6108
|
+
sendJson(res, 200, await portfolio(deps));
|
|
5471
6109
|
return;
|
|
5472
6110
|
}
|
|
5473
|
-
|
|
5474
|
-
|
|
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
|
+
}
|
|
5475
6121
|
return;
|
|
5476
6122
|
}
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
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
|
+
}
|
|
5480
6128
|
return;
|
|
5481
6129
|
}
|
|
5482
|
-
|
|
5483
|
-
|
|
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
|
+
}
|
|
5484
6143
|
return;
|
|
5485
6144
|
}
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
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
|
+
}
|
|
5489
6150
|
return;
|
|
5490
6151
|
}
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
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;
|
|
5494
6158
|
}
|
|
5495
|
-
if (
|
|
5496
|
-
sendJson(res, 200, await
|
|
5497
|
-
return;
|
|
6159
|
+
if (sub === "sessions") {
|
|
6160
|
+
sendJson(res, 200, await sessionsList(ws, nowProvider));
|
|
6161
|
+
return true;
|
|
5498
6162
|
}
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
6163
|
+
const sessionId = matchId(sub, "sessions/");
|
|
6164
|
+
if (sessionId !== null) {
|
|
6165
|
+
sendJson(res, 200, await sessionDetail(ws, sessionId));
|
|
6166
|
+
return true;
|
|
5502
6167
|
}
|
|
5503
|
-
if (
|
|
5504
|
-
sendJson(res, 200, await
|
|
5505
|
-
return;
|
|
6168
|
+
if (sub === "tasks") {
|
|
6169
|
+
sendJson(res, 200, await tasksList(ws));
|
|
6170
|
+
return true;
|
|
5506
6171
|
}
|
|
5507
|
-
|
|
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;
|
|
5508
6194
|
}
|
|
5509
|
-
async function
|
|
6195
|
+
async function handleWorkspacePost(res, sub, ws, body, deps, runExclusive) {
|
|
5510
6196
|
const nowIso = deps.nowProvider().toISOString();
|
|
5511
6197
|
const actionOptions = readActionOptions(body);
|
|
5512
|
-
if (
|
|
6198
|
+
if (sub === "refresh") {
|
|
5513
6199
|
const result = await runExclusive(
|
|
5514
|
-
() => refreshAll({ options: actionOptions, ctx:
|
|
6200
|
+
() => refreshAll({ options: actionOptions, ctx: ws.importCtx, paths: ws.paths, nowIso })
|
|
5515
6201
|
);
|
|
5516
6202
|
sendJson(res, 200, result);
|
|
5517
|
-
return;
|
|
6203
|
+
return true;
|
|
5518
6204
|
}
|
|
5519
|
-
if (
|
|
5520
|
-
sendJson(res, 200, await runExclusive(() => importClaudeCode(actionOptions,
|
|
5521
|
-
return;
|
|
6205
|
+
if (sub === "import/claude-code") {
|
|
6206
|
+
sendJson(res, 200, await runExclusive(() => importClaudeCode(actionOptions, ws.importCtx)));
|
|
6207
|
+
return true;
|
|
5522
6208
|
}
|
|
5523
|
-
if (
|
|
5524
|
-
sendJson(res, 200, await runExclusive(() => importCodex(actionOptions,
|
|
5525
|
-
return;
|
|
6209
|
+
if (sub === "import/codex") {
|
|
6210
|
+
sendJson(res, 200, await runExclusive(() => importCodex(actionOptions, ws.importCtx)));
|
|
6211
|
+
return true;
|
|
5526
6212
|
}
|
|
5527
|
-
if (
|
|
5528
|
-
sendJson(res, 200, await runExclusive(() => regenerateHandoff(
|
|
5529
|
-
return;
|
|
6213
|
+
if (sub === "handoff/generate") {
|
|
6214
|
+
sendJson(res, 200, await runExclusive(() => regenerateHandoff(ws.paths, nowIso)));
|
|
6215
|
+
return true;
|
|
5530
6216
|
}
|
|
5531
|
-
if (
|
|
5532
|
-
sendJson(res, 200, await runExclusive(() => regenerateDecisions(
|
|
5533
|
-
return;
|
|
6217
|
+
if (sub === "decisions/generate") {
|
|
6218
|
+
sendJson(res, 200, await runExclusive(() => regenerateDecisions(ws.paths, nowIso)));
|
|
6219
|
+
return true;
|
|
5534
6220
|
}
|
|
5535
|
-
|
|
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;
|
|
6230
|
+
}
|
|
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 };
|
|
5536
6288
|
}
|
|
5537
|
-
async function overview(
|
|
6289
|
+
async function overview(ws, nowProvider) {
|
|
5538
6290
|
let manifest;
|
|
5539
6291
|
try {
|
|
5540
|
-
manifest = await
|
|
6292
|
+
manifest = await readManifest9(ws.paths);
|
|
5541
6293
|
} catch (error) {
|
|
5542
|
-
if (
|
|
5543
|
-
return { initialized: false, repoRoot:
|
|
6294
|
+
if (findErrorCode15(error, "ENOENT")) {
|
|
6295
|
+
return { initialized: false, repoRoot: ws.repoRoot };
|
|
5544
6296
|
}
|
|
5545
6297
|
throw error;
|
|
5546
6298
|
}
|
|
5547
|
-
const nowIso =
|
|
5548
|
-
const handoff = await renderHandoff3({ paths:
|
|
5549
|
-
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);
|
|
5550
6302
|
return {
|
|
5551
6303
|
initialized: true,
|
|
5552
|
-
repoRoot:
|
|
6304
|
+
repoRoot: ws.repoRoot,
|
|
5553
6305
|
workspace: {
|
|
5554
6306
|
id: manifest.workspace.id,
|
|
5555
6307
|
name: manifest.workspace.name,
|
|
@@ -5567,8 +6319,8 @@ async function overview(deps) {
|
|
|
5567
6319
|
generatedAt: nowIso
|
|
5568
6320
|
};
|
|
5569
6321
|
}
|
|
5570
|
-
async function sessionsList(
|
|
5571
|
-
const entries = await loadSessionEntries3(
|
|
6322
|
+
async function sessionsList(ws, nowProvider) {
|
|
6323
|
+
const entries = await loadSessionEntries3(ws.paths, { now: nowProvider() });
|
|
5572
6324
|
const sessions = entries.map((entry) => ({
|
|
5573
6325
|
sessionId: entry.sessionId,
|
|
5574
6326
|
label: entry.session.session.label ?? null,
|
|
@@ -5583,10 +6335,10 @@ async function sessionsList(deps) {
|
|
|
5583
6335
|
})).reverse();
|
|
5584
6336
|
return { sessions };
|
|
5585
6337
|
}
|
|
5586
|
-
async function sessionDetail(
|
|
6338
|
+
async function sessionDetail(ws, sessionId) {
|
|
5587
6339
|
let session;
|
|
5588
6340
|
try {
|
|
5589
|
-
session = await readSessionYaml3(
|
|
6341
|
+
session = await readSessionYaml3(ws.paths, sessionId);
|
|
5590
6342
|
} catch (error) {
|
|
5591
6343
|
if (error instanceof Error && error.message === "YAML file not found") {
|
|
5592
6344
|
throw new HttpError(404, "Session not found");
|
|
@@ -5594,19 +6346,19 @@ async function sessionDetail(deps, sessionId) {
|
|
|
5594
6346
|
throw error;
|
|
5595
6347
|
}
|
|
5596
6348
|
try {
|
|
5597
|
-
const events = await readAllEvents2(
|
|
6349
|
+
const events = await readAllEvents2(join10(ws.paths.sessions, sessionId));
|
|
5598
6350
|
return { session, events };
|
|
5599
6351
|
} catch {
|
|
5600
6352
|
return { session, events: [], degraded: true };
|
|
5601
6353
|
}
|
|
5602
6354
|
}
|
|
5603
|
-
async function tasksList(
|
|
5604
|
-
const entries = await loadTaskEntries2(
|
|
6355
|
+
async function tasksList(ws) {
|
|
6356
|
+
const entries = await loadTaskEntries2(ws.paths);
|
|
5605
6357
|
return { tasks: entries.map((entry) => entry.task).reverse() };
|
|
5606
6358
|
}
|
|
5607
|
-
async function taskDetail(
|
|
6359
|
+
async function taskDetail(ws, taskId) {
|
|
5608
6360
|
try {
|
|
5609
|
-
const doc = await readTaskFile2(
|
|
6361
|
+
const doc = await readTaskFile2(ws.paths, taskId);
|
|
5610
6362
|
return { task: doc.task, body: doc.body };
|
|
5611
6363
|
} catch (error) {
|
|
5612
6364
|
if (error instanceof Error && error.message === "Task file not found") {
|
|
@@ -5615,22 +6367,22 @@ async function taskDetail(deps, taskId) {
|
|
|
5615
6367
|
throw error;
|
|
5616
6368
|
}
|
|
5617
6369
|
}
|
|
5618
|
-
async function decisionsView(
|
|
5619
|
-
const fromDisk = await readMarkdownFile4(
|
|
6370
|
+
async function decisionsView(ws, nowProvider) {
|
|
6371
|
+
const fromDisk = await readMarkdownFile4(ws.paths.files.decisions);
|
|
5620
6372
|
if (fromDisk !== null) {
|
|
5621
6373
|
return { body: fromDisk, fromDisk: true };
|
|
5622
6374
|
}
|
|
5623
|
-
const nowIso =
|
|
5624
|
-
const result = await renderDecisions3({ paths:
|
|
6375
|
+
const nowIso = nowProvider().toISOString();
|
|
6376
|
+
const result = await renderDecisions3({ paths: ws.paths, nowIso });
|
|
5625
6377
|
return { body: result.body, decisionCount: result.decisionCount, fromDisk: false };
|
|
5626
6378
|
}
|
|
5627
|
-
async function approvalsView(
|
|
5628
|
-
const now =
|
|
5629
|
-
const ids = await enumerateApprovals2(
|
|
6379
|
+
async function approvalsView(ws, nowProvider) {
|
|
6380
|
+
const now = nowProvider();
|
|
6381
|
+
const ids = await enumerateApprovals2(ws.paths);
|
|
5630
6382
|
const toViews = async (list) => {
|
|
5631
6383
|
const views = [];
|
|
5632
6384
|
for (const id of list) {
|
|
5633
|
-
const loaded = await loadApproval2(
|
|
6385
|
+
const loaded = await loadApproval2(ws.paths, id);
|
|
5634
6386
|
if (loaded === null) continue;
|
|
5635
6387
|
views.push({ id, expired: isLazyExpired2(loaded.approval, now), approval: loaded.approval });
|
|
5636
6388
|
}
|
|
@@ -5638,13 +6390,13 @@ async function approvalsView(deps) {
|
|
|
5638
6390
|
};
|
|
5639
6391
|
return { pending: await toViews(ids.pending), resolved: await toViews(ids.resolved) };
|
|
5640
6392
|
}
|
|
5641
|
-
async function handoffView(
|
|
5642
|
-
const fromDisk = await readMarkdownFile4(
|
|
6393
|
+
async function handoffView(ws, nowProvider) {
|
|
6394
|
+
const fromDisk = await readMarkdownFile4(ws.paths.files.handoff);
|
|
5643
6395
|
if (fromDisk !== null) {
|
|
5644
6396
|
return { body: fromDisk, fromDisk: true };
|
|
5645
6397
|
}
|
|
5646
|
-
const nowIso =
|
|
5647
|
-
const result = await renderHandoff3({ paths:
|
|
6398
|
+
const nowIso = nowProvider().toISOString();
|
|
6399
|
+
const result = await renderHandoff3({ paths: ws.paths, nowIso });
|
|
5648
6400
|
return { body: result.body, fromDisk: false };
|
|
5649
6401
|
}
|
|
5650
6402
|
function readActionOptions(body) {
|
|
@@ -5738,8 +6490,18 @@ function parsePort(value) {
|
|
|
5738
6490
|
}
|
|
5739
6491
|
return port;
|
|
5740
6492
|
}
|
|
6493
|
+
function collectPath3(value, previous = []) {
|
|
6494
|
+
return [...previous, value];
|
|
6495
|
+
}
|
|
5741
6496
|
function registerViewCommand(program2) {
|
|
5742
|
-
program2.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
|
+
program2.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) => {
|
|
5743
6505
|
await runView(options);
|
|
5744
6506
|
});
|
|
5745
6507
|
}
|
|
@@ -5753,23 +6515,37 @@ async function runView(options, ctx = {}) {
|
|
|
5753
6515
|
}
|
|
5754
6516
|
async function doRunView(options, ctx) {
|
|
5755
6517
|
const cwd = ctx.cwd ?? process.cwd();
|
|
5756
|
-
const
|
|
5757
|
-
const
|
|
5758
|
-
await
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
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
|
+
}
|
|
5769
6542
|
const port = options.port ?? DEFAULT_PORT;
|
|
5770
6543
|
const handle = await startListening(port, deps);
|
|
5771
6544
|
try {
|
|
5772
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
|
+
}
|
|
5773
6549
|
console.log(
|
|
5774
6550
|
"Localhost only, no authentication. Do not expose this port beyond your machine. Press Ctrl+C to stop."
|
|
5775
6551
|
);
|
|
@@ -5782,11 +6558,69 @@ async function doRunView(options, ctx) {
|
|
|
5782
6558
|
await handle.close();
|
|
5783
6559
|
}
|
|
5784
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
|
+
}
|
|
5785
6619
|
async function startListening(port, deps) {
|
|
5786
6620
|
try {
|
|
5787
6621
|
return await startViewServer({ port, deps });
|
|
5788
6622
|
} catch (error) {
|
|
5789
|
-
if (
|
|
6623
|
+
if (findErrorCode16(error, "EADDRINUSE")) {
|
|
5790
6624
|
throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
|
|
5791
6625
|
cause: error
|
|
5792
6626
|
});
|
|
@@ -5809,7 +6643,7 @@ function openInBrowser(url, override) {
|
|
|
5809
6643
|
}
|
|
5810
6644
|
}
|
|
5811
6645
|
function waitForShutdown(signal) {
|
|
5812
|
-
return new Promise((
|
|
6646
|
+
return new Promise((resolve7) => {
|
|
5813
6647
|
const cleanup = () => {
|
|
5814
6648
|
process.off("SIGINT", onSignal);
|
|
5815
6649
|
process.off("SIGTERM", onSignal);
|
|
@@ -5817,18 +6651,18 @@ function waitForShutdown(signal) {
|
|
|
5817
6651
|
};
|
|
5818
6652
|
const onSignal = () => {
|
|
5819
6653
|
cleanup();
|
|
5820
|
-
|
|
6654
|
+
resolve7();
|
|
5821
6655
|
};
|
|
5822
6656
|
const onAbort = () => {
|
|
5823
6657
|
cleanup();
|
|
5824
|
-
|
|
6658
|
+
resolve7();
|
|
5825
6659
|
};
|
|
5826
6660
|
process.on("SIGINT", onSignal);
|
|
5827
6661
|
process.on("SIGTERM", onSignal);
|
|
5828
6662
|
if (signal !== void 0) {
|
|
5829
6663
|
if (signal.aborted) {
|
|
5830
6664
|
cleanup();
|
|
5831
|
-
|
|
6665
|
+
resolve7();
|
|
5832
6666
|
return;
|
|
5833
6667
|
}
|
|
5834
6668
|
signal.addEventListener("abort", onAbort);
|
|
@@ -5847,11 +6681,11 @@ async function resolveRepositoryRootForView(cwd) {
|
|
|
5847
6681
|
throw error;
|
|
5848
6682
|
}
|
|
5849
6683
|
}
|
|
5850
|
-
async function
|
|
6684
|
+
async function assertWorkspaceInitialized13(basouRoot) {
|
|
5851
6685
|
try {
|
|
5852
|
-
await
|
|
6686
|
+
await assertBasouRootSafe16(basouRoot);
|
|
5853
6687
|
} catch (error) {
|
|
5854
|
-
if (
|
|
6688
|
+
if (findErrorCode16(error, "ENOENT")) {
|
|
5855
6689
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
5856
6690
|
}
|
|
5857
6691
|
throw error;
|
|
@@ -5880,6 +6714,8 @@ function buildProgram() {
|
|
|
5880
6714
|
registerTaskCommand(program2);
|
|
5881
6715
|
registerHandoffCommand(program2);
|
|
5882
6716
|
registerDecisionsCommand(program2);
|
|
6717
|
+
registerReportCommand(program2);
|
|
6718
|
+
registerOrientCommand(program2);
|
|
5883
6719
|
return program2;
|
|
5884
6720
|
}
|
|
5885
6721
|
|