@basou/cli 0.12.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 +2259 -309
- package/dist/index.js.map +1 -1
- package/dist/program.js +2259 -309
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/program.js
CHANGED
|
@@ -2008,11 +2008,177 @@ async function resolveRepositoryRootForInit(cwd) {
|
|
|
2008
2008
|
}
|
|
2009
2009
|
}
|
|
2010
2010
|
|
|
2011
|
-
// src/commands/
|
|
2011
|
+
// src/commands/note.ts
|
|
2012
2012
|
import {
|
|
2013
|
+
acquireLock as acquireLock4,
|
|
2014
|
+
appendEventToExistingSession as appendEventToExistingSession2,
|
|
2013
2015
|
assertBasouRootSafe as assertBasouRootSafe7,
|
|
2014
2016
|
basouPaths as basouPaths7,
|
|
2017
|
+
createAdHocSessionWithEvent as createAdHocSessionWithEvent2,
|
|
2015
2018
|
findErrorCode as findErrorCode6,
|
|
2019
|
+
readManifest as readManifest4,
|
|
2020
|
+
resolveSessionId as resolveSessionId2
|
|
2021
|
+
} from "@basou/core";
|
|
2022
|
+
import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
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(program) {
|
|
2046
|
+
program.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,
|
|
2016
2182
|
renderOrientation as renderOrientation2,
|
|
2017
2183
|
writeMarkdownFile as writeMarkdownFile4
|
|
2018
2184
|
} from "@basou/core";
|
|
@@ -2163,126 +2329,1785 @@ async function refreshAll(args) {
|
|
|
2163
2329
|
}
|
|
2164
2330
|
}
|
|
2165
2331
|
);
|
|
2166
|
-
return {
|
|
2167
|
-
claudeCode,
|
|
2168
|
-
codex,
|
|
2169
|
-
handoff: { status: "generated", ...handoffCounts },
|
|
2170
|
-
decisions: { status: "generated", ...decisionCounts },
|
|
2171
|
-
orientation: { status: "generated", ...orientationCounts },
|
|
2172
|
-
dryRun
|
|
2173
|
-
};
|
|
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(program) {
|
|
2370
|
+
program.command("orient").description("Show the workspace's current position (also writes .basou/orientation.md)").option("-q, --quiet", "Write the file without printing the body").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
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(program) {
|
|
2465
|
+
const project = program.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");
|
|
2174
3517
|
}
|
|
2175
|
-
function
|
|
2176
|
-
|
|
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
|
+
}
|
|
2177
3525
|
}
|
|
2178
|
-
function
|
|
2179
|
-
return
|
|
3526
|
+
function canonicalFileFor(anchorReal, canonicalName) {
|
|
3527
|
+
return join4(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
2180
3528
|
}
|
|
2181
|
-
function
|
|
2182
|
-
return
|
|
3529
|
+
function canonicalLabelFor(canonicalName) {
|
|
3530
|
+
return join4("agents", canonicalName, CANONICAL_FILE);
|
|
2183
3531
|
}
|
|
2184
|
-
async function
|
|
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;
|
|
2185
3540
|
try {
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
}
|
|
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 {
|
|
2192
3556
|
return {
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
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
|
|
2196
3572
|
};
|
|
2197
|
-
} catch {
|
|
2198
|
-
return null;
|
|
2199
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
|
+
};
|
|
2200
3585
|
}
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
3586
|
+
async function applyPresetPlan(anchorReal, plan) {
|
|
3587
|
+
const file = canonicalFileFor(anchorReal, plan.canonicalName);
|
|
3588
|
+
const label = canonicalLabelFor(plan.canonicalName);
|
|
3589
|
+
let isLink = false;
|
|
2205
3590
|
try {
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
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
|
+
}
|
|
2215
3629
|
}
|
|
2216
|
-
throw error;
|
|
2217
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;
|
|
2218
3643
|
}
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
function registerOrientCommand(program) {
|
|
2222
|
-
program.command("orient").description("Show the workspace's current position (also writes .basou/orientation.md)").option("-q, --quiet", "Write the file without printing the body").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
2223
|
-
await runOrient(opts);
|
|
2224
|
-
});
|
|
3644
|
+
function presetActionLabel(action) {
|
|
3645
|
+
return action === "create" ? "\u65B0\u898F\u4F5C\u6210" : "\u66F4\u65B0";
|
|
2225
3646
|
}
|
|
2226
|
-
|
|
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 = {}) {
|
|
2227
3753
|
try {
|
|
2228
|
-
await
|
|
3754
|
+
await doRunProjectArchive(target, options, ctx);
|
|
2229
3755
|
} catch (error) {
|
|
2230
3756
|
renderCliError(error, { verbose: isVerbose(options) });
|
|
2231
3757
|
process.exitCode = 1;
|
|
2232
3758
|
}
|
|
2233
3759
|
}
|
|
2234
|
-
|
|
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) {
|
|
2235
3831
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2236
|
-
const repositoryRoot = await resolveBasouRootForCommand(cwd, "
|
|
2237
|
-
const paths =
|
|
2238
|
-
await
|
|
2239
|
-
const
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
onTaskSkip: (taskId, reason) => printTaskSkip(taskId, reason)
|
|
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
|
|
2252
3847
|
});
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
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`
|
|
2258
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`);
|
|
2259
3897
|
} else {
|
|
2260
|
-
|
|
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("");
|
|
2261
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");
|
|
2262
3942
|
}
|
|
2263
|
-
async function
|
|
3943
|
+
async function runProjectRename(oldPath, newPath, options, ctx = {}) {
|
|
2264
3944
|
try {
|
|
2265
|
-
await
|
|
3945
|
+
await doRunProjectRename(oldPath, newPath, options, ctx);
|
|
2266
3946
|
} catch (error) {
|
|
2267
|
-
|
|
2268
|
-
|
|
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 {
|
|
2269
3966
|
}
|
|
2270
|
-
throw error;
|
|
2271
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");
|
|
2272
4097
|
}
|
|
2273
4098
|
|
|
2274
4099
|
// src/commands/refresh.ts
|
|
2275
|
-
import { assertBasouRootSafe as
|
|
2276
|
-
import { InvalidArgumentError as
|
|
4100
|
+
import { assertBasouRootSafe as assertBasouRootSafe9, basouPaths as basouPaths10, findErrorCode as findErrorCode9 } from "@basou/core";
|
|
4101
|
+
import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
|
|
2277
4102
|
|
|
2278
4103
|
// src/lib/portfolio-config.ts
|
|
2279
4104
|
import { homedir as homedir3 } from "os";
|
|
2280
|
-
import { isAbsolute, join as
|
|
4105
|
+
import { isAbsolute as isAbsolute2, join as join5, resolve as resolve4 } from "path";
|
|
2281
4106
|
import { readYamlFile as readYamlFile3 } from "@basou/core";
|
|
2282
|
-
var DEFAULT_PORTFOLIO_CONFIG_PATH =
|
|
4107
|
+
var DEFAULT_PORTFOLIO_CONFIG_PATH = join5(homedir3(), ".basou", "portfolio.yaml");
|
|
2283
4108
|
function expandTilde(p) {
|
|
2284
4109
|
if (p === "~") return homedir3();
|
|
2285
|
-
if (p.startsWith("~/")) return
|
|
4110
|
+
if (p.startsWith("~/")) return join5(homedir3(), p.slice(2));
|
|
2286
4111
|
return p;
|
|
2287
4112
|
}
|
|
2288
4113
|
function isRecord(value) {
|
|
@@ -2316,12 +4141,12 @@ async function loadPortfolioConfig(configPath = DEFAULT_PORTFOLIO_CONFIG_PATH) {
|
|
|
2316
4141
|
throw new Error("A portfolio workspace 'label' must be a string when present.");
|
|
2317
4142
|
}
|
|
2318
4143
|
const expanded = expandTilde(entry.path.trim());
|
|
2319
|
-
if (!
|
|
4144
|
+
if (!isAbsolute2(expanded)) {
|
|
2320
4145
|
throw new Error(
|
|
2321
4146
|
"Portfolio workspace paths must be absolute (or start with '~'); use --workspace for relative ad-hoc paths."
|
|
2322
4147
|
);
|
|
2323
4148
|
}
|
|
2324
|
-
const abs =
|
|
4149
|
+
const abs = resolve4(expanded);
|
|
2325
4150
|
if (seen.has(abs)) continue;
|
|
2326
4151
|
seen.add(abs);
|
|
2327
4152
|
result.push(entry.label !== void 0 ? { path: abs, label: entry.label } : { path: abs });
|
|
@@ -2335,15 +4160,15 @@ async function loadPortfolioConfig(configPath = DEFAULT_PORTFOLIO_CONFIG_PATH) {
|
|
|
2335
4160
|
// src/commands/refresh-watch.ts
|
|
2336
4161
|
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
2337
4162
|
import { homedir as homedir4 } from "os";
|
|
2338
|
-
import { join as
|
|
2339
|
-
import { findErrorCode as
|
|
4163
|
+
import { join as join6 } from "path";
|
|
4164
|
+
import { findErrorCode as findErrorCode8 } from "@basou/core";
|
|
2340
4165
|
var DEFAULT_WATCH_INTERVAL_SEC = 30;
|
|
2341
4166
|
var MIN_WATCH_INTERVAL_SEC = 5;
|
|
2342
4167
|
var MAX_WATCH_INTERVAL_SEC = 86400;
|
|
2343
4168
|
function watchedRoots(ctx) {
|
|
2344
4169
|
return [
|
|
2345
|
-
ctx.codexSessionsDir ??
|
|
2346
|
-
ctx.claudeProjectsDir ??
|
|
4170
|
+
ctx.codexSessionsDir ?? join6(homedir4(), ".codex", "sessions"),
|
|
4171
|
+
ctx.claudeProjectsDir ?? join6(homedir4(), ".claude", "projects")
|
|
2347
4172
|
];
|
|
2348
4173
|
}
|
|
2349
4174
|
async function scanSourceLogs(roots) {
|
|
@@ -2353,11 +4178,11 @@ async function scanSourceLogs(roots) {
|
|
|
2353
4178
|
try {
|
|
2354
4179
|
entries = await readdir2(dir, { withFileTypes: true });
|
|
2355
4180
|
} catch (error) {
|
|
2356
|
-
if (
|
|
4181
|
+
if (findErrorCode8(error, "ENOENT") || findErrorCode8(error, "ENOTDIR")) return;
|
|
2357
4182
|
throw new Error("Failed to read a source log directory", { cause: error });
|
|
2358
4183
|
}
|
|
2359
4184
|
for (const entry of entries) {
|
|
2360
|
-
const full =
|
|
4185
|
+
const full = join6(dir, entry.name);
|
|
2361
4186
|
if (entry.isDirectory()) {
|
|
2362
4187
|
await walk(full);
|
|
2363
4188
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -2365,7 +4190,7 @@ async function scanSourceLogs(roots) {
|
|
|
2365
4190
|
const info = await stat2(full);
|
|
2366
4191
|
out.set(full, { mtimeMs: info.mtimeMs, size: info.size });
|
|
2367
4192
|
} catch (error) {
|
|
2368
|
-
if (
|
|
4193
|
+
if (findErrorCode8(error, "ENOENT")) continue;
|
|
2369
4194
|
throw new Error("Failed to stat a source log file", { cause: error });
|
|
2370
4195
|
}
|
|
2371
4196
|
}
|
|
@@ -2457,26 +4282,26 @@ function collectPath2(value, previous) {
|
|
|
2457
4282
|
function parseInterval(value) {
|
|
2458
4283
|
const seconds = Number(value);
|
|
2459
4284
|
if (!Number.isInteger(seconds) || seconds < MIN_WATCH_INTERVAL_SEC || seconds > MAX_WATCH_INTERVAL_SEC) {
|
|
2460
|
-
throw new
|
|
4285
|
+
throw new InvalidArgumentError3(
|
|
2461
4286
|
`--interval must be an integer between ${MIN_WATCH_INTERVAL_SEC} and ${MAX_WATCH_INTERVAL_SEC} (seconds).`
|
|
2462
4287
|
);
|
|
2463
4288
|
}
|
|
2464
4289
|
return seconds;
|
|
2465
4290
|
}
|
|
2466
4291
|
function abortableSleep(ms, signal) {
|
|
2467
|
-
return new Promise((
|
|
4292
|
+
return new Promise((resolve8) => {
|
|
2468
4293
|
if (signal.aborted) {
|
|
2469
|
-
|
|
4294
|
+
resolve8();
|
|
2470
4295
|
return;
|
|
2471
4296
|
}
|
|
2472
4297
|
let timer;
|
|
2473
4298
|
const onAbort = () => {
|
|
2474
4299
|
clearTimeout(timer);
|
|
2475
|
-
|
|
4300
|
+
resolve8();
|
|
2476
4301
|
};
|
|
2477
4302
|
timer = setTimeout(() => {
|
|
2478
4303
|
signal.removeEventListener("abort", onAbort);
|
|
2479
|
-
|
|
4304
|
+
resolve8();
|
|
2480
4305
|
}, ms);
|
|
2481
4306
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
2482
4307
|
});
|
|
@@ -2567,8 +4392,8 @@ async function doRunRefreshWatch(options, ctx) {
|
|
|
2567
4392
|
if (options.force === true) throw new Error("--watch cannot be combined with --force.");
|
|
2568
4393
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2569
4394
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "refresh");
|
|
2570
|
-
const paths =
|
|
2571
|
-
await
|
|
4395
|
+
const paths = basouPaths10(repositoryRoot);
|
|
4396
|
+
await assertWorkspaceInitialized8(paths.root);
|
|
2572
4397
|
const intervalMs = (options.interval ?? DEFAULT_WATCH_INTERVAL_SEC) * 1e3;
|
|
2573
4398
|
const controller = new AbortController();
|
|
2574
4399
|
const onSignal = () => controller.abort();
|
|
@@ -2595,8 +4420,8 @@ async function doRunRefreshWatch(options, ctx) {
|
|
|
2595
4420
|
async function computeRefresh(options, ctx) {
|
|
2596
4421
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2597
4422
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "refresh");
|
|
2598
|
-
const paths =
|
|
2599
|
-
await
|
|
4423
|
+
const paths = basouPaths10(repositoryRoot);
|
|
4424
|
+
await assertWorkspaceInitialized8(paths.root);
|
|
2600
4425
|
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2601
4426
|
return refreshAll({
|
|
2602
4427
|
options: {
|
|
@@ -2644,7 +4469,14 @@ function printRefreshSummary(result) {
|
|
|
2644
4469
|
console.log(`handoff: skipped (${result.handoff.reason})`);
|
|
2645
4470
|
}
|
|
2646
4471
|
if (result.decisions.status === "generated") {
|
|
2647
|
-
|
|
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
|
+
}
|
|
2648
4480
|
} else {
|
|
2649
4481
|
console.log(`decisions: skipped (${result.decisions.reason})`);
|
|
2650
4482
|
}
|
|
@@ -2656,11 +4488,11 @@ function printRefreshSummary(result) {
|
|
|
2656
4488
|
console.log(`orientation: skipped (${result.orientation.reason})`);
|
|
2657
4489
|
}
|
|
2658
4490
|
}
|
|
2659
|
-
async function
|
|
4491
|
+
async function assertWorkspaceInitialized8(basouRoot) {
|
|
2660
4492
|
try {
|
|
2661
|
-
await
|
|
4493
|
+
await assertBasouRootSafe9(basouRoot);
|
|
2662
4494
|
} catch (error) {
|
|
2663
|
-
if (
|
|
4495
|
+
if (findErrorCode9(error, "ENOENT")) {
|
|
2664
4496
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
2665
4497
|
}
|
|
2666
4498
|
throw error;
|
|
@@ -2668,14 +4500,14 @@ async function assertWorkspaceInitialized7(basouRoot) {
|
|
|
2668
4500
|
}
|
|
2669
4501
|
|
|
2670
4502
|
// src/commands/report.ts
|
|
2671
|
-
import { isAbsolute as
|
|
4503
|
+
import { isAbsolute as isAbsolute3, resolve as resolve5 } from "path";
|
|
2672
4504
|
import {
|
|
2673
|
-
assertBasouRootSafe as
|
|
2674
|
-
basouPaths as
|
|
2675
|
-
findErrorCode as
|
|
4505
|
+
assertBasouRootSafe as assertBasouRootSafe10,
|
|
4506
|
+
basouPaths as basouPaths11,
|
|
4507
|
+
findErrorCode as findErrorCode10,
|
|
2676
4508
|
renderReport,
|
|
2677
4509
|
resolveRepositoryRoot as resolveRepositoryRoot8,
|
|
2678
|
-
writeMarkdownFile as
|
|
4510
|
+
writeMarkdownFile as writeMarkdownFile6
|
|
2679
4511
|
} from "@basou/core";
|
|
2680
4512
|
function registerReportCommand(program) {
|
|
2681
4513
|
const report = program.command("report").description(
|
|
@@ -2696,8 +4528,8 @@ async function runReportGenerate(options, ctx = {}) {
|
|
|
2696
4528
|
async function doRunReportGenerate(options, ctx) {
|
|
2697
4529
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2698
4530
|
const repositoryRoot = await resolveRepositoryRootForReport(cwd);
|
|
2699
|
-
const paths =
|
|
2700
|
-
await
|
|
4531
|
+
const paths = basouPaths11(repositoryRoot);
|
|
4532
|
+
await assertWorkspaceInitialized9(paths.root);
|
|
2701
4533
|
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2702
4534
|
const result = await renderReport({
|
|
2703
4535
|
paths,
|
|
@@ -2708,8 +4540,8 @@ async function doRunReportGenerate(options, ctx) {
|
|
|
2708
4540
|
onTaskSkip: (taskId, reason) => printTaskSkip(taskId, reason)
|
|
2709
4541
|
});
|
|
2710
4542
|
if (options.out !== void 0) {
|
|
2711
|
-
const outPath =
|
|
2712
|
-
await
|
|
4543
|
+
const outPath = isAbsolute3(options.out) ? options.out : resolve5(cwd, options.out);
|
|
4544
|
+
await writeMarkdownFile6(outPath, result.body);
|
|
2713
4545
|
const { sessions, decisions, tasks } = result.data;
|
|
2714
4546
|
console.error(
|
|
2715
4547
|
`Wrote report to ${options.out} (sessions: ${sessions.total}, decisions: ${decisions.count}, tasks: ${tasks.total})`
|
|
@@ -2734,25 +4566,149 @@ async function resolveRepositoryRootForReport(cwd) {
|
|
|
2734
4566
|
throw error;
|
|
2735
4567
|
}
|
|
2736
4568
|
}
|
|
2737
|
-
async function
|
|
4569
|
+
async function assertWorkspaceInitialized9(basouRoot) {
|
|
2738
4570
|
try {
|
|
2739
|
-
await
|
|
4571
|
+
await assertBasouRootSafe10(basouRoot);
|
|
2740
4572
|
} catch (error) {
|
|
2741
|
-
if (
|
|
4573
|
+
if (findErrorCode10(error, "ENOENT")) {
|
|
2742
4574
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
2743
4575
|
}
|
|
2744
4576
|
throw error;
|
|
2745
4577
|
}
|
|
2746
4578
|
}
|
|
2747
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(program) {
|
|
4597
|
+
program.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
|
+
|
|
2748
4704
|
// src/commands/run.ts
|
|
2749
4705
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
2750
4706
|
import { homedir as homedir5 } from "os";
|
|
2751
|
-
import { join as
|
|
4707
|
+
import { join as join7 } from "path";
|
|
2752
4708
|
import {
|
|
2753
|
-
acquireLock as
|
|
2754
|
-
assertBasouRootSafe as
|
|
2755
|
-
basouPaths as
|
|
4709
|
+
acquireLock as acquireLock5,
|
|
4710
|
+
assertBasouRootSafe as assertBasouRootSafe11,
|
|
4711
|
+
basouPaths as basouPaths13,
|
|
2756
4712
|
ChildProcessRunner as ChildProcessRunner2,
|
|
2757
4713
|
claudeCodeAdapterMetadata,
|
|
2758
4714
|
appendChainedEvent as coreAppendChainedEvent2,
|
|
@@ -2761,7 +4717,7 @@ import {
|
|
|
2761
4717
|
getSnapshot as getSnapshot2,
|
|
2762
4718
|
overwriteYamlFile as overwriteYamlFile2,
|
|
2763
4719
|
prefixedUlid as prefixedUlid4,
|
|
2764
|
-
readManifest as
|
|
4720
|
+
readManifest as readManifest6,
|
|
2765
4721
|
readYamlFile as readYamlFile4,
|
|
2766
4722
|
resolveClaudeCodeCommand,
|
|
2767
4723
|
resolveRepositoryRoot as resolveRepositoryRoot9,
|
|
@@ -2797,17 +4753,17 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2797
4753
|
const { command } = await resolveCommand();
|
|
2798
4754
|
const cwd = options.cwd ?? process.cwd();
|
|
2799
4755
|
const repoRoot = await resolveRepositoryRootForRun(cwd);
|
|
2800
|
-
const paths =
|
|
2801
|
-
await
|
|
2802
|
-
const manifest = await
|
|
4756
|
+
const paths = basouPaths13(repoRoot);
|
|
4757
|
+
await assertBasouRootSafe11(paths.root);
|
|
4758
|
+
const manifest = await readManifest6(paths);
|
|
2803
4759
|
const sessionId = prefixedUlid4("ses");
|
|
2804
|
-
const sessionDir =
|
|
4760
|
+
const sessionDir = join7(paths.sessions, sessionId);
|
|
2805
4761
|
await mkdir2(sessionDir, { recursive: true });
|
|
2806
4762
|
const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
|
|
2807
4763
|
await coreAppendChainedEvent2(paths, sessionId, event);
|
|
2808
4764
|
});
|
|
2809
4765
|
const startedAt = now().toISOString();
|
|
2810
|
-
const sessionYamlPath =
|
|
4766
|
+
const sessionYamlPath = join7(sessionDir, "session.yaml");
|
|
2811
4767
|
const session = buildInitialSession2({
|
|
2812
4768
|
id: sessionId,
|
|
2813
4769
|
command,
|
|
@@ -2840,7 +4796,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2840
4796
|
from: "initialized",
|
|
2841
4797
|
to: "running"
|
|
2842
4798
|
});
|
|
2843
|
-
const runningLock = await
|
|
4799
|
+
const runningLock = await acquireLock5(paths, "session", sessionId);
|
|
2844
4800
|
try {
|
|
2845
4801
|
await mutateSessionYaml2(sessionYamlPath, (s) => {
|
|
2846
4802
|
s.session.status = "running";
|
|
@@ -3150,29 +5106,28 @@ async function resolveRepositoryRootForRun(cwd) {
|
|
|
3150
5106
|
|
|
3151
5107
|
// src/commands/session.ts
|
|
3152
5108
|
import { readFile as readFile2 } from "fs/promises";
|
|
3153
|
-
import { basename as
|
|
5109
|
+
import { basename as basename4, isAbsolute as isAbsolute4, join as join8, relative as relative3 } from "path";
|
|
3154
5110
|
import {
|
|
3155
|
-
acquireLock as
|
|
3156
|
-
appendEventToExistingSession as
|
|
3157
|
-
assertBasouRootSafe as
|
|
3158
|
-
basouPaths as
|
|
5111
|
+
acquireLock as acquireLock6,
|
|
5112
|
+
appendEventToExistingSession as appendEventToExistingSession3,
|
|
5113
|
+
assertBasouRootSafe as assertBasouRootSafe12,
|
|
5114
|
+
basouPaths as basouPaths14,
|
|
3159
5115
|
enumerateSessionDirs as enumerateSessionDirs2,
|
|
3160
|
-
findErrorCode as
|
|
5116
|
+
findErrorCode as findErrorCode11,
|
|
3161
5117
|
importSessionFromJson as importSessionFromJson2,
|
|
3162
5118
|
loadSessionEntries,
|
|
3163
5119
|
readAllEvents,
|
|
3164
|
-
readManifest as
|
|
5120
|
+
readManifest as readManifest7,
|
|
3165
5121
|
readYamlFile as readYamlFile5,
|
|
3166
5122
|
rechainSessionInPlace,
|
|
3167
|
-
|
|
3168
|
-
resolveSessionId as resolveSessionId2,
|
|
5123
|
+
resolveSessionId as resolveSessionId3,
|
|
3169
5124
|
resolveTaskId,
|
|
3170
5125
|
SessionImportPayloadSchema as SessionImportPayloadSchema2,
|
|
3171
5126
|
SessionSchema as SessionSchema3,
|
|
3172
5127
|
SessionStatusSchema,
|
|
3173
5128
|
sessionWorkStatsFromEvents
|
|
3174
5129
|
} from "@basou/core";
|
|
3175
|
-
import { InvalidArgumentError as
|
|
5130
|
+
import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
|
|
3176
5131
|
|
|
3177
5132
|
// src/lib/format-duration.ts
|
|
3178
5133
|
import { formatDurationMs } from "@basou/core";
|
|
@@ -3221,8 +5176,8 @@ async function runSessionList(options, ctx = {}) {
|
|
|
3221
5176
|
async function doRunSessionList(options, ctx) {
|
|
3222
5177
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3223
5178
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "list");
|
|
3224
|
-
const paths =
|
|
3225
|
-
await
|
|
5179
|
+
const paths = basouPaths14(repositoryRoot);
|
|
5180
|
+
await assertWorkspaceInitialized10(paths.root);
|
|
3226
5181
|
const now = /* @__PURE__ */ new Date();
|
|
3227
5182
|
const records = (await loadSessionEntries(paths, {
|
|
3228
5183
|
now,
|
|
@@ -3273,17 +5228,17 @@ async function runSessionShow(idInput, options, ctx = {}) {
|
|
|
3273
5228
|
async function doRunSessionShow(idInput, options, ctx) {
|
|
3274
5229
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3275
5230
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "show");
|
|
3276
|
-
const paths =
|
|
3277
|
-
await
|
|
3278
|
-
const sessionId = await
|
|
3279
|
-
const sessionDir =
|
|
3280
|
-
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");
|
|
3281
5236
|
let session;
|
|
3282
5237
|
try {
|
|
3283
5238
|
const raw = await readYamlFile5(sessionYamlPath);
|
|
3284
5239
|
session = SessionSchema3.parse(raw);
|
|
3285
5240
|
} catch (error) {
|
|
3286
|
-
if (
|
|
5241
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
3287
5242
|
throw new Error(`Session not found: ${idInput}`);
|
|
3288
5243
|
}
|
|
3289
5244
|
throw new Error("Failed to read session", { cause: error });
|
|
@@ -3398,12 +5353,12 @@ function formatSessionWork(session, events, now) {
|
|
|
3398
5353
|
}
|
|
3399
5354
|
function formatWorkingDir(workingDir, repositoryRoot, options) {
|
|
3400
5355
|
if (options.fullPath === true) return workingDir;
|
|
3401
|
-
if (!
|
|
5356
|
+
if (!isAbsolute4(workingDir)) {
|
|
3402
5357
|
if (workingDir === ".") return "<repository_root>";
|
|
3403
5358
|
return workingDir;
|
|
3404
5359
|
}
|
|
3405
5360
|
if (workingDir === repositoryRoot) return "<repository_root>";
|
|
3406
|
-
const rel =
|
|
5361
|
+
const rel = relative3(repositoryRoot, workingDir);
|
|
3407
5362
|
if (rel.length === 0 || rel === ".") return "<repository_root>";
|
|
3408
5363
|
if (rel.startsWith("..")) return rel;
|
|
3409
5364
|
return `./${rel}`;
|
|
@@ -3517,23 +5472,13 @@ function maxLen2(values, floor) {
|
|
|
3517
5472
|
return max;
|
|
3518
5473
|
}
|
|
3519
5474
|
async function resolveRepositoryRootForSession(cwd, subcmd) {
|
|
3520
|
-
|
|
3521
|
-
return await resolveRepositoryRoot10(cwd);
|
|
3522
|
-
} catch (error) {
|
|
3523
|
-
if (error instanceof Error && error.message === "Not a git repository") {
|
|
3524
|
-
throw new Error(
|
|
3525
|
-
`Not a git repository. Run 'git init' first, then re-run 'basou session ${subcmd}'.`,
|
|
3526
|
-
{ cause: error }
|
|
3527
|
-
);
|
|
3528
|
-
}
|
|
3529
|
-
throw error;
|
|
3530
|
-
}
|
|
5475
|
+
return resolveBasouRootForCommand(cwd, `session ${subcmd}`);
|
|
3531
5476
|
}
|
|
3532
|
-
async function
|
|
5477
|
+
async function assertWorkspaceInitialized10(basouRoot) {
|
|
3533
5478
|
try {
|
|
3534
|
-
await
|
|
5479
|
+
await assertBasouRootSafe12(basouRoot);
|
|
3535
5480
|
} catch (error) {
|
|
3536
|
-
if (
|
|
5481
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
3537
5482
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3538
5483
|
}
|
|
3539
5484
|
throw error;
|
|
@@ -3571,9 +5516,9 @@ async function runSessionImport(options, ctx = {}) {
|
|
|
3571
5516
|
async function doRunSessionImport(options, ctx) {
|
|
3572
5517
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3573
5518
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "import");
|
|
3574
|
-
const paths =
|
|
3575
|
-
await
|
|
3576
|
-
const manifest = await
|
|
5519
|
+
const paths = basouPaths14(repositoryRoot);
|
|
5520
|
+
await assertWorkspaceInitialized10(paths.root);
|
|
5521
|
+
const manifest = await readManifest7(paths);
|
|
3577
5522
|
const rawBody = await readInputFile(options.from);
|
|
3578
5523
|
const json = parseJsonStrict(rawBody);
|
|
3579
5524
|
const parsed = SessionImportPayloadSchema2.safeParse(json);
|
|
@@ -3602,10 +5547,10 @@ async function readInputFile(path) {
|
|
|
3602
5547
|
try {
|
|
3603
5548
|
return await readFile2(path, "utf8");
|
|
3604
5549
|
} catch (error) {
|
|
3605
|
-
if (
|
|
5550
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
3606
5551
|
throw new Error("Import source not found", { cause: error });
|
|
3607
5552
|
}
|
|
3608
|
-
if (
|
|
5553
|
+
if (findErrorCode11(error, "EISDIR")) {
|
|
3609
5554
|
throw new Error("Import source is not a file", { cause: error });
|
|
3610
5555
|
}
|
|
3611
5556
|
throw new Error("Failed to read import source", { cause: error });
|
|
@@ -3620,19 +5565,19 @@ function parseJsonStrict(body) {
|
|
|
3620
5565
|
}
|
|
3621
5566
|
function parseImportFormat(raw) {
|
|
3622
5567
|
if (raw !== "json") {
|
|
3623
|
-
throw new
|
|
5568
|
+
throw new InvalidArgumentError5(`Unsupported format: ${raw}. Valid values: json`);
|
|
3624
5569
|
}
|
|
3625
5570
|
return "json";
|
|
3626
5571
|
}
|
|
3627
5572
|
function parseLabelOverride(raw) {
|
|
3628
5573
|
if (raw.length === 0) {
|
|
3629
|
-
throw new
|
|
5574
|
+
throw new InvalidArgumentError5("Label must not be empty");
|
|
3630
5575
|
}
|
|
3631
5576
|
return raw;
|
|
3632
5577
|
}
|
|
3633
5578
|
function parseTaskIdOverride(raw) {
|
|
3634
5579
|
if (raw.length === 0) {
|
|
3635
|
-
throw new
|
|
5580
|
+
throw new InvalidArgumentError5("Task id is empty");
|
|
3636
5581
|
}
|
|
3637
5582
|
return raw;
|
|
3638
5583
|
}
|
|
@@ -3658,7 +5603,7 @@ function printSessionImportResult(options, result) {
|
|
|
3658
5603
|
return;
|
|
3659
5604
|
}
|
|
3660
5605
|
console.log(
|
|
3661
|
-
`Imported session ${sid} (${result.eventCount} events) from ${
|
|
5606
|
+
`Imported session ${sid} (${result.eventCount} events) from ${basename4(options.from)}`
|
|
3662
5607
|
);
|
|
3663
5608
|
}
|
|
3664
5609
|
var NOTE_BODY_PREVIEW_LIMIT = 80;
|
|
@@ -3685,19 +5630,19 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
|
|
|
3685
5630
|
}
|
|
3686
5631
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3687
5632
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "note");
|
|
3688
|
-
const paths =
|
|
3689
|
-
await
|
|
3690
|
-
const sessionId = await
|
|
5633
|
+
const paths = basouPaths14(repositoryRoot);
|
|
5634
|
+
await assertWorkspaceInitialized10(paths.root);
|
|
5635
|
+
const sessionId = await resolveSessionId3(paths, sessionIdInput);
|
|
3691
5636
|
const body = hasBody ? options.body : await readNoteFile(options.fromFile);
|
|
3692
5637
|
if (body.length === 0) {
|
|
3693
5638
|
throw new Error("Note body is empty");
|
|
3694
5639
|
}
|
|
3695
5640
|
const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3696
5641
|
const sesId = sessionId;
|
|
3697
|
-
const sessionLock = await
|
|
5642
|
+
const sessionLock = await acquireLock6(paths, "session", sesId);
|
|
3698
5643
|
let result;
|
|
3699
5644
|
try {
|
|
3700
|
-
result = await
|
|
5645
|
+
result = await appendEventToExistingSession3({
|
|
3701
5646
|
paths,
|
|
3702
5647
|
sessionId: sesId,
|
|
3703
5648
|
eventBuilder: (eventId) => ({
|
|
@@ -3719,10 +5664,10 @@ async function readNoteFile(path) {
|
|
|
3719
5664
|
try {
|
|
3720
5665
|
return await readFile2(path, "utf8");
|
|
3721
5666
|
} catch (error) {
|
|
3722
|
-
if (
|
|
5667
|
+
if (findErrorCode11(error, "ENOENT")) {
|
|
3723
5668
|
throw new Error("Note source not found", { cause: error });
|
|
3724
5669
|
}
|
|
3725
|
-
if (
|
|
5670
|
+
if (findErrorCode11(error, "EISDIR")) {
|
|
3726
5671
|
throw new Error("Note source is not a file", { cause: error });
|
|
3727
5672
|
}
|
|
3728
5673
|
throw new Error("Failed to read note source", { cause: error });
|
|
@@ -3730,7 +5675,7 @@ async function readNoteFile(path) {
|
|
|
3730
5675
|
}
|
|
3731
5676
|
function parseNoteBodyOption(raw) {
|
|
3732
5677
|
if (raw.length === 0) {
|
|
3733
|
-
throw new
|
|
5678
|
+
throw new InvalidArgumentError5("--body must not be empty");
|
|
3734
5679
|
}
|
|
3735
5680
|
return raw;
|
|
3736
5681
|
}
|
|
@@ -3767,9 +5712,9 @@ async function doRunSessionRechain(options, ctx) {
|
|
|
3767
5712
|
}
|
|
3768
5713
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3769
5714
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "rechain");
|
|
3770
|
-
const paths =
|
|
3771
|
-
await
|
|
3772
|
-
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);
|
|
3773
5718
|
const dryRun = options.dryRun === true;
|
|
3774
5719
|
const rows = [];
|
|
3775
5720
|
for (const sessionId of sessionIds) {
|
|
@@ -3821,11 +5766,11 @@ function renderRechainRow(row, dryRun) {
|
|
|
3821
5766
|
|
|
3822
5767
|
// src/commands/stats.ts
|
|
3823
5768
|
import {
|
|
3824
|
-
assertBasouRootSafe as
|
|
3825
|
-
basouPaths as
|
|
5769
|
+
assertBasouRootSafe as assertBasouRootSafe13,
|
|
5770
|
+
basouPaths as basouPaths15,
|
|
3826
5771
|
computeWorkStats,
|
|
3827
|
-
findErrorCode as
|
|
3828
|
-
resolveRepositoryRoot as
|
|
5772
|
+
findErrorCode as findErrorCode12,
|
|
5773
|
+
resolveRepositoryRoot as resolveRepositoryRoot10
|
|
3829
5774
|
} from "@basou/core";
|
|
3830
5775
|
function registerStatsCommand(program) {
|
|
3831
5776
|
program.command("stats").description("Report how much the AI worked (output volume + time proxies) across sessions").option("--by-source", "Break the totals down by session source kind").option("--by-day", "Break billable time and volume down by calendar day").option("--json", "Output the full stats as JSON").option("-v, --verbose", "Show error causes").action(async (options) => {
|
|
@@ -3843,8 +5788,8 @@ async function runStats(options, ctx = {}) {
|
|
|
3843
5788
|
async function doRunStats(options, ctx) {
|
|
3844
5789
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3845
5790
|
const repositoryRoot = await resolveRepositoryRootForStats(cwd);
|
|
3846
|
-
const paths =
|
|
3847
|
-
await
|
|
5791
|
+
const paths = basouPaths15(repositoryRoot);
|
|
5792
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
3848
5793
|
const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
|
|
3849
5794
|
const result = await computeWorkStats({
|
|
3850
5795
|
paths,
|
|
@@ -3928,7 +5873,7 @@ function formatInt(n) {
|
|
|
3928
5873
|
}
|
|
3929
5874
|
async function resolveRepositoryRootForStats(cwd) {
|
|
3930
5875
|
try {
|
|
3931
|
-
return await
|
|
5876
|
+
return await resolveRepositoryRoot10(cwd);
|
|
3932
5877
|
} catch (error) {
|
|
3933
5878
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
3934
5879
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou stats'.", {
|
|
@@ -3938,11 +5883,11 @@ async function resolveRepositoryRootForStats(cwd) {
|
|
|
3938
5883
|
throw error;
|
|
3939
5884
|
}
|
|
3940
5885
|
}
|
|
3941
|
-
async function
|
|
5886
|
+
async function assertWorkspaceInitialized11(basouRoot) {
|
|
3942
5887
|
try {
|
|
3943
|
-
await
|
|
5888
|
+
await assertBasouRootSafe13(basouRoot);
|
|
3944
5889
|
} catch (error) {
|
|
3945
|
-
if (
|
|
5890
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
3946
5891
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3947
5892
|
}
|
|
3948
5893
|
throw error;
|
|
@@ -3951,12 +5896,12 @@ async function assertWorkspaceInitialized10(basouRoot) {
|
|
|
3951
5896
|
|
|
3952
5897
|
// src/commands/status.ts
|
|
3953
5898
|
import {
|
|
3954
|
-
assertBasouRootSafe as
|
|
3955
|
-
basouPaths as
|
|
5899
|
+
assertBasouRootSafe as assertBasouRootSafe14,
|
|
5900
|
+
basouPaths as basouPaths16,
|
|
3956
5901
|
buildStatusSnapshot,
|
|
3957
|
-
findErrorCode as
|
|
3958
|
-
readManifest as
|
|
3959
|
-
resolveRepositoryRoot as
|
|
5902
|
+
findErrorCode as findErrorCode13,
|
|
5903
|
+
readManifest as readManifest8,
|
|
5904
|
+
resolveRepositoryRoot as resolveRepositoryRoot11,
|
|
3960
5905
|
writeStatus
|
|
3961
5906
|
} from "@basou/core";
|
|
3962
5907
|
function registerStatusCommand(program) {
|
|
@@ -3975,20 +5920,20 @@ async function runStatus(options, ctx = {}) {
|
|
|
3975
5920
|
async function doRunStatus(options, ctx) {
|
|
3976
5921
|
const cwd = ctx.cwd ?? process.cwd();
|
|
3977
5922
|
const repositoryRoot = await resolveRepositoryRootForStatus(cwd);
|
|
3978
|
-
const paths =
|
|
5923
|
+
const paths = basouPaths16(repositoryRoot);
|
|
3979
5924
|
try {
|
|
3980
|
-
await
|
|
5925
|
+
await assertBasouRootSafe14(paths.root);
|
|
3981
5926
|
} catch (error) {
|
|
3982
|
-
if (
|
|
5927
|
+
if (findErrorCode13(error, "ENOENT")) {
|
|
3983
5928
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3984
5929
|
}
|
|
3985
5930
|
throw error;
|
|
3986
5931
|
}
|
|
3987
5932
|
let manifest;
|
|
3988
5933
|
try {
|
|
3989
|
-
manifest = await
|
|
5934
|
+
manifest = await readManifest8(paths);
|
|
3990
5935
|
} catch (error) {
|
|
3991
|
-
if (
|
|
5936
|
+
if (findErrorCode13(error, "ENOENT")) {
|
|
3992
5937
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
3993
5938
|
}
|
|
3994
5939
|
throw new Error("Failed to read workspace manifest", { cause: error });
|
|
@@ -4012,7 +5957,7 @@ function renderTextStatus(s) {
|
|
|
4012
5957
|
}
|
|
4013
5958
|
async function resolveRepositoryRootForStatus(cwd) {
|
|
4014
5959
|
try {
|
|
4015
|
-
return await
|
|
5960
|
+
return await resolveRepositoryRoot11(cwd);
|
|
4016
5961
|
} catch (error) {
|
|
4017
5962
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
4018
5963
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou status'.", {
|
|
@@ -4025,34 +5970,34 @@ async function resolveRepositoryRootForStatus(cwd) {
|
|
|
4025
5970
|
|
|
4026
5971
|
// src/commands/task.ts
|
|
4027
5972
|
import { readFile as readFile3 } from "fs/promises";
|
|
4028
|
-
import { join as
|
|
5973
|
+
import { join as join9 } from "path";
|
|
4029
5974
|
import {
|
|
4030
5975
|
archiveTask,
|
|
4031
|
-
assertBasouRootSafe as
|
|
4032
|
-
basouPaths as
|
|
5976
|
+
assertBasouRootSafe as assertBasouRootSafe15,
|
|
5977
|
+
basouPaths as basouPaths17,
|
|
4033
5978
|
createTaskWithEvent,
|
|
4034
5979
|
deleteTask,
|
|
4035
5980
|
editTask,
|
|
4036
5981
|
enumerateArchivedTaskIds,
|
|
4037
|
-
findErrorCode as
|
|
5982
|
+
findErrorCode as findErrorCode14,
|
|
4038
5983
|
loadSessionEntries as loadSessionEntries2,
|
|
4039
5984
|
loadTaskEntries,
|
|
4040
5985
|
prefixedUlid as prefixedUlid5,
|
|
4041
|
-
readManifest as
|
|
5986
|
+
readManifest as readManifest9,
|
|
4042
5987
|
readTaskFile,
|
|
4043
5988
|
readTaskFileWithArchiveFallback,
|
|
4044
5989
|
reconcileAllTasks,
|
|
4045
5990
|
reconcileTask,
|
|
4046
5991
|
refreshTaskLinkedSessions,
|
|
4047
5992
|
replayEvents as replayEvents2,
|
|
4048
|
-
resolveRepositoryRoot as
|
|
4049
|
-
resolveSessionId as
|
|
5993
|
+
resolveRepositoryRoot as resolveRepositoryRoot12,
|
|
5994
|
+
resolveSessionId as resolveSessionId4,
|
|
4050
5995
|
resolveTaskId as resolveTaskId2,
|
|
4051
5996
|
TaskStatusSchema,
|
|
4052
5997
|
TaskWriteAfterEventError,
|
|
4053
5998
|
updateTaskStatusWithEvent
|
|
4054
5999
|
} from "@basou/core";
|
|
4055
|
-
import { InvalidArgumentError as
|
|
6000
|
+
import { InvalidArgumentError as InvalidArgumentError6 } from "commander";
|
|
4056
6001
|
var STATUS_VALUES3 = TaskStatusSchema.options;
|
|
4057
6002
|
function registerTaskCommand(program) {
|
|
4058
6003
|
const task = program.command("task").description("Manage Basou tasks (purpose units that span sessions)");
|
|
@@ -4131,14 +6076,14 @@ async function doRunTaskNew(options, ctx) {
|
|
|
4131
6076
|
}
|
|
4132
6077
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4133
6078
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "new");
|
|
4134
|
-
const paths =
|
|
4135
|
-
await
|
|
6079
|
+
const paths = basouPaths17(repositoryRoot);
|
|
6080
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
4136
6081
|
const description = options.description !== void 0 ? options.description : options.fromFile !== void 0 ? await readDescriptionFile(options.fromFile) : "";
|
|
4137
6082
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
4138
6083
|
const occurredAt = now.toISOString();
|
|
4139
6084
|
const taskId = prefixedUlid5("task");
|
|
4140
6085
|
if (options.session !== void 0) {
|
|
4141
|
-
const sessionId = await
|
|
6086
|
+
const sessionId = await resolveSessionId4(paths, options.session);
|
|
4142
6087
|
const result2 = await createTaskWithEvent({
|
|
4143
6088
|
mode: "attach",
|
|
4144
6089
|
paths,
|
|
@@ -4166,7 +6111,7 @@ async function doRunTaskNew(options, ctx) {
|
|
|
4166
6111
|
});
|
|
4167
6112
|
return;
|
|
4168
6113
|
}
|
|
4169
|
-
const manifest = await
|
|
6114
|
+
const manifest = await readManifest9(paths);
|
|
4170
6115
|
const result = await createTaskWithEvent({
|
|
4171
6116
|
mode: "ad-hoc",
|
|
4172
6117
|
paths,
|
|
@@ -4240,8 +6185,8 @@ async function runTaskList(options, ctx = {}) {
|
|
|
4240
6185
|
async function doRunTaskList(options, ctx) {
|
|
4241
6186
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4242
6187
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "list");
|
|
4243
|
-
const paths =
|
|
4244
|
-
await
|
|
6188
|
+
const paths = basouPaths17(repositoryRoot);
|
|
6189
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
4245
6190
|
const entries = await loadTaskEntries(paths, {
|
|
4246
6191
|
onSkip: (id, reason) => printTaskSkip(id, reason)
|
|
4247
6192
|
});
|
|
@@ -4344,15 +6289,15 @@ async function runTaskShow(idInput, options, ctx = {}) {
|
|
|
4344
6289
|
async function doRunTaskShow(idInput, options, ctx) {
|
|
4345
6290
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4346
6291
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "show");
|
|
4347
|
-
const paths =
|
|
4348
|
-
await
|
|
6292
|
+
const paths = basouPaths17(repositoryRoot);
|
|
6293
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
4349
6294
|
const taskId = await resolveTaskId2(paths, idInput, { includeArchived: true });
|
|
4350
6295
|
const { doc, archived } = await readTaskFileWithArchiveFallback(paths, taskId);
|
|
4351
6296
|
const sessions = await loadSessionEntries2(paths, { now: /* @__PURE__ */ new Date() });
|
|
4352
6297
|
const events = [];
|
|
4353
6298
|
const linkedSessionIds = new Set(doc.task.task.linked_sessions);
|
|
4354
6299
|
for (const s of sessions) {
|
|
4355
|
-
const sessionDir =
|
|
6300
|
+
const sessionDir = join9(paths.sessions, s.sessionId);
|
|
4356
6301
|
try {
|
|
4357
6302
|
for await (const ev of replayEvents2(sessionDir, {
|
|
4358
6303
|
onWarning: (w) => printReplayWarning(w, s.sessionId)
|
|
@@ -4488,13 +6433,13 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
|
|
|
4488
6433
|
const newStatus = parseTaskStatusPositional(newStatusInput);
|
|
4489
6434
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4490
6435
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "status");
|
|
4491
|
-
const paths =
|
|
4492
|
-
await
|
|
6436
|
+
const paths = basouPaths17(repositoryRoot);
|
|
6437
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
4493
6438
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4494
6439
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
4495
6440
|
const occurredAt = now.toISOString();
|
|
4496
6441
|
if (options.session !== void 0) {
|
|
4497
|
-
const sessionId = await
|
|
6442
|
+
const sessionId = await resolveSessionId4(paths, options.session);
|
|
4498
6443
|
const result2 = await updateTaskStatusWithEvent({
|
|
4499
6444
|
mode: "attach",
|
|
4500
6445
|
paths,
|
|
@@ -4514,7 +6459,7 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
|
|
|
4514
6459
|
});
|
|
4515
6460
|
return;
|
|
4516
6461
|
}
|
|
4517
|
-
const manifest = await
|
|
6462
|
+
const manifest = await readManifest9(paths);
|
|
4518
6463
|
const result = await updateTaskStatusWithEvent({
|
|
4519
6464
|
mode: "ad-hoc",
|
|
4520
6465
|
paths,
|
|
@@ -4565,9 +6510,9 @@ async function runTaskReconcile(options, ctx = {}) {
|
|
|
4565
6510
|
async function doRunTaskReconcile(options, ctx) {
|
|
4566
6511
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4567
6512
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "reconcile");
|
|
4568
|
-
const paths =
|
|
4569
|
-
await
|
|
4570
|
-
const manifest = await
|
|
6513
|
+
const paths = basouPaths17(repositoryRoot);
|
|
6514
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
6515
|
+
const manifest = await readManifest9(paths);
|
|
4571
6516
|
const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
|
|
4572
6517
|
const write = options.write === true;
|
|
4573
6518
|
const verbose = isVerbose(options);
|
|
@@ -4745,9 +6690,9 @@ async function doRunTaskRefreshLinkage(taskIdInput, options, ctx) {
|
|
|
4745
6690
|
}
|
|
4746
6691
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4747
6692
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "refresh-linkage");
|
|
4748
|
-
const paths =
|
|
4749
|
-
await
|
|
4750
|
-
const manifest = await
|
|
6693
|
+
const paths = basouPaths17(repositoryRoot);
|
|
6694
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
6695
|
+
const manifest = await readManifest9(paths);
|
|
4751
6696
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4752
6697
|
const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
|
|
4753
6698
|
const write = options.write === true;
|
|
@@ -4825,9 +6770,9 @@ async function doRunTaskEdit(taskIdInput, options, ctx) {
|
|
|
4825
6770
|
}
|
|
4826
6771
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4827
6772
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "edit");
|
|
4828
|
-
const paths =
|
|
4829
|
-
await
|
|
4830
|
-
const manifest = await
|
|
6773
|
+
const paths = basouPaths17(repositoryRoot);
|
|
6774
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
6775
|
+
const manifest = await readManifest9(paths);
|
|
4831
6776
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4832
6777
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
4833
6778
|
const occurredAt = now.toISOString();
|
|
@@ -4881,9 +6826,9 @@ async function doRunTaskDelete(taskIdInput, options, ctx) {
|
|
|
4881
6826
|
}
|
|
4882
6827
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4883
6828
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "delete");
|
|
4884
|
-
const paths =
|
|
4885
|
-
await
|
|
4886
|
-
const manifest = await
|
|
6829
|
+
const paths = basouPaths17(repositoryRoot);
|
|
6830
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
6831
|
+
const manifest = await readManifest9(paths);
|
|
4887
6832
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4888
6833
|
if (options.yes !== true) {
|
|
4889
6834
|
await confirmDestructiveAction("delete", taskId);
|
|
@@ -4926,9 +6871,9 @@ async function doRunTaskArchive(taskIdInput, options, ctx) {
|
|
|
4926
6871
|
}
|
|
4927
6872
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4928
6873
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "archive");
|
|
4929
|
-
const paths =
|
|
4930
|
-
await
|
|
4931
|
-
const manifest = await
|
|
6874
|
+
const paths = basouPaths17(repositoryRoot);
|
|
6875
|
+
await assertWorkspaceInitialized12(paths.root);
|
|
6876
|
+
const manifest = await readManifest9(paths);
|
|
4932
6877
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
4933
6878
|
if (options.yes !== true) {
|
|
4934
6879
|
await confirmDestructiveAction("archive", taskId);
|
|
@@ -4981,20 +6926,20 @@ async function readSingleLineFromStdin() {
|
|
|
4981
6926
|
}
|
|
4982
6927
|
function parseTitle2(raw) {
|
|
4983
6928
|
if (raw.length === 0) {
|
|
4984
|
-
throw new
|
|
6929
|
+
throw new InvalidArgumentError6("Title must not be empty");
|
|
4985
6930
|
}
|
|
4986
6931
|
return raw;
|
|
4987
6932
|
}
|
|
4988
6933
|
function parseLabel(raw) {
|
|
4989
6934
|
if (raw.length === 0) {
|
|
4990
|
-
throw new
|
|
6935
|
+
throw new InvalidArgumentError6("Label must not be empty");
|
|
4991
6936
|
}
|
|
4992
6937
|
return raw;
|
|
4993
6938
|
}
|
|
4994
6939
|
function parseInitialTaskStatus(raw) {
|
|
4995
6940
|
const result = TaskStatusSchema.safeParse(raw);
|
|
4996
6941
|
if (!result.success) {
|
|
4997
|
-
throw new
|
|
6942
|
+
throw new InvalidArgumentError6(
|
|
4998
6943
|
`Initial task status must be one of: ${STATUS_VALUES3.join(", ")}`
|
|
4999
6944
|
);
|
|
5000
6945
|
}
|
|
@@ -5003,7 +6948,7 @@ function parseInitialTaskStatus(raw) {
|
|
|
5003
6948
|
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
5004
6949
|
function parseIsoTimestampOption(raw) {
|
|
5005
6950
|
if (!ISO_DATE_RE.test(raw) || Number.isNaN(Date.parse(raw))) {
|
|
5006
|
-
throw new
|
|
6951
|
+
throw new InvalidArgumentError6(
|
|
5007
6952
|
"Invalid --completed-at value; expected ISO-8601 timestamp like 2026-05-10T12:34:56+09:00"
|
|
5008
6953
|
);
|
|
5009
6954
|
}
|
|
@@ -5012,7 +6957,7 @@ function parseIsoTimestampOption(raw) {
|
|
|
5012
6957
|
function parseTaskStatusFilter(raw) {
|
|
5013
6958
|
const result = TaskStatusSchema.safeParse(raw);
|
|
5014
6959
|
if (!result.success) {
|
|
5015
|
-
throw new
|
|
6960
|
+
throw new InvalidArgumentError6(
|
|
5016
6961
|
`Invalid task status: ${raw}. Valid values: ${STATUS_VALUES3.join(", ")}`
|
|
5017
6962
|
);
|
|
5018
6963
|
}
|
|
@@ -5027,14 +6972,14 @@ function parseTaskStatusPositional(raw) {
|
|
|
5027
6972
|
}
|
|
5028
6973
|
function parseDescriptionOption(raw) {
|
|
5029
6974
|
if (raw.length === 0) {
|
|
5030
|
-
throw new
|
|
6975
|
+
throw new InvalidArgumentError6("Description must not be empty");
|
|
5031
6976
|
}
|
|
5032
6977
|
return raw;
|
|
5033
6978
|
}
|
|
5034
6979
|
function parsePositiveInt2(raw) {
|
|
5035
6980
|
const n = Number.parseInt(raw, 10);
|
|
5036
6981
|
if (!Number.isInteger(n) || n < 1 || raw.trim() !== String(n)) {
|
|
5037
|
-
throw new
|
|
6982
|
+
throw new InvalidArgumentError6(`Invalid number: ${raw}`);
|
|
5038
6983
|
}
|
|
5039
6984
|
return n;
|
|
5040
6985
|
}
|
|
@@ -5042,10 +6987,10 @@ async function readDescriptionFile(path) {
|
|
|
5042
6987
|
try {
|
|
5043
6988
|
return await readFile3(path, "utf8");
|
|
5044
6989
|
} catch (error) {
|
|
5045
|
-
if (
|
|
6990
|
+
if (findErrorCode14(error, "ENOENT")) {
|
|
5046
6991
|
throw new Error("Description source not found", { cause: error });
|
|
5047
6992
|
}
|
|
5048
|
-
if (
|
|
6993
|
+
if (findErrorCode14(error, "EISDIR")) {
|
|
5049
6994
|
throw new Error("Description source is not a file", { cause: error });
|
|
5050
6995
|
}
|
|
5051
6996
|
throw new Error("Failed to read description source", { cause: error });
|
|
@@ -5053,7 +6998,7 @@ async function readDescriptionFile(path) {
|
|
|
5053
6998
|
}
|
|
5054
6999
|
async function resolveRepositoryRootForTask(cwd, subcmd) {
|
|
5055
7000
|
try {
|
|
5056
|
-
return await
|
|
7001
|
+
return await resolveRepositoryRoot12(cwd);
|
|
5057
7002
|
} catch (error) {
|
|
5058
7003
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
5059
7004
|
throw new Error(
|
|
@@ -5064,11 +7009,11 @@ async function resolveRepositoryRootForTask(cwd, subcmd) {
|
|
|
5064
7009
|
throw error;
|
|
5065
7010
|
}
|
|
5066
7011
|
}
|
|
5067
|
-
async function
|
|
7012
|
+
async function assertWorkspaceInitialized12(basouRoot) {
|
|
5068
7013
|
try {
|
|
5069
|
-
await
|
|
7014
|
+
await assertBasouRootSafe15(basouRoot);
|
|
5070
7015
|
} catch (error) {
|
|
5071
|
-
if (
|
|
7016
|
+
if (findErrorCode14(error, "ENOENT")) {
|
|
5072
7017
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
5073
7018
|
}
|
|
5074
7019
|
throw error;
|
|
@@ -5156,12 +7101,12 @@ function maxLen3(values, floor) {
|
|
|
5156
7101
|
|
|
5157
7102
|
// src/commands/verify.ts
|
|
5158
7103
|
import {
|
|
5159
|
-
assertBasouRootSafe as
|
|
5160
|
-
basouPaths as
|
|
7104
|
+
assertBasouRootSafe as assertBasouRootSafe16,
|
|
7105
|
+
basouPaths as basouPaths18,
|
|
5161
7106
|
enumerateSessionDirs as enumerateSessionDirs3,
|
|
5162
|
-
findErrorCode as
|
|
5163
|
-
resolveRepositoryRoot as
|
|
5164
|
-
resolveSessionId as
|
|
7107
|
+
findErrorCode as findErrorCode15,
|
|
7108
|
+
resolveRepositoryRoot as resolveRepositoryRoot13,
|
|
7109
|
+
resolveSessionId as resolveSessionId5,
|
|
5165
7110
|
verifyEventsChain
|
|
5166
7111
|
} from "@basou/core";
|
|
5167
7112
|
function registerVerifyCommand(program) {
|
|
@@ -5183,9 +7128,9 @@ async function doRunVerify(options, ctx) {
|
|
|
5183
7128
|
}
|
|
5184
7129
|
const cwd = ctx.cwd ?? process.cwd();
|
|
5185
7130
|
const repositoryRoot = await resolveRepositoryRootForVerify(cwd);
|
|
5186
|
-
const paths =
|
|
5187
|
-
await
|
|
5188
|
-
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);
|
|
5189
7134
|
const rows = [];
|
|
5190
7135
|
for (const sessionId of sessionIds) {
|
|
5191
7136
|
const verdict = await verifyEventsChain(paths, sessionId);
|
|
@@ -5231,7 +7176,7 @@ function renderVerdict(row) {
|
|
|
5231
7176
|
}
|
|
5232
7177
|
async function resolveRepositoryRootForVerify(cwd) {
|
|
5233
7178
|
try {
|
|
5234
|
-
return await
|
|
7179
|
+
return await resolveRepositoryRoot13(cwd);
|
|
5235
7180
|
} catch (error) {
|
|
5236
7181
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
5237
7182
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou verify'.", {
|
|
@@ -5241,11 +7186,11 @@ async function resolveRepositoryRootForVerify(cwd) {
|
|
|
5241
7186
|
throw error;
|
|
5242
7187
|
}
|
|
5243
7188
|
}
|
|
5244
|
-
async function
|
|
7189
|
+
async function assertWorkspaceInitialized13(basouRoot) {
|
|
5245
7190
|
try {
|
|
5246
|
-
await
|
|
7191
|
+
await assertBasouRootSafe16(basouRoot);
|
|
5247
7192
|
} catch (error) {
|
|
5248
|
-
if (
|
|
7193
|
+
if (findErrorCode15(error, "ENOENT")) {
|
|
5249
7194
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
5250
7195
|
}
|
|
5251
7196
|
throw error;
|
|
@@ -5255,22 +7200,22 @@ async function assertWorkspaceInitialized12(basouRoot) {
|
|
|
5255
7200
|
// src/commands/view.ts
|
|
5256
7201
|
import { spawn } from "child_process";
|
|
5257
7202
|
import { createHash } from "crypto";
|
|
5258
|
-
import { basename as
|
|
7203
|
+
import { basename as basename5, resolve as resolve7 } from "path";
|
|
5259
7204
|
import {
|
|
5260
|
-
assertBasouRootSafe as
|
|
5261
|
-
basouPaths as
|
|
5262
|
-
findErrorCode as
|
|
5263
|
-
readManifest as
|
|
5264
|
-
resolveRepositoryRoot as
|
|
7205
|
+
assertBasouRootSafe as assertBasouRootSafe17,
|
|
7206
|
+
basouPaths as basouPaths19,
|
|
7207
|
+
findErrorCode as findErrorCode17,
|
|
7208
|
+
readManifest as readManifest12,
|
|
7209
|
+
resolveRepositoryRoot as resolveRepositoryRoot14
|
|
5265
7210
|
} from "@basou/core";
|
|
5266
|
-
import { InvalidArgumentError as
|
|
7211
|
+
import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
|
|
5267
7212
|
|
|
5268
7213
|
// src/lib/portfolio-safety.ts
|
|
5269
7214
|
import { execFile } from "child_process";
|
|
5270
7215
|
import { lstat, realpath } from "fs/promises";
|
|
5271
|
-
import { isAbsolute as
|
|
7216
|
+
import { isAbsolute as isAbsolute5, join as join10, relative as relative4, resolve as resolve6 } from "path";
|
|
5272
7217
|
import { promisify } from "util";
|
|
5273
|
-
import { readManifest as
|
|
7218
|
+
import { readManifest as readManifest10 } from "@basou/core";
|
|
5274
7219
|
var execFileAsync = promisify(execFile);
|
|
5275
7220
|
function errorCode(error) {
|
|
5276
7221
|
return error instanceof Error ? error.code : void 0;
|
|
@@ -5279,12 +7224,12 @@ async function canonical(p) {
|
|
|
5279
7224
|
try {
|
|
5280
7225
|
return await realpath(p);
|
|
5281
7226
|
} catch {
|
|
5282
|
-
return
|
|
7227
|
+
return resolve6(p);
|
|
5283
7228
|
}
|
|
5284
7229
|
}
|
|
5285
7230
|
function isInside(child, parent) {
|
|
5286
|
-
const rel =
|
|
5287
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
7231
|
+
const rel = relative4(parent, child);
|
|
7232
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute5(rel);
|
|
5288
7233
|
}
|
|
5289
7234
|
function isBasouPath(p) {
|
|
5290
7235
|
return p === ".basou" || p.startsWith(".basou/") || p.includes("/.basou/") || p.endsWith("/.basou");
|
|
@@ -5292,7 +7237,7 @@ function isBasouPath(p) {
|
|
|
5292
7237
|
async function inspectRepo(repoPath) {
|
|
5293
7238
|
let hasEntry = false;
|
|
5294
7239
|
try {
|
|
5295
|
-
await lstat(
|
|
7240
|
+
await lstat(join10(repoPath, ".basou"));
|
|
5296
7241
|
hasEntry = true;
|
|
5297
7242
|
} catch (error) {
|
|
5298
7243
|
if (errorCode(error) !== "ENOENT") {
|
|
@@ -5323,7 +7268,7 @@ async function checkPortfolioSafety(workspaces) {
|
|
|
5323
7268
|
const wsReal = await canonical(ws.repoRoot);
|
|
5324
7269
|
let sourceRoots = [];
|
|
5325
7270
|
try {
|
|
5326
|
-
const manifest = await
|
|
7271
|
+
const manifest = await readManifest10(ws.paths);
|
|
5327
7272
|
sourceRoots = manifest.import?.source_roots ?? [];
|
|
5328
7273
|
} catch (error) {
|
|
5329
7274
|
if (error instanceof Error && error.message === "YAML file not found") {
|
|
@@ -5341,7 +7286,7 @@ async function checkPortfolioSafety(workspaces) {
|
|
|
5341
7286
|
}
|
|
5342
7287
|
const monitored = /* @__PURE__ */ new Map();
|
|
5343
7288
|
for (const root of sourceRoots) {
|
|
5344
|
-
const display =
|
|
7289
|
+
const display = resolve6(ws.repoRoot, root);
|
|
5345
7290
|
const real = await canonical(display);
|
|
5346
7291
|
if (real !== wsReal) monitored.set(real, display);
|
|
5347
7292
|
}
|
|
@@ -5393,18 +7338,18 @@ function formatSafetyReport(result) {
|
|
|
5393
7338
|
|
|
5394
7339
|
// src/lib/view-server.ts
|
|
5395
7340
|
import { createServer } from "http";
|
|
5396
|
-
import { join as
|
|
7341
|
+
import { join as join11 } from "path";
|
|
5397
7342
|
import {
|
|
5398
7343
|
computeWorkStats as computeWorkStats2,
|
|
5399
7344
|
enumerateApprovals as enumerateApprovals2,
|
|
5400
|
-
findErrorCode as
|
|
7345
|
+
findErrorCode as findErrorCode16,
|
|
5401
7346
|
isLazyExpired as isLazyExpired2,
|
|
5402
7347
|
loadApproval as loadApproval2,
|
|
5403
7348
|
loadSessionEntries as loadSessionEntries3,
|
|
5404
7349
|
loadTaskEntries as loadTaskEntries2,
|
|
5405
7350
|
readAllEvents as readAllEvents2,
|
|
5406
|
-
readManifest as
|
|
5407
|
-
readMarkdownFile as
|
|
7351
|
+
readManifest as readManifest11,
|
|
7352
|
+
readMarkdownFile as readMarkdownFile5,
|
|
5408
7353
|
readSessionYaml as readSessionYaml3,
|
|
5409
7354
|
readTaskFile as readTaskFile2,
|
|
5410
7355
|
renderDecisions as renderDecisions3,
|
|
@@ -5591,7 +7536,9 @@ var VIEW_HTML = `<!doctype html>
|
|
|
5591
7536
|
if (!data) return 'ok';
|
|
5592
7537
|
if (data.claudeCode || data.codex) {
|
|
5593
7538
|
return 'claude-code ' + imp(data.claudeCode) + ', codex ' + imp(data.codex)
|
|
5594
|
-
+ (data.handoff && data.handoff.status === 'generated'
|
|
7539
|
+
+ (data.handoff && data.handoff.status === 'generated'
|
|
7540
|
+
? '; handoff regenerated, decisions: ' + (data.decisions ? data.decisions.decisionCount : 0)
|
|
7541
|
+
: '');
|
|
5595
7542
|
}
|
|
5596
7543
|
if (data.status === 'ran') return imp(data);
|
|
5597
7544
|
if (data.status === 'skipped') return 'skipped (' + data.reason + ')';
|
|
@@ -6048,7 +7995,7 @@ function startViewServer(opts) {
|
|
|
6048
7995
|
};
|
|
6049
7996
|
let boundPort = port;
|
|
6050
7997
|
const getPort = () => boundPort;
|
|
6051
|
-
return new Promise((
|
|
7998
|
+
return new Promise((resolve8, reject) => {
|
|
6052
7999
|
const server = createServer((req, res) => {
|
|
6053
8000
|
handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
|
|
6054
8001
|
sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
|
|
@@ -6059,7 +8006,7 @@ function startViewServer(opts) {
|
|
|
6059
8006
|
const address = server.address();
|
|
6060
8007
|
boundPort = isAddressInfo(address) ? address.port : port;
|
|
6061
8008
|
server.off("error", reject);
|
|
6062
|
-
|
|
8009
|
+
resolve8({
|
|
6063
8010
|
url: `http://${host}:${boundPort}`,
|
|
6064
8011
|
port: boundPort,
|
|
6065
8012
|
close: () => closeServer(server)
|
|
@@ -6071,8 +8018,8 @@ function isAddressInfo(value) {
|
|
|
6071
8018
|
return value !== null && typeof value === "object";
|
|
6072
8019
|
}
|
|
6073
8020
|
function closeServer(server) {
|
|
6074
|
-
return new Promise((
|
|
6075
|
-
server.close(() =>
|
|
8021
|
+
return new Promise((resolve8) => {
|
|
8022
|
+
server.close(() => resolve8());
|
|
6076
8023
|
server.closeAllConnections();
|
|
6077
8024
|
});
|
|
6078
8025
|
}
|
|
@@ -6289,9 +8236,9 @@ async function captureStaleness(ws, nowIso) {
|
|
|
6289
8236
|
async function overview(ws, nowProvider) {
|
|
6290
8237
|
let manifest;
|
|
6291
8238
|
try {
|
|
6292
|
-
manifest = await
|
|
8239
|
+
manifest = await readManifest11(ws.paths);
|
|
6293
8240
|
} catch (error) {
|
|
6294
|
-
if (
|
|
8241
|
+
if (findErrorCode16(error, "ENOENT")) {
|
|
6295
8242
|
return { initialized: false, repoRoot: ws.repoRoot };
|
|
6296
8243
|
}
|
|
6297
8244
|
throw error;
|
|
@@ -6346,7 +8293,7 @@ async function sessionDetail(ws, sessionId) {
|
|
|
6346
8293
|
throw error;
|
|
6347
8294
|
}
|
|
6348
8295
|
try {
|
|
6349
|
-
const events = await readAllEvents2(
|
|
8296
|
+
const events = await readAllEvents2(join11(ws.paths.sessions, sessionId));
|
|
6350
8297
|
return { session, events };
|
|
6351
8298
|
} catch {
|
|
6352
8299
|
return { session, events: [], degraded: true };
|
|
@@ -6368,7 +8315,7 @@ async function taskDetail(ws, taskId) {
|
|
|
6368
8315
|
}
|
|
6369
8316
|
}
|
|
6370
8317
|
async function decisionsView(ws, nowProvider) {
|
|
6371
|
-
const fromDisk = await
|
|
8318
|
+
const fromDisk = await readMarkdownFile5(ws.paths.files.decisions);
|
|
6372
8319
|
if (fromDisk !== null) {
|
|
6373
8320
|
return { body: fromDisk, fromDisk: true };
|
|
6374
8321
|
}
|
|
@@ -6391,7 +8338,7 @@ async function approvalsView(ws, nowProvider) {
|
|
|
6391
8338
|
return { pending: await toViews(ids.pending), resolved: await toViews(ids.resolved) };
|
|
6392
8339
|
}
|
|
6393
8340
|
async function handoffView(ws, nowProvider) {
|
|
6394
|
-
const fromDisk = await
|
|
8341
|
+
const fromDisk = await readMarkdownFile5(ws.paths.files.handoff);
|
|
6395
8342
|
if (fromDisk !== null) {
|
|
6396
8343
|
return { body: fromDisk, fromDisk: true };
|
|
6397
8344
|
}
|
|
@@ -6486,7 +8433,7 @@ var DEFAULT_PORT = 4319;
|
|
|
6486
8433
|
function parsePort(value) {
|
|
6487
8434
|
const port = Number.parseInt(value, 10);
|
|
6488
8435
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
6489
|
-
throw new
|
|
8436
|
+
throw new InvalidArgumentError7("Port must be an integer between 1 and 65535.");
|
|
6490
8437
|
}
|
|
6491
8438
|
return port;
|
|
6492
8439
|
}
|
|
@@ -6560,18 +8507,18 @@ async function doRunView(options, ctx) {
|
|
|
6560
8507
|
}
|
|
6561
8508
|
async function buildSingleDeps(ctx, cwd) {
|
|
6562
8509
|
const repositoryRoot = await resolveRepositoryRootForView(cwd);
|
|
6563
|
-
const paths =
|
|
6564
|
-
await
|
|
8510
|
+
const paths = basouPaths19(repositoryRoot);
|
|
8511
|
+
await assertWorkspaceInitialized14(paths.root);
|
|
6565
8512
|
const entry = await buildWorkspaceEntry(repositoryRoot, ctx);
|
|
6566
8513
|
return { workspaces: [entry], mode: "single", nowProvider: nowProviderOf(ctx) };
|
|
6567
8514
|
}
|
|
6568
8515
|
async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
|
|
6569
|
-
const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path:
|
|
8516
|
+
const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve7(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
|
|
6570
8517
|
const entries = [];
|
|
6571
8518
|
const seenPath = /* @__PURE__ */ new Set();
|
|
6572
8519
|
const seenKey = /* @__PURE__ */ new Set();
|
|
6573
8520
|
for (const spec of specs) {
|
|
6574
|
-
const repoRoot =
|
|
8521
|
+
const repoRoot = resolve7(spec.path);
|
|
6575
8522
|
if (seenPath.has(repoRoot)) continue;
|
|
6576
8523
|
seenPath.add(repoRoot);
|
|
6577
8524
|
const entry = await buildWorkspaceEntry(repoRoot, ctx, spec.label);
|
|
@@ -6584,14 +8531,14 @@ async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
|
|
|
6584
8531
|
return { workspaces: entries, mode: "portfolio", nowProvider: nowProviderOf(ctx) };
|
|
6585
8532
|
}
|
|
6586
8533
|
async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
|
|
6587
|
-
const paths =
|
|
8534
|
+
const paths = basouPaths19(repoRoot);
|
|
6588
8535
|
const importCtx = {
|
|
6589
8536
|
cwd: repoRoot,
|
|
6590
8537
|
...ctx.claudeProjectsDir !== void 0 ? { claudeProjectsDir: ctx.claudeProjectsDir } : {},
|
|
6591
8538
|
...ctx.codexSessionsDir !== void 0 ? { codexSessionsDir: ctx.codexSessionsDir } : {}
|
|
6592
8539
|
};
|
|
6593
8540
|
try {
|
|
6594
|
-
const manifest = await
|
|
8541
|
+
const manifest = await readManifest12(paths);
|
|
6595
8542
|
return {
|
|
6596
8543
|
key: manifest.workspace.id,
|
|
6597
8544
|
label: labelOverride ?? manifest.workspace.name,
|
|
@@ -6604,7 +8551,7 @@ async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
|
|
|
6604
8551
|
const notFound = error instanceof Error && error.message === "YAML file not found";
|
|
6605
8552
|
return {
|
|
6606
8553
|
key: `ws-${createHash("sha1").update(repoRoot).digest("hex").slice(0, 12)}`,
|
|
6607
|
-
label: labelOverride ??
|
|
8554
|
+
label: labelOverride ?? basename5(repoRoot),
|
|
6608
8555
|
paths,
|
|
6609
8556
|
repoRoot,
|
|
6610
8557
|
importCtx,
|
|
@@ -6620,7 +8567,7 @@ async function startListening(port, deps) {
|
|
|
6620
8567
|
try {
|
|
6621
8568
|
return await startViewServer({ port, deps });
|
|
6622
8569
|
} catch (error) {
|
|
6623
|
-
if (
|
|
8570
|
+
if (findErrorCode17(error, "EADDRINUSE")) {
|
|
6624
8571
|
throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
|
|
6625
8572
|
cause: error
|
|
6626
8573
|
});
|
|
@@ -6643,7 +8590,7 @@ function openInBrowser(url, override) {
|
|
|
6643
8590
|
}
|
|
6644
8591
|
}
|
|
6645
8592
|
function waitForShutdown(signal) {
|
|
6646
|
-
return new Promise((
|
|
8593
|
+
return new Promise((resolve8) => {
|
|
6647
8594
|
const cleanup = () => {
|
|
6648
8595
|
process.off("SIGINT", onSignal);
|
|
6649
8596
|
process.off("SIGTERM", onSignal);
|
|
@@ -6651,18 +8598,18 @@ function waitForShutdown(signal) {
|
|
|
6651
8598
|
};
|
|
6652
8599
|
const onSignal = () => {
|
|
6653
8600
|
cleanup();
|
|
6654
|
-
|
|
8601
|
+
resolve8();
|
|
6655
8602
|
};
|
|
6656
8603
|
const onAbort = () => {
|
|
6657
8604
|
cleanup();
|
|
6658
|
-
|
|
8605
|
+
resolve8();
|
|
6659
8606
|
};
|
|
6660
8607
|
process.on("SIGINT", onSignal);
|
|
6661
8608
|
process.on("SIGTERM", onSignal);
|
|
6662
8609
|
if (signal !== void 0) {
|
|
6663
8610
|
if (signal.aborted) {
|
|
6664
8611
|
cleanup();
|
|
6665
|
-
|
|
8612
|
+
resolve8();
|
|
6666
8613
|
return;
|
|
6667
8614
|
}
|
|
6668
8615
|
signal.addEventListener("abort", onAbort);
|
|
@@ -6671,7 +8618,7 @@ function waitForShutdown(signal) {
|
|
|
6671
8618
|
}
|
|
6672
8619
|
async function resolveRepositoryRootForView(cwd) {
|
|
6673
8620
|
try {
|
|
6674
|
-
return await
|
|
8621
|
+
return await resolveRepositoryRoot14(cwd);
|
|
6675
8622
|
} catch (error) {
|
|
6676
8623
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
6677
8624
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {
|
|
@@ -6681,11 +8628,11 @@ async function resolveRepositoryRootForView(cwd) {
|
|
|
6681
8628
|
throw error;
|
|
6682
8629
|
}
|
|
6683
8630
|
}
|
|
6684
|
-
async function
|
|
8631
|
+
async function assertWorkspaceInitialized14(basouRoot) {
|
|
6685
8632
|
try {
|
|
6686
|
-
await
|
|
8633
|
+
await assertBasouRootSafe17(basouRoot);
|
|
6687
8634
|
} catch (error) {
|
|
6688
|
-
if (
|
|
8635
|
+
if (findErrorCode17(error, "ENOENT")) {
|
|
6689
8636
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
6690
8637
|
}
|
|
6691
8638
|
throw error;
|
|
@@ -6711,11 +8658,14 @@ function buildProgram() {
|
|
|
6711
8658
|
registerViewCommand(program);
|
|
6712
8659
|
registerApprovalCommand(program);
|
|
6713
8660
|
registerDecisionCommand(program);
|
|
8661
|
+
registerNoteCommand(program);
|
|
6714
8662
|
registerTaskCommand(program);
|
|
6715
8663
|
registerHandoffCommand(program);
|
|
6716
8664
|
registerDecisionsCommand(program);
|
|
6717
8665
|
registerReportCommand(program);
|
|
6718
8666
|
registerOrientCommand(program);
|
|
8667
|
+
registerReviewGapsCommand(program);
|
|
8668
|
+
registerProjectCommand(program);
|
|
6719
8669
|
return program;
|
|
6720
8670
|
}
|
|
6721
8671
|
export {
|