@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.
Files changed (166) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +237 -2
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +165 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/commands/archive.d.ts +3 -0
  9. package/dist/commands/archive.d.ts.map +1 -0
  10. package/dist/commands/archive.js +55 -0
  11. package/dist/commands/archive.js.map +1 -0
  12. package/dist/commands/config.d.ts +2 -0
  13. package/dist/commands/config.d.ts.map +1 -0
  14. package/dist/commands/config.js +54 -0
  15. package/dist/commands/config.js.map +1 -0
  16. package/dist/commands/context.d.ts +34 -0
  17. package/dist/commands/context.d.ts.map +1 -0
  18. package/dist/commands/context.js +53 -0
  19. package/dist/commands/context.js.map +1 -0
  20. package/dist/commands/daemon.d.ts +3 -0
  21. package/dist/commands/daemon.d.ts.map +1 -0
  22. package/dist/commands/daemon.js +81 -0
  23. package/dist/commands/daemon.js.map +1 -0
  24. package/dist/commands/doctor.d.ts +3 -0
  25. package/dist/commands/doctor.d.ts.map +1 -0
  26. package/dist/commands/doctor.js +119 -0
  27. package/dist/commands/doctor.js.map +1 -0
  28. package/dist/commands/gc.d.ts +3 -0
  29. package/dist/commands/gc.d.ts.map +1 -0
  30. package/dist/commands/gc.js +98 -0
  31. package/dist/commands/gc.js.map +1 -0
  32. package/dist/commands/status.d.ts +4 -0
  33. package/dist/commands/status.d.ts.map +1 -0
  34. package/dist/commands/status.js +52 -0
  35. package/dist/commands/status.js.map +1 -0
  36. package/dist/commands/watch.d.ts +10 -0
  37. package/dist/commands/watch.d.ts.map +1 -0
  38. package/dist/commands/watch.js +41 -0
  39. package/dist/commands/watch.js.map +1 -0
  40. package/dist/core/apply.d.ts +47 -0
  41. package/dist/core/apply.d.ts.map +1 -0
  42. package/dist/core/apply.js +223 -0
  43. package/dist/core/apply.js.map +1 -0
  44. package/dist/core/archive.d.ts +69 -0
  45. package/dist/core/archive.d.ts.map +1 -0
  46. package/dist/core/archive.js +202 -0
  47. package/dist/core/archive.js.map +1 -0
  48. package/dist/core/config.d.ts +69 -0
  49. package/dist/core/config.d.ts.map +1 -0
  50. package/dist/core/config.js +261 -0
  51. package/dist/core/config.js.map +1 -0
  52. package/dist/core/decide.d.ts +102 -0
  53. package/dist/core/decide.d.ts.map +1 -0
  54. package/dist/core/decide.js +148 -0
  55. package/dist/core/decide.js.map +1 -0
  56. package/dist/core/forge.d.ts +39 -0
  57. package/dist/core/forge.d.ts.map +1 -0
  58. package/dist/core/forge.js +190 -0
  59. package/dist/core/forge.js.map +1 -0
  60. package/dist/core/git.d.ts +63 -0
  61. package/dist/core/git.d.ts.map +1 -0
  62. package/dist/core/git.js +194 -0
  63. package/dist/core/git.js.map +1 -0
  64. package/dist/core/plan.d.ts +75 -0
  65. package/dist/core/plan.d.ts.map +1 -0
  66. package/dist/core/plan.js +288 -0
  67. package/dist/core/plan.js.map +1 -0
  68. package/dist/core/render.d.ts +14 -0
  69. package/dist/core/render.d.ts.map +1 -0
  70. package/dist/core/render.js +128 -0
  71. package/dist/core/render.js.map +1 -0
  72. package/dist/core/scan.d.ts +22 -0
  73. package/dist/core/scan.d.ts.map +1 -0
  74. package/dist/core/scan.js +99 -0
  75. package/dist/core/scan.js.map +1 -0
  76. package/dist/core/sessions.d.ts +37 -0
  77. package/dist/core/sessions.d.ts.map +1 -0
  78. package/dist/core/sessions.js +300 -0
  79. package/dist/core/sessions.js.map +1 -0
  80. package/dist/core/size.d.ts +3 -0
  81. package/dist/core/size.d.ts.map +1 -0
  82. package/dist/core/size.js +11 -0
  83. package/dist/core/size.js.map +1 -0
  84. package/dist/daemon/lock.d.ts +10 -0
  85. package/dist/daemon/lock.d.ts.map +1 -0
  86. package/dist/daemon/lock.js +64 -0
  87. package/dist/daemon/lock.js.map +1 -0
  88. package/dist/daemon/service.d.ts +34 -0
  89. package/dist/daemon/service.d.ts.map +1 -0
  90. package/dist/daemon/service.js +257 -0
  91. package/dist/daemon/service.js.map +1 -0
  92. package/dist/daemon/state.d.ts +19 -0
  93. package/dist/daemon/state.d.ts.map +1 -0
  94. package/dist/daemon/state.js +32 -0
  95. package/dist/daemon/state.js.map +1 -0
  96. package/dist/index.d.ts +26 -0
  97. package/dist/index.d.ts.map +1 -0
  98. package/dist/index.js +19 -0
  99. package/dist/index.js.map +1 -0
  100. package/dist/util/bytes.d.ts +4 -0
  101. package/dist/util/bytes.d.ts.map +1 -0
  102. package/dist/util/bytes.js +14 -0
  103. package/dist/util/bytes.js.map +1 -0
  104. package/dist/util/color.d.ts +16 -0
  105. package/dist/util/color.d.ts.map +1 -0
  106. package/dist/util/color.js +32 -0
  107. package/dist/util/color.js.map +1 -0
  108. package/dist/util/exec.d.ts +27 -0
  109. package/dist/util/exec.d.ts.map +1 -0
  110. package/dist/util/exec.js +57 -0
  111. package/dist/util/exec.js.map +1 -0
  112. package/dist/util/log.d.ts +29 -0
  113. package/dist/util/log.d.ts.map +1 -0
  114. package/dist/util/log.js +72 -0
  115. package/dist/util/log.js.map +1 -0
  116. package/dist/util/paths.d.ts +18 -0
  117. package/dist/util/paths.d.ts.map +1 -0
  118. package/dist/util/paths.js +87 -0
  119. package/dist/util/paths.js.map +1 -0
  120. package/dist/util/prompt.d.ts +3 -0
  121. package/dist/util/prompt.d.ts.map +1 -0
  122. package/dist/util/prompt.js +15 -0
  123. package/dist/util/prompt.js.map +1 -0
  124. package/dist/util/redact.d.ts +15 -0
  125. package/dist/util/redact.d.ts.map +1 -0
  126. package/dist/util/redact.js +44 -0
  127. package/dist/util/redact.js.map +1 -0
  128. package/dist/util/time.d.ts +13 -0
  129. package/dist/util/time.d.ts.map +1 -0
  130. package/dist/util/time.js +77 -0
  131. package/dist/util/time.js.map +1 -0
  132. package/docs/integrations.md +128 -0
  133. package/docs/safety.md +62 -0
  134. package/package.json +69 -4
  135. package/src/cli.ts +175 -0
  136. package/src/commands/archive.ts +72 -0
  137. package/src/commands/config.ts +57 -0
  138. package/src/commands/context.ts +86 -0
  139. package/src/commands/daemon.ts +86 -0
  140. package/src/commands/doctor.ts +131 -0
  141. package/src/commands/gc.ts +108 -0
  142. package/src/commands/status.ts +68 -0
  143. package/src/commands/watch.ts +48 -0
  144. package/src/core/apply.ts +276 -0
  145. package/src/core/archive.ts +269 -0
  146. package/src/core/config.ts +339 -0
  147. package/src/core/decide.ts +230 -0
  148. package/src/core/forge.ts +243 -0
  149. package/src/core/git.ts +254 -0
  150. package/src/core/plan.ts +431 -0
  151. package/src/core/render.ts +168 -0
  152. package/src/core/scan.ts +105 -0
  153. package/src/core/sessions.ts +339 -0
  154. package/src/core/size.ts +10 -0
  155. package/src/daemon/lock.ts +65 -0
  156. package/src/daemon/service.ts +321 -0
  157. package/src/daemon/state.ts +49 -0
  158. package/src/index.ts +36 -0
  159. package/src/util/bytes.ts +11 -0
  160. package/src/util/color.ts +33 -0
  161. package/src/util/exec.ts +80 -0
  162. package/src/util/log.ts +88 -0
  163. package/src/util/paths.ts +84 -0
  164. package/src/util/prompt.ts +13 -0
  165. package/src/util/redact.ts +43 -0
  166. package/src/util/time.ts +75 -0
