ghostrail 0.15.2 → 0.17.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/README.md +12 -9
- package/dist/agent/claude-code.d.ts.map +1 -1
- package/dist/agent/claude-code.js +4 -1
- package/dist/agent/claude-code.js.map +1 -1
- package/dist/board/page.d.ts +1 -1
- package/dist/board/page.d.ts.map +1 -1
- package/dist/board/page.js +2 -1
- package/dist/board/page.js.map +1 -1
- package/dist/board/server.d.ts +1 -1
- package/dist/board/server.d.ts.map +1 -1
- package/dist/board/server.js +7 -1
- package/dist/board/server.js.map +1 -1
- package/dist/board/stats.d.ts +1 -1
- package/dist/board/stats.d.ts.map +1 -1
- package/dist/board/stats.js +2 -1
- package/dist/board/stats.js.map +1 -1
- package/dist/claude-profile/index.d.ts +1 -1
- package/dist/claude-profile/index.d.ts.map +1 -1
- package/dist/claude-profile/index.js +1 -1
- package/dist/claude-profile/index.js.map +1 -1
- package/dist/claude-profile/setup.d.ts +7 -0
- package/dist/claude-profile/setup.d.ts.map +1 -1
- package/dist/claude-profile/setup.js +7 -0
- package/dist/claude-profile/setup.js.map +1 -1
- package/dist/cli/commands.d.ts +6 -0
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +204 -18
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/help.d.ts.map +1 -1
- package/dist/commands/help.js +15 -3
- package/dist/commands/help.js.map +1 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +18 -2
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +8 -2
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/dashboard/page.d.ts +7 -1
- package/dist/dashboard/page.d.ts.map +1 -1
- package/dist/dashboard/page.js +204 -39
- package/dist/dashboard/page.js.map +1 -1
- package/dist/dashboard/routes.d.ts +32 -0
- package/dist/dashboard/routes.d.ts.map +1 -1
- package/dist/dashboard/routes.js +45 -0
- package/dist/dashboard/routes.js.map +1 -1
- package/dist/dashboard/server.d.ts +35 -9
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +75 -11
- package/dist/dashboard/server.js.map +1 -1
- package/dist/doctor/checks.d.ts +87 -0
- package/dist/doctor/checks.d.ts.map +1 -0
- package/dist/doctor/checks.js +414 -0
- package/dist/doctor/checks.js.map +1 -0
- package/dist/doctor/index.d.ts +6 -0
- package/dist/doctor/index.d.ts.map +1 -0
- package/dist/doctor/index.js +5 -0
- package/dist/doctor/index.js.map +1 -0
- package/dist/doctor/probes.d.ts +42 -0
- package/dist/doctor/probes.d.ts.map +1 -0
- package/dist/doctor/probes.js +136 -0
- package/dist/doctor/probes.js.map +1 -0
- package/dist/doctor/report.d.ts +13 -0
- package/dist/doctor/report.d.ts.map +1 -0
- package/dist/doctor/report.js +32 -0
- package/dist/doctor/report.js.map +1 -0
- package/dist/doctor/run.d.ts +48 -0
- package/dist/doctor/run.d.ts.map +1 -0
- package/dist/doctor/run.js +109 -0
- package/dist/doctor/run.js.map +1 -0
- package/dist/doctor/types.d.ts +17 -0
- package/dist/doctor/types.d.ts.map +1 -0
- package/dist/doctor/types.js +6 -0
- package/dist/doctor/types.js.map +1 -0
- package/dist/factory/build.d.ts.map +1 -1
- package/dist/factory/build.js +1 -0
- package/dist/factory/build.js.map +1 -1
- package/dist/init/regions.d.ts +76 -0
- package/dist/init/regions.d.ts.map +1 -0
- package/dist/init/regions.js +247 -0
- package/dist/init/regions.js.map +1 -0
- package/dist/init/template.d.ts +7 -1
- package/dist/init/template.d.ts.map +1 -1
- package/dist/init/template.js +37 -2
- package/dist/init/template.js.map +1 -1
- package/dist/loop/budget.d.ts +11 -2
- package/dist/loop/budget.d.ts.map +1 -1
- package/dist/loop/budget.js +13 -3
- package/dist/loop/budget.js.map +1 -1
- package/dist/loop/comments.d.ts +20 -0
- package/dist/loop/comments.d.ts.map +1 -0
- package/dist/loop/comments.js +35 -0
- package/dist/loop/comments.js.map +1 -0
- package/dist/loop/engine.d.ts.map +1 -1
- package/dist/loop/engine.js +18 -0
- package/dist/loop/engine.js.map +1 -1
- package/dist/loop/index.d.ts +1 -0
- package/dist/loop/index.d.ts.map +1 -1
- package/dist/loop/index.js +1 -0
- package/dist/loop/index.js.map +1 -1
- package/dist/publish/gh.d.ts +8 -0
- package/dist/publish/gh.d.ts.map +1 -1
- package/dist/publish/gh.js +12 -0
- package/dist/publish/gh.js.map +1 -1
- package/dist/publish/githost.d.ts +8 -0
- package/dist/publish/githost.d.ts.map +1 -1
- package/dist/publish/githost.js.map +1 -1
- package/dist/publish/index.d.ts +1 -1
- package/dist/publish/index.d.ts.map +1 -1
- package/dist/publish/index.js +1 -1
- package/dist/publish/index.js.map +1 -1
- package/dist/respond/index.d.ts +3 -2
- package/dist/respond/index.d.ts.map +1 -1
- package/dist/respond/index.js +3 -2
- package/dist/respond/index.js.map +1 -1
- package/dist/respond/merge.d.ts +13 -0
- package/dist/respond/merge.d.ts.map +1 -0
- package/dist/respond/merge.js +181 -0
- package/dist/respond/merge.js.map +1 -0
- package/dist/respond/respond.d.ts +25 -5
- package/dist/respond/respond.d.ts.map +1 -1
- package/dist/respond/respond.js +57 -28
- package/dist/respond/respond.js.map +1 -1
- package/dist/respond/select.d.ts +15 -0
- package/dist/respond/select.d.ts.map +1 -1
- package/dist/respond/select.js +17 -0
- package/dist/respond/select.js.map +1 -1
- package/dist/source/dependencies.d.ts +81 -0
- package/dist/source/dependencies.d.ts.map +1 -0
- package/dist/source/dependencies.js +218 -0
- package/dist/source/dependencies.js.map +1 -0
- package/dist/source/index.d.ts +1 -0
- package/dist/source/index.d.ts.map +1 -1
- package/dist/source/index.js +1 -0
- package/dist/source/index.js.map +1 -1
- package/dist/source/linear.d.ts +17 -1
- package/dist/source/linear.d.ts.map +1 -1
- package/dist/source/linear.js +75 -9
- package/dist/source/linear.js.map +1 -1
- package/dist/store/events.d.ts +8 -0
- package/dist/store/events.d.ts.map +1 -1
- package/dist/store/events.js +10 -0
- package/dist/store/events.js.map +1 -1
- package/dist/store/file-store.d.ts +1 -1
- package/dist/store/file-store.d.ts.map +1 -1
- package/dist/store/file-store.js +7 -0
- package/dist/store/file-store.js.map +1 -1
- package/dist/store/index.d.ts +1 -1
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -1
- package/dist/store/index.js.map +1 -1
- package/dist/store/port.d.ts +15 -0
- package/dist/store/port.d.ts.map +1 -1
- package/dist/store/port.js +17 -1
- package/dist/store/port.js.map +1 -1
- package/dist/triage/engine.d.ts.map +1 -1
- package/dist/triage/engine.js +2 -0
- package/dist/triage/engine.js.map +1 -1
- package/package.json +3 -1
- package/skill/ghostrail/SKILL.md +94 -43
- package/templates/code/prompts/resolve-issue.md +20 -4
- package/templates/code/prompts/respond.md +4 -0
- package/templates/code/prompts/triage.md +13 -0
- package/templates/content/prompts/draft.md +13 -0
- package/templates/content/prompts/respond.md +4 -0
package/dist/cli/commands.js
CHANGED
|
@@ -7,10 +7,11 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { spawnCollect } from "../backend/proc.js";
|
|
8
8
|
import { DEFAULT_BOARD_PORT, PORT_FALLBACK_ATTEMPTS, isAddressInUse, isValidPort, portCandidates, } from "../board/port.js";
|
|
9
9
|
import { createBoardServer, isLoopback, normalizeToken } from "../board/server.js";
|
|
10
|
-
import { buildProposal, writeProfile } from "../claude-profile/index.js";
|
|
10
|
+
import { DEFAULT_CLAUDE_PINS, buildProposal, writeProfile } from "../claude-profile/index.js";
|
|
11
11
|
import { parseDuration } from "../config/duration.js";
|
|
12
12
|
import { parseConfig, parseMergedConfig } from "../config/load.js";
|
|
13
13
|
import { createDashboardServer, defaultStateDir } from "../dashboard/index.js";
|
|
14
|
+
import { hasFailure, renderDoctorJson, renderDoctorText, runDoctor, } from "../doctor/index.js";
|
|
14
15
|
import { buildLoopDeps, buildRespondDeps, buildTriageDeps, repoGhRunner, resolveRepoSlug, } from "../factory/build.js";
|
|
15
16
|
import { describeMigration, migrateLegacyStore, resolveWorkRepo, stateDirFor, } from "../factory/state.js";
|
|
16
17
|
import { entryKey, findByPath, findForRepoResolving, replaceEntry, upsert, } from "../global/registry.js";
|
|
@@ -18,6 +19,7 @@ import { canonicalRepoId, localRepoId } from "../global/repo-id.js";
|
|
|
18
19
|
import { GlobalStore, resolveGlobalDir } from "../global/store.js";
|
|
19
20
|
import { DEFAULT_FACTORY_IMAGE, dockerBuildArgs, dockerDir, parseImageFlags, } from "../image/build.js";
|
|
20
21
|
import { extractPlaceholders, isClean, missingSignals, repoPathFor, } from "../init/drift.js";
|
|
22
|
+
import { mergeRegions, parseRegions, regionHashes } from "../init/regions.js";
|
|
21
23
|
import { classify, hashContent, mergeManifest, readManifest, writeManifest, } from "../init/template.js";
|
|
22
24
|
import { FileLock } from "../lock/file-lock.js";
|
|
23
25
|
import { concurrencyAdvice } from "../loop/advice.js";
|
|
@@ -430,10 +432,49 @@ export async function cmdBoard(argv, streams, env = process.env, cwd = process.c
|
|
|
430
432
|
}
|
|
431
433
|
/** The cross-ghostrail dashboard's default port, one above the board's. */
|
|
432
434
|
export const DEFAULT_DASHBOARD_PORT = 5051;
|
|
435
|
+
/**
|
|
436
|
+
* The `owner/repo` slug for a registered ghostrail, for `gh --repo`.
|
|
437
|
+
*
|
|
438
|
+
* The registry's `repo` is the ghostrail's identity and is already canonical
|
|
439
|
+
* (`host/owner/name`), so it answers this without running anything. Two entries
|
|
440
|
+
* cannot: `local:<path>` has no remote by definition, and an entry written
|
|
441
|
+
* before repo identities existed has no `repo` at all. Both fall back to the
|
|
442
|
+
* checkout's origin, which is what `board` does with its one repo.
|
|
443
|
+
*
|
|
444
|
+
* Undefined is not a failure. It leaves `gh` to infer the repo from the
|
|
445
|
+
* checkout, exactly as an unscoped invocation always has.
|
|
446
|
+
*/
|
|
447
|
+
async function dashboardRepoSlug(entry) {
|
|
448
|
+
const repo = entry.repo;
|
|
449
|
+
if (repo !== undefined && !repo.startsWith("local:")) {
|
|
450
|
+
const parts = repo.split("/");
|
|
451
|
+
if (parts.length === 3)
|
|
452
|
+
return `${parts[1]}/${parts[2]}`;
|
|
453
|
+
}
|
|
454
|
+
return resolveBoardRepoSlug(entry.path);
|
|
455
|
+
}
|
|
433
456
|
/** Build the dashboard server's dependencies from the real environment. */
|
|
434
457
|
function dashboardDeps(env, manager) {
|
|
435
458
|
const store = globalStoreFor(env);
|
|
459
|
+
// A ghostrail's slug does not change while the dashboard runs, and resolving
|
|
460
|
+
// it can cost a `git` call. The page refetches PR status for every expanded
|
|
461
|
+
// row every 10s, so without this that cost recurs for as long as a row is
|
|
462
|
+
// open. Keyed on the identity, so two clones of one repo share the answer.
|
|
463
|
+
const slugs = new Map();
|
|
464
|
+
const slugFor = (entry) => {
|
|
465
|
+
const key = entryKey(entry);
|
|
466
|
+
const known = slugs.get(key);
|
|
467
|
+
if (known !== undefined)
|
|
468
|
+
return known;
|
|
469
|
+
const resolving = dashboardRepoSlug(entry);
|
|
470
|
+
slugs.set(key, resolving);
|
|
471
|
+
return resolving;
|
|
472
|
+
};
|
|
436
473
|
return {
|
|
474
|
+
// Constructing this runs nothing; only a request that needs it spends a
|
|
475
|
+
// subprocess. A machine with no `gh` and no credentials still serves the
|
|
476
|
+
// whole dashboard, with PR status reported as unavailable.
|
|
477
|
+
prStatus: async (entry, prNumber) => new GhHost(repoGhRunner(entry.path), await slugFor(entry)).getPrStatus(prNumber),
|
|
437
478
|
readRegistry: () => store.readRegistry(),
|
|
438
479
|
writeRegistry: (entries) => store.writeRegistry(entries),
|
|
439
480
|
health: ghostrailHealth,
|
|
@@ -1130,11 +1171,19 @@ async function promptPathsAt(configPath) {
|
|
|
1130
1171
|
* `ghostrail init <template> --update`: take the template changes this repo
|
|
1131
1172
|
* never edited, and name what a customized file is missing.
|
|
1132
1173
|
*
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1135
|
-
* A file
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
1174
|
+
* Three jobs, ordered by how much can be proved.
|
|
1175
|
+
*
|
|
1176
|
+
* A file still matching its recorded baseline was never touched, so a changed
|
|
1177
|
+
* template can overwrite it whole.
|
|
1178
|
+
*
|
|
1179
|
+
* A file the repo edited is never overwritten, but the managed regions inside
|
|
1180
|
+
* it can be rewritten in place (TJ-1539): ghostrail owns those outright, so
|
|
1181
|
+
* their current text is irrelevant to producing the new text. A region the repo
|
|
1182
|
+
* edited is left alone and reported, never silently discarded.
|
|
1183
|
+
*
|
|
1184
|
+
* Whatever is left is named, not written: the placeholders and result fields
|
|
1185
|
+
* the template gained, which is what every hand port consisted of before
|
|
1186
|
+
* regions existed, and the skill places them in the repo's own wording.
|
|
1138
1187
|
*/
|
|
1139
1188
|
async function applyTemplateUpdate(template, srcDir, repo, streams, env) {
|
|
1140
1189
|
const manifest = await readManifest(repo);
|
|
@@ -1145,8 +1194,11 @@ async function applyTemplateUpdate(template, srcDir, repo, streams, env) {
|
|
|
1145
1194
|
// compared against: a repo can be current with the release and behind main.
|
|
1146
1195
|
streams.out(`comparing against ${template} @ ${VERSION}\n\n`);
|
|
1147
1196
|
const written = {};
|
|
1197
|
+
const writtenRegions = {};
|
|
1148
1198
|
let pending = 0;
|
|
1149
1199
|
let took = 0;
|
|
1200
|
+
let mergedRegions = 0;
|
|
1201
|
+
let mergedFiles = 0;
|
|
1150
1202
|
for (const rel of rels) {
|
|
1151
1203
|
const key = manifestKey(rel);
|
|
1152
1204
|
const repoRel = repoPathFor(key, templatePrompts, repoPrompts);
|
|
@@ -1180,29 +1232,91 @@ async function applyTemplateUpdate(template, srcDir, repo, streams, env) {
|
|
|
1180
1232
|
streams.out(` ${repoRel}\n took the template's version (you had not edited it)\n`);
|
|
1181
1233
|
continue;
|
|
1182
1234
|
}
|
|
1183
|
-
// Customized, or no baseline to prove otherwise.
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1235
|
+
// Customized, or no baseline to prove otherwise. The file as a whole is
|
|
1236
|
+
// never written, but the regions ghostrail owns inside it can be (TJ-1539).
|
|
1237
|
+
let text = current ?? "";
|
|
1238
|
+
const lines = [];
|
|
1239
|
+
const recordedRegions = manifest?.regions?.[key];
|
|
1240
|
+
const merged = mergeRegions(text, templateContent, recordedRegions);
|
|
1241
|
+
if ("error" in merged) {
|
|
1242
|
+
// Malformed markers on either side. Write nothing and fall back to the
|
|
1243
|
+
// signal report, which is always available and always safe.
|
|
1244
|
+
lines.push(` markers unusable: ${merged.error}`);
|
|
1245
|
+
}
|
|
1246
|
+
else if (recordedRegions === undefined && merged.conflicted.length > 0) {
|
|
1247
|
+
// Every region is unprovable for the same one reason, so say it once
|
|
1248
|
+
// rather than per region. Report progress, not just the problem: a
|
|
1249
|
+
// region already matching the template IS recorded by this run, so the
|
|
1250
|
+
// count climbs as regions are ported and the advice cannot loop.
|
|
1251
|
+
//
|
|
1252
|
+
// Pointing at a bare `init` here would loop forever: it keeps a
|
|
1253
|
+
// customized file rather than overwriting it, so it never records a
|
|
1254
|
+
// baseline for the one file that needs one.
|
|
1255
|
+
const adopted = Object.keys(merged.hashes).length;
|
|
1256
|
+
const total = adopted + merged.conflicted.length;
|
|
1257
|
+
lines.push(` adopting regions: ${adopted}/${total} match the template and are recorded`);
|
|
1258
|
+
lines.push(" bring the rest to the template's text and re-run; each one that matches");
|
|
1259
|
+
lines.push(" is recorded, and merges on its own from then on");
|
|
1260
|
+
pending++;
|
|
1261
|
+
if (adopted > 0)
|
|
1262
|
+
writtenRegions[key] = merged.hashes;
|
|
1263
|
+
}
|
|
1264
|
+
else {
|
|
1265
|
+
for (const id of merged.updated)
|
|
1266
|
+
lines.push(` updated ${id}`);
|
|
1267
|
+
for (const id of merged.inserted)
|
|
1268
|
+
lines.push(` inserted ${id}`);
|
|
1269
|
+
for (const id of merged.conflicted) {
|
|
1270
|
+
lines.push(` edited locally ${id} (left alone; port it by hand)`);
|
|
1271
|
+
pending++;
|
|
1272
|
+
}
|
|
1273
|
+
for (const id of merged.unplaceable) {
|
|
1274
|
+
lines.push(` unplaceable ${id} (no shared region to anchor to)`);
|
|
1275
|
+
pending++;
|
|
1276
|
+
}
|
|
1277
|
+
if (merged.updated.length + merged.inserted.length > 0) {
|
|
1278
|
+
text = merged.text;
|
|
1279
|
+
await writeFile(join(repo, repoRel), text, "utf8");
|
|
1280
|
+
mergedRegions += merged.updated.length + merged.inserted.length;
|
|
1281
|
+
mergedFiles++;
|
|
1282
|
+
// Region baselines only. Recording the merged file's whole-file hash
|
|
1283
|
+
// would make `classify` call it `upstream-changed` on the next run,
|
|
1284
|
+
// and that branch overwrites the file, destroying the customization
|
|
1285
|
+
// this path exists to protect.
|
|
1286
|
+
writtenRegions[key] = merged.hashes;
|
|
1287
|
+
}
|
|
1288
|
+
else if (Object.keys(merged.hashes).length > 0) {
|
|
1289
|
+
writtenRegions[key] = merged.hashes;
|
|
1290
|
+
}
|
|
1188
1291
|
}
|
|
1189
|
-
|
|
1292
|
+
// Whatever the regions did or could not do, the editable remainder can
|
|
1293
|
+
// still be missing a signal. Read it off the merged text so a placeholder
|
|
1294
|
+
// just delivered by a region is not reported as still missing.
|
|
1295
|
+
const signals = missingSignals(text, templateContent);
|
|
1190
1296
|
for (const name of signals.placeholders) {
|
|
1191
|
-
|
|
1297
|
+
lines.push(` placeholder {{${name}}}`);
|
|
1192
1298
|
pending++;
|
|
1193
1299
|
}
|
|
1194
1300
|
for (const name of signals.fields) {
|
|
1195
|
-
|
|
1301
|
+
lines.push(` result field "${name}"`);
|
|
1196
1302
|
pending++;
|
|
1197
1303
|
}
|
|
1304
|
+
if (lines.length === 0) {
|
|
1305
|
+
streams.out(` ${repoRel}\n customized, nothing to take\n`);
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
streams.out(` ${repoRel}\n customized:\n${lines.join("\n")}\n`);
|
|
1198
1309
|
}
|
|
1199
|
-
if (Object.keys(written).length > 0) {
|
|
1200
|
-
await writeManifest(repo, mergeManifest(manifest, template, VERSION, written));
|
|
1310
|
+
if (Object.keys(written).length > 0 || Object.keys(writtenRegions).length > 0) {
|
|
1311
|
+
await writeManifest(repo, mergeManifest(manifest, template, VERSION, written, writtenRegions));
|
|
1201
1312
|
}
|
|
1202
1313
|
await registerGhostrail(repo, env, streams);
|
|
1203
1314
|
streams.out("\n");
|
|
1204
1315
|
if (took > 0)
|
|
1205
1316
|
streams.out(`took ${took} file(s) you had not edited\n`);
|
|
1317
|
+
if (mergedRegions > 0) {
|
|
1318
|
+
streams.out(`merged ${mergedRegions} managed region(s) into ${mergedFiles} customized file(s)\n`);
|
|
1319
|
+
}
|
|
1206
1320
|
if (pending === 0) {
|
|
1207
1321
|
streams.out("nothing needs placing by hand\n");
|
|
1208
1322
|
return 0;
|
|
@@ -1284,6 +1398,20 @@ export async function cmdInit(argv, streams, cwd = process.cwd(), env = process.
|
|
|
1284
1398
|
// current content would later read as "never edited", which is worse than
|
|
1285
1399
|
// admitting the baseline is unknown.
|
|
1286
1400
|
const baselines = {};
|
|
1401
|
+
const regionBaselines = {};
|
|
1402
|
+
/**
|
|
1403
|
+
* Record the per-region baselines for a file whose content provably IS the
|
|
1404
|
+
* template's. Without this a repo would customize its editable text, hit the
|
|
1405
|
+
* region path on the next `--update`, find nothing recorded, and be told
|
|
1406
|
+
* every untouched region is a local edit. The merge would then almost never
|
|
1407
|
+
* fire, which is the entire feature.
|
|
1408
|
+
*/
|
|
1409
|
+
const recordRegions = (key, content) => {
|
|
1410
|
+
const parsed = parseRegions(content);
|
|
1411
|
+
if (!parsed.ok || parsed.regions.length === 0)
|
|
1412
|
+
return;
|
|
1413
|
+
regionBaselines[key] = regionHashes(parsed.regions);
|
|
1414
|
+
};
|
|
1287
1415
|
try {
|
|
1288
1416
|
const existing = await readManifest(repo);
|
|
1289
1417
|
for (const rel of await scaffoldFiles(srcDir)) {
|
|
@@ -1301,6 +1429,7 @@ export async function cmdInit(argv, streams, cwd = process.cwd(), env = process.
|
|
|
1301
1429
|
const current = await readIfPresent(to);
|
|
1302
1430
|
if (current !== undefined && hashContent(current) === templateHash) {
|
|
1303
1431
|
baselines[key] = templateHash;
|
|
1432
|
+
recordRegions(key, content);
|
|
1304
1433
|
}
|
|
1305
1434
|
}
|
|
1306
1435
|
continue;
|
|
@@ -1309,8 +1438,9 @@ export async function cmdInit(argv, streams, cwd = process.cwd(), env = process.
|
|
|
1309
1438
|
await writeFile(to, content, "utf8");
|
|
1310
1439
|
written.push(rel);
|
|
1311
1440
|
baselines[key] = templateHash;
|
|
1441
|
+
recordRegions(key, content);
|
|
1312
1442
|
}
|
|
1313
|
-
await writeManifest(repo, mergeManifest(existing, template, VERSION, baselines));
|
|
1443
|
+
await writeManifest(repo, mergeManifest(existing, template, VERSION, baselines, regionBaselines));
|
|
1314
1444
|
}
|
|
1315
1445
|
catch (error) {
|
|
1316
1446
|
streams.err(`init failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
@@ -1675,7 +1805,7 @@ export async function cmdClaudeSetup(argv, streams, env = process.env) {
|
|
|
1675
1805
|
home: env.HOME ?? homedir(),
|
|
1676
1806
|
profile: join(globalDir, "claude"),
|
|
1677
1807
|
write: false,
|
|
1678
|
-
pins: [
|
|
1808
|
+
pins: [...DEFAULT_CLAUDE_PINS],
|
|
1679
1809
|
});
|
|
1680
1810
|
const proposal = await buildProposal(flags.home, flags.profile, flags.pins);
|
|
1681
1811
|
streams.out("Claude profile setup\n");
|
|
@@ -1820,4 +1950,60 @@ async function openSecretsInEditor(path, flags, streams, env, deps) {
|
|
|
1820
1950
|
streams.err(`editor exited ${code}; the file is written. ${fill}`);
|
|
1821
1951
|
return 0;
|
|
1822
1952
|
}
|
|
1953
|
+
/** Parse `doctor` flags: the shared `-c/-C` pair, plus `--json` and `--quick`. */
|
|
1954
|
+
function parseDoctorFlags(argv, defaults) {
|
|
1955
|
+
const base = parseCommandFlags(argv, defaults);
|
|
1956
|
+
return {
|
|
1957
|
+
...base,
|
|
1958
|
+
json: defaults.json || argv.includes("--json"),
|
|
1959
|
+
quick: defaults.quick || argv.includes("--quick"),
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Load `ghostrail.toml` merged with the global settings as a {@link Result},
|
|
1964
|
+
* rather than printing and returning undefined like {@link loadConfig} — unlike
|
|
1965
|
+
* every other command, `doctor` has useful things to say even when config is
|
|
1966
|
+
* broken (credentials, the Claude profile, run history), so it never aborts.
|
|
1967
|
+
*/
|
|
1968
|
+
async function loadDoctorConfig(configPath, env) {
|
|
1969
|
+
let repoToml;
|
|
1970
|
+
try {
|
|
1971
|
+
repoToml = await readFile(configPath, "utf8");
|
|
1972
|
+
}
|
|
1973
|
+
catch {
|
|
1974
|
+
return { ok: false, errors: [{ path: "", message: `cannot read config file: ${configPath}` }] };
|
|
1975
|
+
}
|
|
1976
|
+
const globalToml = await globalStoreFor(env).readSettings();
|
|
1977
|
+
return parseMergedConfig(repoToml, globalToml);
|
|
1978
|
+
}
|
|
1979
|
+
/**
|
|
1980
|
+
* `ghostrail doctor`: probe a setup rather than merely inspect it, and name the
|
|
1981
|
+
* fix for whatever is broken. See specs/doctor-command.spec.md.
|
|
1982
|
+
*/
|
|
1983
|
+
export async function cmdDoctor(argv, streams, env = process.env, deps = {}) {
|
|
1984
|
+
const flags = parseDoctorFlags(argv, {
|
|
1985
|
+
config: "ghostrail.toml",
|
|
1986
|
+
repo: ".",
|
|
1987
|
+
json: false,
|
|
1988
|
+
quick: false,
|
|
1989
|
+
});
|
|
1990
|
+
const secrets = await applySecrets(flags, env);
|
|
1991
|
+
const configPath = isAbsolute(flags.config) ? flags.config : join(flags.repo, flags.config);
|
|
1992
|
+
const config = await loadDoctorConfig(configPath, env);
|
|
1993
|
+
const claudeProfileDir = await resolveClaudeProfile(env);
|
|
1994
|
+
const state = await stateDirFor(flags.repo, config.ok ? config.value : undefined, env);
|
|
1995
|
+
const historyRows = await new FileStore(state.dir).listRuns();
|
|
1996
|
+
const report = await runDoctor({
|
|
1997
|
+
repoPath: flags.repo,
|
|
1998
|
+
config,
|
|
1999
|
+
env,
|
|
2000
|
+
quick: flags.quick,
|
|
2001
|
+
claudeProfileDir,
|
|
2002
|
+
claudeSourceHome: env.HOME ?? homedir(),
|
|
2003
|
+
historyRows,
|
|
2004
|
+
}, deps);
|
|
2005
|
+
const rendered = flags.json ? renderDoctorJson(report) : renderDoctorText(report);
|
|
2006
|
+
streams.out(redactSecrets(rendered, secrets));
|
|
2007
|
+
return hasFailure(report) ? 1 : 0;
|
|
2008
|
+
}
|
|
1823
2009
|
//# sourceMappingURL=commands.js.map
|