@basou/cli 0.11.0 → 0.13.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 +3072 -357
- package/dist/index.js.map +1 -1
- package/dist/program.js +3072 -357
- 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,187 @@ async function resolveRepositoryRootForInit(cwd) {
|
|
|
2000
2008
|
}
|
|
2001
2009
|
}
|
|
2002
2010
|
|
|
2003
|
-
// src/commands/
|
|
2004
|
-
import {
|
|
2011
|
+
// src/commands/note.ts
|
|
2012
|
+
import {
|
|
2013
|
+
acquireLock as acquireLock4,
|
|
2014
|
+
appendEventToExistingSession as appendEventToExistingSession2,
|
|
2015
|
+
assertBasouRootSafe as assertBasouRootSafe7,
|
|
2016
|
+
basouPaths as basouPaths7,
|
|
2017
|
+
createAdHocSessionWithEvent as createAdHocSessionWithEvent2,
|
|
2018
|
+
findErrorCode as findErrorCode6,
|
|
2019
|
+
readManifest as readManifest4,
|
|
2020
|
+
resolveSessionId as resolveSessionId2
|
|
2021
|
+
} from "@basou/core";
|
|
2005
2022
|
import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
2006
2023
|
|
|
2024
|
+
// src/lib/repo-root.ts
|
|
2025
|
+
import { resolveBasouRepositoryRoot } from "@basou/core";
|
|
2026
|
+
async function resolveBasouRootForCommand(cwd, commandName) {
|
|
2027
|
+
try {
|
|
2028
|
+
return await resolveBasouRepositoryRoot(cwd, {
|
|
2029
|
+
onRedirect: ({ via, root }) => console.error(`Resolved workspace view to ${root} (via ${via}).`)
|
|
2030
|
+
});
|
|
2031
|
+
} catch (error) {
|
|
2032
|
+
if (error instanceof Error && error.message === "Not a git repository") {
|
|
2033
|
+
throw new Error(
|
|
2034
|
+
`Not a git repository. Run 'git init' first, then re-run 'basou ${commandName}'.`,
|
|
2035
|
+
{ cause: error }
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
throw error;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
// src/commands/note.ts
|
|
2043
|
+
var LABEL_BODY_MAX = 80;
|
|
2044
|
+
var LABEL_TRUNCATE_HEAD2 = LABEL_BODY_MAX - 3;
|
|
2045
|
+
function registerNoteCommand(program2) {
|
|
2046
|
+
program2.command("note").description("Record a free-text note (orientation surfaces the latest as the next step)").argument("<body>", "Note text", parseBody).option(
|
|
2047
|
+
"--session <session_id>",
|
|
2048
|
+
"Attach to an existing session; otherwise an ad-hoc session is created"
|
|
2049
|
+
).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (body, options) => {
|
|
2050
|
+
await runNote(body, options);
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
async function runNote(body, options, ctx = {}) {
|
|
2054
|
+
try {
|
|
2055
|
+
await doRunNote(body, options, ctx);
|
|
2056
|
+
} catch (error) {
|
|
2057
|
+
renderCliError(error, {
|
|
2058
|
+
verbose: isVerbose(options),
|
|
2059
|
+
classifiers: [failedToFinalizeClassifier]
|
|
2060
|
+
});
|
|
2061
|
+
process.exitCode = 1;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
async function doRunNote(body, options, ctx) {
|
|
2065
|
+
if (body.trim().length === 0) {
|
|
2066
|
+
throw new Error("Note body must not be empty");
|
|
2067
|
+
}
|
|
2068
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
2069
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "note");
|
|
2070
|
+
const paths = basouPaths7(repositoryRoot);
|
|
2071
|
+
await assertWorkspaceInitialized6(paths.root);
|
|
2072
|
+
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
2073
|
+
const occurredAt = now.toISOString();
|
|
2074
|
+
if (options.session !== void 0) {
|
|
2075
|
+
const sessionId = await resolveSessionId2(paths, options.session);
|
|
2076
|
+
const sesId = sessionId;
|
|
2077
|
+
const sessionLock = await acquireLock4(paths, "session", sesId);
|
|
2078
|
+
let result;
|
|
2079
|
+
try {
|
|
2080
|
+
result = await appendEventToExistingSession2({
|
|
2081
|
+
paths,
|
|
2082
|
+
sessionId: sesId,
|
|
2083
|
+
eventBuilder: (eventId) => buildNoteEvent({ eventId, sessionId: sesId, occurredAt, body })
|
|
2084
|
+
});
|
|
2085
|
+
} finally {
|
|
2086
|
+
await sessionLock.release();
|
|
2087
|
+
}
|
|
2088
|
+
printNoteResult(options, {
|
|
2089
|
+
mode: "attached",
|
|
2090
|
+
sessionId,
|
|
2091
|
+
eventId: result.eventId,
|
|
2092
|
+
sessionStatus: result.sessionStatus,
|
|
2093
|
+
body
|
|
2094
|
+
});
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
const manifest = await readManifest4(paths);
|
|
2098
|
+
const adHoc = await createAdHocSessionWithEvent2({
|
|
2099
|
+
paths,
|
|
2100
|
+
manifest,
|
|
2101
|
+
label: buildAdHocLabel2(body),
|
|
2102
|
+
occurredAt,
|
|
2103
|
+
sessionSource: "human",
|
|
2104
|
+
workingDirectory: repositoryRoot,
|
|
2105
|
+
invocation: {
|
|
2106
|
+
command: "basou note",
|
|
2107
|
+
args: [body]
|
|
2108
|
+
},
|
|
2109
|
+
targetEventBuilders: [
|
|
2110
|
+
(sessionId, eventId) => buildNoteEvent({ eventId, sessionId, occurredAt, body })
|
|
2111
|
+
]
|
|
2112
|
+
});
|
|
2113
|
+
printNoteResult(options, {
|
|
2114
|
+
mode: "ad-hoc",
|
|
2115
|
+
sessionId: adHoc.sessionId,
|
|
2116
|
+
eventId: adHoc.targetEventIds[0],
|
|
2117
|
+
sessionStatus: "completed",
|
|
2118
|
+
body
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
function buildNoteEvent(input) {
|
|
2122
|
+
return {
|
|
2123
|
+
schema_version: "0.1.0",
|
|
2124
|
+
id: input.eventId,
|
|
2125
|
+
session_id: input.sessionId,
|
|
2126
|
+
occurred_at: input.occurredAt,
|
|
2127
|
+
source: "local-cli",
|
|
2128
|
+
type: "note_added",
|
|
2129
|
+
body: input.body,
|
|
2130
|
+
// `basou note` is the resume-hint command; mark it so orientation surfaces
|
|
2131
|
+
// it as the next step and a plain `basou session note` annotation does not.
|
|
2132
|
+
kind: "next_step"
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
function buildAdHocLabel2(body) {
|
|
2136
|
+
const oneLine = body.replace(/\s+/g, " ").trim();
|
|
2137
|
+
const truncated = oneLine.length > LABEL_BODY_MAX ? `${oneLine.slice(0, LABEL_TRUNCATE_HEAD2)}...` : oneLine;
|
|
2138
|
+
return `Ad-hoc note: ${truncated}`;
|
|
2139
|
+
}
|
|
2140
|
+
function parseBody(raw) {
|
|
2141
|
+
if (raw.trim().length === 0) {
|
|
2142
|
+
throw new InvalidArgumentError2("Note body must not be empty");
|
|
2143
|
+
}
|
|
2144
|
+
return raw;
|
|
2145
|
+
}
|
|
2146
|
+
function printNoteResult(options, result) {
|
|
2147
|
+
const sid = shortSessionId(result.sessionId);
|
|
2148
|
+
if (options.json === true) {
|
|
2149
|
+
console.log(
|
|
2150
|
+
JSON.stringify({
|
|
2151
|
+
event_id: result.eventId,
|
|
2152
|
+
session_id: result.sessionId,
|
|
2153
|
+
session_status: result.sessionStatus,
|
|
2154
|
+
mode: result.mode,
|
|
2155
|
+
body: result.body
|
|
2156
|
+
})
|
|
2157
|
+
);
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
if (result.mode === "ad-hoc") {
|
|
2161
|
+
console.log(`Recorded note ${result.eventId} in ad-hoc session ${sid}`);
|
|
2162
|
+
} else {
|
|
2163
|
+
console.log(`Recorded note ${result.eventId} in session ${sid} (${result.sessionStatus})`);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
async function assertWorkspaceInitialized6(basouRoot) {
|
|
2167
|
+
try {
|
|
2168
|
+
await assertBasouRootSafe7(basouRoot);
|
|
2169
|
+
} catch (error) {
|
|
2170
|
+
if (findErrorCode6(error, "ENOENT")) {
|
|
2171
|
+
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
2172
|
+
}
|
|
2173
|
+
throw error;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// src/commands/orient.ts
|
|
2178
|
+
import {
|
|
2179
|
+
assertBasouRootSafe as assertBasouRootSafe8,
|
|
2180
|
+
basouPaths as basouPaths8,
|
|
2181
|
+
findErrorCode as findErrorCode7,
|
|
2182
|
+
renderOrientation as renderOrientation2,
|
|
2183
|
+
writeMarkdownFile as writeMarkdownFile4
|
|
2184
|
+
} from "@basou/core";
|
|
2185
|
+
|
|
2007
2186
|
// src/lib/provenance-actions.ts
|
|
2008
2187
|
import {
|
|
2009
2188
|
readMarkdownFile as readMarkdownFile3,
|
|
2010
2189
|
renderDecisions as renderDecisions2,
|
|
2011
2190
|
renderHandoff as renderHandoff2,
|
|
2191
|
+
renderOrientation,
|
|
2012
2192
|
renderWithMarkers as renderWithMarkers3,
|
|
2013
2193
|
writeMarkdownFile as writeMarkdownFile3
|
|
2014
2194
|
} from "@basou/core";
|
|
@@ -2059,6 +2239,7 @@ async function runImport(adapter, fn) {
|
|
|
2059
2239
|
skippedNoAction: readCount(json.skipped_no_action),
|
|
2060
2240
|
skippedAlreadyImported: readCount(json.skipped_already_imported),
|
|
2061
2241
|
skippedLegacyUntracked: readCount(json.skipped_legacy_untracked),
|
|
2242
|
+
skippedUnverifiable: readCount(json.skipped_unverifiable),
|
|
2062
2243
|
eventTotal: readCount(json.event_total),
|
|
2063
2244
|
dryRun: json.dry_run === true
|
|
2064
2245
|
};
|
|
@@ -2105,40 +2286,1889 @@ async function regenerateDecisions(paths, nowIso, callbacks) {
|
|
|
2105
2286
|
paths.files.decisions,
|
|
2106
2287
|
renderWithMarkers3(existing, result.body, "decisions.md")
|
|
2107
2288
|
);
|
|
2108
|
-
return { decisionCount: result.decisionCount };
|
|
2289
|
+
return { decisionCount: result.decisionCount };
|
|
2290
|
+
}
|
|
2291
|
+
async function regenerateOrientation(paths, nowIso, callbacks) {
|
|
2292
|
+
const result = await renderOrientation({ paths, nowIso, ...callbacks });
|
|
2293
|
+
await writeMarkdownFile3(paths.files.orientation, `${result.body}
|
|
2294
|
+
`);
|
|
2295
|
+
return {
|
|
2296
|
+
sessionCount: result.sessionCount,
|
|
2297
|
+
inFlightTaskCount: result.inFlightTaskCount,
|
|
2298
|
+
pendingApprovalsCount: result.pendingApprovalsCount,
|
|
2299
|
+
suspectCount: result.suspectCount
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
async function refreshAll(args) {
|
|
2303
|
+
const { options, ctx, paths, nowIso } = args;
|
|
2304
|
+
const dryRun = options.dryRun === true;
|
|
2305
|
+
const claudeCode = await importClaudeCode(options, ctx);
|
|
2306
|
+
const codex = await importCodex(options, ctx);
|
|
2307
|
+
if (dryRun) {
|
|
2308
|
+
const skipped = { status: "skipped", reason: "dry-run" };
|
|
2309
|
+
return {
|
|
2310
|
+
claudeCode,
|
|
2311
|
+
codex,
|
|
2312
|
+
handoff: skipped,
|
|
2313
|
+
decisions: skipped,
|
|
2314
|
+
orientation: skipped,
|
|
2315
|
+
dryRun
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
const handoffCounts = await regenerateHandoff(paths, nowIso);
|
|
2319
|
+
const decisionCounts = await regenerateDecisions(paths, nowIso);
|
|
2320
|
+
const scoped = options.project !== void 0 && options.project.length > 0;
|
|
2321
|
+
const orientationCounts = await regenerateOrientation(
|
|
2322
|
+
paths,
|
|
2323
|
+
nowIso,
|
|
2324
|
+
scoped ? {} : {
|
|
2325
|
+
staleness: {
|
|
2326
|
+
newSessions: 0,
|
|
2327
|
+
updatedSessions: 0,
|
|
2328
|
+
unverifiableSessions: wouldBlock(claudeCode) + wouldBlock(codex)
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
);
|
|
2332
|
+
return {
|
|
2333
|
+
claudeCode,
|
|
2334
|
+
codex,
|
|
2335
|
+
handoff: { status: "generated", ...handoffCounts },
|
|
2336
|
+
decisions: { status: "generated", ...decisionCounts },
|
|
2337
|
+
orientation: { status: "generated", ...orientationCounts },
|
|
2338
|
+
dryRun
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
function wouldImport(outcome) {
|
|
2342
|
+
return outcome.status === "ran" ? outcome.importedCount : 0;
|
|
2343
|
+
}
|
|
2344
|
+
function wouldUpdate(outcome) {
|
|
2345
|
+
return outcome.status === "ran" ? outcome.reimportedCount + outcome.replacedCount : 0;
|
|
2346
|
+
}
|
|
2347
|
+
function wouldBlock(outcome) {
|
|
2348
|
+
return outcome.status === "ran" ? outcome.skippedUnverifiable : 0;
|
|
2349
|
+
}
|
|
2350
|
+
async function probeStaleness(args) {
|
|
2351
|
+
try {
|
|
2352
|
+
const dry = await refreshAll({
|
|
2353
|
+
options: { dryRun: true },
|
|
2354
|
+
ctx: args.ctx,
|
|
2355
|
+
paths: args.paths,
|
|
2356
|
+
nowIso: args.nowIso
|
|
2357
|
+
});
|
|
2358
|
+
return {
|
|
2359
|
+
newSessions: wouldImport(dry.claudeCode) + wouldImport(dry.codex),
|
|
2360
|
+
updatedSessions: wouldUpdate(dry.claudeCode) + wouldUpdate(dry.codex),
|
|
2361
|
+
unverifiableSessions: wouldBlock(dry.claudeCode) + wouldBlock(dry.codex)
|
|
2362
|
+
};
|
|
2363
|
+
} catch {
|
|
2364
|
+
return null;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
// src/commands/orient.ts
|
|
2369
|
+
function registerOrientCommand(program2) {
|
|
2370
|
+
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) => {
|
|
2371
|
+
await runOrient(opts);
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
async function runOrient(options, ctx = {}) {
|
|
2375
|
+
try {
|
|
2376
|
+
await doRunOrient(options, ctx);
|
|
2377
|
+
} catch (error) {
|
|
2378
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
2379
|
+
process.exitCode = 1;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
async function doRunOrient(options, ctx) {
|
|
2383
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
2384
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "orient");
|
|
2385
|
+
const paths = basouPaths8(repositoryRoot);
|
|
2386
|
+
await assertWorkspaceInitialized7(paths.root);
|
|
2387
|
+
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2388
|
+
const probeCtx = { cwd: repositoryRoot };
|
|
2389
|
+
if (ctx.claudeProjectsDir !== void 0) probeCtx.claudeProjectsDir = ctx.claudeProjectsDir;
|
|
2390
|
+
if (ctx.codexSessionsDir !== void 0) probeCtx.codexSessionsDir = ctx.codexSessionsDir;
|
|
2391
|
+
const staleness = await probeStaleness({ ctx: probeCtx, paths, nowIso });
|
|
2392
|
+
const result = await renderOrientation2({
|
|
2393
|
+
paths,
|
|
2394
|
+
nowIso,
|
|
2395
|
+
staleness,
|
|
2396
|
+
verbose: options.verbose === true,
|
|
2397
|
+
onWarning: (w, sid) => printReplayWarning(w, sid),
|
|
2398
|
+
onSessionSkip: (sid, reason) => printSessionSkip(sid, reason),
|
|
2399
|
+
onTaskSkip: (taskId, reason) => printTaskSkip(taskId, reason)
|
|
2400
|
+
});
|
|
2401
|
+
await writeMarkdownFile4(paths.files.orientation, `${result.body}
|
|
2402
|
+
`);
|
|
2403
|
+
if (options.quiet === true) {
|
|
2404
|
+
console.log(
|
|
2405
|
+
`Generated .basou/orientation.md (sessions: ${result.sessionCount}, in-flight tasks: ${result.inFlightTaskCount}, pending approvals: ${result.pendingApprovalsCount}, suspect: ${result.suspectCount})`
|
|
2406
|
+
);
|
|
2407
|
+
} else {
|
|
2408
|
+
console.log(result.body);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
async function assertWorkspaceInitialized7(basouRoot) {
|
|
2412
|
+
try {
|
|
2413
|
+
await assertBasouRootSafe8(basouRoot);
|
|
2414
|
+
} catch (error) {
|
|
2415
|
+
if (findErrorCode7(error, "ENOENT")) {
|
|
2416
|
+
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
2417
|
+
}
|
|
2418
|
+
throw error;
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
// src/commands/project.ts
|
|
2423
|
+
import {
|
|
2424
|
+
existsSync,
|
|
2425
|
+
lstatSync,
|
|
2426
|
+
mkdirSync,
|
|
2427
|
+
readdirSync,
|
|
2428
|
+
readFileSync,
|
|
2429
|
+
readlinkSync,
|
|
2430
|
+
realpathSync,
|
|
2431
|
+
statSync,
|
|
2432
|
+
symlinkSync,
|
|
2433
|
+
unlinkSync,
|
|
2434
|
+
writeFileSync
|
|
2435
|
+
} from "fs";
|
|
2436
|
+
import { basename as basename3, dirname, isAbsolute, join as join4, relative as relative2, resolve as resolve3 } from "path";
|
|
2437
|
+
import {
|
|
2438
|
+
basouPaths as basouPaths9,
|
|
2439
|
+
GENERATED_END,
|
|
2440
|
+
GENERATED_START,
|
|
2441
|
+
isGitNotFound,
|
|
2442
|
+
parseMarkers,
|
|
2443
|
+
pathBasename,
|
|
2444
|
+
planArchive,
|
|
2445
|
+
planGitignore,
|
|
2446
|
+
planRename,
|
|
2447
|
+
planRosterAdoption,
|
|
2448
|
+
planWorkspaceView,
|
|
2449
|
+
readManifest as readManifest5,
|
|
2450
|
+
readMarkdownFile as readMarkdownFile4,
|
|
2451
|
+
reconcileSourceRoots,
|
|
2452
|
+
renderWithMarkers as renderWithMarkers4,
|
|
2453
|
+
safeSimpleGit,
|
|
2454
|
+
summarizePresetPlan,
|
|
2455
|
+
summarizeRosterDrift,
|
|
2456
|
+
summarizeSymlinkPlan,
|
|
2457
|
+
summarizeWiring,
|
|
2458
|
+
unknownManifestKeys,
|
|
2459
|
+
writeManifest as writeManifest2,
|
|
2460
|
+
writeMarkdownFile as writeMarkdownFile5
|
|
2461
|
+
} from "@basou/core";
|
|
2462
|
+
var INSTRUCTION_FILES = ["AGENTS.md", "CLAUDE.md", ".github/copilot-instructions.md"];
|
|
2463
|
+
var CANONICAL_FILE = "AGENTS.md";
|
|
2464
|
+
function registerProjectCommand(program2) {
|
|
2465
|
+
const project = program2.command("project").description("Inspect a project's declared repo roster (read-only)");
|
|
2466
|
+
project.command("check").description(
|
|
2467
|
+
"Compare the declared repo roster (manifest `repos`) against the capture config (`source_roots`) and surface drift (read-only, advisory)"
|
|
2468
|
+
).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
2469
|
+
await runProjectCheck(opts);
|
|
2470
|
+
});
|
|
2471
|
+
project.command("sync").description(
|
|
2472
|
+
"Reconcile the capture config (`source_roots`) to cover every declared repo (manifest `repos`). Dry-run by default; pass --apply to write. Additive only \u2014 it never removes an existing source root (e.g. the workspace view)"
|
|
2473
|
+
).option(
|
|
2474
|
+
"--apply",
|
|
2475
|
+
"Write the reconciled source_roots to the manifest (default: dry-run preview)"
|
|
2476
|
+
).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
2477
|
+
await runProjectSync(opts);
|
|
2478
|
+
});
|
|
2479
|
+
project.command("adopt").description(
|
|
2480
|
+
"Bootstrap a repo roster (manifest `repos`) from the existing capture config (`source_roots`): classify each by realpath + `.git`, keep the git repos, exclude non-repos (the workspace view, /tmp). Dry-run by default; pass --apply to write (refuses if a roster already exists)"
|
|
2481
|
+
).option("--apply", "Write the bootstrapped roster to the manifest (default: dry-run preview)").option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
2482
|
+
await runProjectAdopt(opts);
|
|
2483
|
+
});
|
|
2484
|
+
project.command("wiring").description(
|
|
2485
|
+
"Inspect each declared repo's agent instruction-file wiring (AGENTS.md, CLAUDE.md, copilot-instructions.md): present? tracked by git? Surfaces privacy risks (a public repo tracking an instruction file) and gaps (read-only, advisory)"
|
|
2486
|
+
).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
2487
|
+
await runProjectWiring(opts);
|
|
2488
|
+
});
|
|
2489
|
+
project.command("gitignore").description(
|
|
2490
|
+
"Reconcile each public-facing repo's .gitignore to exclude the agent instruction files (so the gitignored symlinks never enter public history). Dry-run by default; pass --apply to write. Additive only \u2014 it never removes a line; private repos and unset-visibility repos are left untouched"
|
|
2491
|
+
).option(
|
|
2492
|
+
"--apply",
|
|
2493
|
+
"Append the missing patterns to each repo's .gitignore (default: dry-run preview)"
|
|
2494
|
+
).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
2495
|
+
await runProjectGitignore(opts);
|
|
2496
|
+
});
|
|
2497
|
+
project.command("symlinks").description(
|
|
2498
|
+
"Generate each declared repo's agent instruction-file symlinks (AGENTS.md, CLAUDE.md, copilot-instructions.md) pointing at the project anchor's canonical (agents/<repo>/AGENTS.md). Dry-run by default; pass --apply to create. Non-destructive \u2014 it only creates missing links and never overwrites an existing file or repoints a link"
|
|
2499
|
+
).option("--apply", "Create the missing instruction-file symlinks (default: dry-run preview)").option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
2500
|
+
await runProjectSymlinks(opts);
|
|
2501
|
+
});
|
|
2502
|
+
project.command("workspace").description(
|
|
2503
|
+
"Generate the project's workspace view: a directory (manifest `workspace.view`) that aggregates every declared repo via a `<repo-basename>` symlink (the anchor included). Dry-run by default; pass --apply to create missing links. Creation is non-destructive \u2014 it never overwrites an existing entry or repoints a link. Stray repo links (a view symlink whose repo is no longer in the roster) are reported always and removed only with --prune; pruning removes ONLY a symlink whose relative target resolves to a git repository (never a real file/dir, the view's own instruction files, a broken link, or a non-repo target), and never the linked repo itself"
|
|
2504
|
+
).option("--apply", "Create the missing view symlinks (default: dry-run preview)").option(
|
|
2505
|
+
"--prune",
|
|
2506
|
+
"Remove stray repo symlinks (links the roster no longer backs); default: dry-run preview. Independent of --apply"
|
|
2507
|
+
).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
2508
|
+
await runProjectWorkspace(opts);
|
|
2509
|
+
});
|
|
2510
|
+
project.command("preset").description(
|
|
2511
|
+
"Generate the stable-preset block (source visibility, source language, published surfaces) of each declared repo's canonical instruction file (agents/<repo>/AGENTS.md) from the manifest. Dry-run by default; pass --apply to write. Non-destructive \u2014 it only writes the marker-delimited region (creating an absent canonical, updating an out-of-date one) and never touches hand-authored content or a canonical whose markers are missing/malformed"
|
|
2512
|
+
).option(
|
|
2513
|
+
"--apply",
|
|
2514
|
+
"Write the generated preset block to each canonical (default: dry-run preview)"
|
|
2515
|
+
).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
2516
|
+
await runProjectPreset(opts);
|
|
2517
|
+
});
|
|
2518
|
+
project.command("archive").argument("<repo>", "The roster repo path to archive (as declared, e.g. ../takuhon)").description(
|
|
2519
|
+
"Fold a repo out of the project: remove it from the declared roster (manifest `repos`) and prune its capture entry (`source_roots`). Dry-run by default; pass --apply to write. Manifest-only and reversible (the manifest is git-tracked); it never deletes the repo, its captured history, or its on-disk wiring (view symlink / instruction symlinks / .gitignore / canonical) \u2014 those are reported as a manual teardown checklist. Archiving the anchor (`.`) is refused"
|
|
2520
|
+
).option(
|
|
2521
|
+
"--apply",
|
|
2522
|
+
"Write the pruned roster / source_roots to the manifest (default: dry-run preview)"
|
|
2523
|
+
).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (repo, opts) => {
|
|
2524
|
+
await runProjectArchive(repo, opts);
|
|
2525
|
+
});
|
|
2526
|
+
project.command("rename").argument("<old>", "The current roster repo path (as declared, e.g. ../takuhon)").argument("<new>", "The new roster repo path (e.g. ../takuhon-cli)").description(
|
|
2527
|
+
"Re-path a repo in the project: update its declared roster path (manifest `repos`) and its capture entry (`source_roots`). Dry-run by default; pass --apply to write. Manifest-only and reversible (the manifest is git-tracked); it does not move the repo on disk or rewire it \u2014 when the basename changes, the anchor canonical dir and view symlink that still use the old name are reported as a manual checklist (re-run `basou project symlinks` / `workspace` after). Renaming the anchor (`.`) or onto an existing entry is refused"
|
|
2528
|
+
).option(
|
|
2529
|
+
"--apply",
|
|
2530
|
+
"Write the re-pathed roster / source_roots to the manifest (default: dry-run preview)"
|
|
2531
|
+
).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (oldPath, newPath, opts) => {
|
|
2532
|
+
await runProjectRename(oldPath, newPath, opts);
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2535
|
+
async function runProjectCheck(options, ctx = {}) {
|
|
2536
|
+
try {
|
|
2537
|
+
await doRunProjectCheck(options, ctx);
|
|
2538
|
+
} catch (error) {
|
|
2539
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
2540
|
+
process.exitCode = 1;
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
function effectiveSourceRoots(manifest) {
|
|
2544
|
+
return manifest.import?.source_roots ?? ["."];
|
|
2545
|
+
}
|
|
2546
|
+
function preservedUnknownLines(fields) {
|
|
2547
|
+
if (fields.length === 0) return [];
|
|
2548
|
+
return [
|
|
2549
|
+
`\u2139\uFE0F basou \u304C\u8A8D\u8B58\u3057\u306A\u3044 manifest \u306E\u30C8\u30C3\u30D7\u30EC\u30D9\u30EB\u30D5\u30A3\u30FC\u30EB\u30C9\u3092 ${fields.length} \u4EF6\u4FDD\u6301\u3057\u3066\u3044\u307E\u3059(write \u6642\u3082\u524A\u9664\u3057\u307E\u305B\u3093): ${fields.join(", ")}`,
|
|
2550
|
+
""
|
|
2551
|
+
];
|
|
2552
|
+
}
|
|
2553
|
+
async function doRunProjectCheck(options, ctx) {
|
|
2554
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
2555
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project check");
|
|
2556
|
+
const paths = basouPaths9(repositoryRoot);
|
|
2557
|
+
const manifest = await readManifest5(paths);
|
|
2558
|
+
const summary = summarizeRosterDrift({
|
|
2559
|
+
...manifest.repos !== void 0 ? { repos: manifest.repos } : {},
|
|
2560
|
+
sourceRoots: effectiveSourceRoots(manifest)
|
|
2561
|
+
});
|
|
2562
|
+
if (options.json === true) {
|
|
2563
|
+
console.log(JSON.stringify(summary));
|
|
2564
|
+
} else {
|
|
2565
|
+
console.log(renderProjectCheck(summary));
|
|
2566
|
+
}
|
|
2567
|
+
return summary;
|
|
2568
|
+
}
|
|
2569
|
+
function renderProjectCheck(summary) {
|
|
2570
|
+
const lines = [];
|
|
2571
|
+
lines.push("# \u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u69CB\u6210\u30C1\u30A7\u30C3\u30AF(\u5BA3\u8A00 vs \u6355\u6349)");
|
|
2572
|
+
lines.push("");
|
|
2573
|
+
if (summary.declaredCount === 0) {
|
|
2574
|
+
lines.push(
|
|
2575
|
+
"\u2139\uFE0F repo \u30ED\u30FC\u30B9\u30BF\u30FC\u304C\u672A\u5BA3\u8A00\u3067\u3059(manifest \u306E `repos`)\u3002`source_roots` \u306E\u307F\u3067\u904B\u7528\u4E2D\u306E\u305F\u3081\u3001\u5BA3\u8A00\u3068\u306E\u7167\u5408\u306F\u3067\u304D\u307E\u305B\u3093\u3002"
|
|
2576
|
+
);
|
|
2577
|
+
if (summary.extra.length > 0) {
|
|
2578
|
+
lines.push("");
|
|
2579
|
+
lines.push(`\u6355\u6349\u4E2D\u306E source_roots (${summary.extra.length}):`);
|
|
2580
|
+
for (const p of summary.extra) lines.push(`- ${p}`);
|
|
2581
|
+
}
|
|
2582
|
+
return lines.join("\n");
|
|
2583
|
+
}
|
|
2584
|
+
if (summary.gaps.length === 0) {
|
|
2585
|
+
lines.push(
|
|
2586
|
+
`\u2705 \u5BA3\u8A00\u3055\u308C\u305F ${summary.declaredCount} repo \u306F\u3059\u3079\u3066\u6355\u6349\u5BFE\u8C61(source_roots)\u306B\u542B\u307E\u308C\u3066\u3044\u307E\u3059\u3002`
|
|
2587
|
+
);
|
|
2588
|
+
} else {
|
|
2589
|
+
lines.push(`\u26A0\uFE0F \u5BA3\u8A00\u3055\u308C\u3066\u3044\u308B\u306E\u306B\u6355\u6349\u5BFE\u8C61\u306B\u7121\u3044 repo: ${summary.gaps.length}(\u53D6\u308A\u3053\u307C\u3057)`);
|
|
2590
|
+
for (const g of summary.gaps) {
|
|
2591
|
+
lines.push(`- ${g.path}${g.visibility ? ` [${g.visibility}]` : ""} \u2014 source_roots \u306B\u672A\u767B\u9332`);
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
lines.push("");
|
|
2595
|
+
if (summary.extra.length > 0) {
|
|
2596
|
+
lines.push(
|
|
2597
|
+
`## \u5BA3\u8A00\u5916\u306E\u6355\u6349\u5BFE\u8C61 (${summary.extra.length}) \u2014 workspace view \u304B\u3001\u5BA3\u8A00\u6F0F\u308C\u306E\u53EF\u80FD\u6027`
|
|
2598
|
+
);
|
|
2599
|
+
for (const p of summary.extra) lines.push(`- ${p}`);
|
|
2600
|
+
lines.push("");
|
|
2601
|
+
}
|
|
2602
|
+
lines.push(
|
|
2603
|
+
"\u6CE8: read-only \u306E advisory \u3067\u3059\u3002\u5BA3\u8A00(repos)\u3068\u6355\u6349\u8A2D\u5B9A(source_roots)\u306E\u5DEE\u5206\u306E\u307F\u3092\u8868\u793A\u3057\u3001enforce \u306F\u3057\u307E\u305B\u3093\u3002"
|
|
2604
|
+
);
|
|
2605
|
+
return lines.join("\n");
|
|
2606
|
+
}
|
|
2607
|
+
async function runProjectSync(options, ctx = {}) {
|
|
2608
|
+
try {
|
|
2609
|
+
await doRunProjectSync(options, ctx);
|
|
2610
|
+
} catch (error) {
|
|
2611
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
2612
|
+
process.exitCode = 1;
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
async function doRunProjectSync(options, ctx) {
|
|
2616
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
2617
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project sync");
|
|
2618
|
+
const paths = basouPaths9(repositoryRoot);
|
|
2619
|
+
const manifest = await readManifest5(paths);
|
|
2620
|
+
const hasRoster = manifest.repos !== void 0 && manifest.repos.length > 0;
|
|
2621
|
+
const reconcile = reconcileSourceRoots({
|
|
2622
|
+
...manifest.repos !== void 0 ? { repos: manifest.repos } : {},
|
|
2623
|
+
...manifest.import?.source_roots !== void 0 ? { sourceRoots: manifest.import.source_roots } : {}
|
|
2624
|
+
});
|
|
2625
|
+
const applied = options.apply === true && hasRoster && !reconcile.unchanged;
|
|
2626
|
+
if (applied) {
|
|
2627
|
+
const now = ctx.now ?? (() => /* @__PURE__ */ new Date());
|
|
2628
|
+
await writeManifest2(
|
|
2629
|
+
paths,
|
|
2630
|
+
{
|
|
2631
|
+
...manifest,
|
|
2632
|
+
import: { ...manifest.import, source_roots: reconcile.next },
|
|
2633
|
+
workspace: { ...manifest.workspace, updated_at: now().toISOString() }
|
|
2634
|
+
},
|
|
2635
|
+
{ force: true }
|
|
2636
|
+
);
|
|
2637
|
+
}
|
|
2638
|
+
const result = {
|
|
2639
|
+
...reconcile,
|
|
2640
|
+
hasRoster,
|
|
2641
|
+
applied,
|
|
2642
|
+
preservedUnknownFields: unknownManifestKeys(manifest)
|
|
2643
|
+
};
|
|
2644
|
+
if (options.json === true) {
|
|
2645
|
+
console.log(JSON.stringify(result));
|
|
2646
|
+
} else {
|
|
2647
|
+
console.log(renderProjectSync(result));
|
|
2648
|
+
}
|
|
2649
|
+
return result;
|
|
2650
|
+
}
|
|
2651
|
+
function renderProjectSync(result) {
|
|
2652
|
+
const lines = [];
|
|
2653
|
+
lines.push("# source_roots \u540C\u671F(\u5BA3\u8A00\u30ED\u30FC\u30B9\u30BF\u30FC \u2192 \u6355\u6349\u8A2D\u5B9A)");
|
|
2654
|
+
lines.push("");
|
|
2655
|
+
lines.push(...preservedUnknownLines(result.preservedUnknownFields));
|
|
2656
|
+
if (!result.hasRoster) {
|
|
2657
|
+
lines.push(
|
|
2658
|
+
"\u2139\uFE0F repo \u30ED\u30FC\u30B9\u30BF\u30FC\u304C\u672A\u5BA3\u8A00\u3067\u3059(manifest \u306E `repos`)\u3002\u540C\u671F\u306E\u5143\u306B\u306A\u308B\u5BA3\u8A00\u304C\u7121\u3044\u305F\u3081\u3001\u5909\u66F4\u306F\u3042\u308A\u307E\u305B\u3093\u3002"
|
|
2659
|
+
);
|
|
2660
|
+
return lines.join("\n");
|
|
2661
|
+
}
|
|
2662
|
+
if (result.unchanged) {
|
|
2663
|
+
lines.push("\u2705 source_roots \u306F\u5BA3\u8A00\u30ED\u30FC\u30B9\u30BF\u30FC\u3092\u3059\u3079\u3066\u8986\u3063\u3066\u3044\u307E\u3059(\u540C\u671F\u4E0D\u8981)\u3002");
|
|
2664
|
+
return lines.join("\n");
|
|
2665
|
+
}
|
|
2666
|
+
if (result.applied) {
|
|
2667
|
+
lines.push(`\u2705 source_roots \u306B ${result.added.length} \u4EF6\u8FFD\u52A0\u3057\u307E\u3057\u305F:`);
|
|
2668
|
+
for (const p of result.added) lines.push(`- ${p}`);
|
|
2669
|
+
} else {
|
|
2670
|
+
lines.push(
|
|
2671
|
+
`${result.added.length} \u4EF6\u306E repo \u304C source_roots \u306B\u672A\u767B\u9332\u3067\u3059\u3002\u8FFD\u52A0\u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`
|
|
2672
|
+
);
|
|
2673
|
+
for (const p of result.added) lines.push(`- ${p}`);
|
|
2674
|
+
lines.push("");
|
|
2675
|
+
lines.push("\u6CE8: \u65E2\u5B58\u306E source_roots \u306F\u4FDD\u6301\u3057\u3001\u4E0D\u8DB3\u5206\u306E\u8FFD\u8A18\u306E\u307F\u884C\u3044\u307E\u3059(\u524A\u9664\u306F\u3057\u307E\u305B\u3093)\u3002");
|
|
2676
|
+
}
|
|
2677
|
+
return lines.join("\n");
|
|
2678
|
+
}
|
|
2679
|
+
async function runProjectAdopt(options, ctx = {}) {
|
|
2680
|
+
try {
|
|
2681
|
+
await doRunProjectAdopt(options, ctx);
|
|
2682
|
+
} catch (error) {
|
|
2683
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
2684
|
+
process.exitCode = 1;
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
function classifySourceRoot(repositoryRoot, declaredPath) {
|
|
2688
|
+
const absolute = resolve3(repositoryRoot, declaredPath);
|
|
2689
|
+
let real;
|
|
2690
|
+
try {
|
|
2691
|
+
real = realpathSync(absolute);
|
|
2692
|
+
} catch {
|
|
2693
|
+
return { path: declaredPath, kind: "unresolved" };
|
|
2694
|
+
}
|
|
2695
|
+
return { path: declaredPath, kind: existsSync(join4(real, ".git")) ? "repo" : "non-repo" };
|
|
2696
|
+
}
|
|
2697
|
+
async function doRunProjectAdopt(options, ctx) {
|
|
2698
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
2699
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project adopt");
|
|
2700
|
+
const paths = basouPaths9(repositoryRoot);
|
|
2701
|
+
const manifest = await readManifest5(paths);
|
|
2702
|
+
const alreadyDeclared = manifest.repos !== void 0 && manifest.repos.length > 0;
|
|
2703
|
+
const candidates = effectiveSourceRoots(manifest).map(
|
|
2704
|
+
(r) => classifySourceRoot(repositoryRoot, r)
|
|
2705
|
+
);
|
|
2706
|
+
const plan = planRosterAdoption(candidates);
|
|
2707
|
+
const applied = options.apply === true && !alreadyDeclared && plan.repos.length > 0;
|
|
2708
|
+
if (applied) {
|
|
2709
|
+
const now = ctx.now ?? (() => /* @__PURE__ */ new Date());
|
|
2710
|
+
await writeManifest2(
|
|
2711
|
+
paths,
|
|
2712
|
+
{
|
|
2713
|
+
...manifest,
|
|
2714
|
+
repos: plan.repos,
|
|
2715
|
+
workspace: { ...manifest.workspace, updated_at: now().toISOString() }
|
|
2716
|
+
},
|
|
2717
|
+
{ force: true }
|
|
2718
|
+
);
|
|
2719
|
+
}
|
|
2720
|
+
const result = {
|
|
2721
|
+
...plan,
|
|
2722
|
+
alreadyDeclared,
|
|
2723
|
+
applied,
|
|
2724
|
+
preservedUnknownFields: unknownManifestKeys(manifest)
|
|
2725
|
+
};
|
|
2726
|
+
if (options.json === true) {
|
|
2727
|
+
console.log(JSON.stringify(result));
|
|
2728
|
+
} else {
|
|
2729
|
+
console.log(renderProjectAdopt(result));
|
|
2730
|
+
}
|
|
2731
|
+
return result;
|
|
2732
|
+
}
|
|
2733
|
+
function renderProjectAdopt(result) {
|
|
2734
|
+
const lines = [];
|
|
2735
|
+
lines.push("# repo \u30ED\u30FC\u30B9\u30BF\u30FC\u306E bootstrap(source_roots \u2192 repos)");
|
|
2736
|
+
lines.push("");
|
|
2737
|
+
lines.push(...preservedUnknownLines(result.preservedUnknownFields));
|
|
2738
|
+
if (result.alreadyDeclared) {
|
|
2739
|
+
lines.push(
|
|
2740
|
+
"\u2139\uFE0F repo \u30ED\u30FC\u30B9\u30BF\u30FC(manifest \u306E `repos`)\u306F\u65E2\u306B\u5BA3\u8A00\u6E08\u307F\u3067\u3059\u3002adopt \u306F\u4E00\u5EA6\u304D\u308A\u306E bootstrap \u306E\u305F\u3081\u4F55\u3082\u66F8\u304D\u8FBC\u307F\u307E\u305B\u3093\u3002\u4EE5\u5F8C\u306E\u4FDD\u5B88\u306F `project check` / `project sync` \u3092\u4F7F\u3063\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
2741
|
+
);
|
|
2742
|
+
return lines.join("\n");
|
|
2743
|
+
}
|
|
2744
|
+
if (result.repos.length === 0) {
|
|
2745
|
+
lines.push("\u2139\uFE0F source_roots \u306B git repo \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F(bootstrap \u5BFE\u8C61\u306A\u3057)\u3002");
|
|
2746
|
+
} else if (result.applied) {
|
|
2747
|
+
lines.push(`\u2705 ${result.repos.length} repo \u3092 repos \u30ED\u30FC\u30B9\u30BF\u30FC\u306B\u66F8\u304D\u8FBC\u307F\u307E\u3057\u305F:`);
|
|
2748
|
+
for (const r of result.repos) lines.push(`- ${r.path}`);
|
|
2749
|
+
lines.push("");
|
|
2750
|
+
lines.push(
|
|
2751
|
+
"\u6CE8: visibility \u306F\u672A\u8A2D\u5B9A\u3067\u3059\u3002\u5404 repo \u306B public / private / future-public \u3092\u624B\u52D5\u3067\u4ED8\u4E0E\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
2752
|
+
);
|
|
2753
|
+
} else {
|
|
2754
|
+
lines.push(
|
|
2755
|
+
`${result.repos.length} repo \u3092 repos \u30ED\u30FC\u30B9\u30BF\u30FC\u306B\u5BA3\u8A00\u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`
|
|
2756
|
+
);
|
|
2757
|
+
for (const r of result.repos) lines.push(`- ${r.path}`);
|
|
2758
|
+
lines.push("");
|
|
2759
|
+
lines.push("\u6CE8: visibility \u306F\u672A\u8A2D\u5B9A\u3067\u63D0\u6848\u3057\u307E\u3059\u3002\u53CD\u6620\u5F8C\u306B\u624B\u52D5\u3067\u4ED8\u4E0E\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
|
|
2760
|
+
}
|
|
2761
|
+
if (result.excluded.length > 0) {
|
|
2762
|
+
lines.push("");
|
|
2763
|
+
lines.push(`## \u9664\u5916 (${result.excluded.length}) \u2014 git repo \u3067\u306F\u306A\u3044\u305F\u3081 repos \u306B\u542B\u3081\u307E\u305B\u3093`);
|
|
2764
|
+
for (const e of result.excluded) {
|
|
2765
|
+
const reason = e.kind === "non-repo" ? "\u975E repo(workspace view / tmp \u7B49)" : "\u89E3\u6C7A\u4E0D\u80FD(\u30D1\u30B9\u304C\u5B58\u5728\u3057\u306A\u3044)";
|
|
2766
|
+
lines.push(`- ${e.path} \u2014 ${reason}`);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
return lines.join("\n");
|
|
2770
|
+
}
|
|
2771
|
+
async function runProjectWiring(options, ctx = {}) {
|
|
2772
|
+
try {
|
|
2773
|
+
await doRunProjectWiring(options, ctx);
|
|
2774
|
+
} catch (error) {
|
|
2775
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
2776
|
+
process.exitCode = 1;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
async function isTrackedByGit(repoRoot, relPath) {
|
|
2780
|
+
const out = await safeSimpleGit(repoRoot).raw(["ls-files", "--", relPath]);
|
|
2781
|
+
return out.trim().length > 0;
|
|
2782
|
+
}
|
|
2783
|
+
async function gatherRepoWiring(repositoryRoot, entry) {
|
|
2784
|
+
const base = {
|
|
2785
|
+
path: entry.path,
|
|
2786
|
+
...entry.visibility !== void 0 ? { visibility: entry.visibility } : {}
|
|
2787
|
+
};
|
|
2788
|
+
let real;
|
|
2789
|
+
try {
|
|
2790
|
+
real = realpathSync(resolve3(repositoryRoot, entry.path));
|
|
2791
|
+
} catch {
|
|
2792
|
+
return { ...base, reachable: false, instructionFiles: [] };
|
|
2793
|
+
}
|
|
2794
|
+
if (!existsSync(join4(real, ".git"))) {
|
|
2795
|
+
return { ...base, reachable: false, instructionFiles: [] };
|
|
2796
|
+
}
|
|
2797
|
+
try {
|
|
2798
|
+
const instructionFiles = [];
|
|
2799
|
+
for (const name of INSTRUCTION_FILES) {
|
|
2800
|
+
let present = true;
|
|
2801
|
+
try {
|
|
2802
|
+
lstatSync(join4(real, name));
|
|
2803
|
+
} catch {
|
|
2804
|
+
present = false;
|
|
2805
|
+
}
|
|
2806
|
+
instructionFiles.push({ name, present, tracked: await isTrackedByGit(real, name) });
|
|
2807
|
+
}
|
|
2808
|
+
return { ...base, reachable: true, instructionFiles };
|
|
2809
|
+
} catch (error) {
|
|
2810
|
+
if (isGitNotFound(error)) throw error;
|
|
2811
|
+
return { ...base, reachable: false, instructionFiles: [] };
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
async function doRunProjectWiring(options, ctx) {
|
|
2815
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
2816
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project wiring");
|
|
2817
|
+
const paths = basouPaths9(repositoryRoot);
|
|
2818
|
+
const manifest = await readManifest5(paths);
|
|
2819
|
+
const roster = manifest.repos ?? [];
|
|
2820
|
+
const facts = [];
|
|
2821
|
+
for (const entry of roster) facts.push(await gatherRepoWiring(repositoryRoot, entry));
|
|
2822
|
+
const summary = summarizeWiring(facts);
|
|
2823
|
+
const result = { ...summary, hasRoster: roster.length > 0 };
|
|
2824
|
+
if (options.json === true) {
|
|
2825
|
+
console.log(JSON.stringify(result));
|
|
2826
|
+
} else {
|
|
2827
|
+
console.log(renderProjectWiring(result));
|
|
2828
|
+
}
|
|
2829
|
+
return result;
|
|
2830
|
+
}
|
|
2831
|
+
function renderProjectWiring(result) {
|
|
2832
|
+
const lines = [];
|
|
2833
|
+
lines.push("# \u6307\u793A\u66F8 wiring \u30C1\u30A7\u30C3\u30AF(\u5BA3\u8A00\u30ED\u30FC\u30B9\u30BF\u30FC \xD7 \u6307\u793A\u66F8\u306E\u5B58\u5728/git \u8FFD\u8DE1)");
|
|
2834
|
+
lines.push("");
|
|
2835
|
+
if (!result.hasRoster) {
|
|
2836
|
+
lines.push(
|
|
2837
|
+
"\u2139\uFE0F repo \u30ED\u30FC\u30B9\u30BF\u30FC\u304C\u672A\u5BA3\u8A00\u3067\u3059(manifest \u306E `repos`)\u3002`basou project adopt` \u3067\u5BA3\u8A00\u3057\u3066\u304B\u3089\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
2838
|
+
);
|
|
2839
|
+
return lines.join("\n");
|
|
2840
|
+
}
|
|
2841
|
+
if (result.risks.length > 0) {
|
|
2842
|
+
lines.push(
|
|
2843
|
+
`\u26A0\uFE0F \u516C\u958B\u7CFB repo \u3067\u6307\u793A\u66F8\u304C git \u8FFD\u8DE1\u3055\u308C\u3066\u3044\u307E\u3059: ${result.risks.length}(canonical \u306E\u6F0F\u6D29\u30EA\u30B9\u30AF)`
|
|
2844
|
+
);
|
|
2845
|
+
for (const r of result.risks) {
|
|
2846
|
+
lines.push(
|
|
2847
|
+
`- ${r.repo} [${r.visibility}] \u2014 ${r.file} \u304C tracked(gitignore \u3055\u308C\u305F symlink \u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059)`
|
|
2848
|
+
);
|
|
2849
|
+
}
|
|
2850
|
+
} else if (result.ok) {
|
|
2851
|
+
lines.push("\u2705 \u516C\u958B\u7CFB repo \u3067 git \u8FFD\u8DE1\u3055\u308C\u3066\u3044\u308B\u6307\u793A\u66F8\u306F\u3042\u308A\u307E\u305B\u3093(privacy \u30EA\u30B9\u30AF\u306A\u3057)\u3002");
|
|
2852
|
+
} else {
|
|
2853
|
+
lines.push(
|
|
2854
|
+
"\u2139\uFE0F \u78BA\u5B9A\u3057\u305F privacy \u30EA\u30B9\u30AF\u306F\u3042\u308A\u307E\u305B\u3093\u304C\u3001\u5224\u5B9A\u3067\u304D\u306A\u3044/\u5230\u9054\u3067\u304D\u306A\u3044 repo \u304C\u3042\u308A\u307E\u3059(\u4E0B\u8A18\u53C2\u7167)\u3002"
|
|
2855
|
+
);
|
|
2856
|
+
}
|
|
2857
|
+
lines.push("");
|
|
2858
|
+
if (result.unknown.length > 0) {
|
|
2859
|
+
lines.push(
|
|
2860
|
+
`## visibility \u672A\u8A2D\u5B9A (${result.unknown.length}) \u2014 privacy \u5224\u5B9A\u4E0D\u53EF\u3002manifest \u306E repos \u306B visibility \u3092\u4ED8\u4E0E\u3057\u3066\u304F\u3060\u3055\u3044`
|
|
2861
|
+
);
|
|
2862
|
+
for (const p of result.unknown) lines.push(`- ${p}`);
|
|
2863
|
+
lines.push("");
|
|
2864
|
+
}
|
|
2865
|
+
if (result.incomplete.length > 0) {
|
|
2866
|
+
lines.push(`## \u6307\u793A\u66F8\u306E\u6B20\u843D (${result.incomplete.length}) \u2014 \u5F8C\u7D9A\u306E\u751F\u6210\u30B9\u30E9\u30A4\u30B9\u3067\u88DC\u5B8C\u4E88\u5B9A`);
|
|
2867
|
+
for (const i of result.incomplete) lines.push(`- ${i.repo} \u2014 ${i.missing.join(", ")}`);
|
|
2868
|
+
lines.push("");
|
|
2869
|
+
}
|
|
2870
|
+
if (result.unreachable.length > 0) {
|
|
2871
|
+
lines.push(`## \u5230\u9054\u4E0D\u80FD (${result.unreachable.length}) \u2014 \u30D1\u30B9\u672A\u89E3\u6C7A / git repo \u3067\u306A\u3044`);
|
|
2872
|
+
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
2873
|
+
lines.push("");
|
|
2874
|
+
}
|
|
2875
|
+
lines.push(
|
|
2876
|
+
"\u6CE8: read-only \u306E advisory \u3067\u3059\u3002\u6307\u793A\u66F8\u306E\u5B58\u5728\u3068 git \u8FFD\u8DE1\u72B6\u6CC1\u306E\u307F\u3092\u8868\u793A\u3057\u3001\u751F\u6210\u30FBenforce \u306F\u3057\u307E\u305B\u3093(.basou \u306E\u30D5\u30C3\u30C8\u30D7\u30EA\u30F3\u30C8\u306F `basou view --check`)\u3002"
|
|
2877
|
+
);
|
|
2878
|
+
return lines.join("\n");
|
|
2879
|
+
}
|
|
2880
|
+
async function runProjectGitignore(options, ctx = {}) {
|
|
2881
|
+
try {
|
|
2882
|
+
await doRunProjectGitignore(options, ctx);
|
|
2883
|
+
} catch (error) {
|
|
2884
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
2885
|
+
process.exitCode = 1;
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
function gatherRepoGitignore(repositoryRoot, entry) {
|
|
2889
|
+
const base = {
|
|
2890
|
+
path: entry.path,
|
|
2891
|
+
...entry.visibility !== void 0 ? { visibility: entry.visibility } : {}
|
|
2892
|
+
};
|
|
2893
|
+
let real;
|
|
2894
|
+
try {
|
|
2895
|
+
real = realpathSync(resolve3(repositoryRoot, entry.path));
|
|
2896
|
+
} catch {
|
|
2897
|
+
return { ...base, reachable: false, currentLines: [] };
|
|
2898
|
+
}
|
|
2899
|
+
if (!existsSync(join4(real, ".git"))) {
|
|
2900
|
+
return { ...base, reachable: false, currentLines: [] };
|
|
2901
|
+
}
|
|
2902
|
+
return { ...base, reachable: true, currentLines: readGitignoreLines(join4(real, ".gitignore")) };
|
|
2903
|
+
}
|
|
2904
|
+
function hasErrorCode(error) {
|
|
2905
|
+
return error instanceof Error && typeof error.code === "string";
|
|
2906
|
+
}
|
|
2907
|
+
function readGitignoreLines(file) {
|
|
2908
|
+
try {
|
|
2909
|
+
return readFileSync(file, "utf8").split(/\r?\n/);
|
|
2910
|
+
} catch (error) {
|
|
2911
|
+
if (hasErrorCode(error) && error.code === "ENOENT") return [];
|
|
2912
|
+
throw new Error("Failed to read .gitignore", { cause: error });
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
function applyGitignorePlan(repositoryRoot, plan) {
|
|
2916
|
+
const file = join4(realpathSync(resolve3(repositoryRoot, plan.path)), ".gitignore");
|
|
2917
|
+
let existing = "";
|
|
2918
|
+
try {
|
|
2919
|
+
existing = readFileSync(file, "utf8");
|
|
2920
|
+
} catch (error) {
|
|
2921
|
+
if (!(hasErrorCode(error) && error.code === "ENOENT")) {
|
|
2922
|
+
throw new Error("Failed to read .gitignore", { cause: error });
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
2926
|
+
try {
|
|
2927
|
+
writeFileSync(file, `${existing}${sep}${plan.toAdd.join("\n")}
|
|
2928
|
+
`);
|
|
2929
|
+
} catch (error) {
|
|
2930
|
+
throw new Error("Failed to write .gitignore", { cause: error });
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
async function doRunProjectGitignore(options, ctx) {
|
|
2934
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
2935
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project gitignore");
|
|
2936
|
+
const paths = basouPaths9(repositoryRoot);
|
|
2937
|
+
const manifest = await readManifest5(paths);
|
|
2938
|
+
const roster = manifest.repos ?? [];
|
|
2939
|
+
const facts = roster.map((entry) => gatherRepoGitignore(repositoryRoot, entry));
|
|
2940
|
+
const summary = planGitignore({ repos: facts, required: [...INSTRUCTION_FILES] });
|
|
2941
|
+
const applied = options.apply === true && summary.plans.length > 0;
|
|
2942
|
+
if (applied) {
|
|
2943
|
+
for (const plan of summary.plans) applyGitignorePlan(repositoryRoot, plan);
|
|
2944
|
+
}
|
|
2945
|
+
const result = { ...summary, hasRoster: roster.length > 0, applied };
|
|
2946
|
+
if (options.json === true) {
|
|
2947
|
+
console.log(JSON.stringify(result));
|
|
2948
|
+
} else {
|
|
2949
|
+
console.log(renderProjectGitignore(result));
|
|
2950
|
+
}
|
|
2951
|
+
return result;
|
|
2952
|
+
}
|
|
2953
|
+
function renderProjectGitignore(result) {
|
|
2954
|
+
const lines = [];
|
|
2955
|
+
lines.push("# .gitignore \u751F\u6210(\u516C\u958B\u7CFB repo \u306E\u6307\u793A\u66F8\u3092\u9664\u5916)");
|
|
2956
|
+
lines.push("");
|
|
2957
|
+
if (!result.hasRoster) {
|
|
2958
|
+
lines.push(
|
|
2959
|
+
"\u2139\uFE0F repo \u30ED\u30FC\u30B9\u30BF\u30FC\u304C\u672A\u5BA3\u8A00\u3067\u3059(manifest \u306E `repos`)\u3002`basou project adopt` \u3067\u5BA3\u8A00\u3057\u3066\u304B\u3089\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
2960
|
+
);
|
|
2961
|
+
return lines.join("\n");
|
|
2962
|
+
}
|
|
2963
|
+
if (result.plans.length > 0) {
|
|
2964
|
+
const verb = result.applied ? "\u8FFD\u52A0\u3057\u307E\u3057\u305F" : "\u8FFD\u52A0\u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply)";
|
|
2965
|
+
lines.push(
|
|
2966
|
+
`${result.applied ? "\u2705 " : ""}${result.plans.length} repo \u306E .gitignore \u306B${verb}:`
|
|
2967
|
+
);
|
|
2968
|
+
for (const p of result.plans) lines.push(`- ${p.path} \u2014 ${p.toAdd.join(", ")}`);
|
|
2969
|
+
} else if (result.ok) {
|
|
2970
|
+
lines.push("\u2705 \u516C\u958B\u7CFB repo \u306E .gitignore \u306F\u6307\u793A\u66F8\u3092\u3059\u3079\u3066\u9664\u5916\u6E08\u307F\u3067\u3059(\u8FFD\u52A0\u4E0D\u8981)\u3002");
|
|
2971
|
+
} else {
|
|
2972
|
+
lines.push(
|
|
2973
|
+
"\u2139\uFE0F \u8FFD\u52A0\u304C\u5FC5\u8981\u306A\u516C\u958B\u7CFB repo \u306F\u3042\u308A\u307E\u305B\u3093\u304C\u3001\u5224\u5B9A\u3067\u304D\u306A\u3044/\u5230\u9054\u3067\u304D\u306A\u3044 repo \u304C\u3042\u308A\u307E\u3059(\u4E0B\u8A18\u53C2\u7167)\u3002"
|
|
2974
|
+
);
|
|
2975
|
+
}
|
|
2976
|
+
lines.push("");
|
|
2977
|
+
if (result.unknown.length > 0) {
|
|
2978
|
+
lines.push(
|
|
2979
|
+
`## visibility \u672A\u8A2D\u5B9A (${result.unknown.length}) \u2014 \u5BFE\u8C61\u5916\u3002manifest \u306E repos \u306B visibility \u3092\u4ED8\u4E0E\u3057\u3066\u304F\u3060\u3055\u3044`
|
|
2980
|
+
);
|
|
2981
|
+
for (const p of result.unknown) lines.push(`- ${p}`);
|
|
2982
|
+
lines.push("");
|
|
2983
|
+
}
|
|
2984
|
+
if (result.unreachable.length > 0) {
|
|
2985
|
+
lines.push(`## \u5230\u9054\u4E0D\u80FD (${result.unreachable.length}) \u2014 \u30D1\u30B9\u672A\u89E3\u6C7A / git repo \u3067\u306A\u3044`);
|
|
2986
|
+
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
2987
|
+
lines.push("");
|
|
2988
|
+
}
|
|
2989
|
+
lines.push(
|
|
2990
|
+
"\u6CE8: \u65E2\u5B58\u306E .gitignore \u884C\u306F\u4FDD\u6301\u3057\u3001\u4E0D\u8DB3\u30D1\u30BF\u30FC\u30F3\u306E\u8FFD\u8A18\u306E\u307F\u884C\u3044\u307E\u3059(\u524A\u9664\u306F\u3057\u307E\u305B\u3093)\u3002private / visibility \u672A\u8A2D\u5B9A\u306E repo \u306F\u5BFE\u8C61\u5916\u3067\u3059\u3002"
|
|
2991
|
+
);
|
|
2992
|
+
lines.push(
|
|
2993
|
+
"\u6CE8: .gitignore \u3078\u306E\u8FFD\u8A18\u306F\u3001\u65E2\u306B git \u8FFD\u8DE1\u6E08\u307F\u306E\u30D5\u30A1\u30A4\u30EB\u3092 untrack \u3057\u307E\u305B\u3093\u3002\u8FFD\u8DE1\u6E08\u307F\u306E\u6307\u793A\u66F8\u306F `basou project wiring` \u3067\u691C\u51FA\u3057\u3001`git rm --cached <file>` \u3067\u5916\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
2994
|
+
);
|
|
2995
|
+
return lines.join("\n");
|
|
2996
|
+
}
|
|
2997
|
+
async function runProjectSymlinks(options, ctx = {}) {
|
|
2998
|
+
try {
|
|
2999
|
+
await doRunProjectSymlinks(options, ctx);
|
|
3000
|
+
} catch (error) {
|
|
3001
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
3002
|
+
process.exitCode = 1;
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
function expectedSymlinkTargets(repoDirReal, canonicalFile) {
|
|
3006
|
+
return [
|
|
3007
|
+
{ name: "AGENTS.md", target: relative2(repoDirReal, canonicalFile) },
|
|
3008
|
+
{ name: "CLAUDE.md", target: CANONICAL_FILE },
|
|
3009
|
+
{ name: ".github/copilot-instructions.md", target: `../${CANONICAL_FILE}` }
|
|
3010
|
+
];
|
|
3011
|
+
}
|
|
3012
|
+
function inspectSymlink(filePath, expectedTarget) {
|
|
3013
|
+
let isLink;
|
|
3014
|
+
try {
|
|
3015
|
+
isLink = lstatSync(filePath).isSymbolicLink();
|
|
3016
|
+
} catch (error) {
|
|
3017
|
+
if (hasErrorCode(error) && error.code === "ENOENT") return { state: "missing" };
|
|
3018
|
+
return { state: "blocked" };
|
|
3019
|
+
}
|
|
3020
|
+
if (!isLink) return { state: "occupied" };
|
|
3021
|
+
const actual = readlinkSync(filePath);
|
|
3022
|
+
return actual === expectedTarget ? { state: "correct" } : { state: "mismatch", actualTarget: actual };
|
|
3023
|
+
}
|
|
3024
|
+
function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
3025
|
+
const base = { path: entry.path };
|
|
3026
|
+
let real;
|
|
3027
|
+
try {
|
|
3028
|
+
real = realpathSync(resolve3(repositoryRoot, entry.path));
|
|
3029
|
+
} catch {
|
|
3030
|
+
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
3031
|
+
}
|
|
3032
|
+
if (real === anchorReal) {
|
|
3033
|
+
return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
|
|
3034
|
+
}
|
|
3035
|
+
if (!existsSync(join4(real, ".git"))) {
|
|
3036
|
+
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
3037
|
+
}
|
|
3038
|
+
const canonicalFile = join4(anchorReal, "agents", basename3(real), CANONICAL_FILE);
|
|
3039
|
+
if (!existsSync(canonicalFile)) {
|
|
3040
|
+
return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
|
|
3041
|
+
}
|
|
3042
|
+
const files = expectedSymlinkTargets(real, canonicalFile).map(
|
|
3043
|
+
(spec) => {
|
|
3044
|
+
const { state, actualTarget } = inspectSymlink(join4(real, spec.name), spec.target);
|
|
3045
|
+
return {
|
|
3046
|
+
name: spec.name,
|
|
3047
|
+
expectedTarget: spec.target,
|
|
3048
|
+
state,
|
|
3049
|
+
...actualTarget !== void 0 ? { actualTarget } : {}
|
|
3050
|
+
};
|
|
3051
|
+
}
|
|
3052
|
+
);
|
|
3053
|
+
return {
|
|
3054
|
+
...base,
|
|
3055
|
+
isAnchor: false,
|
|
3056
|
+
reachable: true,
|
|
3057
|
+
canonicalPresent: true,
|
|
3058
|
+
canonicalName: basename3(real),
|
|
3059
|
+
files
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
function applySymlinkPlan(repositoryRoot, plan) {
|
|
3063
|
+
let real;
|
|
3064
|
+
try {
|
|
3065
|
+
real = realpathSync(resolve3(repositoryRoot, plan.path));
|
|
3066
|
+
} catch (error) {
|
|
3067
|
+
const message = failureReason(error);
|
|
3068
|
+
return { created: [], failed: plan.toCreate.map((c) => ({ file: c.name, message })) };
|
|
3069
|
+
}
|
|
3070
|
+
const created = [];
|
|
3071
|
+
const failed = [];
|
|
3072
|
+
for (const { name, target } of plan.toCreate) {
|
|
3073
|
+
const filePath = join4(real, name);
|
|
3074
|
+
try {
|
|
3075
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
3076
|
+
symlinkSync(target, filePath);
|
|
3077
|
+
created.push(name);
|
|
3078
|
+
} catch (error) {
|
|
3079
|
+
failed.push({ file: name, message: failureReason(error) });
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
return { created, failed };
|
|
3083
|
+
}
|
|
3084
|
+
function failureReason(error) {
|
|
3085
|
+
return hasErrorCode(error) ? error.code : "unknown error";
|
|
3086
|
+
}
|
|
3087
|
+
async function doRunProjectSymlinks(options, ctx) {
|
|
3088
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
3089
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project symlinks");
|
|
3090
|
+
const paths = basouPaths9(repositoryRoot);
|
|
3091
|
+
const manifest = await readManifest5(paths);
|
|
3092
|
+
const roster = manifest.repos ?? [];
|
|
3093
|
+
const anchorReal = realpathSync(repositoryRoot);
|
|
3094
|
+
const facts = roster.map((entry) => gatherRepoSymlinks(repositoryRoot, anchorReal, entry));
|
|
3095
|
+
const summary = summarizeSymlinkPlan(facts);
|
|
3096
|
+
const wantApply = options.apply === true && summary.plans.length > 0;
|
|
3097
|
+
const failures = [];
|
|
3098
|
+
let createdCount = 0;
|
|
3099
|
+
if (wantApply) {
|
|
3100
|
+
for (const plan of summary.plans) {
|
|
3101
|
+
const { created, failed } = applySymlinkPlan(repositoryRoot, plan);
|
|
3102
|
+
createdCount += created.length;
|
|
3103
|
+
for (const f of failed) failures.push({ repo: plan.path, file: f.file, message: f.message });
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
const result = {
|
|
3107
|
+
...summary,
|
|
3108
|
+
hasRoster: roster.length > 0,
|
|
3109
|
+
applied: createdCount > 0,
|
|
3110
|
+
failures
|
|
3111
|
+
};
|
|
3112
|
+
if (options.json === true) {
|
|
3113
|
+
console.log(JSON.stringify(result));
|
|
3114
|
+
} else {
|
|
3115
|
+
console.log(renderProjectSymlinks(result));
|
|
3116
|
+
}
|
|
3117
|
+
return result;
|
|
3118
|
+
}
|
|
3119
|
+
function renderProjectSymlinks(result) {
|
|
3120
|
+
const lines = [];
|
|
3121
|
+
lines.push("# \u6307\u793A\u66F8 symlink \u751F\u6210(\u5404 repo \u2192 anchor \u306E canonical)");
|
|
3122
|
+
lines.push("");
|
|
3123
|
+
if (!result.hasRoster) {
|
|
3124
|
+
lines.push(
|
|
3125
|
+
"\u2139\uFE0F repo \u30ED\u30FC\u30B9\u30BF\u30FC\u304C\u672A\u5BA3\u8A00\u3067\u3059(manifest \u306E `repos`)\u3002`basou project adopt` \u3067\u5BA3\u8A00\u3057\u3066\u304B\u3089\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
3126
|
+
);
|
|
3127
|
+
return lines.join("\n");
|
|
3128
|
+
}
|
|
3129
|
+
if (result.plans.length > 0) {
|
|
3130
|
+
const attempted = result.applied || result.failures.length > 0;
|
|
3131
|
+
if (!attempted) {
|
|
3132
|
+
lines.push(
|
|
3133
|
+
`${result.plans.length} repo \u306B\u6307\u793A\u66F8 symlink \u3092\u4F5C\u6210\u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`
|
|
3134
|
+
);
|
|
3135
|
+
for (const p of result.plans) {
|
|
3136
|
+
lines.push(`- ${p.path}`);
|
|
3137
|
+
for (const c of p.toCreate) lines.push(` ${c.name} -> ${c.target}`);
|
|
3138
|
+
}
|
|
3139
|
+
} else {
|
|
3140
|
+
const header = result.failures.length === 0 ? "\u2705 \u6307\u793A\u66F8 symlink \u3092\u4F5C\u6210\u3057\u307E\u3057\u305F:" : result.applied ? "\u6307\u793A\u66F8 symlink \u3092\u4F5C\u6210\u3057\u307E\u3057\u305F(\u4E00\u90E8\u5931\u6557\u3001\u4E0B\u8A18\u53C2\u7167):" : "\u6307\u793A\u66F8 symlink \u3092\u4F5C\u6210\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F(\u4E0B\u8A18\u53C2\u7167):";
|
|
3141
|
+
lines.push(header);
|
|
3142
|
+
for (const p of result.plans) {
|
|
3143
|
+
const failedFiles = new Set(
|
|
3144
|
+
result.failures.filter((f) => f.repo === p.path).map((f) => f.file)
|
|
3145
|
+
);
|
|
3146
|
+
const created = p.toCreate.filter((c) => !failedFiles.has(c.name));
|
|
3147
|
+
if (created.length === 0) continue;
|
|
3148
|
+
lines.push(`- ${p.path}`);
|
|
3149
|
+
for (const c of created) lines.push(` ${c.name} -> ${c.target}`);
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
} else if (result.ok) {
|
|
3153
|
+
lines.push("\u2705 \u5BA3\u8A00\u3055\u308C\u305F\u5168 repo \u306E\u6307\u793A\u66F8 symlink \u306F\u6B63\u3057\u304F\u5F35\u3089\u308C\u3066\u3044\u307E\u3059(\u751F\u6210\u4E0D\u8981)\u3002");
|
|
3154
|
+
} else {
|
|
3155
|
+
lines.push(
|
|
3156
|
+
"\u2139\uFE0F \u751F\u6210\u304C\u5FC5\u8981\u306A symlink \u306F\u3042\u308A\u307E\u305B\u3093\u304C\u3001\u7AF6\u5408 / \u885D\u7A81 / canonical \u4E0D\u5728 / \u5230\u9054\u3067\u304D\u306A\u3044 repo \u304C\u3042\u308A\u307E\u3059(\u4E0B\u8A18\u53C2\u7167)\u3002"
|
|
3157
|
+
);
|
|
3158
|
+
}
|
|
3159
|
+
lines.push("");
|
|
3160
|
+
if (result.failures.length > 0) {
|
|
3161
|
+
lines.push(`## \u4F5C\u6210\u306B\u5931\u6557 (${result.failures.length}) \u2014 \u4E00\u90E8\u306E symlink \u3092\u4F5C\u6210\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F`);
|
|
3162
|
+
for (const f of result.failures) lines.push(`- ${f.repo} \u2014 ${f.file}: ${f.message}`);
|
|
3163
|
+
lines.push("");
|
|
3164
|
+
}
|
|
3165
|
+
if (result.conflicts.length > 0) {
|
|
3166
|
+
lines.push(
|
|
3167
|
+
`## \u7AF6\u5408 (${result.conflicts.length}) \u2014 \u65E2\u5B58\u3092\u4E0A\u66F8\u304D\u3057\u307E\u305B\u3093\u3002\u624B\u52D5\u3067\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044`
|
|
3168
|
+
);
|
|
3169
|
+
for (const c of result.conflicts) {
|
|
3170
|
+
const detail = c.reason === "mismatch" ? `\u5225\u306E\u5834\u6240\u3092\u6307\u3059 symlink(\u73FE\u5728: ${c.actualTarget ?? "?"})` : c.reason === "occupied" ? "symlink \u3067\u306A\u3044\u5B9F\u30D5\u30A1\u30A4\u30EB/\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA" : "\u691C\u67FB\u3067\u304D\u306A\u3044\u30D1\u30B9(\u89AA\u304C\u975E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u7B49)";
|
|
3171
|
+
lines.push(`- ${c.repo} \u2014 ${c.file}: ${detail}`);
|
|
3172
|
+
}
|
|
3173
|
+
lines.push("");
|
|
3174
|
+
}
|
|
3175
|
+
if (result.collisions.length > 0) {
|
|
3176
|
+
lines.push(
|
|
3177
|
+
`## canonical \u885D\u7A81 (${result.collisions.length}) \u2014 \u5225 repo \u304C\u540C\u540D canonical \u3092\u5171\u6709(\u81EA\u52D5\u914D\u7DDA\u3057\u307E\u305B\u3093)`
|
|
3178
|
+
);
|
|
3179
|
+
for (const c of result.collisions) {
|
|
3180
|
+
lines.push(`- agents/${c.canonicalName}/AGENTS.md \u2190 ${c.repos.join(", ")}`);
|
|
3181
|
+
}
|
|
3182
|
+
lines.push("");
|
|
3183
|
+
}
|
|
3184
|
+
if (result.missingCanonical.length > 0) {
|
|
3185
|
+
lines.push(
|
|
3186
|
+
`## canonical \u4E0D\u5728 (${result.missingCanonical.length}) \u2014 anchor \u306B agents/<repo>/AGENTS.md \u304C\u7121\u3044\u305F\u3081\u751F\u6210\u3067\u304D\u307E\u305B\u3093`
|
|
3187
|
+
);
|
|
3188
|
+
for (const p of result.missingCanonical) lines.push(`- ${p}`);
|
|
3189
|
+
lines.push("");
|
|
3190
|
+
}
|
|
3191
|
+
if (result.unreachable.length > 0) {
|
|
3192
|
+
lines.push(`## \u5230\u9054\u4E0D\u80FD (${result.unreachable.length}) \u2014 \u30D1\u30B9\u672A\u89E3\u6C7A / git repo \u3067\u306A\u3044`);
|
|
3193
|
+
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
3194
|
+
lines.push("");
|
|
3195
|
+
}
|
|
3196
|
+
lines.push(
|
|
3197
|
+
"\u6CE8: \u65E2\u5B58\u30D5\u30A1\u30A4\u30EB\u30FB\u5225\u306E\u5834\u6240\u3092\u6307\u3059 symlink \u306F\u4E0A\u66F8\u304D\u305B\u305A\u3001\u4E0D\u8DB3\u5206\u306E\u4F5C\u6210\u306E\u307F\u884C\u3044\u307E\u3059(GEMINI.md \u306F\u5EC3\u6B62\u306E\u305F\u3081\u751F\u6210\u3057\u307E\u305B\u3093)\u3002"
|
|
3198
|
+
);
|
|
3199
|
+
return lines.join("\n");
|
|
3200
|
+
}
|
|
3201
|
+
async function runProjectWorkspace(options, ctx = {}) {
|
|
3202
|
+
try {
|
|
3203
|
+
await doRunProjectWorkspace(options, ctx);
|
|
3204
|
+
} catch (error) {
|
|
3205
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
3206
|
+
process.exitCode = 1;
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
function resolveViewDir(repositoryRoot, viewPath) {
|
|
3210
|
+
const abs = resolve3(repositoryRoot, viewPath);
|
|
3211
|
+
try {
|
|
3212
|
+
return realpathSync(abs);
|
|
3213
|
+
} catch {
|
|
3214
|
+
try {
|
|
3215
|
+
return join4(realpathSync(dirname(abs)), basename3(abs));
|
|
3216
|
+
} catch {
|
|
3217
|
+
return abs;
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
function gatherViewRepo(repositoryRoot, viewDir, entry) {
|
|
3222
|
+
let repoReal;
|
|
3223
|
+
try {
|
|
3224
|
+
repoReal = realpathSync(resolve3(repositoryRoot, entry.path));
|
|
3225
|
+
} catch {
|
|
3226
|
+
return { path: entry.path, reachable: false };
|
|
3227
|
+
}
|
|
3228
|
+
const expectedTarget = relative2(viewDir, repoReal);
|
|
3229
|
+
if (expectedTarget === "" || expectedTarget === ".") {
|
|
3230
|
+
return { path: entry.path, reachable: false };
|
|
3231
|
+
}
|
|
3232
|
+
const linkName = basename3(repoReal);
|
|
3233
|
+
const { state, actualTarget } = inspectSymlink(join4(viewDir, linkName), expectedTarget);
|
|
3234
|
+
return {
|
|
3235
|
+
path: entry.path,
|
|
3236
|
+
reachable: true,
|
|
3237
|
+
linkName,
|
|
3238
|
+
expectedTarget,
|
|
3239
|
+
state,
|
|
3240
|
+
...actualTarget !== void 0 ? { actualTarget } : {}
|
|
3241
|
+
};
|
|
3242
|
+
}
|
|
3243
|
+
function applyViewPlan(viewDir, toCreate) {
|
|
3244
|
+
const created = [];
|
|
3245
|
+
const failed = [];
|
|
3246
|
+
for (const { name, target } of toCreate) {
|
|
3247
|
+
const filePath = join4(viewDir, name);
|
|
3248
|
+
try {
|
|
3249
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
3250
|
+
symlinkSync(target, filePath);
|
|
3251
|
+
created.push(name);
|
|
3252
|
+
} catch (error) {
|
|
3253
|
+
failed.push({ name, message: failureReason(error) });
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
return { created, failed };
|
|
3257
|
+
}
|
|
3258
|
+
var TOP_LEVEL_INSTRUCTION_FILES_LOWER = new Set(
|
|
3259
|
+
INSTRUCTION_FILES.filter((f) => !f.includes("/")).map((f) => f.toLowerCase())
|
|
3260
|
+
);
|
|
3261
|
+
function classifyViewLink(viewDir, name, rosterRealpaths) {
|
|
3262
|
+
const filePath = join4(viewDir, name);
|
|
3263
|
+
let isLink;
|
|
3264
|
+
try {
|
|
3265
|
+
isLink = lstatSync(filePath).isSymbolicLink();
|
|
3266
|
+
} catch {
|
|
3267
|
+
return null;
|
|
3268
|
+
}
|
|
3269
|
+
if (!isLink) return null;
|
|
3270
|
+
let target;
|
|
3271
|
+
try {
|
|
3272
|
+
target = readlinkSync(filePath);
|
|
3273
|
+
} catch {
|
|
3274
|
+
return null;
|
|
3275
|
+
}
|
|
3276
|
+
const resolved = isAbsolute(target) ? target : resolve3(viewDir, target);
|
|
3277
|
+
try {
|
|
3278
|
+
if (rosterRealpaths.has(realpathSync(resolved))) return null;
|
|
3279
|
+
} catch {
|
|
3280
|
+
}
|
|
3281
|
+
if (isAbsolute(target)) return { target, kind: "absolute" };
|
|
3282
|
+
let isDir = false;
|
|
3283
|
+
try {
|
|
3284
|
+
isDir = statSync(resolved).isDirectory();
|
|
3285
|
+
} catch {
|
|
3286
|
+
isDir = false;
|
|
3287
|
+
}
|
|
3288
|
+
if (!isDir) {
|
|
3289
|
+
return { target, kind: existsSync(resolved) ? "non-repo" : "broken" };
|
|
3290
|
+
}
|
|
3291
|
+
return { target, kind: existsSync(join4(resolved, ".git")) ? "repo" : "non-repo" };
|
|
3292
|
+
}
|
|
3293
|
+
function gatherExistingViewLinks(viewDir, rosterRealpaths) {
|
|
3294
|
+
let names;
|
|
3295
|
+
try {
|
|
3296
|
+
names = readdirSync(viewDir);
|
|
3297
|
+
} catch (error) {
|
|
3298
|
+
if (hasErrorCode(error) && error.code === "ENOENT") return [];
|
|
3299
|
+
throw new Error("workspace view \u3092\u8D70\u67FB\u3067\u304D\u307E\u305B\u3093(\u30D1\u30B9/\u7A2E\u5225\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044)", {
|
|
3300
|
+
cause: error
|
|
3301
|
+
});
|
|
3302
|
+
}
|
|
3303
|
+
const links = [];
|
|
3304
|
+
for (const name of names) {
|
|
3305
|
+
if (TOP_LEVEL_INSTRUCTION_FILES_LOWER.has(name.toLowerCase())) continue;
|
|
3306
|
+
const c = classifyViewLink(viewDir, name, rosterRealpaths);
|
|
3307
|
+
if (c === null) continue;
|
|
3308
|
+
links.push({ name, target: c.target, kind: c.kind });
|
|
3309
|
+
}
|
|
3310
|
+
return links;
|
|
3311
|
+
}
|
|
3312
|
+
function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
|
|
3313
|
+
const pruned = [];
|
|
3314
|
+
const failed = [];
|
|
3315
|
+
for (const { name } of toPrune) {
|
|
3316
|
+
const filePath = join4(viewDir, name);
|
|
3317
|
+
const c = classifyViewLink(viewDir, name, rosterRealpaths);
|
|
3318
|
+
if (c === null || c.kind !== "repo") {
|
|
3319
|
+
failed.push({
|
|
3320
|
+
name,
|
|
3321
|
+
message: "\u64A4\u53BB\u5BFE\u8C61\u304C scan \u6642\u3068\u5909\u308F\u308A\u307E\u3057\u305F(basou \u751F\u6210\u306E stray repo link \u3067\u306F\u306A\u304F\u306A\u3063\u305F/\u518D\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044)"
|
|
3322
|
+
});
|
|
3323
|
+
continue;
|
|
3324
|
+
}
|
|
3325
|
+
try {
|
|
3326
|
+
unlinkSync(filePath);
|
|
3327
|
+
pruned.push(name);
|
|
3328
|
+
} catch (error) {
|
|
3329
|
+
failed.push({ name, message: failureReason(error) });
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
return { pruned, failed };
|
|
3333
|
+
}
|
|
3334
|
+
async function doRunProjectWorkspace(options, ctx) {
|
|
3335
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
3336
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project workspace");
|
|
3337
|
+
const paths = basouPaths9(repositoryRoot);
|
|
3338
|
+
const manifest = await readManifest5(paths);
|
|
3339
|
+
const viewPath = manifest.workspace.view;
|
|
3340
|
+
const roster = manifest.repos ?? [];
|
|
3341
|
+
let result;
|
|
3342
|
+
if (viewPath === void 0) {
|
|
3343
|
+
result = {
|
|
3344
|
+
toCreate: [],
|
|
3345
|
+
conflicts: [],
|
|
3346
|
+
collisions: [],
|
|
3347
|
+
unreachable: [],
|
|
3348
|
+
toPrune: [],
|
|
3349
|
+
strayUnknown: [],
|
|
3350
|
+
correctCount: 0,
|
|
3351
|
+
ok: true,
|
|
3352
|
+
hasView: false,
|
|
3353
|
+
applied: false,
|
|
3354
|
+
pruned: false,
|
|
3355
|
+
pruneWithheld: false,
|
|
3356
|
+
failures: [],
|
|
3357
|
+
pruneFailures: []
|
|
3358
|
+
};
|
|
3359
|
+
} else {
|
|
3360
|
+
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
3361
|
+
const facts = roster.map((entry) => gatherViewRepo(repositoryRoot, viewDir, entry));
|
|
3362
|
+
const rosterNames = roster.map((entry) => basename3(resolve3(repositoryRoot, entry.path)));
|
|
3363
|
+
const rosterRealpaths = /* @__PURE__ */ new Set();
|
|
3364
|
+
for (const entry of roster) {
|
|
3365
|
+
try {
|
|
3366
|
+
rosterRealpaths.add(realpathSync(resolve3(repositoryRoot, entry.path)));
|
|
3367
|
+
} catch {
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
const existing = gatherExistingViewLinks(viewDir, rosterRealpaths);
|
|
3371
|
+
const plan = planWorkspaceView(facts, existing, rosterNames);
|
|
3372
|
+
const failures = [];
|
|
3373
|
+
let createdCount = 0;
|
|
3374
|
+
if (options.apply === true && plan.toCreate.length > 0) {
|
|
3375
|
+
const applied = applyViewPlan(viewDir, plan.toCreate);
|
|
3376
|
+
createdCount = applied.created.length;
|
|
3377
|
+
for (const f of applied.failed) failures.push(f);
|
|
3378
|
+
}
|
|
3379
|
+
const pruneWithheld = options.prune === true && plan.toPrune.length > 0 && plan.unreachable.length > 0;
|
|
3380
|
+
const pruneFailures = [];
|
|
3381
|
+
let prunedCount = 0;
|
|
3382
|
+
if (options.prune === true && plan.toPrune.length > 0 && plan.unreachable.length === 0) {
|
|
3383
|
+
const removed = pruneViewLinks(viewDir, plan.toPrune, rosterRealpaths);
|
|
3384
|
+
prunedCount = removed.pruned.length;
|
|
3385
|
+
for (const f of removed.failed) pruneFailures.push(f);
|
|
3386
|
+
}
|
|
3387
|
+
const createsOutstanding = plan.toCreate.length > 0 && !(options.apply === true && failures.length === 0);
|
|
3388
|
+
const prunesOutstanding = plan.toPrune.length > 0 && !(options.prune === true && !pruneWithheld && pruneFailures.length === 0);
|
|
3389
|
+
const ok = plan.conflicts.length === 0 && plan.collisions.length === 0 && plan.unreachable.length === 0 && plan.strayUnknown.length === 0 && !createsOutstanding && !prunesOutstanding;
|
|
3390
|
+
result = {
|
|
3391
|
+
...plan,
|
|
3392
|
+
ok,
|
|
3393
|
+
hasView: true,
|
|
3394
|
+
applied: createdCount > 0,
|
|
3395
|
+
pruned: prunedCount > 0,
|
|
3396
|
+
pruneWithheld,
|
|
3397
|
+
failures,
|
|
3398
|
+
pruneFailures
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
if (options.json === true) {
|
|
3402
|
+
console.log(JSON.stringify(result));
|
|
3403
|
+
} else {
|
|
3404
|
+
console.log(renderProjectWorkspace(result));
|
|
3405
|
+
}
|
|
3406
|
+
return result;
|
|
3407
|
+
}
|
|
3408
|
+
function renderProjectWorkspace(result) {
|
|
3409
|
+
const lines = [];
|
|
3410
|
+
lines.push("# workspace view \u751F\u6210(roster repo \u3092\u96C6\u7D04)");
|
|
3411
|
+
lines.push("");
|
|
3412
|
+
if (!result.hasView) {
|
|
3413
|
+
lines.push(
|
|
3414
|
+
"\u2139\uFE0F view \u304C\u672A\u5BA3\u8A00\u3067\u3059(manifest \u306E `workspace.view`)\u3002\u96C6\u7D04\u5148\u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092\u5BA3\u8A00\u3057\u3066\u304B\u3089\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
3415
|
+
);
|
|
3416
|
+
return lines.join("\n");
|
|
3417
|
+
}
|
|
3418
|
+
if (result.toCreate.length > 0) {
|
|
3419
|
+
const attempted = result.applied || result.failures.length > 0;
|
|
3420
|
+
if (!attempted) {
|
|
3421
|
+
lines.push(
|
|
3422
|
+
`${result.toCreate.length} \u4EF6\u306E repo symlink \u3092 view \u306B\u4F5C\u6210\u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`
|
|
3423
|
+
);
|
|
3424
|
+
for (const c of result.toCreate) lines.push(` ${c.name} -> ${c.target}`);
|
|
3425
|
+
} else {
|
|
3426
|
+
const failed = new Set(result.failures.map((f) => f.name));
|
|
3427
|
+
const header = result.failures.length === 0 ? "\u2705 view \u306B repo symlink \u3092\u4F5C\u6210\u3057\u307E\u3057\u305F:" : result.applied ? "view \u306B repo symlink \u3092\u4F5C\u6210\u3057\u307E\u3057\u305F(\u4E00\u90E8\u5931\u6557\u3001\u4E0B\u8A18\u53C2\u7167):" : "view \u306B repo symlink \u3092\u4F5C\u6210\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F(\u4E0B\u8A18\u53C2\u7167):";
|
|
3428
|
+
lines.push(header);
|
|
3429
|
+
for (const c of result.toCreate) {
|
|
3430
|
+
if (failed.has(c.name)) continue;
|
|
3431
|
+
lines.push(` ${c.name} -> ${c.target}`);
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
} else if (result.ok) {
|
|
3435
|
+
lines.push(
|
|
3436
|
+
`\u2705 view \u306F\u5BA3\u8A00\u3055\u308C\u305F roster \u3092\u3059\u3079\u3066\u96C6\u7D04\u3057\u3066\u3044\u307E\u3059(${result.correctCount} links\u3001\u751F\u6210\u4E0D\u8981)\u3002`
|
|
3437
|
+
);
|
|
3438
|
+
} else {
|
|
3439
|
+
lines.push(
|
|
3440
|
+
"\u2139\uFE0F \u4F5C\u6210\u304C\u5FC5\u8981\u306A symlink \u306F\u3042\u308A\u307E\u305B\u3093\u304C\u3001\u5BFE\u5FDC\u306E\u5FC5\u8981\u306A\u9805\u76EE\u304C\u3042\u308A\u307E\u3059(stray / \u7AF6\u5408 / \u885D\u7A81 / \u5230\u9054\u3067\u304D\u306A\u3044 repo\u3001\u4E0B\u8A18\u53C2\u7167)\u3002"
|
|
3441
|
+
);
|
|
3442
|
+
}
|
|
3443
|
+
lines.push("");
|
|
3444
|
+
if (result.failures.length > 0) {
|
|
3445
|
+
lines.push(`## \u4F5C\u6210\u306B\u5931\u6557 (${result.failures.length}) \u2014 \u4E00\u90E8\u306E symlink \u3092\u4F5C\u6210\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F`);
|
|
3446
|
+
for (const f of result.failures) lines.push(`- ${f.name}: ${f.message}`);
|
|
3447
|
+
lines.push("");
|
|
3448
|
+
}
|
|
3449
|
+
if (result.toPrune.length > 0) {
|
|
3450
|
+
const attempted = result.pruned || result.pruneFailures.length > 0;
|
|
3451
|
+
if (result.pruneWithheld) {
|
|
3452
|
+
lines.push(
|
|
3453
|
+
`${result.toPrune.length} \u4EF6\u306E stray repo symlink \u3092\u64A4\u53BB\u4E88\u5B9A\u3067\u3057\u305F\u304C\u3001\u5230\u9054\u3067\u304D\u306A\u3044 repo \u304C\u3042\u308B\u305F\u3081\u64A4\u53BB\u3092\u4FDD\u7559\u3057\u307E\u3057\u305F(\u5230\u9054\u3067\u304D\u306A\u3044 repo \u306E link \u3068 stray \u3092\u533A\u5225\u3067\u304D\u306A\u3044\u305F\u3081\u3002\u4E0B\u8A18\u306E repo \u3092\u89E3\u6C7A\u3059\u308B\u304B archive \u3057\u3066\u304B\u3089\u518D\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044):`
|
|
3454
|
+
);
|
|
3455
|
+
for (const p of result.toPrune) lines.push(` ${p.name} -> ${p.target}`);
|
|
3456
|
+
} else if (!attempted) {
|
|
3457
|
+
lines.push(
|
|
3458
|
+
`${result.toPrune.length} \u4EF6\u306E stray repo symlink \u3092\u64A4\u53BB\u4E88\u5B9A(dry-run\u3001\u64A4\u53BB\u3059\u308B\u306B\u306F --prune):`
|
|
3459
|
+
);
|
|
3460
|
+
for (const p of result.toPrune) lines.push(` ${p.name} -> ${p.target}`);
|
|
3461
|
+
} else {
|
|
3462
|
+
const failed = new Set(result.pruneFailures.map((f) => f.name));
|
|
3463
|
+
const header = result.pruneFailures.length === 0 ? "\u{1F9F9} stray repo symlink \u3092\u64A4\u53BB\u3057\u307E\u3057\u305F:" : result.pruned ? "stray repo symlink \u3092\u64A4\u53BB\u3057\u307E\u3057\u305F(\u4E00\u90E8\u5931\u6557\u3001\u4E0B\u8A18\u53C2\u7167):" : "stray repo symlink \u3092\u64A4\u53BB\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F(\u4E0B\u8A18\u53C2\u7167):";
|
|
3464
|
+
lines.push(header);
|
|
3465
|
+
for (const p of result.toPrune) {
|
|
3466
|
+
if (failed.has(p.name)) continue;
|
|
3467
|
+
lines.push(` ${p.name} -> ${p.target}`);
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
lines.push("");
|
|
3471
|
+
}
|
|
3472
|
+
if (result.pruneFailures.length > 0) {
|
|
3473
|
+
lines.push(
|
|
3474
|
+
`## \u64A4\u53BB\u306B\u5931\u6557 (${result.pruneFailures.length}) \u2014 \u4E00\u90E8\u306E stray symlink \u3092\u64A4\u53BB\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F`
|
|
3475
|
+
);
|
|
3476
|
+
for (const f of result.pruneFailures) lines.push(`- ${f.name}: ${f.message}`);
|
|
3477
|
+
lines.push("");
|
|
3478
|
+
}
|
|
3479
|
+
if (result.conflicts.length > 0) {
|
|
3480
|
+
lines.push(
|
|
3481
|
+
`## \u7AF6\u5408 (${result.conflicts.length}) \u2014 \u65E2\u5B58\u3092\u4E0A\u66F8\u304D\u3057\u307E\u305B\u3093\u3002\u624B\u52D5\u3067\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044`
|
|
3482
|
+
);
|
|
3483
|
+
for (const c of result.conflicts) {
|
|
3484
|
+
const detail = c.reason === "mismatch" ? `\u5225\u306E\u5834\u6240\u3092\u6307\u3059 symlink(\u73FE\u5728: ${c.actualTarget ?? "?"})` : c.reason === "occupied" ? "symlink \u3067\u306A\u3044\u5B9F\u30D5\u30A1\u30A4\u30EB/\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA" : "\u691C\u67FB\u3067\u304D\u306A\u3044\u30D1\u30B9(\u89AA\u304C\u975E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u7B49)";
|
|
3485
|
+
lines.push(`- ${c.name}: ${detail}`);
|
|
3486
|
+
}
|
|
3487
|
+
lines.push("");
|
|
3488
|
+
}
|
|
3489
|
+
if (result.collisions.length > 0) {
|
|
3490
|
+
lines.push(
|
|
3491
|
+
`## basename \u885D\u7A81 (${result.collisions.length}) \u2014 \u5225 repo \u304C\u540C\u3058 view \u540D\u3092\u53D6\u308A\u5408\u3044(\u81EA\u52D5\u914D\u7DDA\u3057\u307E\u305B\u3093)`
|
|
3492
|
+
);
|
|
3493
|
+
for (const c of result.collisions) lines.push(`- ${c.linkName} \u2190 ${c.repos.join(", ")}`);
|
|
3494
|
+
lines.push("");
|
|
3495
|
+
}
|
|
3496
|
+
if (result.unreachable.length > 0) {
|
|
3497
|
+
lines.push(
|
|
3498
|
+
`## \u5230\u9054\u4E0D\u80FD (${result.unreachable.length}) \u2014 \u30D1\u30B9\u672A\u89E3\u6C7A\u3001\u307E\u305F\u306F view \u81EA\u8EAB\u306B\u89E3\u6C7A\u3059\u308B\u305F\u3081\u96C6\u7D04\u3067\u304D\u307E\u305B\u3093`
|
|
3499
|
+
);
|
|
3500
|
+
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
3501
|
+
lines.push("");
|
|
3502
|
+
}
|
|
3503
|
+
if (result.strayUnknown.length > 0) {
|
|
3504
|
+
lines.push(
|
|
3505
|
+
`## \u672A\u64A4\u53BB\u306E stray (${result.strayUnknown.length}) \u2014 basou \u751F\u6210\u306E repo link \u3068\u78BA\u8A8D\u3067\u304D\u306A\u3044\u305F\u3081\u64A4\u53BB\u3057\u307E\u305B\u3093\u3002\u624B\u52D5\u3067\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044`
|
|
3506
|
+
);
|
|
3507
|
+
for (const s of result.strayUnknown) {
|
|
3508
|
+
const detail = s.reason === "broken" ? "\u30EA\u30F3\u30AF\u5207\u308C(\u30BF\u30FC\u30B2\u30C3\u30C8\u304C\u89E3\u6C7A\u3067\u304D\u307E\u305B\u3093)" : s.reason === "non-repo" ? "git repo \u3067\u306A\u3044\u30BF\u30FC\u30B2\u30C3\u30C8(\u30D5\u30A1\u30A4\u30EB\u3001\u307E\u305F\u306F .git \u306E\u7121\u3044\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA)" : "\u7D76\u5BFE\u30D1\u30B9\u306E\u30BF\u30FC\u30B2\u30C3\u30C8(basou \u306F\u76F8\u5BFE\u30EA\u30F3\u30AF\u306E\u307F\u751F\u6210\u3057\u307E\u3059)";
|
|
3509
|
+
lines.push(`- ${s.name} -> ${s.target}: ${detail}`);
|
|
3510
|
+
}
|
|
3511
|
+
lines.push("");
|
|
3512
|
+
}
|
|
3513
|
+
lines.push(
|
|
3514
|
+
"\u6CE8: \u4F5C\u6210(--apply)\u306F\u65E2\u5B58\u30A8\u30F3\u30C8\u30EA\u3092\u4E0A\u66F8\u304D\u3057\u307E\u305B\u3093\u3002stray repo link \u306E\u64A4\u53BB\u306F --prune \u3067\u884C\u3044\u307E\u3059(symlink \u306E\u307F\u524A\u9664\u3057\u3001\u53C2\u7167\u5148 repo \u306F\u524A\u9664\u3057\u307E\u305B\u3093)\u3002basou \u751F\u6210\u3068\u78BA\u8A8D\u3067\u304D\u306A\u3044 stray(\u30EA\u30F3\u30AF\u5207\u308C / \u975E repo / \u7D76\u5BFE\u30D1\u30B9)\u306F\u64A4\u53BB\u3057\u307E\u305B\u3093\u3002"
|
|
3515
|
+
);
|
|
3516
|
+
return lines.join("\n");
|
|
3517
|
+
}
|
|
3518
|
+
async function runProjectPreset(options, ctx = {}) {
|
|
3519
|
+
try {
|
|
3520
|
+
await doRunProjectPreset(options, ctx);
|
|
3521
|
+
} catch (error) {
|
|
3522
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
3523
|
+
process.exitCode = 1;
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
function canonicalFileFor(anchorReal, canonicalName) {
|
|
3527
|
+
return join4(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
3528
|
+
}
|
|
3529
|
+
function canonicalLabelFor(canonicalName) {
|
|
3530
|
+
return join4("agents", canonicalName, CANONICAL_FILE);
|
|
3531
|
+
}
|
|
3532
|
+
async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
3533
|
+
const declared = {
|
|
3534
|
+
path: entry.path,
|
|
3535
|
+
...entry.visibility !== void 0 ? { visibility: entry.visibility } : {},
|
|
3536
|
+
...entry.language !== void 0 ? { language: entry.language } : {},
|
|
3537
|
+
...entry.publishes !== void 0 ? { publishes: entry.publishes } : {}
|
|
3538
|
+
};
|
|
3539
|
+
let real;
|
|
3540
|
+
try {
|
|
3541
|
+
real = realpathSync(resolve3(repositoryRoot, entry.path));
|
|
3542
|
+
} catch {
|
|
3543
|
+
return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
|
|
3544
|
+
}
|
|
3545
|
+
if (real === anchorReal) {
|
|
3546
|
+
return { ...declared, isAnchor: true, reachable: true, canonicalPresent: false };
|
|
3547
|
+
}
|
|
3548
|
+
if (!existsSync(join4(real, ".git"))) {
|
|
3549
|
+
return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
|
|
3550
|
+
}
|
|
3551
|
+
const canonicalName = basename3(real);
|
|
3552
|
+
let content;
|
|
3553
|
+
try {
|
|
3554
|
+
content = await readMarkdownFile4(canonicalFileFor(anchorReal, canonicalName));
|
|
3555
|
+
} catch {
|
|
3556
|
+
return {
|
|
3557
|
+
...declared,
|
|
3558
|
+
isAnchor: false,
|
|
3559
|
+
reachable: true,
|
|
3560
|
+
canonicalName,
|
|
3561
|
+
canonicalPresent: true,
|
|
3562
|
+
canonicalReadable: false
|
|
3563
|
+
};
|
|
3564
|
+
}
|
|
3565
|
+
if (content === null) {
|
|
3566
|
+
return {
|
|
3567
|
+
...declared,
|
|
3568
|
+
isAnchor: false,
|
|
3569
|
+
reachable: true,
|
|
3570
|
+
canonicalName,
|
|
3571
|
+
canonicalPresent: false
|
|
3572
|
+
};
|
|
3573
|
+
}
|
|
3574
|
+
const section = parseMarkers(content);
|
|
3575
|
+
return {
|
|
3576
|
+
...declared,
|
|
3577
|
+
isAnchor: false,
|
|
3578
|
+
reachable: true,
|
|
3579
|
+
canonicalName,
|
|
3580
|
+
canonicalPresent: true,
|
|
3581
|
+
canonicalReadable: true,
|
|
3582
|
+
markerKind: section.kind,
|
|
3583
|
+
...section.kind === "ok" ? { currentBlock: section.generated } : {}
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
async function applyPresetPlan(anchorReal, plan) {
|
|
3587
|
+
const file = canonicalFileFor(anchorReal, plan.canonicalName);
|
|
3588
|
+
const label = canonicalLabelFor(plan.canonicalName);
|
|
3589
|
+
let isLink = false;
|
|
3590
|
+
try {
|
|
3591
|
+
isLink = lstatSync(file).isSymbolicLink();
|
|
3592
|
+
} catch {
|
|
3593
|
+
isLink = false;
|
|
3594
|
+
}
|
|
3595
|
+
if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
|
|
3596
|
+
if (plan.action === "create") mkdirSync(dirname(file), { recursive: true });
|
|
3597
|
+
const existing = await readMarkdownFile4(file);
|
|
3598
|
+
await writeMarkdownFile5(file, renderWithMarkers4(existing, plan.desiredBlock, label));
|
|
3599
|
+
}
|
|
3600
|
+
function presetFailureReason(error) {
|
|
3601
|
+
if (error instanceof Error && (error.message.startsWith("Markers") || error.message.startsWith("Canonical"))) {
|
|
3602
|
+
return error.message;
|
|
3603
|
+
}
|
|
3604
|
+
const cause = error instanceof Error ? error.cause : void 0;
|
|
3605
|
+
if (hasErrorCode(cause)) return cause.code;
|
|
3606
|
+
if (hasErrorCode(error)) return error.code;
|
|
3607
|
+
return "unknown error";
|
|
3608
|
+
}
|
|
3609
|
+
async function doRunProjectPreset(options, ctx) {
|
|
3610
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
3611
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project preset");
|
|
3612
|
+
const paths = basouPaths9(repositoryRoot);
|
|
3613
|
+
const manifest = await readManifest5(paths);
|
|
3614
|
+
const roster = manifest.repos ?? [];
|
|
3615
|
+
const anchorReal = realpathSync(repositoryRoot);
|
|
3616
|
+
const facts = [];
|
|
3617
|
+
for (const entry of roster) facts.push(await gatherRepoPreset(repositoryRoot, anchorReal, entry));
|
|
3618
|
+
const summary = summarizePresetPlan(facts);
|
|
3619
|
+
const failures = [];
|
|
3620
|
+
let writtenCount = 0;
|
|
3621
|
+
if (options.apply === true && summary.plans.length > 0) {
|
|
3622
|
+
for (const plan of summary.plans) {
|
|
3623
|
+
try {
|
|
3624
|
+
await applyPresetPlan(anchorReal, plan);
|
|
3625
|
+
writtenCount += 1;
|
|
3626
|
+
} catch (error) {
|
|
3627
|
+
failures.push({ repo: plan.path, message: presetFailureReason(error) });
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
const result = {
|
|
3632
|
+
...summary,
|
|
3633
|
+
hasRoster: roster.length > 0,
|
|
3634
|
+
applied: writtenCount > 0,
|
|
3635
|
+
failures
|
|
3636
|
+
};
|
|
3637
|
+
if (options.json === true) {
|
|
3638
|
+
console.log(JSON.stringify(result));
|
|
3639
|
+
} else {
|
|
3640
|
+
console.log(renderProjectPreset(result));
|
|
3641
|
+
}
|
|
3642
|
+
return result;
|
|
3643
|
+
}
|
|
3644
|
+
function presetActionLabel(action) {
|
|
3645
|
+
return action === "create" ? "\u65B0\u898F\u4F5C\u6210" : "\u66F4\u65B0";
|
|
3646
|
+
}
|
|
3647
|
+
function renderProjectPreset(result) {
|
|
3648
|
+
const lines = [];
|
|
3649
|
+
lines.push("# \u6307\u793A\u66F8 A \u30D7\u30EA\u30BB\u30C3\u30C8\u751F\u6210(\u5BA3\u8A00 \u2192 canonical \u306E\u751F\u6210\u9818\u57DF)");
|
|
3650
|
+
lines.push("");
|
|
3651
|
+
if (!result.hasRoster) {
|
|
3652
|
+
lines.push(
|
|
3653
|
+
"\u2139\uFE0F repo \u30ED\u30FC\u30B9\u30BF\u30FC\u304C\u672A\u5BA3\u8A00\u3067\u3059(manifest \u306E `repos`)\u3002`basou project adopt` \u3067\u5BA3\u8A00\u3057\u3066\u304B\u3089\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
3654
|
+
);
|
|
3655
|
+
return lines.join("\n");
|
|
3656
|
+
}
|
|
3657
|
+
if (result.plans.length > 0) {
|
|
3658
|
+
const attempted = result.applied || result.failures.length > 0;
|
|
3659
|
+
if (!attempted) {
|
|
3660
|
+
lines.push(
|
|
3661
|
+
`${result.plans.length} repo \u306E canonical \u306B A \u30D7\u30EA\u30BB\u30C3\u30C8\u3092\u751F\u6210\u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`
|
|
3662
|
+
);
|
|
3663
|
+
for (const p of result.plans) {
|
|
3664
|
+
lines.push(
|
|
3665
|
+
`- ${p.path} [${presetActionLabel(p.action)}] \u2192 ${canonicalLabelFor(p.canonicalName)}`
|
|
3666
|
+
);
|
|
3667
|
+
for (const bl of p.desiredBlock.split("\n")) lines.push(` ${bl}`);
|
|
3668
|
+
}
|
|
3669
|
+
} else {
|
|
3670
|
+
const failed = new Set(result.failures.map((f) => f.repo));
|
|
3671
|
+
const header = result.failures.length === 0 ? "\u2705 canonical \u306B A \u30D7\u30EA\u30BB\u30C3\u30C8\u3092\u751F\u6210\u3057\u307E\u3057\u305F:" : result.applied ? "A \u30D7\u30EA\u30BB\u30C3\u30C8\u3092\u751F\u6210\u3057\u307E\u3057\u305F(\u4E00\u90E8\u5931\u6557\u3001\u4E0B\u8A18\u53C2\u7167):" : "A \u30D7\u30EA\u30BB\u30C3\u30C8\u3092\u751F\u6210\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F(\u4E0B\u8A18\u53C2\u7167):";
|
|
3672
|
+
lines.push(header);
|
|
3673
|
+
for (const p of result.plans) {
|
|
3674
|
+
if (failed.has(p.path)) continue;
|
|
3675
|
+
lines.push(
|
|
3676
|
+
`- ${p.path} [${presetActionLabel(p.action)}] \u2192 ${canonicalLabelFor(p.canonicalName)}`
|
|
3677
|
+
);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
} else if (result.ok) {
|
|
3681
|
+
lines.push("\u2705 \u5BA3\u8A00\u3055\u308C\u305F\u5168 repo \u306E A \u30D7\u30EA\u30BB\u30C3\u30C8\u306F canonical \u3068\u540C\u671F\u6E08\u307F\u3067\u3059(\u751F\u6210\u4E0D\u8981)\u3002");
|
|
3682
|
+
} else {
|
|
3683
|
+
lines.push(
|
|
3684
|
+
"\u2139\uFE0F \u751F\u6210\u304C\u5FC5\u8981\u306A repo \u306F\u3042\u308A\u307E\u305B\u3093\u304C\u3001\u30DE\u30FC\u30AB\u30FC\u7AF6\u5408 / \u885D\u7A81 / \u672A\u5BA3\u8A00 / \u5230\u9054\u3067\u304D\u306A\u3044 repo \u304C\u3042\u308A\u307E\u3059(\u4E0B\u8A18\u53C2\u7167)\u3002"
|
|
3685
|
+
);
|
|
3686
|
+
}
|
|
3687
|
+
lines.push("");
|
|
3688
|
+
if (result.inSync.length > 0) {
|
|
3689
|
+
lines.push(`\u540C\u671F\u6E08\u307F (${result.inSync.length}): ${result.inSync.join(", ")}`);
|
|
3690
|
+
lines.push("");
|
|
3691
|
+
}
|
|
3692
|
+
if (result.failures.length > 0) {
|
|
3693
|
+
lines.push(
|
|
3694
|
+
`## \u66F8\u304D\u8FBC\u307F\u306B\u5931\u6557 (${result.failures.length}) \u2014 \u4E00\u90E8\u306E canonical \u3092\u66F8\u3051\u307E\u305B\u3093\u3067\u3057\u305F`
|
|
3695
|
+
);
|
|
3696
|
+
for (const f of result.failures) lines.push(`- ${f.repo}: ${f.message}`);
|
|
3697
|
+
lines.push("");
|
|
3698
|
+
}
|
|
3699
|
+
if (result.markerConflicts.length > 0) {
|
|
3700
|
+
lines.push(
|
|
3701
|
+
`## \u30DE\u30FC\u30AB\u30FC\u7AF6\u5408 (${result.markerConflicts.length}) \u2014 canonical \u306E\u30DE\u30FC\u30AB\u30FC\u304C\u7121\u3044/\u58CA\u308C\u3066\u3044\u308B\u305F\u3081\u4E0A\u66F8\u304D\u3057\u307E\u305B\u3093`
|
|
3702
|
+
);
|
|
3703
|
+
for (const c of result.markerConflicts) {
|
|
3704
|
+
const detail = c.reason === "no_markers" ? "\u30DE\u30FC\u30AB\u30FC\u9818\u57DF\u304C\u7121\u3044" : `\u30DE\u30FC\u30AB\u30FC\u4E0D\u6574\u5408(${c.reason})`;
|
|
3705
|
+
lines.push(`- ${c.repo}: ${detail}`);
|
|
3706
|
+
}
|
|
3707
|
+
lines.push(
|
|
3708
|
+
` \u5BFE\u51E6: A \u30D7\u30EA\u30BB\u30C3\u30C8\u3092\u5165\u308C\u305F\u3044\u4F4D\u7F6E\u306B\u6B21\u306E2\u884C\u3092\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044 \u2014 \`${GENERATED_START}\` \u3068 \`${GENERATED_END}\`(\u7121\u3051\u308C\u3070 basou \u304C\u65B0\u898F canonical \u3092\u4F5C\u308A\u307E\u3059)\u3002`
|
|
3709
|
+
);
|
|
3710
|
+
lines.push("");
|
|
3711
|
+
}
|
|
3712
|
+
if (result.unreadable.length > 0) {
|
|
3713
|
+
lines.push(
|
|
3714
|
+
`## canonical \u8AAD\u307F\u53D6\u308A\u4E0D\u80FD (${result.unreadable.length}) \u2014 \u30C7\u30A3\u30EC\u30AF\u30C8\u30EA/\u6A29\u9650\u7B49\u3067\u8AAD\u3081\u307E\u305B\u3093`
|
|
3715
|
+
);
|
|
3716
|
+
for (const p of result.unreadable) lines.push(`- ${p}`);
|
|
3717
|
+
lines.push("");
|
|
3718
|
+
}
|
|
3719
|
+
if (result.collisions.length > 0) {
|
|
3720
|
+
lines.push(
|
|
3721
|
+
`## canonical \u885D\u7A81 (${result.collisions.length}) \u2014 \u5225 repo \u304C\u540C\u540D canonical \u3092\u5171\u6709(\u81EA\u52D5\u751F\u6210\u3057\u307E\u305B\u3093)`
|
|
3722
|
+
);
|
|
3723
|
+
for (const c of result.collisions) {
|
|
3724
|
+
lines.push(`- agents/${c.canonicalName}/AGENTS.md \u2190 ${c.repos.join(", ")}`);
|
|
3725
|
+
}
|
|
3726
|
+
lines.push("");
|
|
3727
|
+
}
|
|
3728
|
+
if (result.undeclared.length > 0) {
|
|
3729
|
+
lines.push(
|
|
3730
|
+
`## \u5BA3\u8A00\u306A\u3057 (${result.undeclared.length}) \u2014 visibility / language / publishes \u304C\u672A\u8A2D\u5B9A\u306E\u305F\u3081\u751F\u6210\u3057\u307E\u305B\u3093`
|
|
3731
|
+
);
|
|
3732
|
+
for (const p of result.undeclared) lines.push(`- ${p}`);
|
|
3733
|
+
lines.push("");
|
|
3734
|
+
}
|
|
3735
|
+
if (result.anchors.length > 0) {
|
|
3736
|
+
lines.push(
|
|
3737
|
+
`## anchor (${result.anchors.length}) \u2014 \u81EA\u8EAB\u306E AGENTS.md \u306F\u624B\u3067\u7DAD\u6301\u3059\u308B\u305F\u3081\u30B9\u30AD\u30C3\u30D7`
|
|
3738
|
+
);
|
|
3739
|
+
for (const p of result.anchors) lines.push(`- ${p}`);
|
|
3740
|
+
lines.push("");
|
|
3741
|
+
}
|
|
3742
|
+
if (result.unreachable.length > 0) {
|
|
3743
|
+
lines.push(`## \u5230\u9054\u4E0D\u80FD (${result.unreachable.length}) \u2014 \u30D1\u30B9\u672A\u89E3\u6C7A / git repo \u3067\u306A\u3044`);
|
|
3744
|
+
for (const p of result.unreachable) lines.push(`- ${p}`);
|
|
3745
|
+
lines.push("");
|
|
3746
|
+
}
|
|
3747
|
+
lines.push(
|
|
3748
|
+
"\u6CE8: \u30DE\u30FC\u30AB\u30FC\u9818\u57DF\u306E\u307F\u3092\u751F\u6210\u3057\u3001canonical \u306E\u624B\u66F8\u304D\u90E8\u5206(\u30DE\u30FC\u30AB\u30FC\u5916)\u306F\u4FDD\u6301\u3057\u307E\u3059\u3002\u751F\u6210\u5185\u5BB9\u306F manifest \u306E\u5BA3\u8A00\u304B\u3089\u5C0E\u51FA\u3055\u308C\u307E\u3059\u3002"
|
|
3749
|
+
);
|
|
3750
|
+
return lines.join("\n");
|
|
3751
|
+
}
|
|
3752
|
+
async function runProjectArchive(target, options, ctx = {}) {
|
|
3753
|
+
try {
|
|
3754
|
+
await doRunProjectArchive(target, options, ctx);
|
|
3755
|
+
} catch (error) {
|
|
3756
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
3757
|
+
process.exitCode = 1;
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
3761
|
+
const empty = {
|
|
3762
|
+
inspected: false,
|
|
3763
|
+
viewLink: false,
|
|
3764
|
+
instructionFiles: [],
|
|
3765
|
+
gitignorePatterns: [],
|
|
3766
|
+
canonical: false
|
|
3767
|
+
};
|
|
3768
|
+
let real;
|
|
3769
|
+
try {
|
|
3770
|
+
real = realpathSync(resolve3(repositoryRoot, target));
|
|
3771
|
+
} catch {
|
|
3772
|
+
return empty;
|
|
3773
|
+
}
|
|
3774
|
+
const anchorReal = realpathSync(repositoryRoot);
|
|
3775
|
+
const canonicalName = basename3(real);
|
|
3776
|
+
const instructionFiles = [];
|
|
3777
|
+
for (const name of INSTRUCTION_FILES) {
|
|
3778
|
+
try {
|
|
3779
|
+
lstatSync(join4(real, name));
|
|
3780
|
+
instructionFiles.push(name);
|
|
3781
|
+
} catch {
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
let ignored;
|
|
3785
|
+
try {
|
|
3786
|
+
ignored = new Set(readGitignoreLines(join4(real, ".gitignore")).map((l) => l.trim()));
|
|
3787
|
+
} catch {
|
|
3788
|
+
ignored = /* @__PURE__ */ new Set();
|
|
3789
|
+
}
|
|
3790
|
+
const gitignorePatterns = INSTRUCTION_FILES.filter((p) => ignored.has(p) || ignored.has(`/${p}`));
|
|
3791
|
+
const canonical2 = existsSync(join4(anchorReal, "agents", canonicalName, CANONICAL_FILE));
|
|
3792
|
+
let viewLink = false;
|
|
3793
|
+
const viewPath = manifest.workspace.view;
|
|
3794
|
+
if (viewPath !== void 0) {
|
|
3795
|
+
try {
|
|
3796
|
+
lstatSync(join4(resolveViewDir(repositoryRoot, viewPath), canonicalName));
|
|
3797
|
+
viewLink = true;
|
|
3798
|
+
} catch {
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
return {
|
|
3802
|
+
inspected: true,
|
|
3803
|
+
viewLink,
|
|
3804
|
+
instructionFiles,
|
|
3805
|
+
gitignorePatterns: [...gitignorePatterns],
|
|
3806
|
+
canonical: canonical2
|
|
3807
|
+
};
|
|
3808
|
+
}
|
|
3809
|
+
function omitKey(obj, key) {
|
|
3810
|
+
const clone = { ...obj };
|
|
3811
|
+
delete clone[key];
|
|
3812
|
+
return clone;
|
|
3813
|
+
}
|
|
3814
|
+
function buildArchivedManifest(manifest, plan, updatedAt) {
|
|
3815
|
+
let next = { ...manifest, workspace: { ...manifest.workspace, updated_at: updatedAt } };
|
|
3816
|
+
next = plan.reposEmptied ? omitKey(next, "repos") : { ...next, repos: plan.nextRepos };
|
|
3817
|
+
if (plan.nextSourceRoots !== void 0) {
|
|
3818
|
+
if (plan.nextSourceRoots.length === 0) {
|
|
3819
|
+
const prunedImport = manifest.import !== void 0 ? omitKey(manifest.import, "source_roots") : {};
|
|
3820
|
+
next = Object.keys(prunedImport).length === 0 ? omitKey(next, "import") : { ...next, import: prunedImport };
|
|
3821
|
+
} else {
|
|
3822
|
+
next = {
|
|
3823
|
+
...next,
|
|
3824
|
+
import: { ...manifest.import ?? {}, source_roots: plan.nextSourceRoots }
|
|
3825
|
+
};
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
return next;
|
|
3829
|
+
}
|
|
3830
|
+
async function doRunProjectArchive(target, options, ctx) {
|
|
3831
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
3832
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project archive");
|
|
3833
|
+
const paths = basouPaths9(repositoryRoot);
|
|
3834
|
+
const manifest = await readManifest5(paths);
|
|
3835
|
+
const roster = manifest.repos ?? [];
|
|
3836
|
+
let targetIsAnchor = false;
|
|
3837
|
+
try {
|
|
3838
|
+
targetIsAnchor = realpathSync(resolve3(repositoryRoot, target)) === realpathSync(repositoryRoot);
|
|
3839
|
+
} catch {
|
|
3840
|
+
targetIsAnchor = false;
|
|
3841
|
+
}
|
|
3842
|
+
const plan = planArchive({
|
|
3843
|
+
...manifest.repos !== void 0 ? { repos: manifest.repos } : {},
|
|
3844
|
+
...manifest.import?.source_roots !== void 0 ? { sourceRoots: manifest.import.source_roots } : {},
|
|
3845
|
+
target,
|
|
3846
|
+
targetIsAnchor
|
|
3847
|
+
});
|
|
3848
|
+
const teardown = plan.found && !plan.isAnchor ? gatherArchiveTeardown(repositoryRoot, manifest, target) : {
|
|
3849
|
+
inspected: false,
|
|
3850
|
+
viewLink: false,
|
|
3851
|
+
instructionFiles: [],
|
|
3852
|
+
gitignorePatterns: [],
|
|
3853
|
+
canonical: false
|
|
3854
|
+
};
|
|
3855
|
+
const applied = options.apply === true && plan.found && !plan.isAnchor;
|
|
3856
|
+
if (applied) {
|
|
3857
|
+
const now = ctx.now ?? (() => /* @__PURE__ */ new Date());
|
|
3858
|
+
await writeManifest2(paths, buildArchivedManifest(manifest, plan, now().toISOString()), {
|
|
3859
|
+
force: true
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3862
|
+
const result = {
|
|
3863
|
+
...plan,
|
|
3864
|
+
hasRoster: roster.length > 0,
|
|
3865
|
+
applied,
|
|
3866
|
+
teardown,
|
|
3867
|
+
preservedUnknownFields: unknownManifestKeys(manifest)
|
|
3868
|
+
};
|
|
3869
|
+
if (options.json === true) {
|
|
3870
|
+
console.log(JSON.stringify(result));
|
|
3871
|
+
} else {
|
|
3872
|
+
console.log(renderProjectArchive(result));
|
|
3873
|
+
}
|
|
3874
|
+
return result;
|
|
3875
|
+
}
|
|
3876
|
+
function renderProjectArchive(result) {
|
|
3877
|
+
const lines = [];
|
|
3878
|
+
lines.push("# repo \u306E archive(roster \u304B\u3089\u7573\u3080)");
|
|
3879
|
+
lines.push("");
|
|
3880
|
+
lines.push(...preservedUnknownLines(result.preservedUnknownFields));
|
|
3881
|
+
if (!result.hasRoster) {
|
|
3882
|
+
lines.push("\u2139\uFE0F repo \u30ED\u30FC\u30B9\u30BF\u30FC\u304C\u672A\u5BA3\u8A00\u3067\u3059(manifest \u306E `repos`)\u3002archive \u5BFE\u8C61\u304C\u3042\u308A\u307E\u305B\u3093\u3002");
|
|
3883
|
+
return lines.join("\n");
|
|
3884
|
+
}
|
|
3885
|
+
if (result.isAnchor) {
|
|
3886
|
+
lines.push(
|
|
3887
|
+
`\u26A0\uFE0F \`${result.target}\` \u306F anchor(\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E root)\u3067\u3059\u3002anchor \u306F archive \u3067\u304D\u307E\u305B\u3093(manifest \u306E\u5BB6\u306E\u305F\u3081)\u3002`
|
|
3888
|
+
);
|
|
3889
|
+
return lines.join("\n");
|
|
3890
|
+
}
|
|
3891
|
+
if (!result.found) {
|
|
3892
|
+
lines.push(`\u2139\uFE0F \`${result.target}\` \u306F roster \u306B\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u305B\u3093(archive \u5BFE\u8C61\u306A\u3057)\u3002`);
|
|
3893
|
+
return lines.join("\n");
|
|
3894
|
+
}
|
|
3895
|
+
if (result.applied) {
|
|
3896
|
+
lines.push(`\u2705 \`${result.target}\` \u3092 roster \u304B\u3089\u524A\u9664\u3057\u307E\u3057\u305F\u3002`);
|
|
3897
|
+
} else {
|
|
3898
|
+
lines.push(`\`${result.target}\` \u3092 roster \u304B\u3089\u524A\u9664\u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`);
|
|
3899
|
+
}
|
|
3900
|
+
if (result.sourceRootRemoval !== void 0) {
|
|
3901
|
+
lines.push(
|
|
3902
|
+
`- source_roots \u304B\u3089 ${result.sourceRootRemoval} \u3092 prune${result.applied ? "\u3057\u307E\u3057\u305F" : "\u3057\u307E\u3059"}(\u4EE5\u5F8C refresh \u306E\u5BFE\u8C61\u5916)\u3002`
|
|
3903
|
+
);
|
|
3904
|
+
} else {
|
|
3905
|
+
lines.push("- source_roots \u306B\u8A72\u5F53\u30A8\u30F3\u30C8\u30EA\u306F\u3042\u308A\u307E\u305B\u3093(prune \u4E0D\u8981)\u3002");
|
|
3906
|
+
}
|
|
3907
|
+
if (result.reposEmptied) {
|
|
3908
|
+
lines.push(
|
|
3909
|
+
"- \u3053\u308C\u304C\u6700\u5F8C\u306E\u30E1\u30F3\u30D0\u30FC\u3067\u3059 \u2192 roster \u306F\u7A7A\u306B\u306A\u308A `repos` \u5BA3\u8A00\u306F\u9664\u53BB\u3055\u308C\u307E\u3059(\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u7573\u3080)\u3002"
|
|
3910
|
+
);
|
|
3911
|
+
} else if (result.becomesSolo) {
|
|
3912
|
+
lines.push(
|
|
3913
|
+
"- \u6B8B\u308A 1 repo(solo)\u306B\u306A\u308A\u307E\u3059 \u2192 workspace view \u306F\u4E0D\u8981\u3067\u3059(view \u5BA3\u8A00/\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306E\u64A4\u53BB\u3092\u691C\u8A0E)\u3002"
|
|
3914
|
+
);
|
|
3915
|
+
}
|
|
3916
|
+
lines.push("");
|
|
3917
|
+
const t = result.teardown;
|
|
3918
|
+
const items = [];
|
|
3919
|
+
if (t.viewLink) items.push("workspace view \u306E symlink \u30A8\u30F3\u30C8\u30EA");
|
|
3920
|
+
if (t.instructionFiles.length > 0) items.push(`\u6307\u793A\u66F8(${t.instructionFiles.join(", ")})`);
|
|
3921
|
+
if (t.gitignorePatterns.length > 0)
|
|
3922
|
+
items.push(`.gitignore \u306E\u6307\u793A\u66F8\u30D1\u30BF\u30FC\u30F3(${t.gitignorePatterns.join(", ")})`);
|
|
3923
|
+
if (t.canonical) items.push(`anchor \u306E canonical(agents/${basename3(result.target)}/AGENTS.md)`);
|
|
3924
|
+
if (!t.inspected) {
|
|
3925
|
+
lines.push("## \u624B\u52D5 teardown(repo \u304C\u30C7\u30A3\u30B9\u30AF\u4E0A\u306B\u89E3\u6C7A\u3067\u304D\u306A\u3044\u305F\u3081\u672A\u691C\u67FB)");
|
|
3926
|
+
lines.push(
|
|
3927
|
+
"- repo \u306F\u65E2\u306B\u524A\u9664\u6E08\u307F\u306E\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002view symlink / \u6307\u793A\u66F8 symlink / .gitignore / canonical \u304C\u6B8B\u3063\u3066\u3044\u306A\u3044\u304B\u624B\u52D5\u3067\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
3928
|
+
);
|
|
3929
|
+
lines.push("");
|
|
3930
|
+
} else if (items.length > 0) {
|
|
3931
|
+
lines.push("## \u624B\u52D5 teardown(--apply \u306F\u89E6\u308C\u307E\u305B\u3093\u3002\u6B8B\u3063\u3066\u3044\u308B wiring \u3092\u624B\u3067\u64A4\u53BB\u3057\u3066\u304F\u3060\u3055\u3044)");
|
|
3932
|
+
for (const i of items) lines.push(`- ${i}`);
|
|
3933
|
+
lines.push("");
|
|
3934
|
+
} else {
|
|
3935
|
+
lines.push("repo \u5074\u306E wiring(view/\u6307\u793A\u66F8/.gitignore/canonical)\u306F\u6B8B\u3063\u3066\u3044\u307E\u305B\u3093\u3002");
|
|
3936
|
+
lines.push("");
|
|
3937
|
+
}
|
|
3938
|
+
lines.push(
|
|
3939
|
+
"\u6CE8: archive \u306F manifest(.basou\u3001git \u8FFD\u8DE1=\u53EF\u9006)\u306E\u307F\u3092\u5909\u66F4\u3057\u307E\u3059\u3002repo\u30FB\u6355\u6349\u5C65\u6B74\u30FBon-disk \u306E wiring \u306F\u524A\u9664\u3057\u307E\u305B\u3093\u3002"
|
|
3940
|
+
);
|
|
3941
|
+
return lines.join("\n");
|
|
3942
|
+
}
|
|
3943
|
+
async function runProjectRename(oldPath, newPath, options, ctx = {}) {
|
|
3944
|
+
try {
|
|
3945
|
+
await doRunProjectRename(oldPath, newPath, options, ctx);
|
|
3946
|
+
} catch (error) {
|
|
3947
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
3948
|
+
process.exitCode = 1;
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
function gatherRenameWiring(repositoryRoot, manifest, oldBasename) {
|
|
3952
|
+
let anchorReal;
|
|
3953
|
+
try {
|
|
3954
|
+
anchorReal = realpathSync(repositoryRoot);
|
|
3955
|
+
} catch {
|
|
3956
|
+
return { canonicalDirOld: false, viewLinkOld: false };
|
|
3957
|
+
}
|
|
3958
|
+
const canonicalDirOld = existsSync(join4(anchorReal, "agents", oldBasename));
|
|
3959
|
+
let viewLinkOld = false;
|
|
3960
|
+
const viewPath = manifest.workspace.view;
|
|
3961
|
+
if (viewPath !== void 0) {
|
|
3962
|
+
try {
|
|
3963
|
+
lstatSync(join4(resolveViewDir(repositoryRoot, viewPath), oldBasename));
|
|
3964
|
+
viewLinkOld = true;
|
|
3965
|
+
} catch {
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
return { canonicalDirOld, viewLinkOld };
|
|
3969
|
+
}
|
|
3970
|
+
function buildRenamedManifest(manifest, plan, updatedAt) {
|
|
3971
|
+
const next = {
|
|
3972
|
+
...manifest,
|
|
3973
|
+
workspace: { ...manifest.workspace, updated_at: updatedAt },
|
|
3974
|
+
repos: plan.nextRepos
|
|
3975
|
+
};
|
|
3976
|
+
if (plan.nextSourceRoots !== void 0) {
|
|
3977
|
+
return { ...next, import: { ...manifest.import ?? {}, source_roots: plan.nextSourceRoots } };
|
|
3978
|
+
}
|
|
3979
|
+
return next;
|
|
3980
|
+
}
|
|
3981
|
+
async function doRunProjectRename(oldPath, newPath, options, ctx) {
|
|
3982
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
3983
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project rename");
|
|
3984
|
+
const paths = basouPaths9(repositoryRoot);
|
|
3985
|
+
const manifest = await readManifest5(paths);
|
|
3986
|
+
const roster = manifest.repos ?? [];
|
|
3987
|
+
let oldIsAnchor = false;
|
|
3988
|
+
try {
|
|
3989
|
+
oldIsAnchor = realpathSync(resolve3(repositoryRoot, oldPath)) === realpathSync(repositoryRoot);
|
|
3990
|
+
} catch {
|
|
3991
|
+
oldIsAnchor = false;
|
|
3992
|
+
}
|
|
3993
|
+
const plan = planRename({
|
|
3994
|
+
...manifest.repos !== void 0 ? { repos: manifest.repos } : {},
|
|
3995
|
+
...manifest.import?.source_roots !== void 0 ? { sourceRoots: manifest.import.source_roots } : {},
|
|
3996
|
+
oldPath,
|
|
3997
|
+
newPath,
|
|
3998
|
+
oldIsAnchor
|
|
3999
|
+
});
|
|
4000
|
+
const actionable = plan.found && !plan.isAnchor && !plan.collision && !plan.noop;
|
|
4001
|
+
const wiring = actionable && plan.basenameChanged ? gatherRenameWiring(repositoryRoot, manifest, pathBasename(plan.oldTarget)) : { canonicalDirOld: false, viewLinkOld: false };
|
|
4002
|
+
const applied = options.apply === true && actionable;
|
|
4003
|
+
if (applied) {
|
|
4004
|
+
const now = ctx.now ?? (() => /* @__PURE__ */ new Date());
|
|
4005
|
+
await writeManifest2(paths, buildRenamedManifest(manifest, plan, now().toISOString()), {
|
|
4006
|
+
force: true
|
|
4007
|
+
});
|
|
4008
|
+
}
|
|
4009
|
+
const result = {
|
|
4010
|
+
...plan,
|
|
4011
|
+
hasRoster: roster.length > 0,
|
|
4012
|
+
applied,
|
|
4013
|
+
wiring,
|
|
4014
|
+
preservedUnknownFields: unknownManifestKeys(manifest)
|
|
4015
|
+
};
|
|
4016
|
+
if (options.json === true) {
|
|
4017
|
+
console.log(JSON.stringify(result));
|
|
4018
|
+
} else {
|
|
4019
|
+
console.log(renderProjectRename(result));
|
|
4020
|
+
}
|
|
4021
|
+
return result;
|
|
4022
|
+
}
|
|
4023
|
+
function renderProjectRename(result) {
|
|
4024
|
+
const lines = [];
|
|
4025
|
+
lines.push("# repo \u306E rename(roster \u306E\u30D1\u30B9\u66F4\u65B0)");
|
|
4026
|
+
lines.push("");
|
|
4027
|
+
lines.push(...preservedUnknownLines(result.preservedUnknownFields));
|
|
4028
|
+
if (!result.hasRoster) {
|
|
4029
|
+
lines.push("\u2139\uFE0F repo \u30ED\u30FC\u30B9\u30BF\u30FC\u304C\u672A\u5BA3\u8A00\u3067\u3059(manifest \u306E `repos`)\u3002rename \u5BFE\u8C61\u304C\u3042\u308A\u307E\u305B\u3093\u3002");
|
|
4030
|
+
return lines.join("\n");
|
|
4031
|
+
}
|
|
4032
|
+
if (result.noop) {
|
|
4033
|
+
lines.push(`\u2139\uFE0F \`${result.oldTarget}\` \u3068 \`${result.newTarget}\` \u306F\u540C\u4E00\u3067\u3059(\u5909\u66F4\u306A\u3057)\u3002`);
|
|
4034
|
+
return lines.join("\n");
|
|
4035
|
+
}
|
|
4036
|
+
if (result.isAnchor) {
|
|
4037
|
+
lines.push(
|
|
4038
|
+
`\u26A0\uFE0F \`${result.oldTarget}\` \u306F anchor(\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E root)\u3067\u3059\u3002anchor \u306F rename \u3067\u304D\u307E\u305B\u3093\u3002`
|
|
4039
|
+
);
|
|
4040
|
+
return lines.join("\n");
|
|
4041
|
+
}
|
|
4042
|
+
if (!result.found) {
|
|
4043
|
+
lines.push(`\u2139\uFE0F \`${result.oldTarget}\` \u306F roster \u306B\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u305B\u3093(rename \u5BFE\u8C61\u306A\u3057)\u3002`);
|
|
4044
|
+
return lines.join("\n");
|
|
4045
|
+
}
|
|
4046
|
+
if (result.collision) {
|
|
4047
|
+
lines.push(
|
|
4048
|
+
`\u26A0\uFE0F \`${result.newTarget}\` \u306F\u65E2\u306B roster \u306B\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059\u3002\u91CD\u8907\u3092\u907F\u3051\u308B\u305F\u3081 rename \u3057\u307E\u305B\u3093\u3002`
|
|
4049
|
+
);
|
|
4050
|
+
return lines.join("\n");
|
|
4051
|
+
}
|
|
4052
|
+
if (result.applied) {
|
|
4053
|
+
lines.push(`\u2705 \`${result.oldTarget}\` \u3092 \`${result.newTarget}\` \u306B rename \u3057\u307E\u3057\u305F\u3002`);
|
|
4054
|
+
} else {
|
|
4055
|
+
lines.push(
|
|
4056
|
+
`\`${result.oldTarget}\` \u3092 \`${result.newTarget}\` \u306B rename \u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`
|
|
4057
|
+
);
|
|
4058
|
+
}
|
|
4059
|
+
if (result.sourceRootRenamed !== void 0) {
|
|
4060
|
+
lines.push(
|
|
4061
|
+
`- source_roots \u306E ${result.sourceRootRenamed} \u3092 ${result.newTarget} \u306B\u66F4\u65B0${result.applied ? "\u3057\u307E\u3057\u305F" : "\u3057\u307E\u3059"}\u3002`
|
|
4062
|
+
);
|
|
4063
|
+
} else {
|
|
4064
|
+
lines.push("- source_roots \u306B\u8A72\u5F53\u30A8\u30F3\u30C8\u30EA\u306F\u3042\u308A\u307E\u305B\u3093(\u66F4\u65B0\u4E0D\u8981)\u3002");
|
|
4065
|
+
}
|
|
4066
|
+
lines.push("");
|
|
4067
|
+
if (result.basenameChanged) {
|
|
4068
|
+
const oldName = pathBasename(result.oldTarget);
|
|
4069
|
+
const newName = pathBasename(result.newTarget);
|
|
4070
|
+
const items = [];
|
|
4071
|
+
if (result.wiring.canonicalDirOld)
|
|
4072
|
+
items.push(`anchor canonical: agents/${oldName}/ \u2192 agents/${newName}/`);
|
|
4073
|
+
if (result.wiring.viewLinkOld) items.push(`workspace view \u306E symlink: ${oldName} \u2192 ${newName}`);
|
|
4074
|
+
if (items.length > 0) {
|
|
4075
|
+
lines.push(
|
|
4076
|
+
"## \u624B\u52D5\u30EA\u30CD\u30FC\u30E0(--apply \u306F\u89E6\u308C\u307E\u305B\u3093\u3002basename \u304C\u5909\u308F\u308B\u305F\u3081\u624B\u3067\u66F4\u65B0\u3057\u3066\u304F\u3060\u3055\u3044)"
|
|
4077
|
+
);
|
|
4078
|
+
for (const i of items) lines.push(`- ${i}`);
|
|
4079
|
+
} else {
|
|
4080
|
+
lines.push(
|
|
4081
|
+
`basename \u304C ${oldName} \u2192 ${newName} \u306B\u5909\u308F\u308A\u307E\u3059\u304C\u3001anchor canonical / view symlink \u306F\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F\u3002`
|
|
4082
|
+
);
|
|
4083
|
+
}
|
|
4084
|
+
lines.push(
|
|
4085
|
+
" \u53CD\u6620\u5F8C\u306F `basou project symlinks` / `basou project workspace` \u3067\u6307\u793A\u66F8 symlink \u3068 view \u3092\u518D\u751F\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
4086
|
+
);
|
|
4087
|
+
} else {
|
|
4088
|
+
lines.push(
|
|
4089
|
+
"\u6CE8: basename \u306F\u4E0D\u5909\u3067\u3059\u3002repo \u3092\u5225\u306E\u5834\u6240\u3078\u79FB\u52D5\u3057\u305F\u5834\u5408\u306F `basou project symlinks` / `basou project workspace` \u3067\u76F8\u5BFE\u30BF\u30FC\u30B2\u30C3\u30C8\u3092\u518D\u751F\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
4090
|
+
);
|
|
4091
|
+
}
|
|
4092
|
+
lines.push("");
|
|
4093
|
+
lines.push(
|
|
4094
|
+
"\u6CE8: rename \u306F manifest(.basou\u3001git \u8FFD\u8DE1=\u53EF\u9006)\u306E\u307F\u3092\u5909\u66F4\u3057\u307E\u3059\u3002repo \u306E\u79FB\u52D5\u30FBon-disk \u306E wiring \u66F4\u65B0\u306F\u884C\u3044\u307E\u305B\u3093\u3002"
|
|
4095
|
+
);
|
|
4096
|
+
return lines.join("\n");
|
|
2109
4097
|
}
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
4098
|
+
|
|
4099
|
+
// src/commands/refresh.ts
|
|
4100
|
+
import { assertBasouRootSafe as assertBasouRootSafe9, basouPaths as basouPaths10, findErrorCode as findErrorCode9 } from "@basou/core";
|
|
4101
|
+
import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
|
|
4102
|
+
|
|
4103
|
+
// src/lib/portfolio-config.ts
|
|
4104
|
+
import { homedir as homedir3 } from "os";
|
|
4105
|
+
import { isAbsolute as isAbsolute2, join as join5, resolve as resolve4 } from "path";
|
|
4106
|
+
import { readYamlFile as readYamlFile3 } from "@basou/core";
|
|
4107
|
+
var DEFAULT_PORTFOLIO_CONFIG_PATH = join5(homedir3(), ".basou", "portfolio.yaml");
|
|
4108
|
+
function expandTilde(p) {
|
|
4109
|
+
if (p === "~") return homedir3();
|
|
4110
|
+
if (p.startsWith("~/")) return join5(homedir3(), p.slice(2));
|
|
4111
|
+
return p;
|
|
4112
|
+
}
|
|
4113
|
+
function isRecord(value) {
|
|
4114
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4115
|
+
}
|
|
4116
|
+
async function loadPortfolioConfig(configPath = DEFAULT_PORTFOLIO_CONFIG_PATH) {
|
|
4117
|
+
let raw;
|
|
4118
|
+
try {
|
|
4119
|
+
raw = await readYamlFile3(configPath);
|
|
4120
|
+
} catch (error) {
|
|
4121
|
+
if (error instanceof Error && error.message === "YAML file not found") {
|
|
4122
|
+
throw new Error(
|
|
4123
|
+
"No portfolio config at ~/.basou/portfolio.yaml. Create one (a 'workspaces:' list of repo paths) or pass --workspace <path>."
|
|
4124
|
+
);
|
|
4125
|
+
}
|
|
4126
|
+
if (error instanceof Error && error.message === "Failed to parse YAML content") {
|
|
4127
|
+
throw new Error("~/.basou/portfolio.yaml is not valid YAML.");
|
|
4128
|
+
}
|
|
4129
|
+
throw error;
|
|
2118
4130
|
}
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
4131
|
+
if (!isRecord(raw) || !Array.isArray(raw.workspaces)) {
|
|
4132
|
+
throw new Error("~/.basou/portfolio.yaml must contain a 'workspaces:' list.");
|
|
4133
|
+
}
|
|
4134
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4135
|
+
const result = [];
|
|
4136
|
+
for (const entry of raw.workspaces) {
|
|
4137
|
+
if (!isRecord(entry) || typeof entry.path !== "string" || entry.path.trim().length === 0) {
|
|
4138
|
+
throw new Error("Each portfolio workspace needs a non-empty string 'path'.");
|
|
4139
|
+
}
|
|
4140
|
+
if (entry.label !== void 0 && typeof entry.label !== "string") {
|
|
4141
|
+
throw new Error("A portfolio workspace 'label' must be a string when present.");
|
|
4142
|
+
}
|
|
4143
|
+
const expanded = expandTilde(entry.path.trim());
|
|
4144
|
+
if (!isAbsolute2(expanded)) {
|
|
4145
|
+
throw new Error(
|
|
4146
|
+
"Portfolio workspace paths must be absolute (or start with '~'); use --workspace for relative ad-hoc paths."
|
|
4147
|
+
);
|
|
4148
|
+
}
|
|
4149
|
+
const abs = resolve4(expanded);
|
|
4150
|
+
if (seen.has(abs)) continue;
|
|
4151
|
+
seen.add(abs);
|
|
4152
|
+
result.push(entry.label !== void 0 ? { path: abs, label: entry.label } : { path: abs });
|
|
4153
|
+
}
|
|
4154
|
+
if (result.length === 0) {
|
|
4155
|
+
throw new Error("~/.basou/portfolio.yaml has no workspaces.");
|
|
4156
|
+
}
|
|
4157
|
+
return result;
|
|
2128
4158
|
}
|
|
2129
4159
|
|
|
2130
4160
|
// src/commands/refresh-watch.ts
|
|
2131
4161
|
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
2132
|
-
import { homedir as
|
|
2133
|
-
import { join as
|
|
2134
|
-
import { findErrorCode as
|
|
4162
|
+
import { homedir as homedir4 } from "os";
|
|
4163
|
+
import { join as join6 } from "path";
|
|
4164
|
+
import { findErrorCode as findErrorCode8 } from "@basou/core";
|
|
2135
4165
|
var DEFAULT_WATCH_INTERVAL_SEC = 30;
|
|
2136
4166
|
var MIN_WATCH_INTERVAL_SEC = 5;
|
|
2137
4167
|
var MAX_WATCH_INTERVAL_SEC = 86400;
|
|
2138
4168
|
function watchedRoots(ctx) {
|
|
2139
4169
|
return [
|
|
2140
|
-
ctx.codexSessionsDir ??
|
|
2141
|
-
ctx.claudeProjectsDir ??
|
|
4170
|
+
ctx.codexSessionsDir ?? join6(homedir4(), ".codex", "sessions"),
|
|
4171
|
+
ctx.claudeProjectsDir ?? join6(homedir4(), ".claude", "projects")
|
|
2142
4172
|
];
|
|
2143
4173
|
}
|
|
2144
4174
|
async function scanSourceLogs(roots) {
|
|
@@ -2148,11 +4178,11 @@ async function scanSourceLogs(roots) {
|
|
|
2148
4178
|
try {
|
|
2149
4179
|
entries = await readdir2(dir, { withFileTypes: true });
|
|
2150
4180
|
} catch (error) {
|
|
2151
|
-
if (
|
|
4181
|
+
if (findErrorCode8(error, "ENOENT") || findErrorCode8(error, "ENOTDIR")) return;
|
|
2152
4182
|
throw new Error("Failed to read a source log directory", { cause: error });
|
|
2153
4183
|
}
|
|
2154
4184
|
for (const entry of entries) {
|
|
2155
|
-
const full =
|
|
4185
|
+
const full = join6(dir, entry.name);
|
|
2156
4186
|
if (entry.isDirectory()) {
|
|
2157
4187
|
await walk(full);
|
|
2158
4188
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -2160,7 +4190,7 @@ async function scanSourceLogs(roots) {
|
|
|
2160
4190
|
const info = await stat2(full);
|
|
2161
4191
|
out.set(full, { mtimeMs: info.mtimeMs, size: info.size });
|
|
2162
4192
|
} catch (error) {
|
|
2163
|
-
if (
|
|
4193
|
+
if (findErrorCode8(error, "ENOENT")) continue;
|
|
2164
4194
|
throw new Error("Failed to stat a source log file", { cause: error });
|
|
2165
4195
|
}
|
|
2166
4196
|
}
|
|
@@ -2252,26 +4282,26 @@ function collectPath2(value, previous) {
|
|
|
2252
4282
|
function parseInterval(value) {
|
|
2253
4283
|
const seconds = Number(value);
|
|
2254
4284
|
if (!Number.isInteger(seconds) || seconds < MIN_WATCH_INTERVAL_SEC || seconds > MAX_WATCH_INTERVAL_SEC) {
|
|
2255
|
-
throw new
|
|
4285
|
+
throw new InvalidArgumentError3(
|
|
2256
4286
|
`--interval must be an integer between ${MIN_WATCH_INTERVAL_SEC} and ${MAX_WATCH_INTERVAL_SEC} (seconds).`
|
|
2257
4287
|
);
|
|
2258
4288
|
}
|
|
2259
4289
|
return seconds;
|
|
2260
4290
|
}
|
|
2261
4291
|
function abortableSleep(ms, signal) {
|
|
2262
|
-
return new Promise((
|
|
4292
|
+
return new Promise((resolve8) => {
|
|
2263
4293
|
if (signal.aborted) {
|
|
2264
|
-
|
|
4294
|
+
resolve8();
|
|
2265
4295
|
return;
|
|
2266
4296
|
}
|
|
2267
4297
|
let timer;
|
|
2268
4298
|
const onAbort = () => {
|
|
2269
4299
|
clearTimeout(timer);
|
|
2270
|
-
|
|
4300
|
+
resolve8();
|
|
2271
4301
|
};
|
|
2272
4302
|
timer = setTimeout(() => {
|
|
2273
4303
|
signal.removeEventListener("abort", onAbort);
|
|
2274
|
-
|
|
4304
|
+
resolve8();
|
|
2275
4305
|
}, ms);
|
|
2276
4306
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
2277
4307
|
});
|
|
@@ -2285,6 +4315,9 @@ function registerRefreshCommand(program2) {
|
|
|
2285
4315
|
collectPath2,
|
|
2286
4316
|
[]
|
|
2287
4317
|
).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(
|
|
4318
|
+
"--portfolio",
|
|
4319
|
+
"Refresh every workspace listed in ~/.basou/portfolio.yaml (each with its own source roots)"
|
|
4320
|
+
).option(
|
|
2288
4321
|
"--watch",
|
|
2289
4322
|
"Keep running: re-import + regenerate when the native logs change (Ctrl-C to stop)"
|
|
2290
4323
|
).option(
|
|
@@ -2297,7 +4330,9 @@ function registerRefreshCommand(program2) {
|
|
|
2297
4330
|
}
|
|
2298
4331
|
async function runRefresh(options, ctx = {}) {
|
|
2299
4332
|
try {
|
|
2300
|
-
if (options.
|
|
4333
|
+
if (options.portfolio === true) {
|
|
4334
|
+
await doRunRefreshPortfolio(options, ctx);
|
|
4335
|
+
} else if (options.watch === true) {
|
|
2301
4336
|
await doRunRefreshWatch(options, ctx);
|
|
2302
4337
|
} else {
|
|
2303
4338
|
await doRunRefresh(options, ctx);
|
|
@@ -2307,14 +4342,58 @@ async function runRefresh(options, ctx = {}) {
|
|
|
2307
4342
|
process.exitCode = 1;
|
|
2308
4343
|
}
|
|
2309
4344
|
}
|
|
4345
|
+
async function doRunRefreshPortfolio(options, ctx) {
|
|
4346
|
+
if (options.watch === true) throw new Error("--portfolio cannot be combined with --watch.");
|
|
4347
|
+
if (options.project !== void 0 && options.project.length > 0) {
|
|
4348
|
+
throw new Error(
|
|
4349
|
+
"--portfolio refreshes each workspace with its own source roots; remove --project."
|
|
4350
|
+
);
|
|
4351
|
+
}
|
|
4352
|
+
const workspaces = await loadPortfolioConfig(ctx.portfolioConfigPath);
|
|
4353
|
+
const rollup = [];
|
|
4354
|
+
for (const ws of workspaces) {
|
|
4355
|
+
const label = ws.label ?? ws.path;
|
|
4356
|
+
try {
|
|
4357
|
+
const result = await computeRefresh(
|
|
4358
|
+
{ ...options, portfolio: false },
|
|
4359
|
+
{ ...ctx, cwd: ws.path }
|
|
4360
|
+
);
|
|
4361
|
+
rollup.push({ label, path: ws.path, status: "ok", result });
|
|
4362
|
+
if (options.json !== true) {
|
|
4363
|
+
console.log(`
|
|
4364
|
+
## ${label} (${ws.path})`);
|
|
4365
|
+
printRefreshSummary(result);
|
|
4366
|
+
}
|
|
4367
|
+
} catch (error) {
|
|
4368
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4369
|
+
rollup.push({ label, path: ws.path, status: "failed", error: message });
|
|
4370
|
+
if (options.json !== true) {
|
|
4371
|
+
console.log(`
|
|
4372
|
+
## ${label} (${ws.path})`);
|
|
4373
|
+
console.log(` failed: ${message}`);
|
|
4374
|
+
}
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
if (options.json === true) {
|
|
4378
|
+
console.log(JSON.stringify({ portfolio: true, workspaces: rollup }));
|
|
4379
|
+
} else {
|
|
4380
|
+
const failed = rollup.filter((r) => r.status === "failed").length;
|
|
4381
|
+
const ok = rollup.length - failed;
|
|
4382
|
+
console.log(
|
|
4383
|
+
`
|
|
4384
|
+
portfolio: ${ok}/${rollup.length} refreshed${failed > 0 ? `, ${failed} failed` : ""}.`
|
|
4385
|
+
);
|
|
4386
|
+
}
|
|
4387
|
+
if (rollup.some((r) => r.status === "failed")) process.exitCode = 1;
|
|
4388
|
+
}
|
|
2310
4389
|
async function doRunRefreshWatch(options, ctx) {
|
|
2311
4390
|
if (options.dryRun === true) throw new Error("--watch cannot be combined with --dry-run.");
|
|
2312
4391
|
if (options.json === true) throw new Error("--watch cannot be combined with --json.");
|
|
2313
4392
|
if (options.force === true) throw new Error("--watch cannot be combined with --force.");
|
|
2314
4393
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2315
|
-
const repositoryRoot = await
|
|
2316
|
-
const paths =
|
|
2317
|
-
await
|
|
4394
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "refresh");
|
|
4395
|
+
const paths = basouPaths10(repositoryRoot);
|
|
4396
|
+
await assertWorkspaceInitialized8(paths.root);
|
|
2318
4397
|
const intervalMs = (options.interval ?? DEFAULT_WATCH_INTERVAL_SEC) * 1e3;
|
|
2319
4398
|
const controller = new AbortController();
|
|
2320
4399
|
const onSignal = () => controller.abort();
|
|
@@ -2322,7 +4401,9 @@ async function doRunRefreshWatch(options, ctx) {
|
|
|
2322
4401
|
process.on("SIGTERM", onSignal);
|
|
2323
4402
|
try {
|
|
2324
4403
|
await runRefreshWatch({
|
|
2325
|
-
|
|
4404
|
+
// Watch from a workspace view: import from the resolved planning repo, not
|
|
4405
|
+
// the raw (non-git) view cwd — mirrors the redirect in computeRefresh.
|
|
4406
|
+
ctx: { ...ctx, cwd: repositoryRoot },
|
|
2326
4407
|
paths,
|
|
2327
4408
|
intervalMs,
|
|
2328
4409
|
importOptions: options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
|
|
@@ -2336,22 +4417,27 @@ async function doRunRefreshWatch(options, ctx) {
|
|
|
2336
4417
|
process.off("SIGTERM", onSignal);
|
|
2337
4418
|
}
|
|
2338
4419
|
}
|
|
2339
|
-
async function
|
|
4420
|
+
async function computeRefresh(options, ctx) {
|
|
2340
4421
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2341
|
-
const repositoryRoot = await
|
|
2342
|
-
const paths =
|
|
2343
|
-
await
|
|
4422
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "refresh");
|
|
4423
|
+
const paths = basouPaths10(repositoryRoot);
|
|
4424
|
+
await assertWorkspaceInitialized8(paths.root);
|
|
2344
4425
|
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2345
|
-
|
|
4426
|
+
return refreshAll({
|
|
2346
4427
|
options: {
|
|
2347
4428
|
...options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
|
|
2348
4429
|
...options.force === true ? { force: true } : {},
|
|
2349
4430
|
...options.dryRun === true ? { dryRun: true } : {}
|
|
2350
4431
|
},
|
|
2351
|
-
|
|
4432
|
+
// Import from the resolved repo root, not the raw cwd: a workspace-view cwd
|
|
4433
|
+
// redirects to its planning repo, and the import must run there too.
|
|
4434
|
+
ctx: { ...ctx, cwd: repositoryRoot },
|
|
2352
4435
|
paths,
|
|
2353
4436
|
nowIso
|
|
2354
4437
|
});
|
|
4438
|
+
}
|
|
4439
|
+
async function doRunRefresh(options, ctx) {
|
|
4440
|
+
const result = await computeRefresh(options, ctx);
|
|
2355
4441
|
if (options.json === true) {
|
|
2356
4442
|
console.log(JSON.stringify(result));
|
|
2357
4443
|
} else {
|
|
@@ -2383,28 +4469,30 @@ function printRefreshSummary(result) {
|
|
|
2383
4469
|
console.log(`handoff: skipped (${result.handoff.reason})`);
|
|
2384
4470
|
}
|
|
2385
4471
|
if (result.decisions.status === "generated") {
|
|
2386
|
-
|
|
4472
|
+
if (result.decisions.decisionCount === 0) {
|
|
4473
|
+
const hasSessions = result.handoff.status === "generated" && result.handoff.sessionCount > 0;
|
|
4474
|
+
console.log(
|
|
4475
|
+
hasSessions ? "decisions: 0 (none auto-recorded from these sessions; record any made with 'basou decision record')" : "decisions: 0"
|
|
4476
|
+
);
|
|
4477
|
+
} else {
|
|
4478
|
+
console.log(`decisions: regenerated (${result.decisions.decisionCount})`);
|
|
4479
|
+
}
|
|
2387
4480
|
} else {
|
|
2388
4481
|
console.log(`decisions: skipped (${result.decisions.reason})`);
|
|
2389
4482
|
}
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
|
-
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou refresh'.", {
|
|
2397
|
-
cause: error
|
|
2398
|
-
});
|
|
2399
|
-
}
|
|
2400
|
-
throw error;
|
|
4483
|
+
if (result.orientation.status === "generated") {
|
|
4484
|
+
console.log(
|
|
4485
|
+
`orientation: regenerated (in-flight: ${result.orientation.inFlightTaskCount}, pending approvals: ${result.orientation.pendingApprovalsCount}, suspect: ${result.orientation.suspectCount})`
|
|
4486
|
+
);
|
|
4487
|
+
} else {
|
|
4488
|
+
console.log(`orientation: skipped (${result.orientation.reason})`);
|
|
2401
4489
|
}
|
|
2402
4490
|
}
|
|
2403
|
-
async function
|
|
4491
|
+
async function assertWorkspaceInitialized8(basouRoot) {
|
|
2404
4492
|
try {
|
|
2405
|
-
await
|
|
4493
|
+
await assertBasouRootSafe9(basouRoot);
|
|
2406
4494
|
} catch (error) {
|
|
2407
|
-
if (
|
|
4495
|
+
if (findErrorCode9(error, "ENOENT")) {
|
|
2408
4496
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
2409
4497
|
}
|
|
2410
4498
|
throw error;
|
|
@@ -2412,14 +4500,14 @@ async function assertWorkspaceInitialized6(basouRoot) {
|
|
|
2412
4500
|
}
|
|
2413
4501
|
|
|
2414
4502
|
// src/commands/report.ts
|
|
2415
|
-
import { isAbsolute, resolve as
|
|
4503
|
+
import { isAbsolute as isAbsolute3, resolve as resolve5 } from "path";
|
|
2416
4504
|
import {
|
|
2417
|
-
assertBasouRootSafe as
|
|
2418
|
-
basouPaths as
|
|
2419
|
-
findErrorCode as
|
|
4505
|
+
assertBasouRootSafe as assertBasouRootSafe10,
|
|
4506
|
+
basouPaths as basouPaths11,
|
|
4507
|
+
findErrorCode as findErrorCode10,
|
|
2420
4508
|
renderReport,
|
|
2421
|
-
resolveRepositoryRoot as
|
|
2422
|
-
writeMarkdownFile as
|
|
4509
|
+
resolveRepositoryRoot as resolveRepositoryRoot8,
|
|
4510
|
+
writeMarkdownFile as writeMarkdownFile6
|
|
2423
4511
|
} from "@basou/core";
|
|
2424
4512
|
function registerReportCommand(program2) {
|
|
2425
4513
|
const report = program2.command("report").description(
|
|
@@ -2440,8 +4528,8 @@ async function runReportGenerate(options, ctx = {}) {
|
|
|
2440
4528
|
async function doRunReportGenerate(options, ctx) {
|
|
2441
4529
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2442
4530
|
const repositoryRoot = await resolveRepositoryRootForReport(cwd);
|
|
2443
|
-
const paths =
|
|
2444
|
-
await
|
|
4531
|
+
const paths = basouPaths11(repositoryRoot);
|
|
4532
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
2445
4533
|
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2446
4534
|
const result = await renderReport({
|
|
2447
4535
|
paths,
|
|
@@ -2452,8 +4540,8 @@ async function doRunReportGenerate(options, ctx) {
|
|
|
2452
4540
|
onTaskSkip: (taskId, reason) => printTaskSkip(taskId, reason)
|
|
2453
4541
|
});
|
|
2454
4542
|
if (options.out !== void 0) {
|
|
2455
|
-
const outPath =
|
|
2456
|
-
await
|
|
4543
|
+
const outPath = isAbsolute3(options.out) ? options.out : resolve5(cwd, options.out);
|
|
4544
|
+
await writeMarkdownFile6(outPath, result.body);
|
|
2457
4545
|
const { sessions, decisions, tasks } = result.data;
|
|
2458
4546
|
console.error(
|
|
2459
4547
|
`Wrote report to ${options.out} (sessions: ${sessions.total}, decisions: ${decisions.count}, tasks: ${tasks.total})`
|
|
@@ -2467,7 +4555,7 @@ async function doRunReportGenerate(options, ctx) {
|
|
|
2467
4555
|
}
|
|
2468
4556
|
async function resolveRepositoryRootForReport(cwd) {
|
|
2469
4557
|
try {
|
|
2470
|
-
return await
|
|
4558
|
+
return await resolveRepositoryRoot8(cwd);
|
|
2471
4559
|
} catch (error) {
|
|
2472
4560
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
2473
4561
|
throw new Error(
|
|
@@ -2478,25 +4566,149 @@ async function resolveRepositoryRootForReport(cwd) {
|
|
|
2478
4566
|
throw error;
|
|
2479
4567
|
}
|
|
2480
4568
|
}
|
|
2481
|
-
async function
|
|
4569
|
+
async function assertWorkspaceInitialized9(basouRoot) {
|
|
2482
4570
|
try {
|
|
2483
|
-
await
|
|
4571
|
+
await assertBasouRootSafe10(basouRoot);
|
|
2484
4572
|
} catch (error) {
|
|
2485
|
-
if (
|
|
4573
|
+
if (findErrorCode10(error, "ENOENT")) {
|
|
2486
4574
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
2487
4575
|
}
|
|
2488
4576
|
throw error;
|
|
2489
4577
|
}
|
|
2490
4578
|
}
|
|
2491
4579
|
|
|
4580
|
+
// src/commands/review-gaps.ts
|
|
4581
|
+
import {
|
|
4582
|
+
basouPaths as basouPaths12,
|
|
4583
|
+
findReviewGaps
|
|
4584
|
+
} from "@basou/core";
|
|
4585
|
+
import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
|
|
4586
|
+
function collectRepo(value, previous) {
|
|
4587
|
+
return [...previous, value];
|
|
4588
|
+
}
|
|
4589
|
+
function parseWindow(value) {
|
|
4590
|
+
const hours = Number(value);
|
|
4591
|
+
if (!Number.isInteger(hours) || hours <= 0) {
|
|
4592
|
+
throw new InvalidArgumentError4("--window must be a positive integer (hours).");
|
|
4593
|
+
}
|
|
4594
|
+
return hours;
|
|
4595
|
+
}
|
|
4596
|
+
function registerReviewGapsCommand(program2) {
|
|
4597
|
+
program2.command("review-gaps").description(
|
|
4598
|
+
"Surface units of work committed without a bound cross-model review trail (read-only, advisory)"
|
|
4599
|
+
).option(
|
|
4600
|
+
"--repo <name>",
|
|
4601
|
+
"Restrict to a repo by name (repeatable; default: every repo with captured commits)",
|
|
4602
|
+
collectRepo,
|
|
4603
|
+
[]
|
|
4604
|
+
).option(
|
|
4605
|
+
"--window <hours>",
|
|
4606
|
+
"Hours before a commit to look for a review (default 24)",
|
|
4607
|
+
parseWindow
|
|
4608
|
+
).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
4609
|
+
await runReviewGaps(opts);
|
|
4610
|
+
});
|
|
4611
|
+
}
|
|
4612
|
+
async function runReviewGaps(options, ctx = {}) {
|
|
4613
|
+
try {
|
|
4614
|
+
await doRunReviewGaps(options, ctx);
|
|
4615
|
+
} catch (error) {
|
|
4616
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
4617
|
+
process.exitCode = 1;
|
|
4618
|
+
}
|
|
4619
|
+
}
|
|
4620
|
+
async function doRunReviewGaps(options, ctx) {
|
|
4621
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
4622
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "review-gaps");
|
|
4623
|
+
const paths = basouPaths12(repositoryRoot);
|
|
4624
|
+
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
4625
|
+
const summary = await findReviewGaps({
|
|
4626
|
+
paths,
|
|
4627
|
+
nowIso,
|
|
4628
|
+
...options.repo !== void 0 && options.repo.length > 0 ? { scope: options.repo } : {},
|
|
4629
|
+
...options.window !== void 0 ? { windowHours: options.window } : {},
|
|
4630
|
+
onWarning: (w, sid) => printReplayWarning(w, sid),
|
|
4631
|
+
onSessionSkip: (sid, reason) => printSessionSkip(sid, reason)
|
|
4632
|
+
});
|
|
4633
|
+
if (options.json === true) {
|
|
4634
|
+
console.log(JSON.stringify(summary));
|
|
4635
|
+
} else {
|
|
4636
|
+
console.log(renderReviewGaps(summary));
|
|
4637
|
+
}
|
|
4638
|
+
return summary;
|
|
4639
|
+
}
|
|
4640
|
+
function relAge(iso, now) {
|
|
4641
|
+
if (iso === null) return "(\u4E0D\u660E)";
|
|
4642
|
+
const ms = now.getTime() - Date.parse(iso);
|
|
4643
|
+
if (!Number.isFinite(ms) || ms < 0) return "\u305F\u3063\u305F\u4ECA";
|
|
4644
|
+
const days = Math.floor(ms / 864e5);
|
|
4645
|
+
if (days >= 1) return `${days}\u65E5\u524D`;
|
|
4646
|
+
const hours = Math.floor(ms / 36e5);
|
|
4647
|
+
if (hours >= 1) return `${hours}\u6642\u9593\u524D`;
|
|
4648
|
+
return `${Math.max(1, Math.floor(ms / 6e4))}\u5206\u524D`;
|
|
4649
|
+
}
|
|
4650
|
+
function unitLine(u, now) {
|
|
4651
|
+
const when = relAge(u.lastCommitAt, now);
|
|
4652
|
+
const head = `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"})`;
|
|
4653
|
+
if (u.verdict === "near_unbound") {
|
|
4654
|
+
const ids = u.reviews.map((r) => r.sessionId.slice(0, 14)).join(", ");
|
|
4655
|
+
return `${head} \u2014 \u8FD1\u63A5\u30EC\u30D3\u30E5\u30FC\u306F\u3042\u308B\u304C diff/\u5909\u66F4\u30D5\u30A1\u30A4\u30EB\u3092\u78BA\u8A8D\u3057\u3066\u3044\u306A\u3044 [${ids}]`;
|
|
4656
|
+
}
|
|
4657
|
+
return `${head} \u2014 \u7D10\u3065\u304F\u30AF\u30ED\u30B9\u30E2\u30C7\u30EB\u30EC\u30D3\u30E5\u30FC\u306A\u3057`;
|
|
4658
|
+
}
|
|
4659
|
+
function candidateLine(u, now) {
|
|
4660
|
+
const when = relAge(u.lastCommitAt, now);
|
|
4661
|
+
const cite = u.reviews.map((r) => `${r.sessionId.slice(0, 14)}${r.examinedDiff ? "(diff)" : ""}`).join(", ");
|
|
4662
|
+
return `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"}) \u2014 \u30EC\u30D3\u30E5\u30FC\u5F62\u8DE1: ${cite}`;
|
|
4663
|
+
}
|
|
4664
|
+
function renderReviewGaps(summary) {
|
|
4665
|
+
const now = new Date(summary.generatedAt);
|
|
4666
|
+
const lines = [];
|
|
4667
|
+
const scope = summary.scope ? summary.scope.join(", ") : "\u5168\u30EA\u30DD\u30B8\u30C8\u30EA";
|
|
4668
|
+
lines.push(`# \u30EC\u30D3\u30E5\u30FC\u8A3C\u8DE1\u306E\u30AE\u30E3\u30C3\u30D7 (${scope})`);
|
|
4669
|
+
lines.push("");
|
|
4670
|
+
if (summary.gaps.length === 0) {
|
|
4671
|
+
lines.push("\u2705 \u53D6\u308A\u8FBC\u307F\u6E08\u307F\u306E\u7BC4\u56F2\u3067\u306F\u3001\u30EC\u30D3\u30E5\u30FC\u8A3C\u8DE1\u306A\u3057\u3067\u7740\u5730\u3057\u305F\u4F5C\u696D\u5358\u4F4D\u306F\u3042\u308A\u307E\u305B\u3093\u3002");
|
|
4672
|
+
} else {
|
|
4673
|
+
lines.push(`\u26A0\uFE0F \u30EC\u30D3\u30E5\u30FC\u8A3C\u8DE1\u306A\u3057\u3067\u7740\u5730\u3057\u305F\u4F5C\u696D\u5358\u4F4D: ${summary.gaps.length}`);
|
|
4674
|
+
for (const u of summary.gaps) lines.push(unitLine(u, now));
|
|
4675
|
+
}
|
|
4676
|
+
lines.push("");
|
|
4677
|
+
if (summary.candidates.length > 0) {
|
|
4678
|
+
lines.push(
|
|
4679
|
+
`## \u78BA\u8A8D\u5F85\u3061 (${summary.candidates.length}) \u2014 \u30AF\u30ED\u30B9\u30E2\u30C7\u30EB\u304C\u30EC\u30D3\u30E5\u30FC\u3057\u305F\u5F62\u8DE1\u3042\u308A\u3002\u3053\u306E\u5909\u66F4\u3092\u672C\u5F53\u306B\u898B\u305F\u304B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044`
|
|
4680
|
+
);
|
|
4681
|
+
for (const u of summary.candidates) lines.push(candidateLine(u, now));
|
|
4682
|
+
lines.push("");
|
|
4683
|
+
}
|
|
4684
|
+
if (summary.unknowns.length > 0) {
|
|
4685
|
+
const n = summary.unknowns.reduce((sum, u) => sum + u.commitCount, 0);
|
|
4686
|
+
lines.push(
|
|
4687
|
+
`## \u5C0E\u51FA\u4E0D\u53EF (${summary.unknowns.length} \u5358\u4F4D / ${n} commit) \u2014 repo \u304B\u6642\u523B\u3092\u6355\u6349\u304B\u3089\u5C0E\u3051\u305A\u3001\u5224\u5B9A\u3092\u4FDD\u7559(clear \u3067\u306F\u3042\u308A\u307E\u305B\u3093)`
|
|
4688
|
+
);
|
|
4689
|
+
lines.push("");
|
|
4690
|
+
}
|
|
4691
|
+
lines.push("## \u30EA\u30DD\u30B8\u30C8\u30EA\u5225");
|
|
4692
|
+
for (const r of summary.repos) {
|
|
4693
|
+
lines.push(
|
|
4694
|
+
`- ${r.repo}: ${r.units} \u5358\u4F4D (\u8A3C\u8DE1\u306A\u3057 ${r.omissionUnits} / \u8FD1\u63A5\u306E\u307F ${r.nearUnboundUnits} / \u78BA\u8A8D\u5F85\u3061 ${r.candidateUnits}${r.unknownUnits > 0 ? ` / \u4E0D\u660E ${r.unknownUnits}` : ""})`
|
|
4695
|
+
);
|
|
4696
|
+
}
|
|
4697
|
+
lines.push("");
|
|
4698
|
+
lines.push(
|
|
4699
|
+
`\u6CE8: read-only \u306E advisory \u3067\u3059\u3002\u53D6\u308A\u8FBC\u307F\u6E08\u307F\u306E commit \u306E\u307F\u304C\u5BFE\u8C61\uFF08\u6700\u65B0\u53D6\u8FBC commit: ${summary.newestCommitAt === null ? "\u306A\u3057" : relAge(summary.newestCommitAt, now)}\uFF09\u3002\u30EC\u30D3\u30E5\u30FC\u306E\u300C\u5B9F\u65BD\u300D\u306F\u81EA\u52D5\u5224\u5B9A\u305B\u305A\u3001\u6642\u9593\u7684\u8FD1\u63A5\u3060\u3051\u3067\u306F\u5408\u683C\u306B\u3057\u307E\u305B\u3093\u3002enforce \u306F\u3057\u307E\u305B\u3093\u3002`
|
|
4700
|
+
);
|
|
4701
|
+
return lines.join("\n");
|
|
4702
|
+
}
|
|
4703
|
+
|
|
2492
4704
|
// src/commands/run.ts
|
|
2493
4705
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
2494
|
-
import { homedir as
|
|
2495
|
-
import { join as
|
|
4706
|
+
import { homedir as homedir5 } from "os";
|
|
4707
|
+
import { join as join7 } from "path";
|
|
2496
4708
|
import {
|
|
2497
|
-
acquireLock as
|
|
2498
|
-
assertBasouRootSafe as
|
|
2499
|
-
basouPaths as
|
|
4709
|
+
acquireLock as acquireLock5,
|
|
4710
|
+
assertBasouRootSafe as assertBasouRootSafe11,
|
|
4711
|
+
basouPaths as basouPaths13,
|
|
2500
4712
|
ChildProcessRunner as ChildProcessRunner2,
|
|
2501
4713
|
claudeCodeAdapterMetadata,
|
|
2502
4714
|
appendChainedEvent as coreAppendChainedEvent2,
|
|
@@ -2505,10 +4717,10 @@ import {
|
|
|
2505
4717
|
getSnapshot as getSnapshot2,
|
|
2506
4718
|
overwriteYamlFile as overwriteYamlFile2,
|
|
2507
4719
|
prefixedUlid as prefixedUlid4,
|
|
2508
|
-
readManifest as
|
|
2509
|
-
readYamlFile as
|
|
4720
|
+
readManifest as readManifest6,
|
|
4721
|
+
readYamlFile as readYamlFile4,
|
|
2510
4722
|
resolveClaudeCodeCommand,
|
|
2511
|
-
resolveRepositoryRoot as
|
|
4723
|
+
resolveRepositoryRoot as resolveRepositoryRoot9,
|
|
2512
4724
|
SessionSchema as SessionSchema2,
|
|
2513
4725
|
sanitizeRelatedFiles,
|
|
2514
4726
|
sanitizeWorkingDirectory as sanitizeWorkingDirectory2,
|
|
@@ -2541,17 +4753,17 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2541
4753
|
const { command } = await resolveCommand();
|
|
2542
4754
|
const cwd = options.cwd ?? process.cwd();
|
|
2543
4755
|
const repoRoot = await resolveRepositoryRootForRun(cwd);
|
|
2544
|
-
const paths =
|
|
2545
|
-
await
|
|
2546
|
-
const manifest = await
|
|
4756
|
+
const paths = basouPaths13(repoRoot);
|
|
4757
|
+
await assertBasouRootSafe11(paths.root);
|
|
4758
|
+
const manifest = await readManifest6(paths);
|
|
2547
4759
|
const sessionId = prefixedUlid4("ses");
|
|
2548
|
-
const sessionDir =
|
|
4760
|
+
const sessionDir = join7(paths.sessions, sessionId);
|
|
2549
4761
|
await mkdir2(sessionDir, { recursive: true });
|
|
2550
4762
|
const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
|
|
2551
4763
|
await coreAppendChainedEvent2(paths, sessionId, event);
|
|
2552
4764
|
});
|
|
2553
4765
|
const startedAt = now().toISOString();
|
|
2554
|
-
const sessionYamlPath =
|
|
4766
|
+
const sessionYamlPath = join7(sessionDir, "session.yaml");
|
|
2555
4767
|
const session = buildInitialSession2({
|
|
2556
4768
|
id: sessionId,
|
|
2557
4769
|
command,
|
|
@@ -2584,7 +4796,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2584
4796
|
from: "initialized",
|
|
2585
4797
|
to: "running"
|
|
2586
4798
|
});
|
|
2587
|
-
const runningLock = await
|
|
4799
|
+
const runningLock = await acquireLock5(paths, "session", sessionId);
|
|
2588
4800
|
try {
|
|
2589
4801
|
await mutateSessionYaml2(sessionYamlPath, (s) => {
|
|
2590
4802
|
s.session.status = "running";
|
|
@@ -2677,7 +4889,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2677
4889
|
const rawRelated = computeRelatedFiles(preSnapshot, postSnapshot, diff);
|
|
2678
4890
|
const relatedFiles = sanitizeRelatedFiles(rawRelated, {
|
|
2679
4891
|
workingDirectory: repoRoot,
|
|
2680
|
-
homedir:
|
|
4892
|
+
homedir: homedir5()
|
|
2681
4893
|
}).sanitized;
|
|
2682
4894
|
const finalStatus = decideFinalStatus2(result, signalReceived);
|
|
2683
4895
|
await appendEvent(sessionDir, {
|
|
@@ -2821,7 +5033,7 @@ function buildInitialSession2(input) {
|
|
|
2821
5033
|
source: { ...claudeCodeAdapterMetadata },
|
|
2822
5034
|
started_at: input.startedAt,
|
|
2823
5035
|
status: "initialized",
|
|
2824
|
-
working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir:
|
|
5036
|
+
working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir5() }),
|
|
2825
5037
|
invocation: {
|
|
2826
5038
|
command: input.command,
|
|
2827
5039
|
args: [...input.args],
|
|
@@ -2833,7 +5045,7 @@ function buildInitialSession2(input) {
|
|
|
2833
5045
|
};
|
|
2834
5046
|
}
|
|
2835
5047
|
async function mutateSessionYaml2(filePath, mutator) {
|
|
2836
|
-
const raw = await
|
|
5048
|
+
const raw = await readYamlFile4(filePath);
|
|
2837
5049
|
const parsed = SessionSchema2.parse(raw);
|
|
2838
5050
|
mutator(parsed);
|
|
2839
5051
|
const validated = SessionSchema2.parse(parsed);
|
|
@@ -2881,7 +5093,7 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
|
|
|
2881
5093
|
}
|
|
2882
5094
|
async function resolveRepositoryRootForRun(cwd) {
|
|
2883
5095
|
try {
|
|
2884
|
-
return await
|
|
5096
|
+
return await resolveRepositoryRoot9(cwd);
|
|
2885
5097
|
} catch (error) {
|
|
2886
5098
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
2887
5099
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou run'.", {
|
|
@@ -2894,29 +5106,28 @@ async function resolveRepositoryRootForRun(cwd) {
|
|
|
2894
5106
|
|
|
2895
5107
|
// src/commands/session.ts
|
|
2896
5108
|
import { readFile as readFile2 } from "fs/promises";
|
|
2897
|
-
import { basename as
|
|
5109
|
+
import { basename as basename4, isAbsolute as isAbsolute4, join as join8, relative as relative3 } from "path";
|
|
2898
5110
|
import {
|
|
2899
|
-
acquireLock as
|
|
2900
|
-
appendEventToExistingSession as
|
|
2901
|
-
assertBasouRootSafe as
|
|
2902
|
-
basouPaths as
|
|
5111
|
+
acquireLock as acquireLock6,
|
|
5112
|
+
appendEventToExistingSession as appendEventToExistingSession3,
|
|
5113
|
+
assertBasouRootSafe as assertBasouRootSafe12,
|
|
5114
|
+
basouPaths as basouPaths14,
|
|
2903
5115
|
enumerateSessionDirs as enumerateSessionDirs2,
|
|
2904
|
-
findErrorCode as
|
|
5116
|
+
findErrorCode as findErrorCode11,
|
|
2905
5117
|
importSessionFromJson as importSessionFromJson2,
|
|
2906
5118
|
loadSessionEntries,
|
|
2907
5119
|
readAllEvents,
|
|
2908
|
-
readManifest as
|
|
2909
|
-
readYamlFile as
|
|
5120
|
+
readManifest as readManifest7,
|
|
5121
|
+
readYamlFile as readYamlFile5,
|
|
2910
5122
|
rechainSessionInPlace,
|
|
2911
|
-
|
|
2912
|
-
resolveSessionId as resolveSessionId2,
|
|
5123
|
+
resolveSessionId as resolveSessionId3,
|
|
2913
5124
|
resolveTaskId,
|
|
2914
5125
|
SessionImportPayloadSchema as SessionImportPayloadSchema2,
|
|
2915
5126
|
SessionSchema as SessionSchema3,
|
|
2916
5127
|
SessionStatusSchema,
|
|
2917
5128
|
sessionWorkStatsFromEvents
|
|
2918
5129
|
} from "@basou/core";
|
|
2919
|
-
import { InvalidArgumentError as
|
|
5130
|
+
import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
|
|
2920
5131
|
|
|
2921
5132
|
// src/lib/format-duration.ts
|
|
2922
5133
|
import { formatDurationMs } from "@basou/core";
|
|
@@ -2965,8 +5176,8 @@ async function runSessionList(options, ctx = {}) {
|
|
|
2965
5176
|
async function doRunSessionList(options, ctx) {
|
|
2966
5177
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2967
5178
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "list");
|
|
2968
|
-
const paths =
|
|
2969
|
-
await
|
|
5179
|
+
const paths = basouPaths14(repositoryRoot);
|
|
5180
|
+
await assertWorkspaceInitialized10(paths.root);
|
|
2970
5181
|
const now = /* @__PURE__ */ new Date();
|
|
2971
5182
|
const records = (await loadSessionEntries(paths, {
|
|
2972
5183
|
now,
|
|
@@ -3017,17 +5228,17 @@ async function runSessionShow(idInput, options, ctx = {}) {
|
|
|
3017
5228
|
async function doRunSessionShow(idInput, options, ctx) {
|
|
3018
5229
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3019
5230
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "show");
|
|
3020
|
-
const paths =
|
|
3021
|
-
await
|
|
3022
|
-
const sessionId = await
|
|
3023
|
-
const sessionDir =
|
|
3024
|
-
const sessionYamlPath =
|
|
5231
|
+
const paths = basouPaths14(repositoryRoot);
|
|
5232
|
+
await assertWorkspaceInitialized10(paths.root);
|
|
5233
|
+
const sessionId = await resolveSessionId3(paths, idInput);
|
|
5234
|
+
const sessionDir = join8(paths.sessions, sessionId);
|
|
5235
|
+
const sessionYamlPath = join8(sessionDir, "session.yaml");
|
|
3025
5236
|
let session;
|
|
3026
5237
|
try {
|
|
3027
|
-
const raw = await
|
|
5238
|
+
const raw = await readYamlFile5(sessionYamlPath);
|
|
3028
5239
|
session = SessionSchema3.parse(raw);
|
|
3029
5240
|
} catch (error) {
|
|
3030
|
-
if (
|
|
5241
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
3031
5242
|
throw new Error(`Session not found: ${idInput}`);
|
|
3032
5243
|
}
|
|
3033
5244
|
throw new Error("Failed to read session", { cause: error });
|
|
@@ -3142,12 +5353,12 @@ function formatSessionWork(session, events, now) {
|
|
|
3142
5353
|
}
|
|
3143
5354
|
function formatWorkingDir(workingDir, repositoryRoot, options) {
|
|
3144
5355
|
if (options.fullPath === true) return workingDir;
|
|
3145
|
-
if (!
|
|
5356
|
+
if (!isAbsolute4(workingDir)) {
|
|
3146
5357
|
if (workingDir === ".") return "<repository_root>";
|
|
3147
5358
|
return workingDir;
|
|
3148
5359
|
}
|
|
3149
5360
|
if (workingDir === repositoryRoot) return "<repository_root>";
|
|
3150
|
-
const rel =
|
|
5361
|
+
const rel = relative3(repositoryRoot, workingDir);
|
|
3151
5362
|
if (rel.length === 0 || rel === ".") return "<repository_root>";
|
|
3152
5363
|
if (rel.startsWith("..")) return rel;
|
|
3153
5364
|
return `./${rel}`;
|
|
@@ -3261,23 +5472,13 @@ function maxLen2(values, floor) {
|
|
|
3261
5472
|
return max;
|
|
3262
5473
|
}
|
|
3263
5474
|
async function resolveRepositoryRootForSession(cwd, subcmd) {
|
|
3264
|
-
|
|
3265
|
-
return await resolveRepositoryRoot11(cwd);
|
|
3266
|
-
} catch (error) {
|
|
3267
|
-
if (error instanceof Error && error.message === "Not a git repository") {
|
|
3268
|
-
throw new Error(
|
|
3269
|
-
`Not a git repository. Run 'git init' first, then re-run 'basou session ${subcmd}'.`,
|
|
3270
|
-
{ cause: error }
|
|
3271
|
-
);
|
|
3272
|
-
}
|
|
3273
|
-
throw error;
|
|
3274
|
-
}
|
|
5475
|
+
return resolveBasouRootForCommand(cwd, `session ${subcmd}`);
|
|
3275
5476
|
}
|
|
3276
|
-
async function
|
|
5477
|
+
async function assertWorkspaceInitialized10(basouRoot) {
|
|
3277
5478
|
try {
|
|
3278
|
-
await
|
|
5479
|
+
await assertBasouRootSafe12(basouRoot);
|
|
3279
5480
|
} catch (error) {
|
|
3280
|
-
if (
|
|
5481
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
3281
5482
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3282
5483
|
}
|
|
3283
5484
|
throw error;
|
|
@@ -3315,9 +5516,9 @@ async function runSessionImport(options, ctx = {}) {
|
|
|
3315
5516
|
async function doRunSessionImport(options, ctx) {
|
|
3316
5517
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3317
5518
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "import");
|
|
3318
|
-
const paths =
|
|
3319
|
-
await
|
|
3320
|
-
const manifest = await
|
|
5519
|
+
const paths = basouPaths14(repositoryRoot);
|
|
5520
|
+
await assertWorkspaceInitialized10(paths.root);
|
|
5521
|
+
const manifest = await readManifest7(paths);
|
|
3321
5522
|
const rawBody = await readInputFile(options.from);
|
|
3322
5523
|
const json = parseJsonStrict(rawBody);
|
|
3323
5524
|
const parsed = SessionImportPayloadSchema2.safeParse(json);
|
|
@@ -3346,10 +5547,10 @@ async function readInputFile(path) {
|
|
|
3346
5547
|
try {
|
|
3347
5548
|
return await readFile2(path, "utf8");
|
|
3348
5549
|
} catch (error) {
|
|
3349
|
-
if (
|
|
5550
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
3350
5551
|
throw new Error("Import source not found", { cause: error });
|
|
3351
5552
|
}
|
|
3352
|
-
if (
|
|
5553
|
+
if (findErrorCode11(error, "EISDIR")) {
|
|
3353
5554
|
throw new Error("Import source is not a file", { cause: error });
|
|
3354
5555
|
}
|
|
3355
5556
|
throw new Error("Failed to read import source", { cause: error });
|
|
@@ -3364,19 +5565,19 @@ function parseJsonStrict(body) {
|
|
|
3364
5565
|
}
|
|
3365
5566
|
function parseImportFormat(raw) {
|
|
3366
5567
|
if (raw !== "json") {
|
|
3367
|
-
throw new
|
|
5568
|
+
throw new InvalidArgumentError5(`Unsupported format: ${raw}. Valid values: json`);
|
|
3368
5569
|
}
|
|
3369
5570
|
return "json";
|
|
3370
5571
|
}
|
|
3371
5572
|
function parseLabelOverride(raw) {
|
|
3372
5573
|
if (raw.length === 0) {
|
|
3373
|
-
throw new
|
|
5574
|
+
throw new InvalidArgumentError5("Label must not be empty");
|
|
3374
5575
|
}
|
|
3375
5576
|
return raw;
|
|
3376
5577
|
}
|
|
3377
5578
|
function parseTaskIdOverride(raw) {
|
|
3378
5579
|
if (raw.length === 0) {
|
|
3379
|
-
throw new
|
|
5580
|
+
throw new InvalidArgumentError5("Task id is empty");
|
|
3380
5581
|
}
|
|
3381
5582
|
return raw;
|
|
3382
5583
|
}
|
|
@@ -3402,7 +5603,7 @@ function printSessionImportResult(options, result) {
|
|
|
3402
5603
|
return;
|
|
3403
5604
|
}
|
|
3404
5605
|
console.log(
|
|
3405
|
-
`Imported session ${sid} (${result.eventCount} events) from ${
|
|
5606
|
+
`Imported session ${sid} (${result.eventCount} events) from ${basename4(options.from)}`
|
|
3406
5607
|
);
|
|
3407
5608
|
}
|
|
3408
5609
|
var NOTE_BODY_PREVIEW_LIMIT = 80;
|
|
@@ -3429,19 +5630,19 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
|
|
|
3429
5630
|
}
|
|
3430
5631
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3431
5632
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "note");
|
|
3432
|
-
const paths =
|
|
3433
|
-
await
|
|
3434
|
-
const sessionId = await
|
|
5633
|
+
const paths = basouPaths14(repositoryRoot);
|
|
5634
|
+
await assertWorkspaceInitialized10(paths.root);
|
|
5635
|
+
const sessionId = await resolveSessionId3(paths, sessionIdInput);
|
|
3435
5636
|
const body = hasBody ? options.body : await readNoteFile(options.fromFile);
|
|
3436
5637
|
if (body.length === 0) {
|
|
3437
5638
|
throw new Error("Note body is empty");
|
|
3438
5639
|
}
|
|
3439
5640
|
const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3440
5641
|
const sesId = sessionId;
|
|
3441
|
-
const sessionLock = await
|
|
5642
|
+
const sessionLock = await acquireLock6(paths, "session", sesId);
|
|
3442
5643
|
let result;
|
|
3443
5644
|
try {
|
|
3444
|
-
result = await
|
|
5645
|
+
result = await appendEventToExistingSession3({
|
|
3445
5646
|
paths,
|
|
3446
5647
|
sessionId: sesId,
|
|
3447
5648
|
eventBuilder: (eventId) => ({
|
|
@@ -3463,10 +5664,10 @@ async function readNoteFile(path) {
|
|
|
3463
5664
|
try {
|
|
3464
5665
|
return await readFile2(path, "utf8");
|
|
3465
5666
|
} catch (error) {
|
|
3466
|
-
if (
|
|
5667
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
3467
5668
|
throw new Error("Note source not found", { cause: error });
|
|
3468
5669
|
}
|
|
3469
|
-
if (
|
|
5670
|
+
if (findErrorCode11(error, "EISDIR")) {
|
|
3470
5671
|
throw new Error("Note source is not a file", { cause: error });
|
|
3471
5672
|
}
|
|
3472
5673
|
throw new Error("Failed to read note source", { cause: error });
|
|
@@ -3474,7 +5675,7 @@ async function readNoteFile(path) {
|
|
|
3474
5675
|
}
|
|
3475
5676
|
function parseNoteBodyOption(raw) {
|
|
3476
5677
|
if (raw.length === 0) {
|
|
3477
|
-
throw new
|
|
5678
|
+
throw new InvalidArgumentError5("--body must not be empty");
|
|
3478
5679
|
}
|
|
3479
5680
|
return raw;
|
|
3480
5681
|
}
|
|
@@ -3511,9 +5712,9 @@ async function doRunSessionRechain(options, ctx) {
|
|
|
3511
5712
|
}
|
|
3512
5713
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3513
5714
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "rechain");
|
|
3514
|
-
const paths =
|
|
3515
|
-
await
|
|
3516
|
-
const sessionIds = options.session !== void 0 ? [await
|
|
5715
|
+
const paths = basouPaths14(repositoryRoot);
|
|
5716
|
+
await assertWorkspaceInitialized10(paths.root);
|
|
5717
|
+
const sessionIds = options.session !== void 0 ? [await resolveSessionId3(paths, options.session)] : await enumerateSessionDirs2(paths);
|
|
3517
5718
|
const dryRun = options.dryRun === true;
|
|
3518
5719
|
const rows = [];
|
|
3519
5720
|
for (const sessionId of sessionIds) {
|
|
@@ -3565,11 +5766,11 @@ function renderRechainRow(row, dryRun) {
|
|
|
3565
5766
|
|
|
3566
5767
|
// src/commands/stats.ts
|
|
3567
5768
|
import {
|
|
3568
|
-
assertBasouRootSafe as
|
|
3569
|
-
basouPaths as
|
|
5769
|
+
assertBasouRootSafe as assertBasouRootSafe13,
|
|
5770
|
+
basouPaths as basouPaths15,
|
|
3570
5771
|
computeWorkStats,
|
|
3571
|
-
findErrorCode as
|
|
3572
|
-
resolveRepositoryRoot as
|
|
5772
|
+
findErrorCode as findErrorCode12,
|
|
5773
|
+
resolveRepositoryRoot as resolveRepositoryRoot10
|
|
3573
5774
|
} from "@basou/core";
|
|
3574
5775
|
function registerStatsCommand(program2) {
|
|
3575
5776
|
program2.command("stats").description("Report how much the AI worked (output volume + time proxies) across sessions").option("--by-source", "Break the totals down by session source kind").option("--by-day", "Break billable time and volume down by calendar day").option("--json", "Output the full stats as JSON").option("-v, --verbose", "Show error causes").action(async (options) => {
|
|
@@ -3587,8 +5788,8 @@ async function runStats(options, ctx = {}) {
|
|
|
3587
5788
|
async function doRunStats(options, ctx) {
|
|
3588
5789
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3589
5790
|
const repositoryRoot = await resolveRepositoryRootForStats(cwd);
|
|
3590
|
-
const paths =
|
|
3591
|
-
await
|
|
5791
|
+
const paths = basouPaths15(repositoryRoot);
|
|
5792
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
3592
5793
|
const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
|
|
3593
5794
|
const result = await computeWorkStats({
|
|
3594
5795
|
paths,
|
|
@@ -3672,7 +5873,7 @@ function formatInt(n) {
|
|
|
3672
5873
|
}
|
|
3673
5874
|
async function resolveRepositoryRootForStats(cwd) {
|
|
3674
5875
|
try {
|
|
3675
|
-
return await
|
|
5876
|
+
return await resolveRepositoryRoot10(cwd);
|
|
3676
5877
|
} catch (error) {
|
|
3677
5878
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
3678
5879
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou stats'.", {
|
|
@@ -3682,11 +5883,11 @@ async function resolveRepositoryRootForStats(cwd) {
|
|
|
3682
5883
|
throw error;
|
|
3683
5884
|
}
|
|
3684
5885
|
}
|
|
3685
|
-
async function
|
|
5886
|
+
async function assertWorkspaceInitialized11(basouRoot) {
|
|
3686
5887
|
try {
|
|
3687
|
-
await
|
|
5888
|
+
await assertBasouRootSafe13(basouRoot);
|
|
3688
5889
|
} catch (error) {
|
|
3689
|
-
if (
|
|
5890
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
3690
5891
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3691
5892
|
}
|
|
3692
5893
|
throw error;
|
|
@@ -3695,12 +5896,12 @@ async function assertWorkspaceInitialized9(basouRoot) {
|
|
|
3695
5896
|
|
|
3696
5897
|
// src/commands/status.ts
|
|
3697
5898
|
import {
|
|
3698
|
-
assertBasouRootSafe as
|
|
3699
|
-
basouPaths as
|
|
5899
|
+
assertBasouRootSafe as assertBasouRootSafe14,
|
|
5900
|
+
basouPaths as basouPaths16,
|
|
3700
5901
|
buildStatusSnapshot,
|
|
3701
|
-
findErrorCode as
|
|
3702
|
-
readManifest as
|
|
3703
|
-
resolveRepositoryRoot as
|
|
5902
|
+
findErrorCode as findErrorCode13,
|
|
5903
|
+
readManifest as readManifest8,
|
|
5904
|
+
resolveRepositoryRoot as resolveRepositoryRoot11,
|
|
3704
5905
|
writeStatus
|
|
3705
5906
|
} from "@basou/core";
|
|
3706
5907
|
function registerStatusCommand(program2) {
|
|
@@ -3719,20 +5920,20 @@ async function runStatus(options, ctx = {}) {
|
|
|
3719
5920
|
async function doRunStatus(options, ctx) {
|
|
3720
5921
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3721
5922
|
const repositoryRoot = await resolveRepositoryRootForStatus(cwd);
|
|
3722
|
-
const paths =
|
|
5923
|
+
const paths = basouPaths16(repositoryRoot);
|
|
3723
5924
|
try {
|
|
3724
|
-
await
|
|
5925
|
+
await assertBasouRootSafe14(paths.root);
|
|
3725
5926
|
} catch (error) {
|
|
3726
|
-
if (
|
|
5927
|
+
if (findErrorCode13(error, "ENOENT")) {
|
|
3727
5928
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3728
5929
|
}
|
|
3729
5930
|
throw error;
|
|
3730
5931
|
}
|
|
3731
5932
|
let manifest;
|
|
3732
5933
|
try {
|
|
3733
|
-
manifest = await
|
|
5934
|
+
manifest = await readManifest8(paths);
|
|
3734
5935
|
} catch (error) {
|
|
3735
|
-
if (
|
|
5936
|
+
if (findErrorCode13(error, "ENOENT")) {
|
|
3736
5937
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3737
5938
|
}
|
|
3738
5939
|
throw new Error("Failed to read workspace manifest", { cause: error });
|
|
@@ -3756,7 +5957,7 @@ function renderTextStatus(s) {
|
|
|
3756
5957
|
}
|
|
3757
5958
|
async function resolveRepositoryRootForStatus(cwd) {
|
|
3758
5959
|
try {
|
|
3759
|
-
return await
|
|
5960
|
+
return await resolveRepositoryRoot11(cwd);
|
|
3760
5961
|
} catch (error) {
|
|
3761
5962
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
3762
5963
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou status'.", {
|
|
@@ -3769,34 +5970,34 @@ async function resolveRepositoryRootForStatus(cwd) {
|
|
|
3769
5970
|
|
|
3770
5971
|
// src/commands/task.ts
|
|
3771
5972
|
import { readFile as readFile3 } from "fs/promises";
|
|
3772
|
-
import { join as
|
|
5973
|
+
import { join as join9 } from "path";
|
|
3773
5974
|
import {
|
|
3774
5975
|
archiveTask,
|
|
3775
|
-
assertBasouRootSafe as
|
|
3776
|
-
basouPaths as
|
|
5976
|
+
assertBasouRootSafe as assertBasouRootSafe15,
|
|
5977
|
+
basouPaths as basouPaths17,
|
|
3777
5978
|
createTaskWithEvent,
|
|
3778
5979
|
deleteTask,
|
|
3779
5980
|
editTask,
|
|
3780
5981
|
enumerateArchivedTaskIds,
|
|
3781
|
-
findErrorCode as
|
|
5982
|
+
findErrorCode as findErrorCode14,
|
|
3782
5983
|
loadSessionEntries as loadSessionEntries2,
|
|
3783
5984
|
loadTaskEntries,
|
|
3784
5985
|
prefixedUlid as prefixedUlid5,
|
|
3785
|
-
readManifest as
|
|
5986
|
+
readManifest as readManifest9,
|
|
3786
5987
|
readTaskFile,
|
|
3787
5988
|
readTaskFileWithArchiveFallback,
|
|
3788
5989
|
reconcileAllTasks,
|
|
3789
5990
|
reconcileTask,
|
|
3790
5991
|
refreshTaskLinkedSessions,
|
|
3791
5992
|
replayEvents as replayEvents2,
|
|
3792
|
-
resolveRepositoryRoot as
|
|
3793
|
-
resolveSessionId as
|
|
5993
|
+
resolveRepositoryRoot as resolveRepositoryRoot12,
|
|
5994
|
+
resolveSessionId as resolveSessionId4,
|
|
3794
5995
|
resolveTaskId as resolveTaskId2,
|
|
3795
5996
|
TaskStatusSchema,
|
|
3796
5997
|
TaskWriteAfterEventError,
|
|
3797
5998
|
updateTaskStatusWithEvent
|
|
3798
5999
|
} from "@basou/core";
|
|
3799
|
-
import { InvalidArgumentError as
|
|
6000
|
+
import { InvalidArgumentError as InvalidArgumentError6 } from "commander";
|
|
3800
6001
|
var STATUS_VALUES3 = TaskStatusSchema.options;
|
|
3801
6002
|
function registerTaskCommand(program2) {
|
|
3802
6003
|
const task = program2.command("task").description("Manage Basou tasks (purpose units that span sessions)");
|
|
@@ -3875,14 +6076,14 @@ async function doRunTaskNew(options, ctx) {
|
|
|
3875
6076
|
}
|
|
3876
6077
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3877
6078
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "new");
|
|
3878
|
-
const paths =
|
|
3879
|
-
await
|
|
6079
|
+
const paths = basouPaths17(repositoryRoot);
|
|
6080
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
3880
6081
|
const description = options.description !== void 0 ? options.description : options.fromFile !== void 0 ? await readDescriptionFile(options.fromFile) : "";
|
|
3881
6082
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
3882
6083
|
const occurredAt = now.toISOString();
|
|
3883
6084
|
const taskId = prefixedUlid5("task");
|
|
3884
6085
|
if (options.session !== void 0) {
|
|
3885
|
-
const sessionId = await
|
|
6086
|
+
const sessionId = await resolveSessionId4(paths, options.session);
|
|
3886
6087
|
const result2 = await createTaskWithEvent({
|
|
3887
6088
|
mode: "attach",
|
|
3888
6089
|
paths,
|
|
@@ -3910,7 +6111,7 @@ async function doRunTaskNew(options, ctx) {
|
|
|
3910
6111
|
});
|
|
3911
6112
|
return;
|
|
3912
6113
|
}
|
|
3913
|
-
const manifest = await
|
|
6114
|
+
const manifest = await readManifest9(paths);
|
|
3914
6115
|
const result = await createTaskWithEvent({
|
|
3915
6116
|
mode: "ad-hoc",
|
|
3916
6117
|
paths,
|
|
@@ -3984,8 +6185,8 @@ async function runTaskList(options, ctx = {}) {
|
|
|
3984
6185
|
async function doRunTaskList(options, ctx) {
|
|
3985
6186
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3986
6187
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "list");
|
|
3987
|
-
const paths =
|
|
3988
|
-
await
|
|
6188
|
+
const paths = basouPaths17(repositoryRoot);
|
|
6189
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
3989
6190
|
const entries = await loadTaskEntries(paths, {
|
|
3990
6191
|
onSkip: (id, reason) => printTaskSkip(id, reason)
|
|
3991
6192
|
});
|
|
@@ -4088,15 +6289,15 @@ async function runTaskShow(idInput, options, ctx = {}) {
|
|
|
4088
6289
|
async function doRunTaskShow(idInput, options, ctx) {
|
|
4089
6290
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4090
6291
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "show");
|
|
4091
|
-
const paths =
|
|
4092
|
-
await
|
|
6292
|
+
const paths = basouPaths17(repositoryRoot);
|
|
6293
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
4093
6294
|
const taskId = await resolveTaskId2(paths, idInput, { includeArchived: true });
|
|
4094
6295
|
const { doc, archived } = await readTaskFileWithArchiveFallback(paths, taskId);
|
|
4095
6296
|
const sessions = await loadSessionEntries2(paths, { now: /* @__PURE__ */ new Date() });
|
|
4096
6297
|
const events = [];
|
|
4097
6298
|
const linkedSessionIds = new Set(doc.task.task.linked_sessions);
|
|
4098
6299
|
for (const s of sessions) {
|
|
4099
|
-
const sessionDir =
|
|
6300
|
+
const sessionDir = join9(paths.sessions, s.sessionId);
|
|
4100
6301
|
try {
|
|
4101
6302
|
for await (const ev of replayEvents2(sessionDir, {
|
|
4102
6303
|
onWarning: (w) => printReplayWarning(w, s.sessionId)
|
|
@@ -4232,13 +6433,13 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
|
|
|
4232
6433
|
const newStatus = parseTaskStatusPositional(newStatusInput);
|
|
4233
6434
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4234
6435
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "status");
|
|
4235
|
-
const paths =
|
|
4236
|
-
await
|
|
6436
|
+
const paths = basouPaths17(repositoryRoot);
|
|
6437
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
4237
6438
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4238
6439
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
4239
6440
|
const occurredAt = now.toISOString();
|
|
4240
6441
|
if (options.session !== void 0) {
|
|
4241
|
-
const sessionId = await
|
|
6442
|
+
const sessionId = await resolveSessionId4(paths, options.session);
|
|
4242
6443
|
const result2 = await updateTaskStatusWithEvent({
|
|
4243
6444
|
mode: "attach",
|
|
4244
6445
|
paths,
|
|
@@ -4258,7 +6459,7 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
|
|
|
4258
6459
|
});
|
|
4259
6460
|
return;
|
|
4260
6461
|
}
|
|
4261
|
-
const manifest = await
|
|
6462
|
+
const manifest = await readManifest9(paths);
|
|
4262
6463
|
const result = await updateTaskStatusWithEvent({
|
|
4263
6464
|
mode: "ad-hoc",
|
|
4264
6465
|
paths,
|
|
@@ -4309,9 +6510,9 @@ async function runTaskReconcile(options, ctx = {}) {
|
|
|
4309
6510
|
async function doRunTaskReconcile(options, ctx) {
|
|
4310
6511
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4311
6512
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "reconcile");
|
|
4312
|
-
const paths =
|
|
4313
|
-
await
|
|
4314
|
-
const manifest = await
|
|
6513
|
+
const paths = basouPaths17(repositoryRoot);
|
|
6514
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
6515
|
+
const manifest = await readManifest9(paths);
|
|
4315
6516
|
const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
|
|
4316
6517
|
const write = options.write === true;
|
|
4317
6518
|
const verbose = isVerbose(options);
|
|
@@ -4489,9 +6690,9 @@ async function doRunTaskRefreshLinkage(taskIdInput, options, ctx) {
|
|
|
4489
6690
|
}
|
|
4490
6691
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4491
6692
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "refresh-linkage");
|
|
4492
|
-
const paths =
|
|
4493
|
-
await
|
|
4494
|
-
const manifest = await
|
|
6693
|
+
const paths = basouPaths17(repositoryRoot);
|
|
6694
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
6695
|
+
const manifest = await readManifest9(paths);
|
|
4495
6696
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4496
6697
|
const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
|
|
4497
6698
|
const write = options.write === true;
|
|
@@ -4569,9 +6770,9 @@ async function doRunTaskEdit(taskIdInput, options, ctx) {
|
|
|
4569
6770
|
}
|
|
4570
6771
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4571
6772
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "edit");
|
|
4572
|
-
const paths =
|
|
4573
|
-
await
|
|
4574
|
-
const manifest = await
|
|
6773
|
+
const paths = basouPaths17(repositoryRoot);
|
|
6774
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
6775
|
+
const manifest = await readManifest9(paths);
|
|
4575
6776
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4576
6777
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
4577
6778
|
const occurredAt = now.toISOString();
|
|
@@ -4625,9 +6826,9 @@ async function doRunTaskDelete(taskIdInput, options, ctx) {
|
|
|
4625
6826
|
}
|
|
4626
6827
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4627
6828
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "delete");
|
|
4628
|
-
const paths =
|
|
4629
|
-
await
|
|
4630
|
-
const manifest = await
|
|
6829
|
+
const paths = basouPaths17(repositoryRoot);
|
|
6830
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
6831
|
+
const manifest = await readManifest9(paths);
|
|
4631
6832
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4632
6833
|
if (options.yes !== true) {
|
|
4633
6834
|
await confirmDestructiveAction("delete", taskId);
|
|
@@ -4670,9 +6871,9 @@ async function doRunTaskArchive(taskIdInput, options, ctx) {
|
|
|
4670
6871
|
}
|
|
4671
6872
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4672
6873
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "archive");
|
|
4673
|
-
const paths =
|
|
4674
|
-
await
|
|
4675
|
-
const manifest = await
|
|
6874
|
+
const paths = basouPaths17(repositoryRoot);
|
|
6875
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
6876
|
+
const manifest = await readManifest9(paths);
|
|
4676
6877
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4677
6878
|
if (options.yes !== true) {
|
|
4678
6879
|
await confirmDestructiveAction("archive", taskId);
|
|
@@ -4725,20 +6926,20 @@ async function readSingleLineFromStdin() {
|
|
|
4725
6926
|
}
|
|
4726
6927
|
function parseTitle2(raw) {
|
|
4727
6928
|
if (raw.length === 0) {
|
|
4728
|
-
throw new
|
|
6929
|
+
throw new InvalidArgumentError6("Title must not be empty");
|
|
4729
6930
|
}
|
|
4730
6931
|
return raw;
|
|
4731
6932
|
}
|
|
4732
6933
|
function parseLabel(raw) {
|
|
4733
6934
|
if (raw.length === 0) {
|
|
4734
|
-
throw new
|
|
6935
|
+
throw new InvalidArgumentError6("Label must not be empty");
|
|
4735
6936
|
}
|
|
4736
6937
|
return raw;
|
|
4737
6938
|
}
|
|
4738
6939
|
function parseInitialTaskStatus(raw) {
|
|
4739
6940
|
const result = TaskStatusSchema.safeParse(raw);
|
|
4740
6941
|
if (!result.success) {
|
|
4741
|
-
throw new
|
|
6942
|
+
throw new InvalidArgumentError6(
|
|
4742
6943
|
`Initial task status must be one of: ${STATUS_VALUES3.join(", ")}`
|
|
4743
6944
|
);
|
|
4744
6945
|
}
|
|
@@ -4747,7 +6948,7 @@ function parseInitialTaskStatus(raw) {
|
|
|
4747
6948
|
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
4748
6949
|
function parseIsoTimestampOption(raw) {
|
|
4749
6950
|
if (!ISO_DATE_RE.test(raw) || Number.isNaN(Date.parse(raw))) {
|
|
4750
|
-
throw new
|
|
6951
|
+
throw new InvalidArgumentError6(
|
|
4751
6952
|
"Invalid --completed-at value; expected ISO-8601 timestamp like 2026-05-10T12:34:56+09:00"
|
|
4752
6953
|
);
|
|
4753
6954
|
}
|
|
@@ -4756,7 +6957,7 @@ function parseIsoTimestampOption(raw) {
|
|
|
4756
6957
|
function parseTaskStatusFilter(raw) {
|
|
4757
6958
|
const result = TaskStatusSchema.safeParse(raw);
|
|
4758
6959
|
if (!result.success) {
|
|
4759
|
-
throw new
|
|
6960
|
+
throw new InvalidArgumentError6(
|
|
4760
6961
|
`Invalid task status: ${raw}. Valid values: ${STATUS_VALUES3.join(", ")}`
|
|
4761
6962
|
);
|
|
4762
6963
|
}
|
|
@@ -4771,14 +6972,14 @@ function parseTaskStatusPositional(raw) {
|
|
|
4771
6972
|
}
|
|
4772
6973
|
function parseDescriptionOption(raw) {
|
|
4773
6974
|
if (raw.length === 0) {
|
|
4774
|
-
throw new
|
|
6975
|
+
throw new InvalidArgumentError6("Description must not be empty");
|
|
4775
6976
|
}
|
|
4776
6977
|
return raw;
|
|
4777
6978
|
}
|
|
4778
6979
|
function parsePositiveInt2(raw) {
|
|
4779
6980
|
const n = Number.parseInt(raw, 10);
|
|
4780
6981
|
if (!Number.isInteger(n) || n < 1 || raw.trim() !== String(n)) {
|
|
4781
|
-
throw new
|
|
6982
|
+
throw new InvalidArgumentError6(`Invalid number: ${raw}`);
|
|
4782
6983
|
}
|
|
4783
6984
|
return n;
|
|
4784
6985
|
}
|
|
@@ -4786,10 +6987,10 @@ async function readDescriptionFile(path) {
|
|
|
4786
6987
|
try {
|
|
4787
6988
|
return await readFile3(path, "utf8");
|
|
4788
6989
|
} catch (error) {
|
|
4789
|
-
if (
|
|
6990
|
+
if (findErrorCode14(error, "ENOENT")) {
|
|
4790
6991
|
throw new Error("Description source not found", { cause: error });
|
|
4791
6992
|
}
|
|
4792
|
-
if (
|
|
6993
|
+
if (findErrorCode14(error, "EISDIR")) {
|
|
4793
6994
|
throw new Error("Description source is not a file", { cause: error });
|
|
4794
6995
|
}
|
|
4795
6996
|
throw new Error("Failed to read description source", { cause: error });
|
|
@@ -4797,7 +6998,7 @@ async function readDescriptionFile(path) {
|
|
|
4797
6998
|
}
|
|
4798
6999
|
async function resolveRepositoryRootForTask(cwd, subcmd) {
|
|
4799
7000
|
try {
|
|
4800
|
-
return await
|
|
7001
|
+
return await resolveRepositoryRoot12(cwd);
|
|
4801
7002
|
} catch (error) {
|
|
4802
7003
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
4803
7004
|
throw new Error(
|
|
@@ -4808,11 +7009,11 @@ async function resolveRepositoryRootForTask(cwd, subcmd) {
|
|
|
4808
7009
|
throw error;
|
|
4809
7010
|
}
|
|
4810
7011
|
}
|
|
4811
|
-
async function
|
|
7012
|
+
async function assertWorkspaceInitialized12(basouRoot) {
|
|
4812
7013
|
try {
|
|
4813
|
-
await
|
|
7014
|
+
await assertBasouRootSafe15(basouRoot);
|
|
4814
7015
|
} catch (error) {
|
|
4815
|
-
if (
|
|
7016
|
+
if (findErrorCode14(error, "ENOENT")) {
|
|
4816
7017
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
4817
7018
|
}
|
|
4818
7019
|
throw error;
|
|
@@ -4900,12 +7101,12 @@ function maxLen3(values, floor) {
|
|
|
4900
7101
|
|
|
4901
7102
|
// src/commands/verify.ts
|
|
4902
7103
|
import {
|
|
4903
|
-
assertBasouRootSafe as
|
|
4904
|
-
basouPaths as
|
|
7104
|
+
assertBasouRootSafe as assertBasouRootSafe16,
|
|
7105
|
+
basouPaths as basouPaths18,
|
|
4905
7106
|
enumerateSessionDirs as enumerateSessionDirs3,
|
|
4906
|
-
findErrorCode as
|
|
4907
|
-
resolveRepositoryRoot as
|
|
4908
|
-
resolveSessionId as
|
|
7107
|
+
findErrorCode as findErrorCode15,
|
|
7108
|
+
resolveRepositoryRoot as resolveRepositoryRoot13,
|
|
7109
|
+
resolveSessionId as resolveSessionId5,
|
|
4909
7110
|
verifyEventsChain
|
|
4910
7111
|
} from "@basou/core";
|
|
4911
7112
|
function registerVerifyCommand(program2) {
|
|
@@ -4927,9 +7128,9 @@ async function doRunVerify(options, ctx) {
|
|
|
4927
7128
|
}
|
|
4928
7129
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4929
7130
|
const repositoryRoot = await resolveRepositoryRootForVerify(cwd);
|
|
4930
|
-
const paths =
|
|
4931
|
-
await
|
|
4932
|
-
const sessionIds = options.session !== void 0 ? [await
|
|
7131
|
+
const paths = basouPaths18(repositoryRoot);
|
|
7132
|
+
await assertWorkspaceInitialized13(paths.root);
|
|
7133
|
+
const sessionIds = options.session !== void 0 ? [await resolveSessionId5(paths, options.session)] : await enumerateSessionDirs3(paths);
|
|
4933
7134
|
const rows = [];
|
|
4934
7135
|
for (const sessionId of sessionIds) {
|
|
4935
7136
|
const verdict = await verifyEventsChain(paths, sessionId);
|
|
@@ -4975,7 +7176,7 @@ function renderVerdict(row) {
|
|
|
4975
7176
|
}
|
|
4976
7177
|
async function resolveRepositoryRootForVerify(cwd) {
|
|
4977
7178
|
try {
|
|
4978
|
-
return await
|
|
7179
|
+
return await resolveRepositoryRoot13(cwd);
|
|
4979
7180
|
} catch (error) {
|
|
4980
7181
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
4981
7182
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou verify'.", {
|
|
@@ -4985,11 +7186,11 @@ async function resolveRepositoryRootForVerify(cwd) {
|
|
|
4985
7186
|
throw error;
|
|
4986
7187
|
}
|
|
4987
7188
|
}
|
|
4988
|
-
async function
|
|
7189
|
+
async function assertWorkspaceInitialized13(basouRoot) {
|
|
4989
7190
|
try {
|
|
4990
|
-
await
|
|
7191
|
+
await assertBasouRootSafe16(basouRoot);
|
|
4991
7192
|
} catch (error) {
|
|
4992
|
-
if (
|
|
7193
|
+
if (findErrorCode15(error, "ENOENT")) {
|
|
4993
7194
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
4994
7195
|
}
|
|
4995
7196
|
throw error;
|
|
@@ -4998,27 +7199,162 @@ async function assertWorkspaceInitialized11(basouRoot) {
|
|
|
4998
7199
|
|
|
4999
7200
|
// src/commands/view.ts
|
|
5000
7201
|
import { spawn } from "child_process";
|
|
5001
|
-
import {
|
|
5002
|
-
import {
|
|
7202
|
+
import { createHash } from "crypto";
|
|
7203
|
+
import { basename as basename5, resolve as resolve7 } from "path";
|
|
7204
|
+
import {
|
|
7205
|
+
assertBasouRootSafe as assertBasouRootSafe17,
|
|
7206
|
+
basouPaths as basouPaths19,
|
|
7207
|
+
findErrorCode as findErrorCode17,
|
|
7208
|
+
readManifest as readManifest12,
|
|
7209
|
+
resolveRepositoryRoot as resolveRepositoryRoot14
|
|
7210
|
+
} from "@basou/core";
|
|
7211
|
+
import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
|
|
7212
|
+
|
|
7213
|
+
// src/lib/portfolio-safety.ts
|
|
7214
|
+
import { execFile } from "child_process";
|
|
7215
|
+
import { lstat, realpath } from "fs/promises";
|
|
7216
|
+
import { isAbsolute as isAbsolute5, join as join10, relative as relative4, resolve as resolve6 } from "path";
|
|
7217
|
+
import { promisify } from "util";
|
|
7218
|
+
import { readManifest as readManifest10 } from "@basou/core";
|
|
7219
|
+
var execFileAsync = promisify(execFile);
|
|
7220
|
+
function errorCode(error) {
|
|
7221
|
+
return error instanceof Error ? error.code : void 0;
|
|
7222
|
+
}
|
|
7223
|
+
async function canonical(p) {
|
|
7224
|
+
try {
|
|
7225
|
+
return await realpath(p);
|
|
7226
|
+
} catch {
|
|
7227
|
+
return resolve6(p);
|
|
7228
|
+
}
|
|
7229
|
+
}
|
|
7230
|
+
function isInside(child, parent) {
|
|
7231
|
+
const rel = relative4(parent, child);
|
|
7232
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute5(rel);
|
|
7233
|
+
}
|
|
7234
|
+
function isBasouPath(p) {
|
|
7235
|
+
return p === ".basou" || p.startsWith(".basou/") || p.includes("/.basou/") || p.endsWith("/.basou");
|
|
7236
|
+
}
|
|
7237
|
+
async function inspectRepo(repoPath) {
|
|
7238
|
+
let hasEntry = false;
|
|
7239
|
+
try {
|
|
7240
|
+
await lstat(join10(repoPath, ".basou"));
|
|
7241
|
+
hasEntry = true;
|
|
7242
|
+
} catch (error) {
|
|
7243
|
+
if (errorCode(error) !== "ENOENT") {
|
|
7244
|
+
return {
|
|
7245
|
+
kind: "unverifiable",
|
|
7246
|
+
detail: `could not check for a .basou here (${errorCode(error) ?? "unknown error"}) \u2014 treat as unsafe`
|
|
7247
|
+
};
|
|
7248
|
+
}
|
|
7249
|
+
}
|
|
7250
|
+
try {
|
|
7251
|
+
const { stdout } = await execFileAsync("git", ["-C", repoPath, "ls-files", "-z"]);
|
|
7252
|
+
const tracked = stdout.split("\0").some((f) => f.length > 0 && isBasouPath(f));
|
|
7253
|
+
if (tracked) {
|
|
7254
|
+
return {
|
|
7255
|
+
kind: "footprint",
|
|
7256
|
+
detail: "a .basou/ entry is tracked by git here and would be pushed"
|
|
7257
|
+
};
|
|
7258
|
+
}
|
|
7259
|
+
} catch {
|
|
7260
|
+
}
|
|
7261
|
+
if (hasEntry) return { kind: "footprint", detail: "a .basou/ entry exists here" };
|
|
7262
|
+
return null;
|
|
7263
|
+
}
|
|
7264
|
+
async function checkPortfolioSafety(workspaces) {
|
|
7265
|
+
const findings = [];
|
|
7266
|
+
let monitoredReposChecked = 0;
|
|
7267
|
+
for (const ws of workspaces) {
|
|
7268
|
+
const wsReal = await canonical(ws.repoRoot);
|
|
7269
|
+
let sourceRoots = [];
|
|
7270
|
+
try {
|
|
7271
|
+
const manifest = await readManifest10(ws.paths);
|
|
7272
|
+
sourceRoots = manifest.import?.source_roots ?? [];
|
|
7273
|
+
} catch (error) {
|
|
7274
|
+
if (error instanceof Error && error.message === "YAML file not found") {
|
|
7275
|
+
sourceRoots = [];
|
|
7276
|
+
} else {
|
|
7277
|
+
findings.push({
|
|
7278
|
+
workspaceLabel: ws.label,
|
|
7279
|
+
workspaceRoot: ws.repoRoot,
|
|
7280
|
+
monitoredRepo: ws.repoRoot,
|
|
7281
|
+
kind: "unverifiable",
|
|
7282
|
+
detail: "the workspace manifest is present but unreadable \u2014 cannot determine which repos it monitors; treat as unsafe"
|
|
7283
|
+
});
|
|
7284
|
+
continue;
|
|
7285
|
+
}
|
|
7286
|
+
}
|
|
7287
|
+
const monitored = /* @__PURE__ */ new Map();
|
|
7288
|
+
for (const root of sourceRoots) {
|
|
7289
|
+
const display = resolve6(ws.repoRoot, root);
|
|
7290
|
+
const real = await canonical(display);
|
|
7291
|
+
if (real !== wsReal) monitored.set(real, display);
|
|
7292
|
+
}
|
|
7293
|
+
for (const [real, display] of monitored) {
|
|
7294
|
+
monitoredReposChecked++;
|
|
7295
|
+
if (isInside(wsReal, real)) {
|
|
7296
|
+
findings.push({
|
|
7297
|
+
workspaceLabel: ws.label,
|
|
7298
|
+
workspaceRoot: ws.repoRoot,
|
|
7299
|
+
monitoredRepo: display,
|
|
7300
|
+
kind: "overlap",
|
|
7301
|
+
detail: "the workspace (where .basou/ is written) is inside this monitored repo"
|
|
7302
|
+
});
|
|
7303
|
+
}
|
|
7304
|
+
const inspection = await inspectRepo(real);
|
|
7305
|
+
if (inspection !== null) {
|
|
7306
|
+
findings.push({
|
|
7307
|
+
workspaceLabel: ws.label,
|
|
7308
|
+
workspaceRoot: ws.repoRoot,
|
|
7309
|
+
monitoredRepo: display,
|
|
7310
|
+
kind: inspection.kind,
|
|
7311
|
+
detail: inspection.detail
|
|
7312
|
+
});
|
|
7313
|
+
}
|
|
7314
|
+
}
|
|
7315
|
+
}
|
|
7316
|
+
return { findings, workspacesChecked: workspaces.length, monitoredReposChecked };
|
|
7317
|
+
}
|
|
7318
|
+
function formatSafetyReport(result) {
|
|
7319
|
+
if (result.findings.length === 0) {
|
|
7320
|
+
if (result.monitoredReposChecked === 0) {
|
|
7321
|
+
return [
|
|
7322
|
+
`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).`
|
|
7323
|
+
];
|
|
7324
|
+
}
|
|
7325
|
+
return [
|
|
7326
|
+
`Portfolio safety: OK. ${result.workspacesChecked} workspace(s), ${result.monitoredReposChecked} monitored repo(s) checked \u2014 no .basou footprint, no overlap.`
|
|
7327
|
+
];
|
|
7328
|
+
}
|
|
7329
|
+
const lines = [`Portfolio safety: DANGER \u2014 ${result.findings.length} finding(s):`];
|
|
7330
|
+
for (const f of result.findings) {
|
|
7331
|
+
lines.push(` [${f.kind}] ${f.monitoredRepo} (workspace "${f.workspaceLabel}"): ${f.detail}`);
|
|
7332
|
+
}
|
|
7333
|
+
lines.push(
|
|
7334
|
+
"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."
|
|
7335
|
+
);
|
|
7336
|
+
return lines;
|
|
7337
|
+
}
|
|
5003
7338
|
|
|
5004
7339
|
// src/lib/view-server.ts
|
|
5005
7340
|
import { createServer } from "http";
|
|
5006
|
-
import { join as
|
|
7341
|
+
import { join as join11 } from "path";
|
|
5007
7342
|
import {
|
|
5008
7343
|
computeWorkStats as computeWorkStats2,
|
|
5009
7344
|
enumerateApprovals as enumerateApprovals2,
|
|
5010
|
-
findErrorCode as
|
|
7345
|
+
findErrorCode as findErrorCode16,
|
|
5011
7346
|
isLazyExpired as isLazyExpired2,
|
|
5012
7347
|
loadApproval as loadApproval2,
|
|
5013
7348
|
loadSessionEntries as loadSessionEntries3,
|
|
5014
7349
|
loadTaskEntries as loadTaskEntries2,
|
|
5015
7350
|
readAllEvents as readAllEvents2,
|
|
5016
|
-
readManifest as
|
|
5017
|
-
readMarkdownFile as
|
|
7351
|
+
readManifest as readManifest11,
|
|
7352
|
+
readMarkdownFile as readMarkdownFile5,
|
|
5018
7353
|
readSessionYaml as readSessionYaml3,
|
|
5019
7354
|
readTaskFile as readTaskFile2,
|
|
5020
7355
|
renderDecisions as renderDecisions3,
|
|
5021
|
-
renderHandoff as renderHandoff3
|
|
7356
|
+
renderHandoff as renderHandoff3,
|
|
7357
|
+
summarizeOrientation
|
|
5022
7358
|
} from "@basou/core";
|
|
5023
7359
|
|
|
5024
7360
|
// src/lib/view-ui.ts
|
|
@@ -5040,8 +7376,13 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5040
7376
|
button.primary { background: #2563eb; color: #fff; border-color: #2563eb; }
|
|
5041
7377
|
button:disabled { opacity: .5; cursor: default; }
|
|
5042
7378
|
label.chk { font-size: 13px; opacity: .85; }
|
|
7379
|
+
/* On the portfolio landing there is no selected workspace, so the per-workspace action bar is hidden. */
|
|
7380
|
+
body.landing #project, body.landing label.chk,
|
|
7381
|
+
body.landing #btn-refresh, body.landing #btn-import-claude, body.landing #btn-import-codex,
|
|
7382
|
+
body.landing #btn-gen-handoff, body.landing #btn-gen-decisions { display: none; }
|
|
5043
7383
|
#status { padding: 6px 16px; font-size: 13px; min-height: 20px; border-bottom: 1px solid #8884; white-space: pre-wrap; }
|
|
5044
7384
|
#status.err { color: #dc2626; }
|
|
7385
|
+
.err { color: #dc2626; }
|
|
5045
7386
|
nav { display: flex; gap: 2px; padding: 6px 12px; border-bottom: 1px solid #8884; flex-wrap: wrap; }
|
|
5046
7387
|
nav button { border: none; border-radius: 6px; background: transparent; }
|
|
5047
7388
|
nav button.active { background: #2563eb22; font-weight: 600; }
|
|
@@ -5055,6 +7396,8 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5055
7396
|
#detail { padding: 12px 16px; overflow: auto; max-height: 80vh; }
|
|
5056
7397
|
.badge { display: inline-block; padding: 0 6px; border-radius: 6px; background: #8882; font-size: 12px; }
|
|
5057
7398
|
.badge.warn { background: #f59e0b33; }
|
|
7399
|
+
.badge.danger { background: #ef444433; }
|
|
7400
|
+
.badge.ok { background: #22c55e33; }
|
|
5058
7401
|
pre { background: #8881; padding: 12px; border-radius: 8px; overflow: auto; white-space: pre-wrap; word-break: break-word; }
|
|
5059
7402
|
table.kv { border-collapse: collapse; }
|
|
5060
7403
|
table.kv td { padding: 2px 10px 2px 0; vertical-align: top; }
|
|
@@ -5063,6 +7406,11 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5063
7406
|
.card { border: 1px solid #8884; border-radius: 8px; padding: 10px 14px; min-width: 120px; }
|
|
5064
7407
|
.card .n { font-size: 22px; font-weight: 700; }
|
|
5065
7408
|
.card .l { font-size: 12px; opacity: .7; }
|
|
7409
|
+
.pcard { min-width: 240px; max-width: 340px; }
|
|
7410
|
+
.pcard.open { cursor: pointer; }
|
|
7411
|
+
.pcard.open:hover { background: #8881; }
|
|
7412
|
+
.pcard .l { font-size: 14px; font-weight: 700; opacity: 1; margin-bottom: 4px; }
|
|
7413
|
+
.pcard .f { font-size: 13px; }
|
|
5066
7414
|
.tl { border-left: 2px solid #8885; margin-left: 6px; padding-left: 12px; }
|
|
5067
7415
|
.tl .ev { margin-bottom: 8px; }
|
|
5068
7416
|
.tl .ev .t { font-size: 12px; opacity: .65; }
|
|
@@ -5072,6 +7420,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5072
7420
|
<body>
|
|
5073
7421
|
<header>
|
|
5074
7422
|
<h1>basou view</h1>
|
|
7423
|
+
<button id="btn-back" style="display:none">← portfolio</button>
|
|
5075
7424
|
<input type="text" id="project" placeholder="source root (optional override)" />
|
|
5076
7425
|
<button class="primary" id="btn-refresh">Refresh all</button>
|
|
5077
7426
|
<button id="btn-import-claude">Import claude-code</button>
|
|
@@ -5091,7 +7440,12 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5091
7440
|
<script>
|
|
5092
7441
|
(function () {
|
|
5093
7442
|
var TABS = ['overview', 'stats', 'sessions', 'tasks', 'decisions', 'approvals', 'handoff'];
|
|
5094
|
-
|
|
7443
|
+
// base is the API prefix for the active workspace: '/api' in single mode,
|
|
7444
|
+
// '/api/ws/<key>' once a portfolio card is opened.
|
|
7445
|
+
// canAct gates the mutating action bar: true only when a concrete workspace
|
|
7446
|
+
// is active (single mode, or a portfolio card opened). It is the real safety
|
|
7447
|
+
// guard \u2014 body.landing also hides the buttons, but that is cosmetic.
|
|
7448
|
+
var state = { tab: 'overview', repoRoot: '', base: '/api', mode: 'single', wsKey: null, canAct: false };
|
|
5095
7449
|
|
|
5096
7450
|
function $(id) { return document.getElementById(id); }
|
|
5097
7451
|
function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }
|
|
@@ -5155,7 +7509,15 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5155
7509
|
for (var i = 0; i < ids.length; i++) $(ids[i]).disabled = busy;
|
|
5156
7510
|
}
|
|
5157
7511
|
|
|
7512
|
+
// Enable the action bar only when a workspace is active; disabled buttons
|
|
7513
|
+
// cannot post to a stale/wrong workspace even if a CSS regression un-hides them.
|
|
7514
|
+
function updateActionBar() {
|
|
7515
|
+
var ids = ['btn-refresh', 'btn-import-claude', 'btn-import-codex', 'btn-gen-handoff', 'btn-gen-decisions'];
|
|
7516
|
+
for (var i = 0; i < ids.length; i++) $(ids[i]).disabled = !state.canAct;
|
|
7517
|
+
}
|
|
7518
|
+
|
|
5158
7519
|
function post(path, label) {
|
|
7520
|
+
if (!state.canAct) { setStatus('Open a workspace first.', true); return; }
|
|
5159
7521
|
setBusy(true);
|
|
5160
7522
|
setStatus(label + '...', false);
|
|
5161
7523
|
fetchJson(path, {
|
|
@@ -5174,7 +7536,9 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5174
7536
|
if (!data) return 'ok';
|
|
5175
7537
|
if (data.claudeCode || data.codex) {
|
|
5176
7538
|
return 'claude-code ' + imp(data.claudeCode) + ', codex ' + imp(data.codex)
|
|
5177
|
-
+ (data.handoff && data.handoff.status === 'generated'
|
|
7539
|
+
+ (data.handoff && data.handoff.status === 'generated'
|
|
7540
|
+
? '; handoff regenerated, decisions: ' + (data.decisions ? data.decisions.decisionCount : 0)
|
|
7541
|
+
: '');
|
|
5178
7542
|
}
|
|
5179
7543
|
if (data.status === 'ran') return imp(data);
|
|
5180
7544
|
if (data.status === 'skipped') return 'skipped (' + data.reason + ')';
|
|
@@ -5188,6 +7552,155 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5188
7552
|
return (o.dryRun ? 'would import ' : 'imported ') + o.importedCount + ' (' + o.eventTotal + ' events)';
|
|
5189
7553
|
}
|
|
5190
7554
|
|
|
7555
|
+
// --- portfolio landing --------------------------------------------------
|
|
7556
|
+
|
|
7557
|
+
function boot() {
|
|
7558
|
+
fetchJson('/api/portfolio').then(function (d) {
|
|
7559
|
+
if (d && d.mode === 'portfolio') { state.mode = 'portfolio'; showLanding(d); }
|
|
7560
|
+
else { enterSingle(); }
|
|
7561
|
+
}).catch(function () {
|
|
7562
|
+
// First-load bootstrap failure: the single-workspace view is the safe default.
|
|
7563
|
+
enterSingle();
|
|
7564
|
+
});
|
|
7565
|
+
}
|
|
7566
|
+
|
|
7567
|
+
// Re-render the portfolio landing (the back button). Unlike boot(), a fetch
|
|
7568
|
+
// failure here keeps the inert landing and shows an error rather than silently
|
|
7569
|
+
// dropping into single mode pointed at the first workspace.
|
|
7570
|
+
function backToPortfolio() {
|
|
7571
|
+
enterLandingChrome();
|
|
7572
|
+
fetchJson('/api/portfolio').then(function (d) {
|
|
7573
|
+
if (d && d.workspaces) renderCards(d);
|
|
7574
|
+
else portfolioError('Portfolio unavailable.');
|
|
7575
|
+
}).catch(function (err) { portfolioError('Could not load portfolio: ' + err.message); });
|
|
7576
|
+
}
|
|
7577
|
+
|
|
7578
|
+
function enterSingle() {
|
|
7579
|
+
state.mode = 'single';
|
|
7580
|
+
state.base = '/api';
|
|
7581
|
+
state.wsKey = null;
|
|
7582
|
+
state.canAct = true;
|
|
7583
|
+
document.body.classList.remove('landing');
|
|
7584
|
+
$('btn-back').style.display = 'none';
|
|
7585
|
+
updateActionBar();
|
|
7586
|
+
buildTabs();
|
|
7587
|
+
loadTab('overview');
|
|
7588
|
+
}
|
|
7589
|
+
|
|
7590
|
+
// Landing chrome: no workspace is active, so actions are disabled (and hidden
|
|
7591
|
+
// by body.landing). The disable is the safety guard; the hide is cosmetic.
|
|
7592
|
+
function enterLandingChrome() {
|
|
7593
|
+
state.wsKey = null;
|
|
7594
|
+
state.canAct = false;
|
|
7595
|
+
document.body.classList.add('landing');
|
|
7596
|
+
$('btn-back').style.display = 'none';
|
|
7597
|
+
setStatus('', false);
|
|
7598
|
+
clear($('tabs'));
|
|
7599
|
+
updateActionBar();
|
|
7600
|
+
single(true);
|
|
7601
|
+
}
|
|
7602
|
+
|
|
7603
|
+
function showLanding(d) { enterLandingChrome(); renderCards(d); }
|
|
7604
|
+
|
|
7605
|
+
function renderCards(d) {
|
|
7606
|
+
var detail = $('detail');
|
|
7607
|
+
clear(detail);
|
|
7608
|
+
var ws = d.workspaces || [];
|
|
7609
|
+
detail.appendChild(el('p', { class: 'muted', text: 'Portfolio \u2014 ' + ws.length + ' workspace(s). Click a card to open it.' }));
|
|
7610
|
+
var cards = el('div', { class: 'cards' }, []);
|
|
7611
|
+
ws.forEach(function (w) { cards.appendChild(portfolioCard(w, d.generatedAt)); });
|
|
7612
|
+
detail.appendChild(cards);
|
|
7613
|
+
}
|
|
7614
|
+
|
|
7615
|
+
function portfolioError(msg) {
|
|
7616
|
+
var detail = $('detail');
|
|
7617
|
+
clear(detail);
|
|
7618
|
+
detail.appendChild(el('p', { class: 'err', text: msg }));
|
|
7619
|
+
detail.appendChild(el('button', { text: 'Retry', onclick: backToPortfolio }));
|
|
7620
|
+
}
|
|
7621
|
+
|
|
7622
|
+
function highestRisk(approvals) {
|
|
7623
|
+
var order = ['critical', 'high', 'medium', 'low'];
|
|
7624
|
+
for (var i = 0; i < order.length; i++) {
|
|
7625
|
+
for (var j = 0; j < approvals.length; j++) {
|
|
7626
|
+
if (approvals[j].risk === order[i]) return order[i];
|
|
7627
|
+
}
|
|
7628
|
+
}
|
|
7629
|
+
return approvals.length ? approvals[0].risk : '';
|
|
7630
|
+
}
|
|
7631
|
+
|
|
7632
|
+
// Human-readable age of an ISO timestamp relative to the portfolio's
|
|
7633
|
+
// generatedAt ("now"), so a stale capture reads as "3d ago" not a raw ISO.
|
|
7634
|
+
function relAge(iso, nowIso) {
|
|
7635
|
+
if (!iso) return '(none)';
|
|
7636
|
+
var ms = Date.parse(nowIso) - Date.parse(iso);
|
|
7637
|
+
if (!isFinite(ms)) return iso;
|
|
7638
|
+
if (ms < 60000) return 'just now';
|
|
7639
|
+
var m = Math.floor(ms / 60000); if (m < 60) return m + 'm ago';
|
|
7640
|
+
var h = Math.floor(m / 60); if (h < 48) return h + 'h ago';
|
|
7641
|
+
return Math.floor(h / 24) + 'd ago';
|
|
7642
|
+
}
|
|
7643
|
+
|
|
7644
|
+
// A "run refresh" badge when a dry-run found uncaptured/changed native sessions,
|
|
7645
|
+
// an "up to date" badge when the capture is current, and nothing loud when the
|
|
7646
|
+
// staleness probe could not run (degrades to a quiet note).
|
|
7647
|
+
function stalenessBadge(st) {
|
|
7648
|
+
if (!st) return null;
|
|
7649
|
+
if (!st.checked) return el('span', { class: 'badge', text: 'freshness unknown' });
|
|
7650
|
+
if (st.unverifiableSessions > 0)
|
|
7651
|
+
return el('span', { class: 'badge danger', text: '\u26A0 ' + st.unverifiableSessions + ' unverifiable \u2014 run verify' });
|
|
7652
|
+
if (st.newSessions > 0)
|
|
7653
|
+
return el('span', { class: 'badge danger', text: '\u26A0 ' + st.newSessions + ' uncaptured \u2014 run refresh' });
|
|
7654
|
+
if (st.updatedSessions > 0)
|
|
7655
|
+
return el('span', { class: 'badge warn', text: st.updatedSessions + ' updated \u2014 run refresh' });
|
|
7656
|
+
return el('span', { class: 'badge ok', text: 'up to date' });
|
|
7657
|
+
}
|
|
7658
|
+
|
|
7659
|
+
function portfolioCard(w, generatedAt) {
|
|
7660
|
+
if (!w.initialized) {
|
|
7661
|
+
return el('div', { class: 'card pcard muted' }, [
|
|
7662
|
+
el('div', { class: 'l', text: w.label }),
|
|
7663
|
+
el('div', { class: 'f', text: w.error ? ('unreadable: ' + w.error) : 'not initialized' })
|
|
7664
|
+
]);
|
|
7665
|
+
}
|
|
7666
|
+
if (w.error) {
|
|
7667
|
+
return el('div', { class: 'card pcard' }, [
|
|
7668
|
+
el('div', { class: 'l', text: w.label }),
|
|
7669
|
+
el('div', { class: 'f' }, [el('span', { class: 'badge warn', text: 'unreadable: ' + w.error })])
|
|
7670
|
+
]);
|
|
7671
|
+
}
|
|
7672
|
+
var pend = w.pendingApprovals || [];
|
|
7673
|
+
var pendText = 'pending ' + pend.length + (pend.length ? ' (' + highestRisk(pend) + ')' : '');
|
|
7674
|
+
var now = w.latestSession ? ((w.latestSession.label || '(session)') + ' [' + w.latestSession.status + ']') : '(no live sessions)';
|
|
7675
|
+
var dec = w.latestDecision ? w.latestDecision.title : '(no decisions yet)';
|
|
7676
|
+
var newest = (w.freshness && w.freshness.newestStartedAt) ? w.freshness.newestStartedAt : null;
|
|
7677
|
+
var badge = stalenessBadge(w.staleness);
|
|
7678
|
+
return el('div', { class: 'card pcard open', onclick: function () { openWorkspace(w.key, w.label); } }, [
|
|
7679
|
+
el('div', { class: 'l' }, [
|
|
7680
|
+
el('span', { text: w.label }),
|
|
7681
|
+
badge ? el('span', { text: ' ' }) : null,
|
|
7682
|
+
badge
|
|
7683
|
+
]),
|
|
7684
|
+
el('div', { class: 'f', text: 'now: ' + now }),
|
|
7685
|
+
el('div', { class: 'f', text: 'latest: ' + dec }),
|
|
7686
|
+
el('div', { class: 'f', text: 'in-flight ' + w.inFlightCount + ' | ' + pendText + ' | suspect ' + w.suspectCount }),
|
|
7687
|
+
el('div', { class: 'f muted', text: 'sessions ' + w.sessionCount + ' | newest ' + relAge(newest, generatedAt) })
|
|
7688
|
+
]);
|
|
7689
|
+
}
|
|
7690
|
+
|
|
7691
|
+
function openWorkspace(key, label) {
|
|
7692
|
+
state.mode = 'portfolio';
|
|
7693
|
+
state.wsKey = key;
|
|
7694
|
+
state.base = '/api/ws/' + encodeURIComponent(key);
|
|
7695
|
+
state.canAct = true;
|
|
7696
|
+
document.body.classList.remove('landing');
|
|
7697
|
+
$('btn-back').style.display = '';
|
|
7698
|
+
updateActionBar();
|
|
7699
|
+
setStatus('workspace: ' + label, false);
|
|
7700
|
+
buildTabs();
|
|
7701
|
+
loadTab('overview');
|
|
7702
|
+
}
|
|
7703
|
+
|
|
5191
7704
|
// --- tabs ---------------------------------------------------------------
|
|
5192
7705
|
|
|
5193
7706
|
function buildTabs() {
|
|
@@ -5211,16 +7724,16 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5211
7724
|
if (name === 'stats') return loadStats();
|
|
5212
7725
|
if (name === 'sessions') return loadSessions();
|
|
5213
7726
|
if (name === 'tasks') return loadTasks();
|
|
5214
|
-
if (name === 'decisions') return loadMarkdown('/
|
|
7727
|
+
if (name === 'decisions') return loadMarkdown(state.base + '/decisions', 'decisions');
|
|
5215
7728
|
if (name === 'approvals') return loadApprovals();
|
|
5216
|
-
if (name === 'handoff') return loadMarkdown('/
|
|
7729
|
+
if (name === 'handoff') return loadMarkdown(state.base + '/handoff', 'handoff');
|
|
5217
7730
|
}
|
|
5218
7731
|
|
|
5219
7732
|
function fail(err) { setStatus(err.message, true); }
|
|
5220
7733
|
|
|
5221
7734
|
function loadOverview() {
|
|
5222
7735
|
single(true);
|
|
5223
|
-
fetchJson('/
|
|
7736
|
+
fetchJson(state.base + '/overview').then(function (d) {
|
|
5224
7737
|
var detail = $('detail');
|
|
5225
7738
|
if (!d || d.initialized === false) {
|
|
5226
7739
|
detail.appendChild(el('p', { class: 'muted', text: 'Workspace not initialized.' }));
|
|
@@ -5269,7 +7782,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5269
7782
|
|
|
5270
7783
|
function loadStats() {
|
|
5271
7784
|
single(true);
|
|
5272
|
-
fetchJson('/
|
|
7785
|
+
fetchJson(state.base + '/stats').then(function (d) {
|
|
5273
7786
|
var detail = $('detail');
|
|
5274
7787
|
var t = d.totals;
|
|
5275
7788
|
detail.appendChild(el('p', { text: 'Sessions: ' + t.sessionCount }));
|
|
@@ -5330,7 +7843,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5330
7843
|
|
|
5331
7844
|
function loadSessions() {
|
|
5332
7845
|
single(false);
|
|
5333
|
-
fetchJson('/
|
|
7846
|
+
fetchJson(state.base + '/sessions').then(function (d) {
|
|
5334
7847
|
var list = $('list');
|
|
5335
7848
|
var rows = (d && d.sessions) || [];
|
|
5336
7849
|
if (rows.length === 0) { list.appendChild(el('div', { class: 'row muted', text: 'no sessions' })); return; }
|
|
@@ -5349,7 +7862,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5349
7862
|
row.classList.add('active');
|
|
5350
7863
|
var detail = $('detail');
|
|
5351
7864
|
clear(detail);
|
|
5352
|
-
fetchJson('/
|
|
7865
|
+
fetchJson(state.base + '/sessions/' + encodeURIComponent(id)).then(function (d) {
|
|
5353
7866
|
var s = d.session.session;
|
|
5354
7867
|
detail.appendChild(el('h3', { text: s.label || id }));
|
|
5355
7868
|
detail.appendChild(kv([
|
|
@@ -5382,7 +7895,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5382
7895
|
|
|
5383
7896
|
function loadTasks() {
|
|
5384
7897
|
single(false);
|
|
5385
|
-
fetchJson('/
|
|
7898
|
+
fetchJson(state.base + '/tasks').then(function (d) {
|
|
5386
7899
|
var list = $('list');
|
|
5387
7900
|
var rows = (d && d.tasks) || [];
|
|
5388
7901
|
if (rows.length === 0) { list.appendChild(el('div', { class: 'row muted', text: 'no tasks' })); return; }
|
|
@@ -5401,7 +7914,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5401
7914
|
row.classList.add('active');
|
|
5402
7915
|
var detail = $('detail');
|
|
5403
7916
|
clear(detail);
|
|
5404
|
-
fetchJson('/
|
|
7917
|
+
fetchJson(state.base + '/tasks/' + encodeURIComponent(id)).then(function (d) {
|
|
5405
7918
|
detail.appendChild(el('h3', { text: (d.task && (d.task.title || d.task.label)) || id }));
|
|
5406
7919
|
detail.appendChild(el('pre', { text: JSON.stringify(d.task, null, 2) }));
|
|
5407
7920
|
if (d.body) detail.appendChild(el('pre', { text: d.body }));
|
|
@@ -5420,7 +7933,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5420
7933
|
|
|
5421
7934
|
function loadApprovals() {
|
|
5422
7935
|
single(true);
|
|
5423
|
-
fetchJson('/
|
|
7936
|
+
fetchJson(state.base + '/approvals').then(function (d) {
|
|
5424
7937
|
var detail = $('detail');
|
|
5425
7938
|
var groups = [['pending', d.pending || []], ['resolved', d.resolved || []]];
|
|
5426
7939
|
groups.forEach(function (g) {
|
|
@@ -5445,14 +7958,14 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5445
7958
|
|
|
5446
7959
|
// --- wire up ------------------------------------------------------------
|
|
5447
7960
|
|
|
5448
|
-
$('btn-
|
|
5449
|
-
$('btn-
|
|
5450
|
-
$('btn-import-
|
|
5451
|
-
$('btn-
|
|
5452
|
-
$('btn-gen-
|
|
7961
|
+
$('btn-back').addEventListener('click', function () { backToPortfolio(); });
|
|
7962
|
+
$('btn-refresh').addEventListener('click', function () { post(state.base + '/refresh', 'Refresh all'); });
|
|
7963
|
+
$('btn-import-claude').addEventListener('click', function () { post(state.base + '/import/claude-code', 'Import claude-code'); });
|
|
7964
|
+
$('btn-import-codex').addEventListener('click', function () { post(state.base + '/import/codex', 'Import codex'); });
|
|
7965
|
+
$('btn-gen-handoff').addEventListener('click', function () { post(state.base + '/handoff/generate', 'Regenerate handoff'); });
|
|
7966
|
+
$('btn-gen-decisions').addEventListener('click', function () { post(state.base + '/decisions/generate', 'Regenerate decisions'); });
|
|
5453
7967
|
|
|
5454
|
-
|
|
5455
|
-
loadTab('overview');
|
|
7968
|
+
boot();
|
|
5456
7969
|
})();
|
|
5457
7970
|
</script>
|
|
5458
7971
|
</body>
|
|
@@ -5467,6 +7980,8 @@ var HttpError = class extends Error {
|
|
|
5467
7980
|
status;
|
|
5468
7981
|
};
|
|
5469
7982
|
var MAX_BODY_BYTES = 64 * 1024;
|
|
7983
|
+
var API_PREFIX = "/api/";
|
|
7984
|
+
var WS_PREFIX = "/api/ws/";
|
|
5470
7985
|
function startViewServer(opts) {
|
|
5471
7986
|
const { port, host = "127.0.0.1", deps } = opts;
|
|
5472
7987
|
let actionQueue = Promise.resolve();
|
|
@@ -5480,7 +7995,7 @@ function startViewServer(opts) {
|
|
|
5480
7995
|
};
|
|
5481
7996
|
let boundPort = port;
|
|
5482
7997
|
const getPort = () => boundPort;
|
|
5483
|
-
return new Promise((
|
|
7998
|
+
return new Promise((resolve8, reject) => {
|
|
5484
7999
|
const server = createServer((req, res) => {
|
|
5485
8000
|
handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
|
|
5486
8001
|
sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
|
|
@@ -5491,7 +8006,7 @@ function startViewServer(opts) {
|
|
|
5491
8006
|
const address = server.address();
|
|
5492
8007
|
boundPort = isAddressInfo(address) ? address.port : port;
|
|
5493
8008
|
server.off("error", reject);
|
|
5494
|
-
|
|
8009
|
+
resolve8({
|
|
5495
8010
|
url: `http://${host}:${boundPort}`,
|
|
5496
8011
|
port: boundPort,
|
|
5497
8012
|
close: () => closeServer(server)
|
|
@@ -5503,8 +8018,8 @@ function isAddressInfo(value) {
|
|
|
5503
8018
|
return value !== null && typeof value === "object";
|
|
5504
8019
|
}
|
|
5505
8020
|
function closeServer(server) {
|
|
5506
|
-
return new Promise((
|
|
5507
|
-
server.close(() =>
|
|
8021
|
+
return new Promise((resolve8) => {
|
|
8022
|
+
server.close(() => resolve8());
|
|
5508
8023
|
server.closeAllConnections();
|
|
5509
8024
|
});
|
|
5510
8025
|
}
|
|
@@ -5536,90 +8051,204 @@ async function handleGet(res, pathname, deps) {
|
|
|
5536
8051
|
sendHtml(res, VIEW_HTML);
|
|
5537
8052
|
return;
|
|
5538
8053
|
}
|
|
5539
|
-
if (pathname === "/api/
|
|
5540
|
-
sendJson(res, 200, await
|
|
8054
|
+
if (pathname === "/api/portfolio") {
|
|
8055
|
+
sendJson(res, 200, await portfolio(deps));
|
|
5541
8056
|
return;
|
|
5542
8057
|
}
|
|
5543
|
-
|
|
5544
|
-
|
|
8058
|
+
const scoped = matchWsRoute(pathname);
|
|
8059
|
+
if (scoped !== null) {
|
|
8060
|
+
const ws = findWorkspace(deps, scoped.key);
|
|
8061
|
+
if (ws === null) {
|
|
8062
|
+
sendError(res, 404, "Unknown workspace");
|
|
8063
|
+
return;
|
|
8064
|
+
}
|
|
8065
|
+
if (!await handleWorkspaceGet(res, scoped.sub, ws, deps.nowProvider)) {
|
|
8066
|
+
sendError(res, 404, "Not found");
|
|
8067
|
+
}
|
|
5545
8068
|
return;
|
|
5546
8069
|
}
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
8070
|
+
if (pathname.startsWith(API_PREFIX)) {
|
|
8071
|
+
const sub = pathname.slice(API_PREFIX.length);
|
|
8072
|
+
if (!await handleWorkspaceGet(res, sub, primaryWorkspace(deps), deps.nowProvider)) {
|
|
8073
|
+
sendError(res, 404, "Not found");
|
|
8074
|
+
}
|
|
5550
8075
|
return;
|
|
5551
8076
|
}
|
|
5552
|
-
|
|
5553
|
-
|
|
8077
|
+
sendError(res, 404, "Not found");
|
|
8078
|
+
}
|
|
8079
|
+
async function handlePost(res, pathname, body, deps, runExclusive) {
|
|
8080
|
+
const scoped = matchWsRoute(pathname);
|
|
8081
|
+
if (scoped !== null) {
|
|
8082
|
+
const ws = findWorkspace(deps, scoped.key);
|
|
8083
|
+
if (ws === null) {
|
|
8084
|
+
sendError(res, 404, "Unknown workspace");
|
|
8085
|
+
return;
|
|
8086
|
+
}
|
|
8087
|
+
if (!await handleWorkspacePost(res, scoped.sub, ws, body, deps, runExclusive)) {
|
|
8088
|
+
sendError(res, 404, "Not found");
|
|
8089
|
+
}
|
|
5554
8090
|
return;
|
|
5555
8091
|
}
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
8092
|
+
if (pathname.startsWith(API_PREFIX)) {
|
|
8093
|
+
const sub = pathname.slice(API_PREFIX.length);
|
|
8094
|
+
if (!await handleWorkspacePost(res, sub, primaryWorkspace(deps), body, deps, runExclusive)) {
|
|
8095
|
+
sendError(res, 404, "Not found");
|
|
8096
|
+
}
|
|
5559
8097
|
return;
|
|
5560
8098
|
}
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
8099
|
+
sendError(res, 404, "Not found");
|
|
8100
|
+
}
|
|
8101
|
+
async function handleWorkspaceGet(res, sub, ws, nowProvider) {
|
|
8102
|
+
if (sub === "overview") {
|
|
8103
|
+
sendJson(res, 200, await overview(ws, nowProvider));
|
|
8104
|
+
return true;
|
|
5564
8105
|
}
|
|
5565
|
-
if (
|
|
5566
|
-
sendJson(res, 200, await
|
|
5567
|
-
return;
|
|
8106
|
+
if (sub === "sessions") {
|
|
8107
|
+
sendJson(res, 200, await sessionsList(ws, nowProvider));
|
|
8108
|
+
return true;
|
|
5568
8109
|
}
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
8110
|
+
const sessionId = matchId(sub, "sessions/");
|
|
8111
|
+
if (sessionId !== null) {
|
|
8112
|
+
sendJson(res, 200, await sessionDetail(ws, sessionId));
|
|
8113
|
+
return true;
|
|
5572
8114
|
}
|
|
5573
|
-
if (
|
|
5574
|
-
sendJson(res, 200, await
|
|
5575
|
-
return;
|
|
8115
|
+
if (sub === "tasks") {
|
|
8116
|
+
sendJson(res, 200, await tasksList(ws));
|
|
8117
|
+
return true;
|
|
5576
8118
|
}
|
|
5577
|
-
|
|
8119
|
+
const taskId = matchId(sub, "tasks/");
|
|
8120
|
+
if (taskId !== null) {
|
|
8121
|
+
sendJson(res, 200, await taskDetail(ws, taskId));
|
|
8122
|
+
return true;
|
|
8123
|
+
}
|
|
8124
|
+
if (sub === "decisions") {
|
|
8125
|
+
sendJson(res, 200, await decisionsView(ws, nowProvider));
|
|
8126
|
+
return true;
|
|
8127
|
+
}
|
|
8128
|
+
if (sub === "approvals") {
|
|
8129
|
+
sendJson(res, 200, await approvalsView(ws, nowProvider));
|
|
8130
|
+
return true;
|
|
8131
|
+
}
|
|
8132
|
+
if (sub === "handoff") {
|
|
8133
|
+
sendJson(res, 200, await handoffView(ws, nowProvider));
|
|
8134
|
+
return true;
|
|
8135
|
+
}
|
|
8136
|
+
if (sub === "stats") {
|
|
8137
|
+
sendJson(res, 200, await computeWorkStats2({ paths: ws.paths, now: nowProvider() }));
|
|
8138
|
+
return true;
|
|
8139
|
+
}
|
|
8140
|
+
return false;
|
|
5578
8141
|
}
|
|
5579
|
-
async function
|
|
8142
|
+
async function handleWorkspacePost(res, sub, ws, body, deps, runExclusive) {
|
|
5580
8143
|
const nowIso = deps.nowProvider().toISOString();
|
|
5581
8144
|
const actionOptions = readActionOptions(body);
|
|
5582
|
-
if (
|
|
8145
|
+
if (sub === "refresh") {
|
|
5583
8146
|
const result = await runExclusive(
|
|
5584
|
-
() => refreshAll({ options: actionOptions, ctx:
|
|
8147
|
+
() => refreshAll({ options: actionOptions, ctx: ws.importCtx, paths: ws.paths, nowIso })
|
|
5585
8148
|
);
|
|
5586
8149
|
sendJson(res, 200, result);
|
|
5587
|
-
return;
|
|
8150
|
+
return true;
|
|
5588
8151
|
}
|
|
5589
|
-
if (
|
|
5590
|
-
sendJson(res, 200, await runExclusive(() => importClaudeCode(actionOptions,
|
|
5591
|
-
return;
|
|
8152
|
+
if (sub === "import/claude-code") {
|
|
8153
|
+
sendJson(res, 200, await runExclusive(() => importClaudeCode(actionOptions, ws.importCtx)));
|
|
8154
|
+
return true;
|
|
5592
8155
|
}
|
|
5593
|
-
if (
|
|
5594
|
-
sendJson(res, 200, await runExclusive(() => importCodex(actionOptions,
|
|
5595
|
-
return;
|
|
8156
|
+
if (sub === "import/codex") {
|
|
8157
|
+
sendJson(res, 200, await runExclusive(() => importCodex(actionOptions, ws.importCtx)));
|
|
8158
|
+
return true;
|
|
5596
8159
|
}
|
|
5597
|
-
if (
|
|
5598
|
-
sendJson(res, 200, await runExclusive(() => regenerateHandoff(
|
|
5599
|
-
return;
|
|
8160
|
+
if (sub === "handoff/generate") {
|
|
8161
|
+
sendJson(res, 200, await runExclusive(() => regenerateHandoff(ws.paths, nowIso)));
|
|
8162
|
+
return true;
|
|
5600
8163
|
}
|
|
5601
|
-
if (
|
|
5602
|
-
sendJson(res, 200, await runExclusive(() => regenerateDecisions(
|
|
5603
|
-
return;
|
|
8164
|
+
if (sub === "decisions/generate") {
|
|
8165
|
+
sendJson(res, 200, await runExclusive(() => regenerateDecisions(ws.paths, nowIso)));
|
|
8166
|
+
return true;
|
|
5604
8167
|
}
|
|
5605
|
-
|
|
8168
|
+
return false;
|
|
8169
|
+
}
|
|
8170
|
+
function primaryWorkspace(deps) {
|
|
8171
|
+
const first = deps.workspaces[0];
|
|
8172
|
+
if (first === void 0) throw new HttpError(500, "No workspace configured");
|
|
8173
|
+
return first;
|
|
8174
|
+
}
|
|
8175
|
+
function findWorkspace(deps, key) {
|
|
8176
|
+
return deps.workspaces.find((w) => w.key === key) ?? null;
|
|
8177
|
+
}
|
|
8178
|
+
function matchWsRoute(pathname) {
|
|
8179
|
+
if (!pathname.startsWith(WS_PREFIX)) return null;
|
|
8180
|
+
const rest = pathname.slice(WS_PREFIX.length);
|
|
8181
|
+
const slash = rest.indexOf("/");
|
|
8182
|
+
if (slash <= 0) return null;
|
|
8183
|
+
const sub = rest.slice(slash + 1);
|
|
8184
|
+
if (sub.length === 0) return null;
|
|
8185
|
+
let key;
|
|
8186
|
+
try {
|
|
8187
|
+
key = decodeURIComponent(rest.slice(0, slash));
|
|
8188
|
+
} catch {
|
|
8189
|
+
return null;
|
|
8190
|
+
}
|
|
8191
|
+
if (key.length === 0 || key.includes("/") || key.includes("\0")) return null;
|
|
8192
|
+
return { key, sub };
|
|
8193
|
+
}
|
|
8194
|
+
async function portfolio(deps) {
|
|
8195
|
+
const nowIso = deps.nowProvider().toISOString();
|
|
8196
|
+
const workspaces = await Promise.all(deps.workspaces.map((ws) => portfolioCard(ws, nowIso)));
|
|
8197
|
+
for (let i = 0; i < deps.workspaces.length; i++) {
|
|
8198
|
+
const card = workspaces[i];
|
|
8199
|
+
const ws = deps.workspaces[i];
|
|
8200
|
+
if (ws !== void 0 && card !== void 0 && card.initialized === true && card.error === void 0) {
|
|
8201
|
+
card.staleness = await captureStaleness(ws, nowIso);
|
|
8202
|
+
}
|
|
8203
|
+
}
|
|
8204
|
+
return { mode: deps.mode, generatedAt: nowIso, workspaces };
|
|
8205
|
+
}
|
|
8206
|
+
async function portfolioCard(ws, nowIso) {
|
|
8207
|
+
const base = { key: ws.key, label: ws.label, repoRoot: ws.repoRoot };
|
|
8208
|
+
if (!ws.initialized) {
|
|
8209
|
+
return ws.manifestError !== void 0 ? { ...base, initialized: false, error: ws.manifestError } : { ...base, initialized: false };
|
|
8210
|
+
}
|
|
8211
|
+
try {
|
|
8212
|
+
const s = await summarizeOrientation({ paths: ws.paths, nowIso });
|
|
8213
|
+
return {
|
|
8214
|
+
...base,
|
|
8215
|
+
initialized: true,
|
|
8216
|
+
sessionCount: s.sessionCount,
|
|
8217
|
+
suspectCount: s.suspects.length,
|
|
8218
|
+
inFlightCount: s.inFlightTasks.length,
|
|
8219
|
+
pendingApprovals: s.pendingApprovals.map((a) => ({
|
|
8220
|
+
risk: a.risk,
|
|
8221
|
+
kind: a.kind,
|
|
8222
|
+
expired: a.expired
|
|
8223
|
+
})),
|
|
8224
|
+
latestDecision: s.latestDecision !== null ? { title: s.latestDecision.title } : null,
|
|
8225
|
+
latestSession: s.latestSession !== null ? { label: s.latestSession.label, status: s.latestSession.status } : null,
|
|
8226
|
+
freshness: { newestStartedAt: s.freshness.newestStartedAt, bySource: s.freshness.bySource }
|
|
8227
|
+
};
|
|
8228
|
+
} catch (error) {
|
|
8229
|
+
return { ...base, initialized: true, error: pathlessMessage(error) };
|
|
8230
|
+
}
|
|
8231
|
+
}
|
|
8232
|
+
async function captureStaleness(ws, nowIso) {
|
|
8233
|
+
const probe = await probeStaleness({ ctx: ws.importCtx, paths: ws.paths, nowIso });
|
|
8234
|
+
return probe === null ? { checked: false } : { checked: true, ...probe };
|
|
5606
8235
|
}
|
|
5607
|
-
async function overview(
|
|
8236
|
+
async function overview(ws, nowProvider) {
|
|
5608
8237
|
let manifest;
|
|
5609
8238
|
try {
|
|
5610
|
-
manifest = await
|
|
8239
|
+
manifest = await readManifest11(ws.paths);
|
|
5611
8240
|
} catch (error) {
|
|
5612
|
-
if (
|
|
5613
|
-
return { initialized: false, repoRoot:
|
|
8241
|
+
if (findErrorCode16(error, "ENOENT")) {
|
|
8242
|
+
return { initialized: false, repoRoot: ws.repoRoot };
|
|
5614
8243
|
}
|
|
5615
8244
|
throw error;
|
|
5616
8245
|
}
|
|
5617
|
-
const nowIso =
|
|
5618
|
-
const handoff = await renderHandoff3({ paths:
|
|
5619
|
-
const approvals = await enumerateApprovals2(
|
|
8246
|
+
const nowIso = nowProvider().toISOString();
|
|
8247
|
+
const handoff = await renderHandoff3({ paths: ws.paths, nowIso });
|
|
8248
|
+
const approvals = await enumerateApprovals2(ws.paths);
|
|
5620
8249
|
return {
|
|
5621
8250
|
initialized: true,
|
|
5622
|
-
repoRoot:
|
|
8251
|
+
repoRoot: ws.repoRoot,
|
|
5623
8252
|
workspace: {
|
|
5624
8253
|
id: manifest.workspace.id,
|
|
5625
8254
|
name: manifest.workspace.name,
|
|
@@ -5637,8 +8266,8 @@ async function overview(deps) {
|
|
|
5637
8266
|
generatedAt: nowIso
|
|
5638
8267
|
};
|
|
5639
8268
|
}
|
|
5640
|
-
async function sessionsList(
|
|
5641
|
-
const entries = await loadSessionEntries3(
|
|
8269
|
+
async function sessionsList(ws, nowProvider) {
|
|
8270
|
+
const entries = await loadSessionEntries3(ws.paths, { now: nowProvider() });
|
|
5642
8271
|
const sessions = entries.map((entry) => ({
|
|
5643
8272
|
sessionId: entry.sessionId,
|
|
5644
8273
|
label: entry.session.session.label ?? null,
|
|
@@ -5653,10 +8282,10 @@ async function sessionsList(deps) {
|
|
|
5653
8282
|
})).reverse();
|
|
5654
8283
|
return { sessions };
|
|
5655
8284
|
}
|
|
5656
|
-
async function sessionDetail(
|
|
8285
|
+
async function sessionDetail(ws, sessionId) {
|
|
5657
8286
|
let session;
|
|
5658
8287
|
try {
|
|
5659
|
-
session = await readSessionYaml3(
|
|
8288
|
+
session = await readSessionYaml3(ws.paths, sessionId);
|
|
5660
8289
|
} catch (error) {
|
|
5661
8290
|
if (error instanceof Error && error.message === "YAML file not found") {
|
|
5662
8291
|
throw new HttpError(404, "Session not found");
|
|
@@ -5664,19 +8293,19 @@ async function sessionDetail(deps, sessionId) {
|
|
|
5664
8293
|
throw error;
|
|
5665
8294
|
}
|
|
5666
8295
|
try {
|
|
5667
|
-
const events = await readAllEvents2(
|
|
8296
|
+
const events = await readAllEvents2(join11(ws.paths.sessions, sessionId));
|
|
5668
8297
|
return { session, events };
|
|
5669
8298
|
} catch {
|
|
5670
8299
|
return { session, events: [], degraded: true };
|
|
5671
8300
|
}
|
|
5672
8301
|
}
|
|
5673
|
-
async function tasksList(
|
|
5674
|
-
const entries = await loadTaskEntries2(
|
|
8302
|
+
async function tasksList(ws) {
|
|
8303
|
+
const entries = await loadTaskEntries2(ws.paths);
|
|
5675
8304
|
return { tasks: entries.map((entry) => entry.task).reverse() };
|
|
5676
8305
|
}
|
|
5677
|
-
async function taskDetail(
|
|
8306
|
+
async function taskDetail(ws, taskId) {
|
|
5678
8307
|
try {
|
|
5679
|
-
const doc = await readTaskFile2(
|
|
8308
|
+
const doc = await readTaskFile2(ws.paths, taskId);
|
|
5680
8309
|
return { task: doc.task, body: doc.body };
|
|
5681
8310
|
} catch (error) {
|
|
5682
8311
|
if (error instanceof Error && error.message === "Task file not found") {
|
|
@@ -5685,22 +8314,22 @@ async function taskDetail(deps, taskId) {
|
|
|
5685
8314
|
throw error;
|
|
5686
8315
|
}
|
|
5687
8316
|
}
|
|
5688
|
-
async function decisionsView(
|
|
5689
|
-
const fromDisk = await
|
|
8317
|
+
async function decisionsView(ws, nowProvider) {
|
|
8318
|
+
const fromDisk = await readMarkdownFile5(ws.paths.files.decisions);
|
|
5690
8319
|
if (fromDisk !== null) {
|
|
5691
8320
|
return { body: fromDisk, fromDisk: true };
|
|
5692
8321
|
}
|
|
5693
|
-
const nowIso =
|
|
5694
|
-
const result = await renderDecisions3({ paths:
|
|
8322
|
+
const nowIso = nowProvider().toISOString();
|
|
8323
|
+
const result = await renderDecisions3({ paths: ws.paths, nowIso });
|
|
5695
8324
|
return { body: result.body, decisionCount: result.decisionCount, fromDisk: false };
|
|
5696
8325
|
}
|
|
5697
|
-
async function approvalsView(
|
|
5698
|
-
const now =
|
|
5699
|
-
const ids = await enumerateApprovals2(
|
|
8326
|
+
async function approvalsView(ws, nowProvider) {
|
|
8327
|
+
const now = nowProvider();
|
|
8328
|
+
const ids = await enumerateApprovals2(ws.paths);
|
|
5700
8329
|
const toViews = async (list) => {
|
|
5701
8330
|
const views = [];
|
|
5702
8331
|
for (const id of list) {
|
|
5703
|
-
const loaded = await loadApproval2(
|
|
8332
|
+
const loaded = await loadApproval2(ws.paths, id);
|
|
5704
8333
|
if (loaded === null) continue;
|
|
5705
8334
|
views.push({ id, expired: isLazyExpired2(loaded.approval, now), approval: loaded.approval });
|
|
5706
8335
|
}
|
|
@@ -5708,13 +8337,13 @@ async function approvalsView(deps) {
|
|
|
5708
8337
|
};
|
|
5709
8338
|
return { pending: await toViews(ids.pending), resolved: await toViews(ids.resolved) };
|
|
5710
8339
|
}
|
|
5711
|
-
async function handoffView(
|
|
5712
|
-
const fromDisk = await
|
|
8340
|
+
async function handoffView(ws, nowProvider) {
|
|
8341
|
+
const fromDisk = await readMarkdownFile5(ws.paths.files.handoff);
|
|
5713
8342
|
if (fromDisk !== null) {
|
|
5714
8343
|
return { body: fromDisk, fromDisk: true };
|
|
5715
8344
|
}
|
|
5716
|
-
const nowIso =
|
|
5717
|
-
const result = await renderHandoff3({ paths:
|
|
8345
|
+
const nowIso = nowProvider().toISOString();
|
|
8346
|
+
const result = await renderHandoff3({ paths: ws.paths, nowIso });
|
|
5718
8347
|
return { body: result.body, fromDisk: false };
|
|
5719
8348
|
}
|
|
5720
8349
|
function readActionOptions(body) {
|
|
@@ -5804,12 +8433,22 @@ var DEFAULT_PORT = 4319;
|
|
|
5804
8433
|
function parsePort(value) {
|
|
5805
8434
|
const port = Number.parseInt(value, 10);
|
|
5806
8435
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
5807
|
-
throw new
|
|
8436
|
+
throw new InvalidArgumentError7("Port must be an integer between 1 and 65535.");
|
|
5808
8437
|
}
|
|
5809
8438
|
return port;
|
|
5810
8439
|
}
|
|
8440
|
+
function collectPath3(value, previous = []) {
|
|
8441
|
+
return [...previous, value];
|
|
8442
|
+
}
|
|
5811
8443
|
function registerViewCommand(program2) {
|
|
5812
|
-
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(
|
|
8444
|
+
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(
|
|
8445
|
+
"--portfolio",
|
|
8446
|
+
"Serve every workspace listed in ~/.basou/portfolio.yaml (cross-repo orientation)"
|
|
8447
|
+
).option(
|
|
8448
|
+
"--workspace <path>",
|
|
8449
|
+
"Workspace repo path to include (repeatable; implies portfolio mode; resolved against the cwd)",
|
|
8450
|
+
collectPath3
|
|
8451
|
+
).option("--check", "Run the portfolio safety preflight and exit (no server)").option("--skip-safety-check", "Skip the portfolio safety preflight on start (not recommended)").option("-v, --verbose", "Show error causes").action(async (options) => {
|
|
5813
8452
|
await runView(options);
|
|
5814
8453
|
});
|
|
5815
8454
|
}
|
|
@@ -5823,23 +8462,37 @@ async function runView(options, ctx = {}) {
|
|
|
5823
8462
|
}
|
|
5824
8463
|
async function doRunView(options, ctx) {
|
|
5825
8464
|
const cwd = ctx.cwd ?? process.cwd();
|
|
5826
|
-
const
|
|
5827
|
-
const
|
|
5828
|
-
await
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
8465
|
+
const workspaceFlags = options.workspace ?? [];
|
|
8466
|
+
const isPortfolio = workspaceFlags.length > 0 || options.portfolio === true;
|
|
8467
|
+
const deps = isPortfolio ? await buildPortfolioDeps(workspaceFlags, ctx, cwd) : await buildSingleDeps(ctx, cwd);
|
|
8468
|
+
if (options.check === true) {
|
|
8469
|
+
const result = await checkPortfolioSafety(deps.workspaces);
|
|
8470
|
+
for (const line of formatSafetyReport(result)) console.log(line);
|
|
8471
|
+
if (result.findings.length > 0) process.exitCode = 1;
|
|
8472
|
+
return;
|
|
8473
|
+
}
|
|
8474
|
+
if (deps.mode === "portfolio" && options.skipSafetyCheck !== true) {
|
|
8475
|
+
const result = await checkPortfolioSafety(deps.workspaces);
|
|
8476
|
+
const blocking = result.findings.filter((f) => f.kind === "footprint" || f.kind === "overlap");
|
|
8477
|
+
if (blocking.length > 0) {
|
|
8478
|
+
for (const line of formatSafetyReport(result)) console.error(line);
|
|
8479
|
+
throw new Error(
|
|
8480
|
+
"Portfolio safety preflight failed (see findings above). Fix the monitored repos, or re-run with --skip-safety-check to override."
|
|
8481
|
+
);
|
|
8482
|
+
}
|
|
8483
|
+
if (result.findings.length > 0) {
|
|
8484
|
+
console.error(
|
|
8485
|
+
`Portfolio safety: ${result.findings.length} unverifiable item(s) \u2014 the read-only view will still open; run 'basou view --check' for detail.`
|
|
8486
|
+
);
|
|
8487
|
+
}
|
|
8488
|
+
}
|
|
5839
8489
|
const port = options.port ?? DEFAULT_PORT;
|
|
5840
8490
|
const handle = await startListening(port, deps);
|
|
5841
8491
|
try {
|
|
5842
8492
|
console.log(`basou view running at ${handle.url}`);
|
|
8493
|
+
if (deps.mode === "portfolio") {
|
|
8494
|
+
console.log(`Portfolio mode: ${deps.workspaces.length} workspace(s).`);
|
|
8495
|
+
}
|
|
5843
8496
|
console.log(
|
|
5844
8497
|
"Localhost only, no authentication. Do not expose this port beyond your machine. Press Ctrl+C to stop."
|
|
5845
8498
|
);
|
|
@@ -5852,11 +8505,69 @@ async function doRunView(options, ctx) {
|
|
|
5852
8505
|
await handle.close();
|
|
5853
8506
|
}
|
|
5854
8507
|
}
|
|
8508
|
+
async function buildSingleDeps(ctx, cwd) {
|
|
8509
|
+
const repositoryRoot = await resolveRepositoryRootForView(cwd);
|
|
8510
|
+
const paths = basouPaths19(repositoryRoot);
|
|
8511
|
+
await assertWorkspaceInitialized14(paths.root);
|
|
8512
|
+
const entry = await buildWorkspaceEntry(repositoryRoot, ctx);
|
|
8513
|
+
return { workspaces: [entry], mode: "single", nowProvider: nowProviderOf(ctx) };
|
|
8514
|
+
}
|
|
8515
|
+
async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
|
|
8516
|
+
const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve7(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
|
|
8517
|
+
const entries = [];
|
|
8518
|
+
const seenPath = /* @__PURE__ */ new Set();
|
|
8519
|
+
const seenKey = /* @__PURE__ */ new Set();
|
|
8520
|
+
for (const spec of specs) {
|
|
8521
|
+
const repoRoot = resolve7(spec.path);
|
|
8522
|
+
if (seenPath.has(repoRoot)) continue;
|
|
8523
|
+
seenPath.add(repoRoot);
|
|
8524
|
+
const entry = await buildWorkspaceEntry(repoRoot, ctx, spec.label);
|
|
8525
|
+
let key = entry.key;
|
|
8526
|
+
for (let n = 1; seenKey.has(key); n++) key = `${entry.key}-${n}`;
|
|
8527
|
+
seenKey.add(key);
|
|
8528
|
+
entries.push({ ...entry, key });
|
|
8529
|
+
}
|
|
8530
|
+
if (entries.length === 0) throw new Error("No workspaces to show.");
|
|
8531
|
+
return { workspaces: entries, mode: "portfolio", nowProvider: nowProviderOf(ctx) };
|
|
8532
|
+
}
|
|
8533
|
+
async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
|
|
8534
|
+
const paths = basouPaths19(repoRoot);
|
|
8535
|
+
const importCtx = {
|
|
8536
|
+
cwd: repoRoot,
|
|
8537
|
+
...ctx.claudeProjectsDir !== void 0 ? { claudeProjectsDir: ctx.claudeProjectsDir } : {},
|
|
8538
|
+
...ctx.codexSessionsDir !== void 0 ? { codexSessionsDir: ctx.codexSessionsDir } : {}
|
|
8539
|
+
};
|
|
8540
|
+
try {
|
|
8541
|
+
const manifest = await readManifest12(paths);
|
|
8542
|
+
return {
|
|
8543
|
+
key: manifest.workspace.id,
|
|
8544
|
+
label: labelOverride ?? manifest.workspace.name,
|
|
8545
|
+
paths,
|
|
8546
|
+
repoRoot,
|
|
8547
|
+
importCtx,
|
|
8548
|
+
initialized: true
|
|
8549
|
+
};
|
|
8550
|
+
} catch (error) {
|
|
8551
|
+
const notFound = error instanceof Error && error.message === "YAML file not found";
|
|
8552
|
+
return {
|
|
8553
|
+
key: `ws-${createHash("sha1").update(repoRoot).digest("hex").slice(0, 12)}`,
|
|
8554
|
+
label: labelOverride ?? basename5(repoRoot),
|
|
8555
|
+
paths,
|
|
8556
|
+
repoRoot,
|
|
8557
|
+
importCtx,
|
|
8558
|
+
initialized: false,
|
|
8559
|
+
...notFound ? {} : { manifestError: "manifest unreadable or invalid" }
|
|
8560
|
+
};
|
|
8561
|
+
}
|
|
8562
|
+
}
|
|
8563
|
+
function nowProviderOf(ctx) {
|
|
8564
|
+
return ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
|
|
8565
|
+
}
|
|
5855
8566
|
async function startListening(port, deps) {
|
|
5856
8567
|
try {
|
|
5857
8568
|
return await startViewServer({ port, deps });
|
|
5858
8569
|
} catch (error) {
|
|
5859
|
-
if (
|
|
8570
|
+
if (findErrorCode17(error, "EADDRINUSE")) {
|
|
5860
8571
|
throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
|
|
5861
8572
|
cause: error
|
|
5862
8573
|
});
|
|
@@ -5879,7 +8590,7 @@ function openInBrowser(url, override) {
|
|
|
5879
8590
|
}
|
|
5880
8591
|
}
|
|
5881
8592
|
function waitForShutdown(signal) {
|
|
5882
|
-
return new Promise((
|
|
8593
|
+
return new Promise((resolve8) => {
|
|
5883
8594
|
const cleanup = () => {
|
|
5884
8595
|
process.off("SIGINT", onSignal);
|
|
5885
8596
|
process.off("SIGTERM", onSignal);
|
|
@@ -5887,18 +8598,18 @@ function waitForShutdown(signal) {
|
|
|
5887
8598
|
};
|
|
5888
8599
|
const onSignal = () => {
|
|
5889
8600
|
cleanup();
|
|
5890
|
-
|
|
8601
|
+
resolve8();
|
|
5891
8602
|
};
|
|
5892
8603
|
const onAbort = () => {
|
|
5893
8604
|
cleanup();
|
|
5894
|
-
|
|
8605
|
+
resolve8();
|
|
5895
8606
|
};
|
|
5896
8607
|
process.on("SIGINT", onSignal);
|
|
5897
8608
|
process.on("SIGTERM", onSignal);
|
|
5898
8609
|
if (signal !== void 0) {
|
|
5899
8610
|
if (signal.aborted) {
|
|
5900
8611
|
cleanup();
|
|
5901
|
-
|
|
8612
|
+
resolve8();
|
|
5902
8613
|
return;
|
|
5903
8614
|
}
|
|
5904
8615
|
signal.addEventListener("abort", onAbort);
|
|
@@ -5907,7 +8618,7 @@ function waitForShutdown(signal) {
|
|
|
5907
8618
|
}
|
|
5908
8619
|
async function resolveRepositoryRootForView(cwd) {
|
|
5909
8620
|
try {
|
|
5910
|
-
return await
|
|
8621
|
+
return await resolveRepositoryRoot14(cwd);
|
|
5911
8622
|
} catch (error) {
|
|
5912
8623
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
5913
8624
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {
|
|
@@ -5917,11 +8628,11 @@ async function resolveRepositoryRootForView(cwd) {
|
|
|
5917
8628
|
throw error;
|
|
5918
8629
|
}
|
|
5919
8630
|
}
|
|
5920
|
-
async function
|
|
8631
|
+
async function assertWorkspaceInitialized14(basouRoot) {
|
|
5921
8632
|
try {
|
|
5922
|
-
await
|
|
8633
|
+
await assertBasouRootSafe17(basouRoot);
|
|
5923
8634
|
} catch (error) {
|
|
5924
|
-
if (
|
|
8635
|
+
if (findErrorCode17(error, "ENOENT")) {
|
|
5925
8636
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
5926
8637
|
}
|
|
5927
8638
|
throw error;
|
|
@@ -5947,10 +8658,14 @@ function buildProgram() {
|
|
|
5947
8658
|
registerViewCommand(program2);
|
|
5948
8659
|
registerApprovalCommand(program2);
|
|
5949
8660
|
registerDecisionCommand(program2);
|
|
8661
|
+
registerNoteCommand(program2);
|
|
5950
8662
|
registerTaskCommand(program2);
|
|
5951
8663
|
registerHandoffCommand(program2);
|
|
5952
8664
|
registerDecisionsCommand(program2);
|
|
5953
8665
|
registerReportCommand(program2);
|
|
8666
|
+
registerOrientCommand(program2);
|
|
8667
|
+
registerReviewGapsCommand(program2);
|
|
8668
|
+
registerProjectCommand(program2);
|
|
5954
8669
|
return program2;
|
|
5955
8670
|
}
|
|
5956
8671
|
|