@yoavsh/treekeeper 0.0.0-stage → 0.1.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/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +237 -2
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +165 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/archive.d.ts +3 -0
- package/dist/commands/archive.d.ts.map +1 -0
- package/dist/commands/archive.js +55 -0
- package/dist/commands/archive.js.map +1 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +54 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/context.d.ts +34 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +53 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/daemon.d.ts +3 -0
- package/dist/commands/daemon.d.ts.map +1 -0
- package/dist/commands/daemon.js +81 -0
- package/dist/commands/daemon.js.map +1 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +119 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/gc.d.ts +3 -0
- package/dist/commands/gc.d.ts.map +1 -0
- package/dist/commands/gc.js +98 -0
- package/dist/commands/gc.js.map +1 -0
- package/dist/commands/status.d.ts +4 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +52 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/watch.d.ts +10 -0
- package/dist/commands/watch.d.ts.map +1 -0
- package/dist/commands/watch.js +41 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/core/apply.d.ts +47 -0
- package/dist/core/apply.d.ts.map +1 -0
- package/dist/core/apply.js +223 -0
- package/dist/core/apply.js.map +1 -0
- package/dist/core/archive.d.ts +69 -0
- package/dist/core/archive.d.ts.map +1 -0
- package/dist/core/archive.js +202 -0
- package/dist/core/archive.js.map +1 -0
- package/dist/core/config.d.ts +69 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +261 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/decide.d.ts +102 -0
- package/dist/core/decide.d.ts.map +1 -0
- package/dist/core/decide.js +148 -0
- package/dist/core/decide.js.map +1 -0
- package/dist/core/forge.d.ts +39 -0
- package/dist/core/forge.d.ts.map +1 -0
- package/dist/core/forge.js +190 -0
- package/dist/core/forge.js.map +1 -0
- package/dist/core/git.d.ts +63 -0
- package/dist/core/git.d.ts.map +1 -0
- package/dist/core/git.js +194 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/plan.d.ts +75 -0
- package/dist/core/plan.d.ts.map +1 -0
- package/dist/core/plan.js +288 -0
- package/dist/core/plan.js.map +1 -0
- package/dist/core/render.d.ts +14 -0
- package/dist/core/render.d.ts.map +1 -0
- package/dist/core/render.js +128 -0
- package/dist/core/render.js.map +1 -0
- package/dist/core/scan.d.ts +22 -0
- package/dist/core/scan.d.ts.map +1 -0
- package/dist/core/scan.js +99 -0
- package/dist/core/scan.js.map +1 -0
- package/dist/core/sessions.d.ts +37 -0
- package/dist/core/sessions.d.ts.map +1 -0
- package/dist/core/sessions.js +300 -0
- package/dist/core/sessions.js.map +1 -0
- package/dist/core/size.d.ts +3 -0
- package/dist/core/size.d.ts.map +1 -0
- package/dist/core/size.js +11 -0
- package/dist/core/size.js.map +1 -0
- package/dist/daemon/lock.d.ts +10 -0
- package/dist/daemon/lock.d.ts.map +1 -0
- package/dist/daemon/lock.js +64 -0
- package/dist/daemon/lock.js.map +1 -0
- package/dist/daemon/service.d.ts +34 -0
- package/dist/daemon/service.d.ts.map +1 -0
- package/dist/daemon/service.js +257 -0
- package/dist/daemon/service.js.map +1 -0
- package/dist/daemon/state.d.ts +19 -0
- package/dist/daemon/state.d.ts.map +1 -0
- package/dist/daemon/state.js +32 -0
- package/dist/daemon/state.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/util/bytes.d.ts +4 -0
- package/dist/util/bytes.d.ts.map +1 -0
- package/dist/util/bytes.js +14 -0
- package/dist/util/bytes.js.map +1 -0
- package/dist/util/color.d.ts +16 -0
- package/dist/util/color.d.ts.map +1 -0
- package/dist/util/color.js +32 -0
- package/dist/util/color.js.map +1 -0
- package/dist/util/exec.d.ts +27 -0
- package/dist/util/exec.d.ts.map +1 -0
- package/dist/util/exec.js +57 -0
- package/dist/util/exec.js.map +1 -0
- package/dist/util/log.d.ts +29 -0
- package/dist/util/log.d.ts.map +1 -0
- package/dist/util/log.js +72 -0
- package/dist/util/log.js.map +1 -0
- package/dist/util/paths.d.ts +18 -0
- package/dist/util/paths.d.ts.map +1 -0
- package/dist/util/paths.js +87 -0
- package/dist/util/paths.js.map +1 -0
- package/dist/util/prompt.d.ts +3 -0
- package/dist/util/prompt.d.ts.map +1 -0
- package/dist/util/prompt.js +15 -0
- package/dist/util/prompt.js.map +1 -0
- package/dist/util/redact.d.ts +15 -0
- package/dist/util/redact.d.ts.map +1 -0
- package/dist/util/redact.js +44 -0
- package/dist/util/redact.js.map +1 -0
- package/dist/util/time.d.ts +13 -0
- package/dist/util/time.d.ts.map +1 -0
- package/dist/util/time.js +77 -0
- package/dist/util/time.js.map +1 -0
- package/docs/integrations.md +128 -0
- package/docs/safety.md +62 -0
- package/package.json +69 -4
- package/src/cli.ts +175 -0
- package/src/commands/archive.ts +72 -0
- package/src/commands/config.ts +57 -0
- package/src/commands/context.ts +86 -0
- package/src/commands/daemon.ts +86 -0
- package/src/commands/doctor.ts +131 -0
- package/src/commands/gc.ts +108 -0
- package/src/commands/status.ts +68 -0
- package/src/commands/watch.ts +48 -0
- package/src/core/apply.ts +276 -0
- package/src/core/archive.ts +269 -0
- package/src/core/config.ts +339 -0
- package/src/core/decide.ts +230 -0
- package/src/core/forge.ts +243 -0
- package/src/core/git.ts +254 -0
- package/src/core/plan.ts +431 -0
- package/src/core/render.ts +168 -0
- package/src/core/scan.ts +105 -0
- package/src/core/sessions.ts +339 -0
- package/src/core/size.ts +10 -0
- package/src/daemon/lock.ts +65 -0
- package/src/daemon/service.ts +321 -0
- package/src/daemon/state.ts +49 -0
- package/src/index.ts +36 -0
- package/src/util/bytes.ts +11 -0
- package/src/util/color.ts +33 -0
- package/src/util/exec.ts +80 -0
- package/src/util/log.ts +88 -0
- package/src/util/paths.ts +84 -0
- package/src/util/prompt.ts +13 -0
- package/src/util/redact.ts +43 -0
- package/src/util/time.ts +75 -0
package/dist/util/log.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { color } from "./color.js";
|
|
4
|
+
import { redactSecrets, redactValue } from "./redact.js";
|
|
5
|
+
import { formatClock } from "./time.js";
|
|
6
|
+
const ORDER = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
7
|
+
export class Logger {
|
|
8
|
+
level;
|
|
9
|
+
file;
|
|
10
|
+
quiet;
|
|
11
|
+
fileBroken = false;
|
|
12
|
+
constructor(opts = {}) {
|
|
13
|
+
this.level = opts.level ?? "info";
|
|
14
|
+
this.file = opts.file;
|
|
15
|
+
this.quiet = opts.quiet ?? false;
|
|
16
|
+
}
|
|
17
|
+
debug(msg, fields) {
|
|
18
|
+
this.emit("debug", msg, fields);
|
|
19
|
+
}
|
|
20
|
+
info(msg, fields) {
|
|
21
|
+
this.emit("info", msg, fields);
|
|
22
|
+
}
|
|
23
|
+
warn(msg, fields) {
|
|
24
|
+
this.emit("warn", msg, fields);
|
|
25
|
+
}
|
|
26
|
+
error(msg, fields) {
|
|
27
|
+
this.emit("error", msg, fields);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A durable record of something treekeeper *did* (or decided not to do).
|
|
31
|
+
* Always written to the log file regardless of level, because this is the
|
|
32
|
+
* audit trail a developer reads after a worktree disappears.
|
|
33
|
+
*/
|
|
34
|
+
event(type, fields = {}) {
|
|
35
|
+
this.write({ ts: new Date().toISOString(), level: "info", type, ...redactValue(fields) });
|
|
36
|
+
}
|
|
37
|
+
emit(level, rawMsg, rawFields) {
|
|
38
|
+
const msg = redactSecrets(rawMsg);
|
|
39
|
+
const fields = rawFields ? redactValue(rawFields) : undefined;
|
|
40
|
+
this.write({ ts: new Date().toISOString(), level, msg, ...(fields ?? {}) });
|
|
41
|
+
if (this.quiet || ORDER[level] < ORDER[this.level])
|
|
42
|
+
return;
|
|
43
|
+
const tag = level === "error" ? color.red("error") :
|
|
44
|
+
level === "warn" ? color.yellow("warn") :
|
|
45
|
+
level === "debug" ? color.gray("debug") :
|
|
46
|
+
color.gray(formatClock(Date.now()));
|
|
47
|
+
const extra = fields && Object.keys(fields).length > 0 ? color.gray(` ${compact(fields)}`) : "";
|
|
48
|
+
// stderr, so that `--json` keeps stdout machine-readable.
|
|
49
|
+
process.stderr.write(`${tag} ${msg}${extra}\n`);
|
|
50
|
+
}
|
|
51
|
+
write(record) {
|
|
52
|
+
if (!this.file || this.fileBroken)
|
|
53
|
+
return;
|
|
54
|
+
try {
|
|
55
|
+
mkdirSync(dirname(this.file), { recursive: true });
|
|
56
|
+
appendFileSync(this.file, JSON.stringify(record) + "\n");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// A broken log file must never take the daemon down; report once.
|
|
60
|
+
this.fileBroken = true;
|
|
61
|
+
if (!this.quiet) {
|
|
62
|
+
process.stderr.write(color.yellow(`warn could not write log file ${this.file}\n`));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function compact(fields) {
|
|
68
|
+
return Object.entries(fields)
|
|
69
|
+
.map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`)
|
|
70
|
+
.join(" ");
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/util/log.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Expand a leading `~` and resolve to an absolute path. */
|
|
2
|
+
export declare function expandPath(input: string, base?: string): string;
|
|
3
|
+
/** Collapse `$HOME` back to `~` for display. */
|
|
4
|
+
export declare function shortenPath(p: string): string;
|
|
5
|
+
/** True when `child` is `parent` or lives underneath it. Pure string compare. */
|
|
6
|
+
export declare function isInside(parent: string, child: string): boolean;
|
|
7
|
+
/** realpath that falls back to the input when the path does not exist. */
|
|
8
|
+
export declare function realpathSafe(p: string): Promise<string>;
|
|
9
|
+
/** Where treekeeper keeps config, logs, cache and archives. */
|
|
10
|
+
export declare function treekeeperHome(): string;
|
|
11
|
+
/**
|
|
12
|
+
* Very small glob matcher: supports `*` (not crossing `/`), `**` (crossing it)
|
|
13
|
+
* and `?`. Enough for branch names and path patterns, and it keeps the
|
|
14
|
+
* dependency count at zero.
|
|
15
|
+
*/
|
|
16
|
+
export declare function matchGlob(pattern: string, value: string): boolean;
|
|
17
|
+
export declare function matchAnyGlob(patterns: readonly string[], value: string): boolean;
|
|
18
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/util/paths.ts"],"names":[],"mappings":"AAIA,4DAA4D;AAC5D,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,SAAgB,GAAG,MAAM,CAKtE;AAED,gDAAgD;AAChD,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAG7C;AAED,iFAAiF;AACjF,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAI/D;AAED,0EAA0E;AAC1E,wBAAsB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAM7D;AAED,+DAA+D;AAC/D,wBAAgB,cAAc,IAAI,MAAM,CAIvC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAEhF"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
3
|
+
import { realpath } from "node:fs/promises";
|
|
4
|
+
/** Expand a leading `~` and resolve to an absolute path. */
|
|
5
|
+
export function expandPath(input, base = process.cwd()) {
|
|
6
|
+
let p = input.trim();
|
|
7
|
+
if (p === "~")
|
|
8
|
+
p = homedir();
|
|
9
|
+
else if (p.startsWith("~/") || p.startsWith(`~${sep}`))
|
|
10
|
+
p = join(homedir(), p.slice(2));
|
|
11
|
+
return isAbsolute(p) ? resolve(p) : resolve(base, p);
|
|
12
|
+
}
|
|
13
|
+
/** Collapse `$HOME` back to `~` for display. */
|
|
14
|
+
export function shortenPath(p) {
|
|
15
|
+
const home = homedir();
|
|
16
|
+
return p === home || p.startsWith(home + sep) ? "~" + p.slice(home.length) : p;
|
|
17
|
+
}
|
|
18
|
+
/** True when `child` is `parent` or lives underneath it. Pure string compare. */
|
|
19
|
+
export function isInside(parent, child) {
|
|
20
|
+
if (parent === child)
|
|
21
|
+
return true;
|
|
22
|
+
const base = parent.endsWith(sep) ? parent : parent + sep;
|
|
23
|
+
return child.startsWith(base);
|
|
24
|
+
}
|
|
25
|
+
/** realpath that falls back to the input when the path does not exist. */
|
|
26
|
+
export async function realpathSafe(p) {
|
|
27
|
+
try {
|
|
28
|
+
return await realpath(p);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return p;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Where treekeeper keeps config, logs, cache and archives. */
|
|
35
|
+
export function treekeeperHome() {
|
|
36
|
+
const override = process.env.TREEKEEPER_HOME;
|
|
37
|
+
if (override && override.trim() !== "")
|
|
38
|
+
return expandPath(override);
|
|
39
|
+
return join(homedir(), ".treekeeper");
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Very small glob matcher: supports `*` (not crossing `/`), `**` (crossing it)
|
|
43
|
+
* and `?`. Enough for branch names and path patterns, and it keeps the
|
|
44
|
+
* dependency count at zero.
|
|
45
|
+
*/
|
|
46
|
+
export function matchGlob(pattern, value) {
|
|
47
|
+
const re = globToRegExp(pattern);
|
|
48
|
+
return re.test(value);
|
|
49
|
+
}
|
|
50
|
+
export function matchAnyGlob(patterns, value) {
|
|
51
|
+
return patterns.some((p) => matchGlob(p, value));
|
|
52
|
+
}
|
|
53
|
+
const cache = new Map();
|
|
54
|
+
function globToRegExp(pattern) {
|
|
55
|
+
const hit = cache.get(pattern);
|
|
56
|
+
if (hit)
|
|
57
|
+
return hit;
|
|
58
|
+
let out = "^";
|
|
59
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
60
|
+
const c = pattern[i];
|
|
61
|
+
if (c === "*") {
|
|
62
|
+
if (pattern[i + 1] === "*") {
|
|
63
|
+
i += 1;
|
|
64
|
+
if (pattern[i + 1] === "/")
|
|
65
|
+
i += 1;
|
|
66
|
+
out += "(?:.*\\/)?";
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
out += "[^/]*";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (c === "?") {
|
|
73
|
+
out += "[^/]";
|
|
74
|
+
}
|
|
75
|
+
else if ("\\^$.|+()[]{}".includes(c)) {
|
|
76
|
+
out += "\\" + c;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
out += c;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
out += "$";
|
|
83
|
+
const re = new RegExp(out);
|
|
84
|
+
cache.set(pattern, re);
|
|
85
|
+
return re;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/util/paths.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/util/prompt.ts"],"names":[],"mappings":"AAEA,yEAAyE;AACzE,wBAAsB,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,UAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CASlF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
/** Ask a yes/no question. Returns `fallback` when stdin is not a TTY. */
|
|
3
|
+
export async function confirm(question, fallback = false) {
|
|
4
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
5
|
+
return fallback;
|
|
6
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
7
|
+
try {
|
|
8
|
+
const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
|
|
9
|
+
return answer === "y" || answer === "yes";
|
|
10
|
+
}
|
|
11
|
+
finally {
|
|
12
|
+
rl.close();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.js","sourceRoot":"","sources":["../../src/util/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,yEAAyE;AACzE,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,QAAgB,EAAE,QAAQ,GAAG,KAAK;IAC9D,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK;QAAE,OAAO,QAAQ,CAAC;IACnE,MAAM,EAAE,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7E,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,GAAG,QAAQ,SAAS,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC9E,OAAO,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,KAAK,CAAC;IAC5C,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,KAAK,EAAE,CAAC;IACb,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential scrubbing for anything that might reach a log file or stdout.
|
|
3
|
+
*
|
|
4
|
+
* Remotes routinely carry secrets — `https://user:ghp_xxx@github.com/o/r.git`
|
|
5
|
+
* is a perfectly ordinary origin URL — and git and gh echo them back in error
|
|
6
|
+
* messages. treekeeper writes a persistent log, so a token must never make it
|
|
7
|
+
* that far.
|
|
8
|
+
*/
|
|
9
|
+
/** `https://user:token@host/path` -> `https://host/path`. */
|
|
10
|
+
export declare function redactRemote(url: string): string;
|
|
11
|
+
/** Remove credentials from free text. */
|
|
12
|
+
export declare function redactSecrets(text: string): string;
|
|
13
|
+
/** Recursively redact strings in a value destined for a log record. */
|
|
14
|
+
export declare function redactValue<T>(value: T): T;
|
|
15
|
+
//# sourceMappingURL=redact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../../src/util/redact.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,6DAA6D;AAC7D,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEhD;AAaD,yCAAyC;AACzC,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIlD;AAED,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAS1C"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential scrubbing for anything that might reach a log file or stdout.
|
|
3
|
+
*
|
|
4
|
+
* Remotes routinely carry secrets — `https://user:ghp_xxx@github.com/o/r.git`
|
|
5
|
+
* is a perfectly ordinary origin URL — and git and gh echo them back in error
|
|
6
|
+
* messages. treekeeper writes a persistent log, so a token must never make it
|
|
7
|
+
* that far.
|
|
8
|
+
*/
|
|
9
|
+
/** `https://user:token@host/path` -> `https://host/path`. */
|
|
10
|
+
export function redactRemote(url) {
|
|
11
|
+
return url.replace(/^([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^@/]*@/, "$1");
|
|
12
|
+
}
|
|
13
|
+
const PATTERNS = [
|
|
14
|
+
// userinfo in any URL
|
|
15
|
+
[/([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^@/\s]*@/g, "$1"],
|
|
16
|
+
// well-known token shapes, even when they appear bare
|
|
17
|
+
[/\bgh[pousr]_[A-Za-z0-9]{16,}/g, "[redacted]"],
|
|
18
|
+
[/\bgithub_pat_[A-Za-z0-9_]{20,}/g, "[redacted]"],
|
|
19
|
+
[/\bglpat-[A-Za-z0-9_-]{16,}/g, "[redacted]"],
|
|
20
|
+
[/\b(?:xox[baprs]-)[A-Za-z0-9-]{10,}/g, "[redacted]"],
|
|
21
|
+
[/\bAKIA[0-9A-Z]{16}\b/g, "[redacted]"],
|
|
22
|
+
];
|
|
23
|
+
/** Remove credentials from free text. */
|
|
24
|
+
export function redactSecrets(text) {
|
|
25
|
+
let out = text;
|
|
26
|
+
for (const [re, replacement] of PATTERNS)
|
|
27
|
+
out = out.replace(re, replacement);
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
/** Recursively redact strings in a value destined for a log record. */
|
|
31
|
+
export function redactValue(value) {
|
|
32
|
+
if (typeof value === "string")
|
|
33
|
+
return redactSecrets(value);
|
|
34
|
+
if (Array.isArray(value))
|
|
35
|
+
return value.map((v) => redactValue(v));
|
|
36
|
+
if (value !== null && typeof value === "object") {
|
|
37
|
+
const out = {};
|
|
38
|
+
for (const [k, v] of Object.entries(value))
|
|
39
|
+
out[k] = redactValue(v);
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/util/redact.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,6DAA6D;AAC7D,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,OAAO,GAAG,CAAC,OAAO,CAAC,wCAAwC,EAAE,IAAI,CAAC,CAAC;AACrE,CAAC;AAED,MAAM,QAAQ,GAA4B;IACxC,sBAAsB;IACtB,CAAC,0CAA0C,EAAE,IAAI,CAAC;IAClD,sDAAsD;IACtD,CAAC,+BAA+B,EAAE,YAAY,CAAC;IAC/C,CAAC,iCAAiC,EAAE,YAAY,CAAC;IACjD,CAAC,6BAA6B,EAAE,YAAY,CAAC;IAC7C,CAAC,qCAAqC,EAAE,YAAY,CAAC;IACrD,CAAC,uBAAuB,EAAE,YAAY,CAAC;CACxC,CAAC;AAEF,yCAAyC;AACzC,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,GAAG,GAAG,IAAI,CAAC;IACf,KAAK,MAAM,CAAC,EAAE,EAAE,WAAW,CAAC,IAAI,QAAQ;QAAE,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC;IAC7E,OAAO,GAAG,CAAC;AACb,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,WAAW,CAAI,KAAQ;IACrC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,aAAa,CAAC,KAAK,CAAiB,CAAC;IAC3E,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,CAAiB,CAAC;IAClF,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,GAAG,GAA4B,EAAE,CAAC;QACxC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;QACpE,OAAO,GAAmB,CAAC;IAC7B,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a duration like "30m", "2h", "7d", "1h30m", "0" or a bare number of
|
|
3
|
+
* milliseconds. Throws on anything else so a typo in a config file is loud
|
|
4
|
+
* rather than silently becoming 0 (which would disable a safety gate).
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseDuration(input: string | number): number;
|
|
7
|
+
/** "3m", "2h 10m", "4d" — compact, at most two units. */
|
|
8
|
+
export declare function formatDuration(ms: number): string;
|
|
9
|
+
/** "12m ago" / "just now" */
|
|
10
|
+
export declare function formatAgo(timestamp: number, now?: number): string;
|
|
11
|
+
export declare function formatClock(timestamp: number): string;
|
|
12
|
+
export declare function formatStamp(timestamp: number): string;
|
|
13
|
+
//# sourceMappingURL=time.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time.d.ts","sourceRoot":"","sources":["../../src/util/time.ts"],"names":[],"mappings":"AASA;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAwB5D;AAED,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAWjD;AAED,6BAA6B;AAC7B,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,MAAM,CAIrE;AAED,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIrD;AAED,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAOrD"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const UNITS = {
|
|
2
|
+
ms: 1,
|
|
3
|
+
s: 1_000,
|
|
4
|
+
m: 60_000,
|
|
5
|
+
h: 3_600_000,
|
|
6
|
+
d: 86_400_000,
|
|
7
|
+
w: 604_800_000,
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Parse a duration like "30m", "2h", "7d", "1h30m", "0" or a bare number of
|
|
11
|
+
* milliseconds. Throws on anything else so a typo in a config file is loud
|
|
12
|
+
* rather than silently becoming 0 (which would disable a safety gate).
|
|
13
|
+
*/
|
|
14
|
+
export function parseDuration(input) {
|
|
15
|
+
if (typeof input === "number") {
|
|
16
|
+
if (!Number.isFinite(input) || input < 0)
|
|
17
|
+
throw new Error(`invalid duration: ${input}`);
|
|
18
|
+
return Math.round(input);
|
|
19
|
+
}
|
|
20
|
+
const text = input.trim().toLowerCase();
|
|
21
|
+
if (text === "")
|
|
22
|
+
throw new Error("invalid duration: empty string");
|
|
23
|
+
if (/^\d+$/.test(text))
|
|
24
|
+
return Number(text);
|
|
25
|
+
const re = /(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)/g;
|
|
26
|
+
let total = 0;
|
|
27
|
+
let matched = 0;
|
|
28
|
+
let consumed = 0;
|
|
29
|
+
for (const m of text.matchAll(re)) {
|
|
30
|
+
const [whole, value, unit] = m;
|
|
31
|
+
total += Number(value) * UNITS[unit];
|
|
32
|
+
matched += 1;
|
|
33
|
+
consumed += whole.length;
|
|
34
|
+
}
|
|
35
|
+
const stripped = text.replace(/[\s,]/g, "");
|
|
36
|
+
if (matched === 0 || consumed < stripped.length) {
|
|
37
|
+
throw new Error(`invalid duration: ${input} (use e.g. 30m, 2h, 7d)`);
|
|
38
|
+
}
|
|
39
|
+
return Math.round(total);
|
|
40
|
+
}
|
|
41
|
+
/** "3m", "2h 10m", "4d" — compact, at most two units. */
|
|
42
|
+
export function formatDuration(ms) {
|
|
43
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
44
|
+
return "?";
|
|
45
|
+
if (ms < 1_000)
|
|
46
|
+
return `${Math.round(ms)}ms`;
|
|
47
|
+
const d = Math.floor(ms / UNITS.d);
|
|
48
|
+
const h = Math.floor((ms % UNITS.d) / UNITS.h);
|
|
49
|
+
const m = Math.floor((ms % UNITS.h) / UNITS.m);
|
|
50
|
+
const s = Math.floor((ms % UNITS.m) / UNITS.s);
|
|
51
|
+
if (d > 0)
|
|
52
|
+
return h > 0 ? `${d}d ${h}h` : `${d}d`;
|
|
53
|
+
if (h > 0)
|
|
54
|
+
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
55
|
+
if (m > 0)
|
|
56
|
+
return `${m}m`;
|
|
57
|
+
return `${s}s`;
|
|
58
|
+
}
|
|
59
|
+
/** "12m ago" / "just now" */
|
|
60
|
+
export function formatAgo(timestamp, now = Date.now()) {
|
|
61
|
+
const delta = now - timestamp;
|
|
62
|
+
if (delta < 45_000)
|
|
63
|
+
return "just now";
|
|
64
|
+
return `${formatDuration(delta)} ago`;
|
|
65
|
+
}
|
|
66
|
+
export function formatClock(timestamp) {
|
|
67
|
+
const d = new Date(timestamp);
|
|
68
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
69
|
+
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
70
|
+
}
|
|
71
|
+
export function formatStamp(timestamp) {
|
|
72
|
+
const d = new Date(timestamp);
|
|
73
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
74
|
+
return (`${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}` +
|
|
75
|
+
`-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`);
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=time.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time.js","sourceRoot":"","sources":["../../src/util/time.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Integrating treekeeper with an agent harness
|
|
2
|
+
|
|
3
|
+
treekeeper decides from state — git, your forge, and what the agents left on disk — not from harness callbacks. So the best integration is usually **no integration**:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
treekeeper daemon install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
That installs a launchd agent (macOS) or a systemd user timer (Linux) that sweeps every `interval`. It works for every harness at once, including ones you install next month.
|
|
10
|
+
|
|
11
|
+
The snippets below are for when you want a sweep tied to a specific moment instead.
|
|
12
|
+
|
|
13
|
+
## The command to run
|
|
14
|
+
|
|
15
|
+
Whatever the harness, the hook body is the same:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx -y @yoavsh/treekeeper gc --all --apply --yes --quiet
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- `--yes` skips the confirmation prompt (hooks have no TTY)
|
|
22
|
+
- `--quiet` prints only the summary line; the full record still goes to `~/.treekeeper/treekeeper.log`
|
|
23
|
+
- exit code is 0 on success, 75 if another sweep holds the lock, 1 if something could not be archived
|
|
24
|
+
|
|
25
|
+
Drop `--all` to limit the sweep to the repo the hook fired in.
|
|
26
|
+
|
|
27
|
+
## Claude Code
|
|
28
|
+
|
|
29
|
+
Add a `SessionEnd` hook to `~/.claude/settings.json`:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"hooks": {
|
|
34
|
+
"SessionEnd": [
|
|
35
|
+
{
|
|
36
|
+
"hooks": [
|
|
37
|
+
{ "type": "command", "command": "npx -y @yoavsh/treekeeper gc --all --apply --yes --quiet" }
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
treekeeper reads `~/.claude/projects/<slugified-cwd>/*.jsonl` to tell when a Claude Code session last touched a worktree, so a sweep triggered by one session will not remove a worktree another session is still using. Set `CLAUDE_CONFIG_DIR` if your config lives elsewhere.
|
|
46
|
+
|
|
47
|
+
A slash command is often nicer than a hook, because you see the plan first. `~/.claude/commands/gc.md`:
|
|
48
|
+
|
|
49
|
+
```markdown
|
|
50
|
+
---
|
|
51
|
+
description: Remove worktrees and branches whose work is already merged
|
|
52
|
+
allowed-tools: Bash(treekeeper:*)
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
Run `treekeeper gc --all` and show me the plan. If it looks right, run it with `--apply --yes`.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Codex CLI
|
|
59
|
+
|
|
60
|
+
treekeeper detects Codex activity from `~/.codex/sessions/**/*.jsonl` (matching the recorded `cwd`). To sweep on a schedule instead, use the daemon. To sweep from a notify hook in `~/.codex/config.toml`:
|
|
61
|
+
|
|
62
|
+
```toml
|
|
63
|
+
notify = ["bash", "-lc", "npx -y @yoavsh/treekeeper gc --all --apply --yes --quiet"]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Cursor / Windsurf / JetBrains agents
|
|
67
|
+
|
|
68
|
+
These keep session state in editor-internal databases, so treekeeper relies on its generic detectors instead — the `process` detector (an agent working in a directory is a process whose cwd is that directory) and `mtime`. Both are on by default and need no setup; just run the daemon.
|
|
69
|
+
|
|
70
|
+
## Aider
|
|
71
|
+
|
|
72
|
+
`.aider.chat.history.md` inside the worktree is the activity signal, and it is on by default. For a sweep after each session, wrap your invocation:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
aider "$@"; npx -y @yoavsh/treekeeper gc --apply --yes --quiet
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## A harness treekeeper has never heard of
|
|
79
|
+
|
|
80
|
+
Teach it the activity signal rather than wiring up a hook. Either point at a file whose mtime tracks the session:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"sessions": {
|
|
85
|
+
"extraPaths": ["{worktree}/.myagent/session.log", "~/.myharness/state/{worktree}.json"]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
...or give it a command, where exit 0 means "an agent is still working here":
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"sessions": {
|
|
95
|
+
"commands": [
|
|
96
|
+
{ "name": "myharness", "command": "pgrep -f \"myharness.*$TREEKEEPER_WORKTREE\" > /dev/null" }
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`$TREEKEEPER_WORKTREE` is set to the worktree being considered.
|
|
103
|
+
|
|
104
|
+
## Reading the plan from a script
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
treekeeper gc --all --json > plan.json
|
|
108
|
+
|
|
109
|
+
jq '.summary' plan.json
|
|
110
|
+
jq -r '.repos[].worktrees[] | select(.verdict.action == "reclaim") | "\(.path)\t\(.sizeKb)\t\(.verdict.reason)"' plan.json
|
|
111
|
+
jq -r '.repos[].worktrees[] | select(.verdict.action == "keep") | "\(.path)\t\(.verdict.code)"' plan.json
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Each worktree carries a `verdict` with a stable `code` (`merged-ancestor`, `unmerged`, `session-active`, `too-young`, `dirty`, `closed-pr-grace`, `protected-branch`, …), a human `reason`, and the `risks` that would need archiving. Script against `code`, show `reason` to people.
|
|
115
|
+
|
|
116
|
+
With `--apply --json` you get `{ "plan": …, "result": … }`, where `result` lists what was removed, what was archived and what was skipped.
|
|
117
|
+
|
|
118
|
+
## CI
|
|
119
|
+
|
|
120
|
+
treekeeper is built for developer machines, where worktrees outlive their usefulness. In CI, where the checkout is thrown away anyway, it buys you nothing — except on a long-lived self-hosted runner, where a nightly job is reasonable:
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
- run: npx -y @yoavsh/treekeeper gc --root "$RUNNER_WORKDIR" --apply --yes
|
|
124
|
+
env:
|
|
125
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Set `archive.enabled: false` there if the runner has no business keeping archives.
|
package/docs/safety.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# What treekeeper will and will not delete
|
|
2
|
+
|
|
3
|
+
treekeeper deletes directories and git branches without being asked, so the rules matter more than the features. They live in one pure function, [`src/core/decide.ts`](../src/core/decide.ts), with a truth table in [`test/decide.test.ts`](../test/decide.test.ts).
|
|
4
|
+
|
|
5
|
+
## The invariant
|
|
6
|
+
|
|
7
|
+
> A worktree is removed only when its commits are reachable from the default branch, or its pull request was merged or closed.
|
|
8
|
+
|
|
9
|
+
Nothing overrides this. `--force` does not. If treekeeper cannot establish that the work landed — no ancestry, no PR data, `gh` missing, GitHub unreachable — the answer is *keep*. Being unable to tell is treated as "not finished", never as "no PR exists".
|
|
10
|
+
|
|
11
|
+
## Never touched, under any flags
|
|
12
|
+
|
|
13
|
+
- the repo's main worktree
|
|
14
|
+
- the worktree containing your current directory
|
|
15
|
+
- a worktree held by `git worktree lock`
|
|
16
|
+
- a detached HEAD (no branch to reason about)
|
|
17
|
+
- a branch matching `protectBranches` (`main`, `master`, `trunk`, `develop`, `dev`, `release/*` by default)
|
|
18
|
+
- a path matching `protectPaths`
|
|
19
|
+
- a repo with `{"enabled": false}` in its `.treekeeper.json`
|
|
20
|
+
- the default branch, and any branch currently checked out somewhere
|
|
21
|
+
|
|
22
|
+
## Held back until the coast is clear
|
|
23
|
+
|
|
24
|
+
These are the gates `--force` bypasses, except where noted:
|
|
25
|
+
|
|
26
|
+
| Gate | Default | `--force` |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| Agent session activity within `sessions.grace` | 20m | **still held** |
|
|
29
|
+
| A process whose cwd is inside the worktree | — | **still held** |
|
|
30
|
+
| Worktree touched within `minAge` | 30m | bypassed |
|
|
31
|
+
| Uncommitted changes (`onDirty`) | archive, then remove | bypassed |
|
|
32
|
+
| Merged PR with extra local commits (`onExtraCommits`) | keep | bypassed |
|
|
33
|
+
| Closed-unmerged PR inside `closedPrGrace` (`onClosedPr`) | 7d hold, then archive | bypassed |
|
|
34
|
+
|
|
35
|
+
A live session or a live process always wins. Everything else is a policy you can set.
|
|
36
|
+
|
|
37
|
+
## Before every removal
|
|
38
|
+
|
|
39
|
+
1. **Archive.** Commits not on the default branch become a `git bundle`; uncommitted changes become a `git diff HEAD --binary` patch; untracked non-ignored files become a tarball. Each archive has a `RESTORE.md` with the commands to undo it. If archiving fails, the worktree is kept.
|
|
40
|
+
2. **Re-verify.** The plan is a snapshot, and a daemon may have built it minutes ago. Immediately before removal treekeeper re-checks that the directory still exists, the branch and HEAD have not moved, the worktree is not newly locked, and no uncommitted changes appeared. Anything that moved is left for the next sweep.
|
|
41
|
+
3. **Remove with git.** `git worktree remove`, never a blind `rm -rf`, so git's own bookkeeping stays correct. `--force` is used only after a successful archive.
|
|
42
|
+
4. **Log.** A JSON line per removal in `~/.treekeeper/treekeeper.log`: path, branch, size, reason, risks, archive location.
|
|
43
|
+
|
|
44
|
+
## Concurrency
|
|
45
|
+
|
|
46
|
+
Multiple agents and multiple sessions share a machine, so:
|
|
47
|
+
|
|
48
|
+
- a lock file (`~/.treekeeper/sweep.lock`) allows one sweep at a time; a second exits with code 75 rather than racing
|
|
49
|
+
- a stale lock from a killed process is detected by pid liveness and cleared
|
|
50
|
+
- the `process` detector means a worktree another session is actively sitting in is never a candidate, even if its branch is merged
|
|
51
|
+
|
|
52
|
+
## Credentials
|
|
53
|
+
|
|
54
|
+
Remotes with embedded tokens (`https://user:ghp_…@github.com/o/r.git`) are common, and `git` and `gh` echo them in error messages. treekeeper scrubs credentials at the logging chokepoint, so no token reaches the log file, the PR cache or stdout. See [`src/util/redact.ts`](../src/util/redact.ts).
|
|
55
|
+
|
|
56
|
+
## Branches
|
|
57
|
+
|
|
58
|
+
Deleting a branch only drops a pointer, but it can still orphan commits, so the same rules apply: unmerged branches are never deleted, and a closed-PR branch is bundled before it goes. A branch still checked out in a worktree treekeeper decided to keep is kept with it.
|
|
59
|
+
|
|
60
|
+
## Reporting a problem
|
|
61
|
+
|
|
62
|
+
If treekeeper removed something it should not have, the archive directory and `~/.treekeeper/treekeeper.log` together say exactly what happened and why. Both are worth attaching to an issue.
|