@@ -0,0 +1,84 @@
1
+ import { homedir } from "node:os";
2
+ import { isAbsolute, join, resolve, sep } from "node:path";
3
+ import { realpath } from "node:fs/promises";
4
+
5
+ /** Expand a leading `~` and resolve to an absolute path. */
6
+ export function expandPath(input: string, base = process.cwd()): string {
7
+ let p = input.trim();
8
+ if (p === "~") p = homedir();
9
+ else if (p.startsWith("~/") || p.startsWith(`~${sep}`)) p = join(homedir(), p.slice(2));
10
+ return isAbsolute(p) ? resolve(p) : resolve(base, p);
11
+ }
12
+
13
+ /** Collapse `$HOME` back to `~` for display. */
14
+ export function shortenPath(p: string): string {
15
+ const home = homedir();
16
+ return p === home || p.startsWith(home + sep) ? "~" + p.slice(home.length) : p;
17
+ }
18
+
19
+ /** True when `child` is `parent` or lives underneath it. Pure string compare. */
20
+ export function isInside(parent: string, child: string): boolean {
21
+ if (parent === child) return true;
22
+ const base = parent.endsWith(sep) ? parent : parent + sep;
23
+ return child.startsWith(base);
24
+ }
25
+
26
+ /** realpath that falls back to the input when the path does not exist. */
27
+ export async function realpathSafe(p: string): Promise<string> {
28
+ try {
29
+ return await realpath(p);
30
+ } catch {
31
+ return p;
32
+ }
33
+ }
34
+
35
+ /** Where treekeeper keeps config, logs, cache and archives. */
36
+ export function treekeeperHome(): string {
37
+ const override = process.env.TREEKEEPER_HOME;
38
+ if (override && override.trim() !== "") return expandPath(override);
39
+ return join(homedir(), ".treekeeper");
40
+ }
41
+
42
+ /**
43
+ * Very small glob matcher: supports `*` (not crossing `/`), `**` (crossing it)
44
+ * and `?`. Enough for branch names and path patterns, and it keeps the
45
+ * dependency count at zero.
46
+ */
47
+ export function matchGlob(pattern: string, value: string): boolean {
48
+ const re = globToRegExp(pattern);
49
+ return re.test(value);
50
+ }
51
+
52
+ export function matchAnyGlob(patterns: readonly string[], value: string): boolean {
53
+ return patterns.some((p) => matchGlob(p, value));
54
+ }
55
+
56
+ const cache = new Map<string, RegExp>();
57
+
58
+ function globToRegExp(pattern: string): RegExp {
59
+ const hit = cache.get(pattern);
60
+ if (hit) return hit;
61
+ let out = "^";
62
+ for (let i = 0; i < pattern.length; i += 1) {
63
+ const c = pattern[i]!;
64
+ if (c === "*") {
65
+ if (pattern[i + 1] === "*") {
66
+ i += 1;
67
+ if (pattern[i + 1] === "/") i += 1;
68
+ out += "(?:.*\\/)?";
69
+ } else {
70
+ out += "[^/]*";
71
+ }
72
+ } else if (c === "?") {
73
+ out += "[^/]";
74
+ } else if ("\\^$.|+()[]{}".includes(c)) {
75
+ out += "\\" + c;
76
+ } else {
77
+ out += c;
78
+ }
79
+ }
80
+ out += "$";
81
+ const re = new RegExp(out);
82
+ cache.set(pattern, re);
83
+ return re;
84
+ }
@@ -0,0 +1,13 @@
1
+ import { createInterface } from "node:readline/promises";
2
+
3
+ /** Ask a yes/no question. Returns `fallback` when stdin is not a TTY. */
4
+ export async function confirm(question: string, fallback = false): Promise<boolean> {
5
+ if (!process.stdin.isTTY || !process.stdout.isTTY) 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
+ } finally {
11
+ rl.close();
12
+ }
13
+ }
@@ -0,0 +1,43 @@
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
+
10
+ /** `https://user:token@host/path` -> `https://host/path`. */
11
+ export function redactRemote(url: string): string {
12
+ return url.replace(/^([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^@/]*@/, "$1");
13
+ }
14
+
15
+ const PATTERNS: Array<[RegExp, string]> = [
16
+ // userinfo in any URL
17
+ [/([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^@/\s]*@/g, "$1"],
18
+ // well-known token shapes, even when they appear bare
19
+ [/\bgh[pousr]_[A-Za-z0-9]{16,}/g, "[redacted]"],
20
+ [/\bgithub_pat_[A-Za-z0-9_]{20,}/g, "[redacted]"],
21
+ [/\bglpat-[A-Za-z0-9_-]{16,}/g, "[redacted]"],
22
+ [/\b(?:xox[baprs]-)[A-Za-z0-9-]{10,}/g, "[redacted]"],
23
+ [/\bAKIA[0-9A-Z]{16}\b/g, "[redacted]"],
24
+ ];
25
+
26
+ /** Remove credentials from free text. */
27
+ export function redactSecrets(text: string): string {
28
+ let out = text;
29
+ for (const [re, replacement] of PATTERNS) out = out.replace(re, replacement);
30
+ return out;
31
+ }
32
+
33
+ /** Recursively redact strings in a value destined for a log record. */
34
+ export function redactValue<T>(value: T): T {
35
+ if (typeof value === "string") return redactSecrets(value) as unknown as T;
36
+ if (Array.isArray(value)) return value.map((v) => redactValue(v)) as unknown as T;
37
+ if (value !== null && typeof value === "object") {
38
+ const out: Record<string, unknown> = {};
39
+ for (const [k, v] of Object.entries(value)) out[k] = redactValue(v);
40
+ return out as unknown as T;
41
+ }
42
+ return value;
43
+ }
@@ -0,0 +1,75 @@
1
+ const UNITS: Record<string, number> = {
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
+ /**
11
+ * Parse a duration like "30m", "2h", "7d", "1h30m", "0" or a bare number of
12
+ * milliseconds. Throws on anything else so a typo in a config file is loud
13
+ * rather than silently becoming 0 (which would disable a safety gate).
14
+ */
15
+ export function parseDuration(input: string | number): number {
16
+ if (typeof input === "number") {
17
+ if (!Number.isFinite(input) || input < 0) throw new Error(`invalid duration: ${input}`);
18
+ return Math.round(input);
19
+ }
20
+ const text = input.trim().toLowerCase();
21
+ if (text === "" ) throw new Error("invalid duration: empty string");
22
+ if (/^\d+$/.test(text)) return Number(text);
23
+
24
+ const re = /(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)/g;
25
+ let total = 0;
26
+ let matched = 0;
27
+ let consumed = 0;
28
+ for (const m of text.matchAll(re)) {
29
+ const [whole, value, unit] = m;
30
+ total += Number(value) * UNITS[unit as string]!;
31
+ matched += 1;
32
+ consumed += whole.length;
33
+ }
34
+ const stripped = text.replace(/[\s,]/g, "");
35
+ if (matched === 0 || consumed < stripped.length) {
36
+ throw new Error(`invalid duration: ${input} (use e.g. 30m, 2h, 7d)`);
37
+ }
38
+ return Math.round(total);
39
+ }
40
+
41
+ /** "3m", "2h 10m", "4d" — compact, at most two units. */
42
+ export function formatDuration(ms: number): string {
43
+ if (!Number.isFinite(ms) || ms < 0) return "?";
44
+ if (ms < 1_000) return `${Math.round(ms)}ms`;
45
+ const d = Math.floor(ms / UNITS.d!);
46
+ const h = Math.floor((ms % UNITS.d!) / UNITS.h!);
47
+ const m = Math.floor((ms % UNITS.h!) / UNITS.m!);
48
+ const s = Math.floor((ms % UNITS.m!) / UNITS.s!);
49
+ if (d > 0) return h > 0 ? `${d}d ${h}h` : `${d}d`;
50
+ if (h > 0) return m > 0 ? `${h}h ${m}m` : `${h}h`;
51
+ if (m > 0) return `${m}m`;
52
+ return `${s}s`;
53
+ }
54
+
55
+ /** "12m ago" / "just now" */
56
+ export function formatAgo(timestamp: number, now = Date.now()): string {
57
+ const delta = now - timestamp;
58
+ if (delta < 45_000) return "just now";
59
+ return `${formatDuration(delta)} ago`;
60
+ }
61
+
62
+ export function formatClock(timestamp: number): string {
63
+ const d = new Date(timestamp);
64
+ const pad = (n: number) => String(n).padStart(2, "0");
65
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
66
+ }
67
+
68
+ export function formatStamp(timestamp: number): string {
69
+ const d = new Date(timestamp);
70
+ const pad = (n: number) => String(n).padStart(2, "0");
71
+ return (
72
+ `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}` +
73
+ `-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`
74
+ );
75
+ }