@de-otio/repo-aegis-core 0.2.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 (191) hide show
  1. package/dist/age.d.ts +32 -0
  2. package/dist/age.d.ts.map +1 -0
  3. package/dist/age.js +98 -0
  4. package/dist/age.js.map +1 -0
  5. package/dist/audit-log.d.ts +50 -0
  6. package/dist/audit-log.d.ts.map +1 -0
  7. package/dist/audit-log.js +183 -0
  8. package/dist/audit-log.js.map +1 -0
  9. package/dist/audit-log.test.d.ts +2 -0
  10. package/dist/audit-log.test.d.ts.map +1 -0
  11. package/dist/audit-log.test.js +181 -0
  12. package/dist/audit-log.test.js.map +1 -0
  13. package/dist/deny-set.d.ts +43 -0
  14. package/dist/deny-set.d.ts.map +1 -0
  15. package/dist/deny-set.js +165 -0
  16. package/dist/deny-set.js.map +1 -0
  17. package/dist/deny-set.test.d.ts +2 -0
  18. package/dist/deny-set.test.d.ts.map +1 -0
  19. package/dist/deny-set.test.js +155 -0
  20. package/dist/deny-set.test.js.map +1 -0
  21. package/dist/exceptions.d.ts +96 -0
  22. package/dist/exceptions.d.ts.map +1 -0
  23. package/dist/exceptions.js +143 -0
  24. package/dist/exceptions.js.map +1 -0
  25. package/dist/exit-codes.d.ts +4 -0
  26. package/dist/exit-codes.d.ts.map +1 -0
  27. package/dist/exit-codes.js +6 -0
  28. package/dist/exit-codes.js.map +1 -0
  29. package/dist/first-touch.d.ts +57 -0
  30. package/dist/first-touch.d.ts.map +1 -0
  31. package/dist/first-touch.js +112 -0
  32. package/dist/first-touch.js.map +1 -0
  33. package/dist/import-graph.test.d.ts +2 -0
  34. package/dist/import-graph.test.d.ts.map +1 -0
  35. package/dist/import-graph.test.js +210 -0
  36. package/dist/import-graph.test.js.map +1 -0
  37. package/dist/index.d.ts +37 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +68 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/lock.d.ts +22 -0
  42. package/dist/lock.d.ts.map +1 -0
  43. package/dist/lock.js +86 -0
  44. package/dist/lock.js.map +1 -0
  45. package/dist/lock.test.d.ts +2 -0
  46. package/dist/lock.test.d.ts.map +1 -0
  47. package/dist/lock.test.js +125 -0
  48. package/dist/lock.test.js.map +1 -0
  49. package/dist/paths.d.ts +22 -0
  50. package/dist/paths.d.ts.map +1 -0
  51. package/dist/paths.js +46 -0
  52. package/dist/paths.js.map +1 -0
  53. package/dist/paths.test.d.ts +2 -0
  54. package/dist/paths.test.d.ts.map +1 -0
  55. package/dist/paths.test.js +78 -0
  56. package/dist/paths.test.js.map +1 -0
  57. package/dist/redaction.d.ts +29 -0
  58. package/dist/redaction.d.ts.map +1 -0
  59. package/dist/redaction.js +48 -0
  60. package/dist/redaction.js.map +1 -0
  61. package/dist/redaction.test.d.ts +2 -0
  62. package/dist/redaction.test.d.ts.map +1 -0
  63. package/dist/redaction.test.js +67 -0
  64. package/dist/redaction.test.js.map +1 -0
  65. package/dist/regex-safety.d.ts +87 -0
  66. package/dist/regex-safety.d.ts.map +1 -0
  67. package/dist/regex-safety.js +322 -0
  68. package/dist/regex-safety.js.map +1 -0
  69. package/dist/regex-safety.test.d.ts +2 -0
  70. package/dist/regex-safety.test.d.ts.map +1 -0
  71. package/dist/regex-safety.test.js +149 -0
  72. package/dist/regex-safety.test.js.map +1 -0
  73. package/dist/registry-mutate.d.ts +35 -0
  74. package/dist/registry-mutate.d.ts.map +1 -0
  75. package/dist/registry-mutate.js +149 -0
  76. package/dist/registry-mutate.js.map +1 -0
  77. package/dist/registry-mutate.test.d.ts +2 -0
  78. package/dist/registry-mutate.test.d.ts.map +1 -0
  79. package/dist/registry-mutate.test.js +96 -0
  80. package/dist/registry-mutate.test.js.map +1 -0
  81. package/dist/registry.d.ts +64 -0
  82. package/dist/registry.d.ts.map +1 -0
  83. package/dist/registry.js +120 -0
  84. package/dist/registry.js.map +1 -0
  85. package/dist/registry.test.d.ts +2 -0
  86. package/dist/registry.test.d.ts.map +1 -0
  87. package/dist/registry.test.js +316 -0
  88. package/dist/registry.test.js.map +1 -0
  89. package/dist/remote-url.d.ts +18 -0
  90. package/dist/remote-url.d.ts.map +1 -0
  91. package/dist/remote-url.js +66 -0
  92. package/dist/remote-url.js.map +1 -0
  93. package/dist/remote-url.test.d.ts +2 -0
  94. package/dist/remote-url.test.d.ts.map +1 -0
  95. package/dist/remote-url.test.js +116 -0
  96. package/dist/remote-url.test.js.map +1 -0
  97. package/dist/render.d.ts +54 -0
  98. package/dist/render.d.ts.map +1 -0
  99. package/dist/render.js +182 -0
  100. package/dist/render.js.map +1 -0
  101. package/dist/render.test.d.ts +2 -0
  102. package/dist/render.test.d.ts.map +1 -0
  103. package/dist/render.test.js +152 -0
  104. package/dist/render.test.js.map +1 -0
  105. package/dist/repo.d.ts +40 -0
  106. package/dist/repo.d.ts.map +1 -0
  107. package/dist/repo.js +214 -0
  108. package/dist/repo.js.map +1 -0
  109. package/dist/repo.test.d.ts +2 -0
  110. package/dist/repo.test.d.ts.map +1 -0
  111. package/dist/repo.test.js +234 -0
  112. package/dist/repo.test.js.map +1 -0
  113. package/dist/scan.d.ts +103 -0
  114. package/dist/scan.d.ts.map +1 -0
  115. package/dist/scan.js +436 -0
  116. package/dist/scan.js.map +1 -0
  117. package/dist/scan.test.d.ts +2 -0
  118. package/dist/scan.test.d.ts.map +1 -0
  119. package/dist/scan.test.js +437 -0
  120. package/dist/scan.test.js.map +1 -0
  121. package/dist/schemas.d.ts +50 -0
  122. package/dist/schemas.d.ts.map +1 -0
  123. package/dist/schemas.js +190 -0
  124. package/dist/schemas.js.map +1 -0
  125. package/dist/secret-markers.d.ts +34 -0
  126. package/dist/secret-markers.d.ts.map +1 -0
  127. package/dist/secret-markers.js +118 -0
  128. package/dist/secret-markers.js.map +1 -0
  129. package/dist/secret-markers.test.d.ts +2 -0
  130. package/dist/secret-markers.test.d.ts.map +1 -0
  131. package/dist/secret-markers.test.js +154 -0
  132. package/dist/secret-markers.test.js.map +1 -0
  133. package/dist/trust-boundary.d.ts +33 -0
  134. package/dist/trust-boundary.d.ts.map +1 -0
  135. package/dist/trust-boundary.js +77 -0
  136. package/dist/trust-boundary.js.map +1 -0
  137. package/dist/trust-boundary.test.d.ts +2 -0
  138. package/dist/trust-boundary.test.d.ts.map +1 -0
  139. package/dist/trust-boundary.test.js +170 -0
  140. package/dist/trust-boundary.test.js.map +1 -0
  141. package/dist/types.d.ts +47 -0
  142. package/dist/types.d.ts.map +1 -0
  143. package/dist/types.js +8 -0
  144. package/dist/types.js.map +1 -0
  145. package/dist/working-tree.d.ts +38 -0
  146. package/dist/working-tree.d.ts.map +1 -0
  147. package/dist/working-tree.js +133 -0
  148. package/dist/working-tree.js.map +1 -0
  149. package/dist/working-tree.test.d.ts +2 -0
  150. package/dist/working-tree.test.d.ts.map +1 -0
  151. package/dist/working-tree.test.js +162 -0
  152. package/dist/working-tree.test.js.map +1 -0
  153. package/package.json +40 -0
  154. package/src/age.ts +113 -0
  155. package/src/audit-log.test.ts +222 -0
  156. package/src/audit-log.ts +215 -0
  157. package/src/deny-set.test.ts +208 -0
  158. package/src/deny-set.ts +231 -0
  159. package/src/exceptions.ts +134 -0
  160. package/src/exit-codes.ts +5 -0
  161. package/src/first-touch.ts +172 -0
  162. package/src/import-graph.test.ts +239 -0
  163. package/src/index.ts +191 -0
  164. package/src/lock.test.ts +151 -0
  165. package/src/lock.ts +88 -0
  166. package/src/paths.test.ts +94 -0
  167. package/src/paths.ts +55 -0
  168. package/src/redaction.test.ts +81 -0
  169. package/src/redaction.ts +49 -0
  170. package/src/regex-safety.test.ts +194 -0
  171. package/src/regex-safety.ts +349 -0
  172. package/src/registry-mutate.test.ts +134 -0
  173. package/src/registry-mutate.ts +185 -0
  174. package/src/registry.test.ts +460 -0
  175. package/src/registry.ts +178 -0
  176. package/src/remote-url.test.ts +121 -0
  177. package/src/remote-url.ts +78 -0
  178. package/src/render.test.ts +206 -0
  179. package/src/render.ts +215 -0
  180. package/src/repo.test.ts +275 -0
  181. package/src/repo.ts +245 -0
  182. package/src/scan.test.ts +580 -0
  183. package/src/scan.ts +531 -0
  184. package/src/schemas.ts +207 -0
  185. package/src/secret-markers.test.ts +183 -0
  186. package/src/secret-markers.ts +145 -0
  187. package/src/trust-boundary.test.ts +198 -0
  188. package/src/trust-boundary.ts +98 -0
  189. package/src/types.ts +55 -0
  190. package/src/working-tree.test.ts +193 -0
  191. package/src/working-tree.ts +130 -0
package/src/repo.ts ADDED
@@ -0,0 +1,245 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Richard Myers and contributors.
3
+ import { execFileSync } from "node:child_process";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { parse } from "yaml";
7
+ import { z } from "zod";
8
+ import { NotAGitRepoError } from "./exceptions.js";
9
+ import {
10
+ repoOverrideSchema,
11
+ REPO_CLASSES as REPO_CLASSES_SCHEMA,
12
+ formatZodError,
13
+ type RepoClassLiteral,
14
+ } from "./schemas.js";
15
+
16
+ export type RepoClass = RepoClassLiteral;
17
+
18
+ export const REPO_CLASSES: readonly RepoClass[] = REPO_CLASSES_SCHEMA;
19
+
20
+ export interface RepoConfig {
21
+ cwd: string;
22
+ isGitRepo: boolean;
23
+ class: RepoClass;
24
+ classExplicit: boolean;
25
+ engagements: string[];
26
+ /** True when class came from a checked-in `.repo-aegis.yml`. */
27
+ classFromOverride?: boolean;
28
+ /** True when engagements came from a checked-in `.repo-aegis.yml`. */
29
+ engagementsFromOverride?: boolean;
30
+ }
31
+
32
+ export interface RepoOverride {
33
+ class?: RepoClass;
34
+ engagements?: string[];
35
+ }
36
+
37
+ export const OVERRIDE_FILENAME = ".repo-aegis.yml";
38
+
39
+ export class RepoOverrideError extends Error {
40
+ readonly code = "REPO_OVERRIDE_PARSE_ERROR";
41
+ constructor(public readonly path: string, message: string) {
42
+ super(`${path}: ${message}`);
43
+ }
44
+ }
45
+
46
+ function loadOverride(cwd: string): { override: RepoOverride; path: string } | null {
47
+ // Look for `.repo-aegis.yml` at git toplevel; fall back to cwd if not in a
48
+ // git repo (so the override still works in scratch dirs).
49
+ let root = cwd;
50
+ try {
51
+ const top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
52
+ cwd,
53
+ encoding: "utf8",
54
+ stdio: ["ignore", "pipe", "ignore"],
55
+ }).trim();
56
+ if (top) root = top;
57
+ } catch {
58
+ /* not a git repo; use cwd as root */
59
+ }
60
+ const path = join(root, OVERRIDE_FILENAME);
61
+ if (!existsSync(path)) return null;
62
+
63
+ let parsed: unknown;
64
+ try {
65
+ parsed = parse(readFileSync(path, "utf8"));
66
+ } catch (err) {
67
+ throw new RepoOverrideError(path, `failed to parse YAML: ${(err as Error).message}`);
68
+ }
69
+ if (parsed === null || parsed === undefined) {
70
+ return { override: {}, path };
71
+ }
72
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
73
+ throw new RepoOverrideError(path, "must be a YAML mapping");
74
+ }
75
+
76
+ let validated;
77
+ try {
78
+ validated = repoOverrideSchema.parse(parsed);
79
+ } catch (err) {
80
+ if (err instanceof z.ZodError) {
81
+ throw new RepoOverrideError(path, formatZodError(err, "override"));
82
+ }
83
+ throw err;
84
+ }
85
+
86
+ const out: RepoOverride = {};
87
+ if (validated.class !== undefined) out.class = validated.class;
88
+ if (validated.engagements !== undefined) out.engagements = validated.engagements;
89
+ return { override: out, path };
90
+ }
91
+
92
+ interface GitResult {
93
+ ok: boolean;
94
+ stdout: string;
95
+ }
96
+
97
+ function git(cwd: string, args: string[]): GitResult {
98
+ try {
99
+ const stdout = execFileSync("git", args, {
100
+ cwd,
101
+ encoding: "utf8",
102
+ stdio: ["ignore", "pipe", "ignore"],
103
+ }).trim();
104
+ return { ok: true, stdout };
105
+ } catch {
106
+ return { ok: false, stdout: "" };
107
+ }
108
+ }
109
+
110
+ function isValidClass(s: string): s is RepoClass {
111
+ return (REPO_CLASSES as readonly string[]).includes(s);
112
+ }
113
+
114
+ export function readRepoConfig(cwd: string = process.cwd()): RepoConfig {
115
+ const inside = git(cwd, ["rev-parse", "--is-inside-work-tree"]);
116
+ const isGitRepo = inside.ok && inside.stdout === "true";
117
+
118
+ // .repo-aegis.yml may exist even outside a git repo (scratch dir);
119
+ // load it either way. Throws RepoOverrideError on malformed YAML.
120
+ const overrideResult = loadOverride(cwd);
121
+ const override = overrideResult?.override ?? {};
122
+
123
+ if (!isGitRepo) {
124
+ return {
125
+ cwd,
126
+ isGitRepo: false,
127
+ class: override.class ?? "private-strict",
128
+ classExplicit: override.class !== undefined,
129
+ engagements: override.engagements ?? [],
130
+ ...(override.class !== undefined && { classFromOverride: true }),
131
+ ...(override.engagements !== undefined && { engagementsFromOverride: true }),
132
+ };
133
+ }
134
+
135
+ // Precedence: git config wins where set, .repo-aegis.yml provides
136
+ // the project-default fallback. This matches the .editorconfig
137
+ // model: the file in the repo declares intent; per-clone git
138
+ // config can override locally without changing the file.
139
+ const cls = git(cwd, ["config", "--get", "repo-aegis.class"]);
140
+ const eng = git(cwd, ["config", "--get-all", "repo-aegis.engagement"]);
141
+ const cfgClassSet = cls.ok && cls.stdout !== "";
142
+ const cfgEngagements = eng.ok && eng.stdout ? eng.stdout.split("\n").filter(Boolean) : [];
143
+
144
+ let classValue: RepoClass;
145
+ let classExplicit = false;
146
+ let classFromOverride = false;
147
+ if (cfgClassSet && isValidClass(cls.stdout)) {
148
+ classValue = cls.stdout;
149
+ classExplicit = true;
150
+ } else if (override.class !== undefined) {
151
+ classValue = override.class;
152
+ classExplicit = true;
153
+ classFromOverride = true;
154
+ } else {
155
+ classValue = "private-strict";
156
+ }
157
+
158
+ let engagements: string[];
159
+ let engagementsFromOverride = false;
160
+ if (cfgEngagements.length > 0) {
161
+ engagements = cfgEngagements;
162
+ } else if (override.engagements !== undefined) {
163
+ engagements = override.engagements;
164
+ engagementsFromOverride = true;
165
+ } else {
166
+ engagements = [];
167
+ }
168
+
169
+ return {
170
+ cwd,
171
+ isGitRepo: true,
172
+ class: classValue,
173
+ classExplicit,
174
+ engagements,
175
+ ...(classFromOverride && { classFromOverride: true }),
176
+ ...(engagementsFromOverride && { engagementsFromOverride: true }),
177
+ };
178
+ }
179
+
180
+ function escapeRegex(s: string): string {
181
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
182
+ }
183
+
184
+ /**
185
+ * Idempotent add. Returns true if added, false if already present.
186
+ */
187
+ export function addEngagement(id: string, cwd: string = process.cwd()): boolean {
188
+ const cfg = readRepoConfig(cwd);
189
+ if (!cfg.isGitRepo) throw new NotAGitRepoError();
190
+ if (cfg.engagements.includes(id)) return false;
191
+ execFileSync("git", ["config", "--add", "repo-aegis.engagement", id], { cwd });
192
+ return true;
193
+ }
194
+
195
+ /**
196
+ * Idempotent multi-add. Returns the list of ids that were newly added.
197
+ */
198
+ export function addEngagements(ids: string[], cwd: string = process.cwd()): string[] {
199
+ const cfg = readRepoConfig(cwd);
200
+ if (!cfg.isGitRepo) throw new NotAGitRepoError();
201
+ const existing = new Set(cfg.engagements);
202
+ const added: string[] = [];
203
+ for (const id of ids) {
204
+ if (existing.has(id)) continue;
205
+ execFileSync("git", ["config", "--add", "repo-aegis.engagement", id], { cwd });
206
+ existing.add(id);
207
+ added.push(id);
208
+ }
209
+ return added;
210
+ }
211
+
212
+ /**
213
+ * Returns true if removed, false if not present.
214
+ */
215
+ export function removeEngagement(id: string, cwd: string = process.cwd()): boolean {
216
+ const cfg = readRepoConfig(cwd);
217
+ if (!cfg.isGitRepo) throw new NotAGitRepoError();
218
+ if (!cfg.engagements.includes(id)) return false;
219
+ execFileSync(
220
+ "git",
221
+ ["config", "--unset-all", "repo-aegis.engagement", `^${escapeRegex(id)}$`],
222
+ { cwd },
223
+ );
224
+ return true;
225
+ }
226
+
227
+ export function setClass(cls: RepoClass, cwd: string = process.cwd()): void {
228
+ const cfg = readRepoConfig(cwd);
229
+ if (!cfg.isGitRepo) throw new NotAGitRepoError();
230
+ execFileSync("git", ["config", "repo-aegis.class", cls], { cwd });
231
+ }
232
+
233
+ export function unsetClass(cwd: string = process.cwd()): void {
234
+ const cfg = readRepoConfig(cwd);
235
+ if (!cfg.isGitRepo) throw new NotAGitRepoError();
236
+ // --unset is idempotent enough; ignore "no value found" errors.
237
+ try {
238
+ execFileSync("git", ["config", "--unset", "repo-aegis.class"], {
239
+ cwd,
240
+ stdio: ["ignore", "ignore", "ignore"],
241
+ });
242
+ } catch {
243
+ /* not set; nothing to do */
244
+ }
245
+ }