baller-maester 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/dist/index.js ADDED
@@ -0,0 +1,1819 @@
1
+ import { existsSync, promises } from 'fs';
2
+ import { readFile, mkdir, writeFile, rm, mkdtemp, readdir, cp, rename } from 'fs/promises';
3
+ import { parseDocument, isMap } from 'yaml';
4
+ import path, { resolve, dirname, relative, extname } from 'path';
5
+ import { z } from 'zod';
6
+ import { execFile as execFile$1 } from 'child_process';
7
+ import { promisify } from 'util';
8
+ import { simpleGit } from 'simple-git';
9
+ import picomatch from 'picomatch';
10
+ import matter from 'gray-matter';
11
+
12
+ var __defProp = Object.defineProperty;
13
+ var __export = (target, all) => {
14
+ for (var name in all)
15
+ __defProp(target, name, { get: all[name], enumerable: true });
16
+ };
17
+ var STATE_VALUES = ["draft", "canon"];
18
+ var StateSchema = z.enum(STATE_VALUES);
19
+ var DEFAULT_STATE = "draft";
20
+ function parseState(value) {
21
+ if (value === void 0 || value === null) return { kind: "absent" };
22
+ const raw = typeof value === "string" ? value : String(value);
23
+ if (raw.length === 0) return { kind: "absent" };
24
+ const result = StateSchema.safeParse(raw);
25
+ if (result.success) return { kind: "valid", value: result.data };
26
+ return { kind: "invalid", raw };
27
+ }
28
+
29
+ // src/schemas/citadel.ts
30
+ var SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
31
+ var ENV_VAR_RE = /^[A-Z][A-Z0-9_]*$/;
32
+ var URL_FORMS = [/^https:\/\/\S+$/i, /^ssh:\/\/\S+$/i, /^git@[^\s:]+:\S+$/, /^file:\/\/\S+$/i];
33
+ function isValidGitUrl(url) {
34
+ if (/\s/.test(url)) return false;
35
+ return URL_FORMS.some((re) => re.test(url));
36
+ }
37
+ function isSafeRelativePath(value) {
38
+ if (value.length === 0) return false;
39
+ if (value.startsWith("/")) return false;
40
+ if (value.split(/[\\/]+/).some((seg) => seg === "..")) return false;
41
+ return true;
42
+ }
43
+ function isSafeIncludesEntry(value) {
44
+ if (value.length === 0 || /^\s+$/.test(value)) return false;
45
+ if (value.startsWith("/")) return false;
46
+ if (value.split(/[\\/]+/).some((seg) => seg === "..")) return false;
47
+ return true;
48
+ }
49
+ var AuthRefNoneSchema = z.object({
50
+ type: z.literal("none")
51
+ }).strict();
52
+ var AuthRefTokenSchema = z.object({
53
+ type: z.literal("token"),
54
+ envVar: z.string().min(1, "envVar is required for token auth").regex(ENV_VAR_RE, "envVar must be uppercase letters, digits, and underscores")
55
+ }).strict();
56
+ var AuthRefSchema = z.discriminatedUnion("type", [AuthRefNoneSchema, AuthRefTokenSchema]);
57
+ var IncludesPathSchema = z.string().min(1).refine(
58
+ isSafeIncludesEntry,
59
+ "includes entry must be a repo-relative path or glob; no leading '/' and no '..'"
60
+ );
61
+ var IncludeEntryObjectSchema = z.object({
62
+ path: IncludesPathSchema,
63
+ state: StateSchema.optional()
64
+ }).strict();
65
+ var IncludeEntrySchema = z.union([IncludesPathSchema, IncludeEntryObjectSchema]);
66
+ function normalizeIncludeEntry(entry) {
67
+ if (typeof entry === "string") return { path: entry };
68
+ if (entry.state === void 0) return { path: entry.path };
69
+ return { path: entry.path, state: entry.state };
70
+ }
71
+ var SourceSchema = z.object({
72
+ name: z.string().min(1).regex(SLUG_RE, "name must be a kebab-case slug starting with a letter or digit"),
73
+ url: z.string().refine(isValidGitUrl, "url must be https://, ssh://, or git@host:path"),
74
+ ref: z.string().min(1).optional(),
75
+ includes: z.array(IncludeEntrySchema).min(1, "includes must declare at least one entry when present").optional(),
76
+ auth: AuthRefSchema.optional(),
77
+ destination: z.string().min(1).refine(isSafeRelativePath, "destination must be a repo-relative path with no '..' segments").optional(),
78
+ description: z.string().min(1).optional(),
79
+ tags: z.array(z.string().min(1).regex(SLUG_RE, "tags must be slugs")).optional()
80
+ }).strict();
81
+ var DEFAULT_BASE_DIR = "citadel";
82
+ function applyCombinedInvariants(data, ctx) {
83
+ if (data.sources.length === 0) {
84
+ ctx.addIssue({
85
+ code: z.ZodIssueCode.custom,
86
+ message: "citadel must declare at least one source",
87
+ path: ["sources"]
88
+ });
89
+ return;
90
+ }
91
+ const baseDir = data.baseDir ?? DEFAULT_BASE_DIR;
92
+ const namesSeen = /* @__PURE__ */ new Map();
93
+ const destsSeen = /* @__PURE__ */ new Map();
94
+ for (let i = 0; i < data.sources.length; i++) {
95
+ const entry = data.sources[i];
96
+ if (!entry?.name) continue;
97
+ const priorIndex = namesSeen.get(entry.name);
98
+ if (priorIndex !== void 0) {
99
+ ctx.addIssue({
100
+ code: z.ZodIssueCode.custom,
101
+ message: `duplicate name '${entry.name}' \u2014 also used by sources[${priorIndex}]`,
102
+ path: ["sources", i, "name"]
103
+ });
104
+ } else {
105
+ namesSeen.set(entry.name, i);
106
+ }
107
+ const resolved = entry.destination ? resolve("/_citadel_root_", entry.destination) : resolve("/_citadel_root_", baseDir, entry.name);
108
+ const prior = destsSeen.get(resolved);
109
+ if (prior !== void 0) {
110
+ ctx.addIssue({
111
+ code: z.ZodIssueCode.custom,
112
+ message: `destination collision: sources '${entry.name}' and '${prior.name}' (sources[${prior.index}]) both resolve to the same path`,
113
+ path: ["sources", i, "destination"]
114
+ });
115
+ } else {
116
+ destsSeen.set(resolved, { index: i, name: entry.name });
117
+ }
118
+ }
119
+ }
120
+ var CitadelConfigSchema = z.object({
121
+ schemaVersion: z.literal(1),
122
+ baseDir: z.string().min(1).refine(isSafeRelativePath, "baseDir must be a repo-relative path with no '..' segments").optional(),
123
+ sources: z.array(SourceSchema).optional().default([])
124
+ }).strict().superRefine((data, ctx) => {
125
+ applyCombinedInvariants(data, ctx);
126
+ });
127
+ function isSafeRepoRelative(value) {
128
+ if (value.length === 0 || /^\s+$/.test(value)) return false;
129
+ if (value.startsWith("/")) return false;
130
+ if (value.split(/[\\/]+/).some((seg) => seg === "..")) return false;
131
+ return true;
132
+ }
133
+ var PublishedDocumentSchema = z.object({
134
+ path: z.string().min(1).refine(
135
+ isSafeRepoRelative,
136
+ "path must be a repo-relative file or glob; no leading '/' and no '..'"
137
+ ),
138
+ description: z.string().min(1).optional(),
139
+ category: z.string().min(1).regex(SLUG_RE, "category must be a kebab-case slug").optional(),
140
+ tags: z.array(z.string().min(1).regex(SLUG_RE, "tags must be slugs")).optional(),
141
+ state: StateSchema.optional()
142
+ }).strict();
143
+ var MaesterConfigSchema = z.object({
144
+ schemaVersion: z.literal(1),
145
+ documents: z.array(PublishedDocumentSchema).min(1, "at least one published document must be declared").superRefine((docs, ctx) => {
146
+ const seen = /* @__PURE__ */ new Map();
147
+ for (let i = 0; i < docs.length; i++) {
148
+ const p = docs[i]?.path;
149
+ if (!p) continue;
150
+ const prior = seen.get(p);
151
+ if (prior !== void 0) {
152
+ ctx.addIssue({
153
+ code: z.ZodIssueCode.custom,
154
+ message: `duplicate path '${p}' (also at index ${prior})`,
155
+ path: [i, "path"]
156
+ });
157
+ } else {
158
+ seen.set(p, i);
159
+ }
160
+ }
161
+ })
162
+ }).strict();
163
+
164
+ // src/core/errors.ts
165
+ var MaesterError = class extends Error {
166
+ code;
167
+ cause;
168
+ constructor(code, message, options) {
169
+ super(message);
170
+ this.name = "MaesterError";
171
+ this.code = code;
172
+ if (options?.cause !== void 0) {
173
+ this.cause = options.cause;
174
+ }
175
+ }
176
+ };
177
+ var ConfigError = class extends MaesterError {
178
+ filePath;
179
+ line;
180
+ column;
181
+ constructor(message, detail = {}) {
182
+ super("CONFIG_ERROR", message, detail.cause !== void 0 ? { cause: detail.cause } : {});
183
+ this.name = "ConfigError";
184
+ this.filePath = detail.filePath;
185
+ if (detail.line !== void 0) this.line = detail.line;
186
+ if (detail.column !== void 0) this.column = detail.column;
187
+ }
188
+ };
189
+ var AuthError = class extends MaesterError {
190
+ envVar;
191
+ constructor(envVar, message) {
192
+ super("AUTH_ERROR", message ?? `Environment variable ${envVar} is not set.`);
193
+ this.name = "AuthError";
194
+ this.envVar = envVar;
195
+ }
196
+ };
197
+ var RefNotFoundError = class extends MaesterError {
198
+ ref;
199
+ url;
200
+ constructor(ref, url, cause) {
201
+ super(
202
+ "REF_NOT_FOUND",
203
+ `ref \`${ref}\` not found on \`${url}\``,
204
+ cause !== void 0 ? { cause } : {}
205
+ );
206
+ this.name = "RefNotFoundError";
207
+ this.ref = ref;
208
+ this.url = url;
209
+ }
210
+ };
211
+ var DestinationBlockedError = class extends MaesterError {
212
+ destination;
213
+ constructor(destination, message) {
214
+ super("DESTINATION_BLOCKED", message);
215
+ this.name = "DestinationBlockedError";
216
+ this.destination = destination;
217
+ }
218
+ };
219
+ var CITADEL_CONFIG_FILENAME = "citadel.yaml";
220
+ var MAESTER_CONFIG_FILENAME = "maester.yaml";
221
+ var CACHE_DIR_NAME = ".maester";
222
+ var CACHE_SUBDIR = ".maester/cache";
223
+ function citadelConfigPath(repoRoot) {
224
+ return resolve(repoRoot, CITADEL_CONFIG_FILENAME);
225
+ }
226
+ function maesterConfigPath(repoRoot) {
227
+ return resolve(repoRoot, MAESTER_CONFIG_FILENAME);
228
+ }
229
+ function cachePathForSource(repoRoot, sourceName) {
230
+ return resolve(repoRoot, CACHE_SUBDIR, sourceName);
231
+ }
232
+ function defaultDestinationFor(repoRoot, sourceName, baseDir) {
233
+ return resolve(repoRoot, baseDir ?? DEFAULT_BASE_DIR, sourceName);
234
+ }
235
+
236
+ // src/core/config/loader.ts
237
+ async function loadCitadelConfig(repoRoot) {
238
+ const path4 = citadelConfigPath(repoRoot);
239
+ if (!existsSync(path4)) {
240
+ throw new ConfigError(
241
+ "No citadel.yaml found at the repository root. Run `npx maester init` to create one.",
242
+ { filePath: path4 }
243
+ );
244
+ }
245
+ const raw = await readFile(path4, "utf8");
246
+ return parseAndValidate(raw, CitadelConfigSchema, path4);
247
+ }
248
+ async function loadMaesterConfig(repoRoot) {
249
+ const path4 = maesterConfigPath(repoRoot);
250
+ if (!existsSync(path4)) {
251
+ throw new ConfigError(
252
+ "No maester.yaml found at the repository root. Run `npx maester publish` to create one.",
253
+ { filePath: path4 }
254
+ );
255
+ }
256
+ const raw = await readFile(path4, "utf8");
257
+ return parseAndValidate(raw, MaesterConfigSchema, path4);
258
+ }
259
+ function parseAndValidate(raw, schema, filePath) {
260
+ const data = parseYaml(raw, filePath);
261
+ return runSchema(data, schema, filePath);
262
+ }
263
+ function parseYaml(raw, filePath) {
264
+ const doc = parseDocument(raw, { keepSourceTokens: false });
265
+ const yamlErrors = doc.errors;
266
+ if (yamlErrors.length > 0) {
267
+ const first = yamlErrors[0];
268
+ const pos = positionFromError(first, raw);
269
+ throw new ConfigError(`YAML parse error: ${first.message}`, {
270
+ filePath,
271
+ line: pos.line,
272
+ column: pos.column,
273
+ cause: first
274
+ });
275
+ }
276
+ return doc.toJS({ maxAliasCount: -1 });
277
+ }
278
+ function runSchema(data, schema, filePath) {
279
+ const result = schema.safeParse(data);
280
+ if (!result.success) {
281
+ const issue = result.error.issues[0];
282
+ const where = issue?.path?.length ? ` at \`${issue.path.join(".")}\`` : "";
283
+ throw new ConfigError(`${filePath}: ${issue?.message ?? "validation failed"}${where}`, {
284
+ filePath,
285
+ cause: result.error
286
+ });
287
+ }
288
+ return result.data;
289
+ }
290
+ function positionFromError(err, raw) {
291
+ const pos = err.pos;
292
+ if (!pos) return { line: 1, column: 1 };
293
+ const offset = pos[0];
294
+ let line = 1;
295
+ let lastLineStart = 0;
296
+ for (let i = 0; i < offset && i < raw.length; i++) {
297
+ if (raw[i] === "\n") {
298
+ line++;
299
+ lastLineStart = i + 1;
300
+ }
301
+ }
302
+ return { line, column: offset - lastLineStart + 1 };
303
+ }
304
+
305
+ // src/core/auth/resolver.ts
306
+ function resolveAuth(auth, env = process.env) {
307
+ if (!auth || auth.type === "none") return { type: "delegated" };
308
+ const value = env[auth.envVar];
309
+ if (value === void 0 || value.length === 0) {
310
+ throw new AuthError(
311
+ auth.envVar,
312
+ `${auth.envVar} is not set. Define it in your shell, .env loader, or CI secret manager before syncing.`
313
+ );
314
+ }
315
+ return { type: "token", value };
316
+ }
317
+ var execFile = promisify(execFile$1);
318
+ var cachedCapabilities;
319
+ async function detectGitCapabilities() {
320
+ if (cachedCapabilities) return cachedCapabilities;
321
+ const { stdout } = await execFile("git", ["--version"]);
322
+ const match = stdout.match(/git version (\d+)\.(\d+)(?:\.(\d+))?/);
323
+ if (!match) {
324
+ cachedCapabilities = { version: stdout.trim(), supportsPartialClone: false };
325
+ return cachedCapabilities;
326
+ }
327
+ const major = Number(match[1] ?? 0);
328
+ const minor = Number(match[2] ?? 0);
329
+ const supportsPartialClone = major > 2 || major === 2 && minor >= 27;
330
+ cachedCapabilities = { version: `${major}.${minor}`, supportsPartialClone };
331
+ return cachedCapabilities;
332
+ }
333
+ async function shallowSparseClone(input) {
334
+ await mkdir(input.destination, { recursive: true });
335
+ const caps = await detectGitCapabilities();
336
+ const url = input.useTokenInUrl ? injectToken(input.url, input.useTokenInUrl) : input.url;
337
+ const git = simpleGit(input.destination);
338
+ if (caps.supportsPartialClone) {
339
+ const cloneArgs = ["--filter=blob:none", "--depth=1", "--no-checkout", "--sparse"];
340
+ if (input.ref) cloneArgs.push("--branch", input.ref);
341
+ try {
342
+ await git.clone(url, ".", cloneArgs);
343
+ } catch (err) {
344
+ if (input.ref && refNotFoundFromError(err)) {
345
+ throw new RefNotFoundError(input.ref, input.url, err);
346
+ }
347
+ throw err;
348
+ }
349
+ } else {
350
+ const cloneArgs = ["--depth=1"];
351
+ if (input.ref) cloneArgs.push("--branch", input.ref);
352
+ try {
353
+ await git.clone(url, ".", cloneArgs);
354
+ } catch (err) {
355
+ if (input.ref && refNotFoundFromError(err)) {
356
+ throw new RefNotFoundError(input.ref, input.url, err);
357
+ }
358
+ throw err;
359
+ }
360
+ }
361
+ return git;
362
+ }
363
+ async function setSparsePatterns(workDir, patterns) {
364
+ const git = simpleGit(workDir);
365
+ if (patterns.length === 0) {
366
+ await git.raw(["sparse-checkout", "disable"]);
367
+ return;
368
+ }
369
+ await git.raw(["sparse-checkout", "set", "--no-cone", ...patterns]);
370
+ }
371
+ async function checkoutRef(workDir, ref) {
372
+ const git = simpleGit(workDir);
373
+ try {
374
+ if (ref) await git.checkout(ref);
375
+ else await git.checkout(["HEAD"]);
376
+ } catch (err) {
377
+ if (ref && refNotFoundFromError(err)) {
378
+ throw new RefNotFoundError(ref, await tryReadOriginUrl(workDir), err);
379
+ }
380
+ throw err;
381
+ }
382
+ const sha = (await git.revparse(["HEAD"])).trim();
383
+ return sha;
384
+ }
385
+ async function fetchHead(workDir, ref) {
386
+ const git = simpleGit(workDir);
387
+ const args = ref ? ["--depth=1", "origin", ref] : ["--depth=1", "origin"];
388
+ try {
389
+ await git.fetch(args);
390
+ } catch (err) {
391
+ if (ref && refNotFoundFromError(err)) {
392
+ throw new RefNotFoundError(ref, await tryReadOriginUrl(workDir), err);
393
+ }
394
+ throw err;
395
+ }
396
+ await git.raw(["reset", "--hard", "FETCH_HEAD"]);
397
+ return (await git.revparse(["HEAD"])).trim();
398
+ }
399
+ async function clearWorktree(workDir) {
400
+ await rm(workDir, { recursive: true, force: true });
401
+ }
402
+ async function listRemoteRef(input) {
403
+ const url = input.useTokenInUrl ? injectToken(input.url, input.useTokenInUrl) : input.url;
404
+ const git = simpleGit();
405
+ const target = input.ref ?? "HEAD";
406
+ let output;
407
+ try {
408
+ output = await git.listRemote([url, target]);
409
+ } catch (err) {
410
+ throw new RefNotFoundError(target, input.url, err);
411
+ }
412
+ const sha = parseListRemoteOutput(output, target);
413
+ if (!sha) {
414
+ throw new RefNotFoundError(target, input.url);
415
+ }
416
+ return sha;
417
+ }
418
+ function parseListRemoteOutput(output, target) {
419
+ const lines = output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
420
+ if (lines.length === 0) return void 0;
421
+ let primary;
422
+ let peeled;
423
+ for (const line of lines) {
424
+ const match = line.match(/^([0-9a-f]{40})\s+(\S+)$/);
425
+ if (!match) continue;
426
+ const sha = match[1];
427
+ const refName = match[2];
428
+ if (!sha || !refName) continue;
429
+ if (refName === target) {
430
+ primary = sha;
431
+ continue;
432
+ }
433
+ if (matchesRefName(refName, target)) {
434
+ primary = sha;
435
+ }
436
+ if (refName.endsWith("^{}") && matchesRefName(refName.slice(0, -3), target)) {
437
+ peeled = sha;
438
+ }
439
+ }
440
+ return peeled ?? primary;
441
+ }
442
+ function matchesRefName(refName, target) {
443
+ if (refName === target) return true;
444
+ if (refName === `refs/heads/${target}`) return true;
445
+ if (refName === `refs/tags/${target}`) return true;
446
+ if (refName === `refs/remotes/origin/${target}`) return true;
447
+ return false;
448
+ }
449
+ function refNotFoundFromError(err) {
450
+ const msg = err instanceof Error ? `${err.message}
451
+ ${err.stack ?? ""}` : String(err);
452
+ return /Remote branch .+ not found/i.test(msg) || /couldn't find remote ref/i.test(msg) || /not a tree/i.test(msg) || /unknown revision or path/i.test(msg);
453
+ }
454
+ function injectToken(url, token) {
455
+ if (!url.startsWith("https://")) return url;
456
+ const after = url.slice("https://".length);
457
+ return `https://x-access-token:${token}@${after}`;
458
+ }
459
+ async function tryReadOriginUrl(workDir) {
460
+ try {
461
+ const git = simpleGit(workDir);
462
+ const remotes = await git.getRemotes(true);
463
+ return remotes.find((r) => r.name === "origin")?.refs.fetch ?? "";
464
+ } catch {
465
+ return "";
466
+ }
467
+ }
468
+ var MAESTER_MANIFEST_FILENAME = "maester.yaml";
469
+ async function fetchSource(entry, ctx) {
470
+ if (entry.includes && entry.includes.length > 0) {
471
+ const normalized = entry.includes.map(normalizeIncludeEntry);
472
+ return fetchWithExplicitIncludes(entry, ctx, normalized);
473
+ }
474
+ return fetchWithRemoteManifest(entry, ctx);
475
+ }
476
+ async function fetchWithRemoteManifest(entry, ctx) {
477
+ if (!ctx.cacheExists) {
478
+ await shallowSparseClone({
479
+ url: entry.url,
480
+ destination: ctx.cacheDir,
481
+ ref: entry.ref,
482
+ ...ctx.tokenForUrl ? { useTokenInUrl: ctx.tokenForUrl } : {}
483
+ });
484
+ await setSparsePatterns(ctx.cacheDir, [MAESTER_MANIFEST_FILENAME]);
485
+ await checkoutRef(ctx.cacheDir, entry.ref);
486
+ } else {
487
+ await fetchHead(ctx.cacheDir, entry.ref);
488
+ }
489
+ const discovery = await discoverManifestFromCache(ctx.cacheDir);
490
+ if (discovery.mode === "no-manifest") {
491
+ throw manifestError(entry.name, discovery.reason);
492
+ }
493
+ await setSparsePatterns(ctx.cacheDir, discovery.patterns);
494
+ const commitSha = await checkoutRef(ctx.cacheDir, entry.ref);
495
+ return {
496
+ name: entry.name,
497
+ cacheDir: ctx.cacheDir,
498
+ commitSha,
499
+ filterSet: discovery.patterns,
500
+ rules: discovery.rules,
501
+ warnings: []
502
+ };
503
+ }
504
+ async function fetchWithExplicitIncludes(entry, ctx, normalized) {
505
+ if (!ctx.cacheExists) {
506
+ await shallowSparseClone({
507
+ url: entry.url,
508
+ destination: ctx.cacheDir,
509
+ ref: entry.ref,
510
+ ...ctx.tokenForUrl ? { useTokenInUrl: ctx.tokenForUrl } : {}
511
+ });
512
+ } else {
513
+ await fetchHead(ctx.cacheDir, entry.ref);
514
+ }
515
+ const patterns = normalized.map((entry2) => entry2.path);
516
+ await setSparsePatterns(ctx.cacheDir, patterns);
517
+ const commitSha = await checkoutRef(ctx.cacheDir, entry.ref);
518
+ const matchedFileCount = await countMaterializedFiles(ctx.cacheDir);
519
+ const warnings = [];
520
+ if (matchedFileCount === 0) {
521
+ warnings.push({ type: "no-matches", name: entry.name, includes: patterns });
522
+ }
523
+ const rules = normalized.map(
524
+ (entry2) => entry2.state === void 0 ? { pattern: entry2.path } : { pattern: entry2.path, state: entry2.state }
525
+ );
526
+ return {
527
+ name: entry.name,
528
+ cacheDir: ctx.cacheDir,
529
+ commitSha,
530
+ filterSet: patterns,
531
+ rules,
532
+ warnings
533
+ };
534
+ }
535
+ function manifestError(name, reason) {
536
+ if (reason === "absent") {
537
+ return new MaesterError(
538
+ "MAESTER_MANIFEST_MISSING",
539
+ `source '${name}' does not publish a maester.yaml manifest at the configured ref. Sync will not fall back to the full tree \u2014 either add a maester.yaml to the source repo or declare an \`includes\` list on this source.`
540
+ );
541
+ }
542
+ return new MaesterError(
543
+ "MAESTER_MANIFEST_INVALID",
544
+ `source '${name}' publishes a maester.yaml that failed schema validation. Sync will not fall back to the full tree \u2014 fix the manifest in the source repo or declare an \`includes\` list on this source.`
545
+ );
546
+ }
547
+ async function discoverManifestFromCache(cacheDir) {
548
+ const path4 = resolve(cacheDir, MAESTER_MANIFEST_FILENAME);
549
+ if (!existsSync(path4)) return { mode: "no-manifest", reason: "absent" };
550
+ try {
551
+ const raw = await readFile(path4, "utf8");
552
+ const doc = parseDocument(raw);
553
+ if (doc.errors.length > 0) return { mode: "no-manifest", reason: "invalid" };
554
+ const parsed = MaesterConfigSchema.safeParse(doc.toJS({ maxAliasCount: -1 }));
555
+ if (!parsed.success) return { mode: "no-manifest", reason: "invalid" };
556
+ const rules = parsed.data.documents.map(
557
+ (d) => d.state === void 0 ? { pattern: d.path } : { pattern: d.path, state: d.state }
558
+ );
559
+ const patterns = parsed.data.documents.map((d) => d.path);
560
+ if (patterns.length === 0) return { mode: "no-manifest", reason: "invalid" };
561
+ if (!patterns.includes(MAESTER_MANIFEST_FILENAME)) patterns.unshift(MAESTER_MANIFEST_FILENAME);
562
+ return { mode: "manifest", patterns, rules };
563
+ } catch {
564
+ return { mode: "no-manifest", reason: "invalid" };
565
+ }
566
+ }
567
+ async function countMaterializedFiles(cacheDir) {
568
+ let count = 0;
569
+ async function walk2(dir) {
570
+ let entries;
571
+ try {
572
+ entries = await readdir(dir, {
573
+ withFileTypes: true,
574
+ encoding: "utf8"
575
+ });
576
+ } catch {
577
+ return;
578
+ }
579
+ for (const entry of entries) {
580
+ if (entry.name === ".git") continue;
581
+ const fullPath = resolve(dir, entry.name);
582
+ if (entry.isDirectory()) {
583
+ await walk2(fullPath);
584
+ } else if (entry.isFile()) {
585
+ count++;
586
+ }
587
+ }
588
+ }
589
+ await walk2(cacheDir);
590
+ return count;
591
+ }
592
+
593
+ // src/core/state/html.ts
594
+ var html_exports = {};
595
+ __export(html_exports, {
596
+ parse: () => parse,
597
+ write: () => write
598
+ });
599
+ var STATE_COMMENT_RE = /^<!--\s*state:\s*([^\s-]+)\s*-->$/;
600
+ function splitFirstLine(text) {
601
+ const crlfIdx = text.indexOf("\r\n");
602
+ const lfIdx = text.indexOf("\n");
603
+ if (crlfIdx !== -1 && (lfIdx === -1 || crlfIdx <= lfIdx)) {
604
+ return { firstLine: text.slice(0, crlfIdx), rest: text.slice(crlfIdx + 2), eol: "\r\n" };
605
+ }
606
+ if (lfIdx !== -1) {
607
+ return { firstLine: text.slice(0, lfIdx), rest: text.slice(lfIdx + 1), eol: "\n" };
608
+ }
609
+ return { firstLine: text, rest: "", eol: "\n" };
610
+ }
611
+ function parse(buf) {
612
+ const text = buf.toString("utf8");
613
+ const { firstLine } = splitFirstLine(text);
614
+ const match = firstLine.match(STATE_COMMENT_RE);
615
+ if (!match) return { kind: "absent" };
616
+ return parseState(match[1]);
617
+ }
618
+ function write(buf, state) {
619
+ const text = buf.toString("utf8");
620
+ const { firstLine, rest, eol } = splitFirstLine(text);
621
+ const desiredLine = `<!-- state: ${state} -->`;
622
+ const existingMatch = firstLine.match(STATE_COMMENT_RE);
623
+ if (existingMatch) {
624
+ if (firstLine === desiredLine) return buf;
625
+ return Buffer.from(`${desiredLine}${eol}${rest}`, "utf8");
626
+ }
627
+ const prefix = `${desiredLine}
628
+ `;
629
+ return Buffer.from(`${prefix}${text}`, "utf8");
630
+ }
631
+
632
+ // src/core/state/json.ts
633
+ var json_exports = {};
634
+ __export(json_exports, {
635
+ parse: () => parse2,
636
+ write: () => write2
637
+ });
638
+ function detectIndent(text) {
639
+ const lines = text.split("\n");
640
+ for (const line of lines) {
641
+ if (line.startsWith(" ")) return " ";
642
+ const match = line.match(/^( +)\S/);
643
+ if (match) return match[1]?.length ?? 2;
644
+ }
645
+ return 2;
646
+ }
647
+ function detectTrailingNewline(text) {
648
+ return text.endsWith("\n") ? "\n" : "";
649
+ }
650
+ function parse2(buf) {
651
+ let value;
652
+ try {
653
+ value = JSON.parse(buf.toString("utf8"));
654
+ } catch {
655
+ return { kind: "absent" };
656
+ }
657
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
658
+ return { kind: "absent" };
659
+ }
660
+ return parseState(value.state);
661
+ }
662
+ function write2(buf, state) {
663
+ const text = buf.toString("utf8");
664
+ let value;
665
+ try {
666
+ value = JSON.parse(text);
667
+ } catch {
668
+ return buf;
669
+ }
670
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
671
+ return buf;
672
+ }
673
+ const obj = value;
674
+ if (obj.state === state) return buf;
675
+ obj.state = state;
676
+ const indent = detectIndent(text);
677
+ const trailing = detectTrailingNewline(text);
678
+ const serialized = JSON.stringify(obj, null, indent);
679
+ return Buffer.from(`${serialized}${trailing}`, "utf8");
680
+ }
681
+
682
+ // src/core/state/markdown.ts
683
+ var markdown_exports = {};
684
+ __export(markdown_exports, {
685
+ parse: () => parse3,
686
+ write: () => write3
687
+ });
688
+ function parse3(buf) {
689
+ let parsed;
690
+ try {
691
+ parsed = matter(buf.toString("utf8"));
692
+ } catch {
693
+ return { kind: "absent" };
694
+ }
695
+ const raw = parsed.data.state;
696
+ return parseState(raw);
697
+ }
698
+ function write3(buf, state) {
699
+ let parsed;
700
+ try {
701
+ parsed = matter(buf.toString("utf8"));
702
+ } catch {
703
+ return buf;
704
+ }
705
+ const sourceData = parsed.data;
706
+ if (sourceData.state === state) return buf;
707
+ const nextData = { ...sourceData, state };
708
+ const next = matter.stringify(parsed.content, nextData);
709
+ return Buffer.from(next, "utf8");
710
+ }
711
+
712
+ // src/core/state/plaintext.ts
713
+ var plaintext_exports = {};
714
+ __export(plaintext_exports, {
715
+ parse: () => parse4,
716
+ write: () => write4
717
+ });
718
+ var STATE_LINE_RE = /^state:\s+(\S+)\s*$/;
719
+ function splitFirstLine2(text) {
720
+ const crlfIdx = text.indexOf("\r\n");
721
+ const lfIdx = text.indexOf("\n");
722
+ if (crlfIdx !== -1 && (lfIdx === -1 || crlfIdx <= lfIdx)) {
723
+ return { firstLine: text.slice(0, crlfIdx), rest: text.slice(crlfIdx + 2), eol: "\r\n" };
724
+ }
725
+ if (lfIdx !== -1) {
726
+ return { firstLine: text.slice(0, lfIdx), rest: text.slice(lfIdx + 1), eol: "\n" };
727
+ }
728
+ return { firstLine: text, rest: "", eol: "\n" };
729
+ }
730
+ function parse4(buf) {
731
+ const text = buf.toString("utf8");
732
+ const { firstLine } = splitFirstLine2(text);
733
+ const match = firstLine.match(STATE_LINE_RE);
734
+ if (!match) return { kind: "absent" };
735
+ return parseState(match[1]);
736
+ }
737
+ function write4(buf, state) {
738
+ const text = buf.toString("utf8");
739
+ const { firstLine, rest, eol } = splitFirstLine2(text);
740
+ const desiredLine = `state: ${state}`;
741
+ const existingMatch = firstLine.match(STATE_LINE_RE);
742
+ if (existingMatch) {
743
+ if (firstLine === desiredLine) return buf;
744
+ return Buffer.from(`${desiredLine}${eol}${rest}`, "utf8");
745
+ }
746
+ return Buffer.from(`${desiredLine}
747
+ ${text}`, "utf8");
748
+ }
749
+
750
+ // src/core/state/yaml.ts
751
+ var yaml_exports = {};
752
+ __export(yaml_exports, {
753
+ parse: () => parse5,
754
+ write: () => write5
755
+ });
756
+ function parse5(buf) {
757
+ let doc;
758
+ try {
759
+ doc = parseDocument(buf.toString("utf8"));
760
+ } catch {
761
+ return { kind: "absent" };
762
+ }
763
+ if (doc.errors.length > 0) return { kind: "absent" };
764
+ if (!isMap(doc.contents)) return { kind: "absent" };
765
+ const raw = doc.get("state");
766
+ return parseState(raw);
767
+ }
768
+ function write5(buf, state) {
769
+ const text = buf.toString("utf8");
770
+ let doc;
771
+ try {
772
+ doc = parseDocument(text);
773
+ } catch {
774
+ return buf;
775
+ }
776
+ if (doc.errors.length > 0) return buf;
777
+ if (!isMap(doc.contents)) return buf;
778
+ if (doc.get("state") === state) return buf;
779
+ doc.set("state", state);
780
+ return Buffer.from(doc.toString(), "utf8");
781
+ }
782
+
783
+ // src/core/state/format.ts
784
+ var HANDLERS = {
785
+ ".md": markdown_exports,
786
+ ".markdown": markdown_exports,
787
+ ".html": html_exports,
788
+ ".htm": html_exports,
789
+ ".yaml": yaml_exports,
790
+ ".yml": yaml_exports,
791
+ ".json": json_exports,
792
+ ".txt": plaintext_exports
793
+ };
794
+ function handlerFor(filePath) {
795
+ const ext = extname(filePath).toLowerCase();
796
+ return HANDLERS[ext];
797
+ }
798
+
799
+ // src/core/state/applier.ts
800
+ var MATCHER_OPTIONS = { dot: false, nocase: false };
801
+ function compileMatchers(rules) {
802
+ return rules.map((r) => picomatch(r.pattern, MATCHER_OPTIONS));
803
+ }
804
+ function findRuleState(filePath, rules, matchers) {
805
+ for (let i = 0; i < rules.length; i++) {
806
+ const match = matchers[i];
807
+ const rule = rules[i];
808
+ if (!match || !rule) continue;
809
+ if (!match(filePath)) continue;
810
+ if (rule.state !== void 0) return rule.state;
811
+ return void 0;
812
+ }
813
+ return void 0;
814
+ }
815
+ async function* walk(dir, root) {
816
+ let entries;
817
+ try {
818
+ entries = await readdir(dir, { withFileTypes: true, encoding: "utf8" });
819
+ } catch {
820
+ return;
821
+ }
822
+ const isRoot = dir === root;
823
+ for (const entry of entries) {
824
+ if (entry.name === ".git") continue;
825
+ if (entry.name === ".maester-source.json") continue;
826
+ if (isRoot && entry.name === "maester.yaml") continue;
827
+ const full = resolve(dir, entry.name);
828
+ if (entry.isDirectory()) {
829
+ yield* walk(full, root);
830
+ } else if (entry.isFile()) {
831
+ yield relative(root, full);
832
+ }
833
+ }
834
+ }
835
+ async function applyState(stagedDir, rules) {
836
+ const matchers = compileMatchers(rules);
837
+ const breakdown = { canon: 0, draft: 0, untagged: 0 };
838
+ const warnings = [];
839
+ const details = [];
840
+ for await (const relPath of walk(stagedDir, stagedDir)) {
841
+ const handler = handlerFor(relPath);
842
+ if (!handler) {
843
+ breakdown.untagged++;
844
+ details.push({ file: relPath, state: "untagged", sourceOfTruth: "untagged" });
845
+ continue;
846
+ }
847
+ const fullPath = resolve(stagedDir, relPath);
848
+ const buf = await readFile(fullPath);
849
+ const parsed = handler.parse(buf);
850
+ let resolved;
851
+ let sourceOfTruth;
852
+ if (parsed.kind === "valid") {
853
+ resolved = parsed.value;
854
+ sourceOfTruth = "inline";
855
+ const ruleState = findRuleState(relPath, rules, matchers);
856
+ if (ruleState !== void 0 && ruleState !== resolved) {
857
+ warnings.push({
858
+ type: "disagreement",
859
+ file: relPath,
860
+ inline: resolved,
861
+ rule: ruleState
862
+ });
863
+ }
864
+ } else {
865
+ if (parsed.kind === "invalid") {
866
+ warnings.push({ type: "bad-inline-state", file: relPath, raw: parsed.raw });
867
+ }
868
+ const ruleState = findRuleState(relPath, rules, matchers);
869
+ if (ruleState !== void 0) {
870
+ resolved = ruleState;
871
+ sourceOfTruth = "rule";
872
+ } else {
873
+ resolved = DEFAULT_STATE;
874
+ sourceOfTruth = "default";
875
+ }
876
+ }
877
+ const next = handler.write(buf, resolved);
878
+ if (next !== buf) {
879
+ await writeFile(fullPath, next);
880
+ }
881
+ breakdown[resolved]++;
882
+ details.push({ file: relPath, state: resolved, sourceOfTruth });
883
+ }
884
+ return { breakdown, warnings, details };
885
+ }
886
+ var PROVENANCE_FILENAME = ".maester-source.json";
887
+ async function writeProvenanceMarker(destination, marker) {
888
+ const path4 = resolve(destination, PROVENANCE_FILENAME);
889
+ const body = `${JSON.stringify(marker, null, 2)}
890
+ `;
891
+ await writeFile(path4, body, "utf8");
892
+ return path4;
893
+ }
894
+ async function readProvenanceMarker(destination) {
895
+ const path4 = resolve(destination, PROVENANCE_FILENAME);
896
+ if (!existsSync(path4)) return void 0;
897
+ try {
898
+ const text = await readFile(path4, "utf8");
899
+ const parsed = JSON.parse(text);
900
+ if (typeof parsed.sourceName !== "string" || typeof parsed.sourceUrl !== "string" || typeof parsed.commitSha !== "string" || !Array.isArray(parsed.filterSet)) {
901
+ return void 0;
902
+ }
903
+ return {
904
+ sourceName: parsed.sourceName,
905
+ sourceUrl: parsed.sourceUrl,
906
+ ref: typeof parsed.ref === "string" ? parsed.ref : void 0,
907
+ commitSha: parsed.commitSha,
908
+ filterSet: parsed.filterSet,
909
+ syncedAt: typeof parsed.syncedAt === "string" ? parsed.syncedAt : (/* @__PURE__ */ new Date(0)).toISOString()
910
+ };
911
+ } catch {
912
+ return void 0;
913
+ }
914
+ }
915
+ function filterSetMatches(a, b) {
916
+ if (a.length !== b.length) return false;
917
+ for (let i = 0; i < a.length; i++) {
918
+ if (a[i] !== b[i]) return false;
919
+ }
920
+ return true;
921
+ }
922
+ async function stageDestination(input) {
923
+ await assertDestinationSafe(input.destination, input.marker.sourceName);
924
+ const tempDir = `${input.destination}.tmp-${Math.random().toString(36).slice(2, 10)}`;
925
+ await mkdir(dirname(input.destination), { recursive: true });
926
+ await mkdir(tempDir, { recursive: true });
927
+ await copyCacheToTemp(input.cacheDir, tempDir);
928
+ const beforePromoteResult = input.beforePromote ? await input.beforePromote(tempDir) : void 0;
929
+ await writeProvenanceMarker(tempDir, input.marker);
930
+ await promoteTempToDestination(tempDir, input.destination);
931
+ if (beforePromoteResult !== void 0) {
932
+ return { staged: true, finalPath: input.destination, beforePromoteResult };
933
+ }
934
+ return { staged: true, finalPath: input.destination };
935
+ }
936
+ async function copyCacheToTemp(cacheDir, tempDir) {
937
+ if (!existsSync(cacheDir)) return;
938
+ const entries = await readdir(cacheDir, { withFileTypes: true });
939
+ for (const entry of entries) {
940
+ if (entry.name === ".git") continue;
941
+ const src = resolve(cacheDir, entry.name);
942
+ const dst = resolve(tempDir, entry.name);
943
+ await cp(src, dst, { recursive: true, force: true, errorOnExist: false });
944
+ }
945
+ }
946
+ async function promoteTempToDestination(tempDir, destination) {
947
+ if (existsSync(destination)) {
948
+ const backup = `${destination}.old-${Math.random().toString(36).slice(2, 10)}`;
949
+ await rename(destination, backup);
950
+ try {
951
+ await rename(tempDir, destination);
952
+ } catch (err) {
953
+ await rename(backup, destination).catch(() => void 0);
954
+ throw err;
955
+ }
956
+ await rm(backup, { recursive: true, force: true });
957
+ return;
958
+ }
959
+ await rename(tempDir, destination);
960
+ }
961
+ async function assertDestinationSafe(destination, expectedSourceName) {
962
+ if (!existsSync(destination)) return;
963
+ const entries = await readdir(destination);
964
+ if (entries.length === 0) return;
965
+ if (entries.length === 1 && entries[0] === PROVENANCE_FILENAME) {
966
+ const marker2 = await readProvenanceMarker(destination);
967
+ if (!marker2 || marker2.sourceName === expectedSourceName) return;
968
+ }
969
+ const marker = await readProvenanceMarker(destination);
970
+ if (marker && marker.sourceName === expectedSourceName) return;
971
+ throw new DestinationBlockedError(
972
+ destination,
973
+ `Refusing to overwrite ${destination}: contains content that was not produced by '${expectedSourceName}'. Remove the directory or choose a different destination.`
974
+ );
975
+ }
976
+
977
+ // src/core/sync/runner.ts
978
+ var DEFAULT_CONCURRENCY = 4;
979
+ async function runSync(config, options) {
980
+ const env = options.env ?? process.env;
981
+ const scope = options.scope?.length ? new Set(options.scope) : void 0;
982
+ const baseDir = options.baseDir ?? config.baseDir;
983
+ if (scope) {
984
+ const known = new Set(config.sources.map((s) => s.name));
985
+ for (const name of scope) {
986
+ if (!known.has(name)) {
987
+ throw new MaesterError(
988
+ "UNKNOWN_SOURCE",
989
+ `Unknown source '${name}' \u2014 not declared in citadel.yaml.`
990
+ );
991
+ }
992
+ }
993
+ }
994
+ const entries = config.sources.filter((s) => !scope || scope.has(s.name));
995
+ const limit = Math.min(
996
+ Math.max(1, options.concurrency ?? DEFAULT_CONCURRENCY),
997
+ entries.length || 1
998
+ );
999
+ const outcomes = new Array(entries.length);
1000
+ await mkdir(resolve(options.repoRoot, CACHE_SUBDIR), { recursive: true });
1001
+ let cursor = 0;
1002
+ const workers = [];
1003
+ for (let i = 0; i < limit; i++) {
1004
+ workers.push(
1005
+ (async () => {
1006
+ while (true) {
1007
+ const index = cursor++;
1008
+ if (index >= entries.length) return;
1009
+ const entry = entries[index];
1010
+ if (!entry) return;
1011
+ outcomes[index] = await processEntry(entry, options, env, baseDir);
1012
+ }
1013
+ })()
1014
+ );
1015
+ }
1016
+ await Promise.all(workers);
1017
+ const failed = outcomes.filter((o) => o.status === "failed").length;
1018
+ return { outcomes, failed };
1019
+ }
1020
+ async function processEntry(source, options, env, baseDir) {
1021
+ const cacheDir = cachePathForSource(options.repoRoot, source.name);
1022
+ const destination = source.destination ? resolve(options.repoRoot, source.destination) : defaultDestinationFor(options.repoRoot, source.name, baseDir);
1023
+ options.onProgress?.({ type: "start", name: source.name });
1024
+ try {
1025
+ const auth = resolveAuth(source.auth, env);
1026
+ const tokenForUrl = auth.type === "token" ? auth.value : void 0;
1027
+ const cacheExists = existsSync(cacheDir);
1028
+ const tree = await fetchSource(source, {
1029
+ cacheDir,
1030
+ cacheExists,
1031
+ tokenForUrl
1032
+ });
1033
+ options.onProgress?.({ type: "fetched", name: source.name, commitSha: tree.commitSha });
1034
+ for (const warning of tree.warnings) {
1035
+ options.onProgress?.({ type: "warning", name: source.name, warning });
1036
+ }
1037
+ const existingMarker = await readProvenanceMarker(destination);
1038
+ const wasUnchanged = !!existingMarker && existingMarker.commitSha === tree.commitSha && existingMarker.sourceName === source.name && filterSetMatches(existingMarker.filterSet, tree.filterSet) && existsSync(destination);
1039
+ if (wasUnchanged) {
1040
+ options.onProgress?.({ type: "staged", name: source.name, status: "unchanged" });
1041
+ return {
1042
+ name: source.name,
1043
+ status: "unchanged",
1044
+ destination,
1045
+ ref: source.ref,
1046
+ commitSha: tree.commitSha,
1047
+ warnings: tree.warnings
1048
+ };
1049
+ }
1050
+ const stageResult = await stageDestination({
1051
+ cacheDir: tree.cacheDir,
1052
+ destination,
1053
+ marker: {
1054
+ sourceName: tree.name,
1055
+ sourceUrl: source.url,
1056
+ ref: source.ref,
1057
+ commitSha: tree.commitSha,
1058
+ filterSet: tree.filterSet,
1059
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString()
1060
+ },
1061
+ beforePromote: (stagedDir) => applyState(stagedDir, tree.rules)
1062
+ });
1063
+ const stateResult = stageResult.beforePromoteResult;
1064
+ const status = existingMarker ? "updated" : "added";
1065
+ options.onProgress?.({ type: "staged", name: source.name, status });
1066
+ return {
1067
+ name: source.name,
1068
+ status,
1069
+ destination,
1070
+ ref: source.ref,
1071
+ commitSha: tree.commitSha,
1072
+ warnings: tree.warnings,
1073
+ ...stateResult ? {
1074
+ stateBreakdown: stateResult.breakdown,
1075
+ stateWarnings: stateResult.warnings,
1076
+ stateDetails: stateResult.details
1077
+ } : {}
1078
+ };
1079
+ } catch (err) {
1080
+ const message = errorMessage(err);
1081
+ options.onProgress?.({ type: "failed", name: source.name, error: message });
1082
+ try {
1083
+ await clearWorktree(cacheDir);
1084
+ } catch {
1085
+ }
1086
+ return {
1087
+ name: source.name,
1088
+ status: "failed",
1089
+ destination,
1090
+ ref: source.ref,
1091
+ warnings: [],
1092
+ error: message
1093
+ };
1094
+ }
1095
+ }
1096
+ function errorMessage(err) {
1097
+ if (err instanceof AuthError) return err.message;
1098
+ if (err instanceof RefNotFoundError) return err.message;
1099
+ if (err instanceof MaesterError) return err.message;
1100
+ if (err instanceof Error) return err.message;
1101
+ return String(err);
1102
+ }
1103
+ var SHA_RE = /^[0-9a-f]{40}$/;
1104
+ var MAESTER_MANIFEST_FILENAME2 = "maester.yaml";
1105
+ var STATUS_TEMP_PREFIX = ".status-";
1106
+ async function probeCommitSha(source, ctx) {
1107
+ if (source.ref && SHA_RE.test(source.ref)) {
1108
+ return source.ref;
1109
+ }
1110
+ return listRemoteRef({
1111
+ url: source.url,
1112
+ ref: source.ref,
1113
+ ...ctx.tokenForUrl ? { useTokenInUrl: ctx.tokenForUrl } : {}
1114
+ });
1115
+ }
1116
+ async function probeManifest(source, ctx) {
1117
+ const tempRoot = resolve(ctx.repoRoot, CACHE_DIR_NAME);
1118
+ await mkdir(tempRoot, { recursive: true });
1119
+ const tempDir = await mkdtemp(resolve(tempRoot, STATUS_TEMP_PREFIX));
1120
+ try {
1121
+ await shallowSparseClone({
1122
+ url: source.url,
1123
+ destination: tempDir,
1124
+ ref: source.ref,
1125
+ ...ctx.tokenForUrl ? { useTokenInUrl: ctx.tokenForUrl } : {}
1126
+ });
1127
+ await setSparsePatterns(tempDir, [MAESTER_MANIFEST_FILENAME2]);
1128
+ await checkoutRef(tempDir, source.ref);
1129
+ const discovery = await discoverManifestFromCache(tempDir);
1130
+ if (discovery.mode === "no-manifest") {
1131
+ throw manifestError2(source.name, discovery.reason);
1132
+ }
1133
+ return { filterSet: discovery.patterns };
1134
+ } finally {
1135
+ await rm(tempDir, { recursive: true, force: true });
1136
+ }
1137
+ }
1138
+ function manifestError2(name, reason) {
1139
+ if (reason === "absent") {
1140
+ return new MaesterError(
1141
+ "MAESTER_MANIFEST_MISSING",
1142
+ `source '${name}' does not publish a maester.yaml manifest at the configured ref.`
1143
+ );
1144
+ }
1145
+ return new MaesterError(
1146
+ "MAESTER_MANIFEST_INVALID",
1147
+ `source '${name}' publishes a maester.yaml that failed schema validation.`
1148
+ );
1149
+ }
1150
+
1151
+ // src/core/status/runner.ts
1152
+ var DEFAULT_CONCURRENCY2 = 4;
1153
+ async function runStatus(config, options) {
1154
+ const env = options.env ?? process.env;
1155
+ const scope = options.scope?.length ? new Set(options.scope) : void 0;
1156
+ const baseDir = options.baseDir ?? config.baseDir;
1157
+ if (scope) {
1158
+ const known = new Set(config.sources.map((s) => s.name));
1159
+ for (const name of scope) {
1160
+ if (!known.has(name)) {
1161
+ throw new MaesterError(
1162
+ "UNKNOWN_SOURCE",
1163
+ `Unknown source '${name}' \u2014 not declared in citadel.yaml.`
1164
+ );
1165
+ }
1166
+ }
1167
+ }
1168
+ const entries = config.sources.filter((s) => !scope || scope.has(s.name));
1169
+ if (entries.length === 0) {
1170
+ return { outcomes: [], counts: { upToDate: 0, behind: 0, failed: 0 } };
1171
+ }
1172
+ const limit = Math.min(Math.max(1, options.concurrency ?? DEFAULT_CONCURRENCY2), entries.length);
1173
+ const outcomes = new Array(entries.length);
1174
+ let cursor = 0;
1175
+ const workers = [];
1176
+ for (let i = 0; i < limit; i++) {
1177
+ workers.push(
1178
+ (async () => {
1179
+ while (true) {
1180
+ const index = cursor++;
1181
+ if (index >= entries.length) return;
1182
+ const entry = entries[index];
1183
+ if (!entry) return;
1184
+ outcomes[index] = await checkSource(entry, options, env, baseDir);
1185
+ }
1186
+ })()
1187
+ );
1188
+ }
1189
+ await Promise.all(workers);
1190
+ const counts = { upToDate: 0, behind: 0, failed: 0 };
1191
+ for (const outcome of outcomes) {
1192
+ if (outcome.verdict === "up-to-date") counts.upToDate++;
1193
+ else if (outcome.verdict === "behind") counts.behind++;
1194
+ else counts.failed++;
1195
+ }
1196
+ return { outcomes, counts };
1197
+ }
1198
+ async function checkSource(source, options, env, baseDir) {
1199
+ const destination = source.destination ? resolve(options.repoRoot, source.destination) : defaultDestinationFor(options.repoRoot, source.name, baseDir);
1200
+ const marker = await readProvenanceMarker(destination);
1201
+ if (!marker) {
1202
+ return {
1203
+ name: source.name,
1204
+ verdict: "behind",
1205
+ reasons: ["never-synced"]
1206
+ };
1207
+ }
1208
+ try {
1209
+ const auth = resolveAuth(source.auth, env);
1210
+ const tokenForUrl = auth.type === "token" ? auth.value : void 0;
1211
+ const probeCtx = { repoRoot: options.repoRoot, tokenForUrl };
1212
+ const resolvedSha = await probeCommitSha(source, probeCtx);
1213
+ const reasons = [];
1214
+ if (resolvedSha !== marker.commitSha) {
1215
+ reasons.push("remote-ref-advanced");
1216
+ }
1217
+ const isManifestDriven = !source.includes || source.includes.length === 0;
1218
+ if (isManifestDriven) {
1219
+ const { filterSet } = await probeManifest(source, probeCtx);
1220
+ if (!filterSetsEqual(filterSet, marker.filterSet)) {
1221
+ reasons.push("manifest-changed");
1222
+ }
1223
+ }
1224
+ if (reasons.length === 0) {
1225
+ return {
1226
+ name: source.name,
1227
+ verdict: "up-to-date",
1228
+ commitSha: resolvedSha
1229
+ };
1230
+ }
1231
+ return {
1232
+ name: source.name,
1233
+ verdict: "behind",
1234
+ reasons,
1235
+ commitSha: marker.commitSha,
1236
+ resolvedSha
1237
+ };
1238
+ } catch (err) {
1239
+ return {
1240
+ name: source.name,
1241
+ verdict: "failed",
1242
+ error: errorMessage2(err)
1243
+ };
1244
+ }
1245
+ }
1246
+ function filterSetsEqual(a, b) {
1247
+ const left = [...new Set(a)].sort();
1248
+ const right = [...new Set(b)].sort();
1249
+ if (left.length !== right.length) return false;
1250
+ for (let i = 0; i < left.length; i++) {
1251
+ if (left[i] !== right[i]) return false;
1252
+ }
1253
+ return true;
1254
+ }
1255
+ function errorMessage2(err) {
1256
+ if (err instanceof AuthError) return err.message;
1257
+ if (err instanceof RefNotFoundError) return err.message;
1258
+ if (err instanceof MaesterError) return err.message;
1259
+ if (err instanceof Error) return err.message;
1260
+ return String(err);
1261
+ }
1262
+
1263
+ // src/core/skill/managed-region.ts
1264
+ var BEGIN_MARKER_RE = /<!--\s*maester:skill:begin(?:\s+v=([^\s>]+))?\s*-->/;
1265
+ var END_MARKER_LITERAL = "<!-- maester:skill:end -->";
1266
+ function extractMarkdownRegion(text) {
1267
+ const beginMatch = BEGIN_MARKER_RE.exec(text);
1268
+ if (!beginMatch) return void 0;
1269
+ const beginIdx = beginMatch.index;
1270
+ const afterBegin = beginIdx + beginMatch[0].length;
1271
+ const endIdx = text.indexOf(END_MARKER_LITERAL, afterBegin);
1272
+ if (endIdx < 0) return void 0;
1273
+ const suffixStart = endIdx + END_MARKER_LITERAL.length;
1274
+ return {
1275
+ prefix: text.slice(0, beginIdx),
1276
+ suffix: text.slice(suffixStart),
1277
+ version: beginMatch[1]
1278
+ };
1279
+ }
1280
+ function replaceMarkdownRegion(existing, body, version, preambleWhenAbsent = "") {
1281
+ const region = renderManagedRegion(body, version);
1282
+ if (existing === void 0) {
1283
+ if (preambleWhenAbsent.length === 0) {
1284
+ return `${region}
1285
+ `;
1286
+ }
1287
+ return `${preambleWhenAbsent}${preambleWhenAbsent.endsWith("\n") ? "" : "\n"}${region}
1288
+ `;
1289
+ }
1290
+ const found = extractMarkdownRegion(existing);
1291
+ if (!found) {
1292
+ const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
1293
+ return `${existing}${sep}${region}
1294
+ `;
1295
+ }
1296
+ return `${found.prefix}${region}${found.suffix}`;
1297
+ }
1298
+ function renderManagedRegion(body, version) {
1299
+ const inner = body.endsWith("\n") ? body.slice(0, -1) : body;
1300
+ return `<!-- maester:skill:begin v=${version} -->
1301
+ ${inner}
1302
+ ${END_MARKER_LITERAL}`;
1303
+ }
1304
+ function replaceJsonMaesterKey(existingText, maesterBlock) {
1305
+ const parsed = existingText && existingText.trim().length > 0 ? JSON.parse(existingText) : {};
1306
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1307
+ throw new Error("Expected .claude/settings.json to be a JSON object at the top level.");
1308
+ }
1309
+ const rebuilt = {};
1310
+ let placed = false;
1311
+ for (const [key, value] of Object.entries(parsed)) {
1312
+ if (key === "maester") {
1313
+ rebuilt[key] = maesterBlock;
1314
+ placed = true;
1315
+ } else {
1316
+ rebuilt[key] = value;
1317
+ }
1318
+ }
1319
+ if (!placed) {
1320
+ rebuilt.maester = maesterBlock;
1321
+ }
1322
+ return `${JSON.stringify(rebuilt, null, 2)}
1323
+ `;
1324
+ }
1325
+ function readJsonMaesterKey(existingText) {
1326
+ if (!existingText || existingText.trim().length === 0) return void 0;
1327
+ let parsed;
1328
+ try {
1329
+ parsed = JSON.parse(existingText);
1330
+ } catch {
1331
+ return void 0;
1332
+ }
1333
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
1334
+ const block = parsed.maester;
1335
+ if (typeof block !== "object" || block === null || Array.isArray(block)) return void 0;
1336
+ return block;
1337
+ }
1338
+
1339
+ // src/core/skill/templates/content/citadel-awareness.md
1340
+ var citadel_awareness_default = '## Citadel awareness\n\nThis repository is a **citadel** \u2014 it pulls curated documentation from multiple\nremote sources into a single tree, managed by the `maester` CLI.\n\n- The citadel\'s aggregated content lives under `{{baseDir}}/` at the repository\n root. Each direct subdirectory of `{{baseDir}}/` corresponds to one remote\n source declared in `citadel.yaml` (`{{baseDir}}/<source-name>/...`).\n- The configuration that declares those sources is in `citadel.yaml` at the\n repository root. It names each source, the git remote it pulls from, and the\n ref it pins to.\n- When answering questions about anything the citadel covers, prefer citing\n files under `{{baseDir}}/` over external knowledge. Cite the file path\n relative to the repository root so the user can open it.\n- Each materialized file may carry a `state` value in its frontmatter or\n inline (see "State awareness" below). Surface that state alongside any\n citation so the user knows whether the source is canonical or draft.\n';
1341
+
1342
+ // src/core/skill/templates/content/freshness-awareness.md
1343
+ var freshness_awareness_default = '## Freshness awareness\n\nCitadel content can drift out of date when remote sources advance after the\nlast sync. The Grand Maester is **auto-sync directed**: when the citadel is\nbehind, refresh it before reading rather than relying on stale content.\n\n**Auto-sync policy**\n\nWhen you discover that the citadel is behind \u2014 either from the Claude Code\npre-read hook\'s `additionalContext` message, or from a manual run of\n`npx maester status` \u2014 follow these steps without prompting the user:\n\n1. Run `npx maester sync` to refresh every configured source.\n - Exits 0 on success.\n - Exits non-zero if any source failed (missing auth env var, ref typo,\n network error). Report the failure to the user and proceed cautiously.\n2. **Invalidate the pre-read hook\'s debounce cache** so the next read\n observes fresh state instead of the stale verdict captured before the\n sync:\n ```\n rm -f .maester/.skill-cache.json\n ```\n3. Proceed with the original read of the citadel file.\n\nYou do not need to ask the user before syncing. `maester sync` is read-only\nagainst the remote sources it pulls from; it only materializes content the\n`citadel.yaml` already declared. The same sync the user would run manually.\n\n**Don\'t loop on failures.** If `maester sync` fails (or the hook reports a\n`failed` verdict from `maester status`), do **not** retry sync repeatedly.\nSurface the failure to the user, proceed with the read, and flag that cited\ncontent may be stale.\n\n**Avoid redundant syncs within a session.** Once you have synced and\ninvalidated the cache, ignore any further "citadel is behind" messages that\narrive before you have done another citadel read \u2014 they are cached signals\ncaptured before your sync completed.\n\n**Manual status check**\n\n```\nnpx maester status\n```\n\nExit codes:\n\n- **`0`** \u2014 every source is up to date.\n- **`1`** \u2014 at least one source is behind (remote advanced, manifest\n changed, or never-synced). Run the auto-sync policy above.\n- **`2`** \u2014 the status check itself failed. Surface to the user; proceed\n with a caveat that staleness cannot be verified.\n\nFor machine-readable output, pass `--json` and parse the NDJSON stream on\nstdout. The final line contains `{ "type": "summary", "upToDate": N,\n"behind": N, "failed": N }`.\n\n**On Claude Code specifically**, a `PreToolUse` hook installed by\n`maester skill install` runs the status check automatically before any\n`Read`, `Glob`, or `Grep` targeting a path under `{{baseDir}}/`. The\nhook debounces (default 300s, override with `MAESTER_SKILL_STATUS_TTL`) so\nthe check does not run more than once per session for routine reads.\n';
1344
+
1345
+ // src/core/skill/templates/content/state-awareness.md
1346
+ var state_awareness_default = '## State awareness (canon vs draft)\n\nEvery citadel file may declare a publication state of `canon` (authoritative)\nor `draft` (work-in-progress). The state lives **inline** in the file using\nthe format\'s native convention:\n\n- **Markdown / MDX (`.md`, `.mdx`)** \u2014 `state` field inside YAML frontmatter\n at the top of the file:\n ```\n ---\n state: canon\n ---\n ```\n- **HTML (`.html`, `.htm`)** \u2014 first-line HTML comment:\n `<!-- state: canon -->`\n- **YAML / JSON (`.yaml`, `.yml`, `.json`)** \u2014 a top-level `state` key.\n- **Plain text (`.txt`)** \u2014 `state: canon` as the very first line.\n\nFiles without inline state default to `draft`.\n\n**Policy when answering from the citadel:**\n\n1. **Prefer `canon` files** as the authoritative source of truth. When a\n `canon` file answers the question, cite it and stop there.\n2. **`draft` files are informational only.** Cite them when no `canon`\n alternative exists, but mark the citation explicitly: "(draft \u2014 work in\n progress)" alongside the file path so the user knows the source is not yet\n stable.\n3. **Never mix the two without labeling.** If you draw from both canon and\n draft files in one answer, separate the two and tell the user which fact\n came from which kind of source.\n';
1347
+
1348
+ // src/core/skill/templates/shells/claude-code.ts
1349
+ var SKILL_FRONTMATTER_DESCRIPTION = "Citadel-aware guidance for reading aggregated documentation under the citadel base directory. Prefers canon files over draft and runs maester status before substantial citadel reads.";
1350
+ function renderClaudeSkillBody(opts) {
1351
+ return [
1352
+ "# Grand Maester (Claude Code skill)",
1353
+ "",
1354
+ "Use this guidance whenever you read files under the citadel base directory",
1355
+ `(\`${opts.baseDir}/\`) in this repository.`,
1356
+ "",
1357
+ interpolate(citadel_awareness_default, opts),
1358
+ "",
1359
+ interpolate(state_awareness_default, opts),
1360
+ "",
1361
+ interpolate(freshness_awareness_default, opts)
1362
+ ].join("\n");
1363
+ }
1364
+ function renderClaudeSkillFile(body) {
1365
+ return [
1366
+ "---",
1367
+ "name: grand-maester",
1368
+ `description: ${SKILL_FRONTMATTER_DESCRIPTION}`,
1369
+ "---",
1370
+ "",
1371
+ body
1372
+ ].join("\n");
1373
+ }
1374
+ function buildClaudeMaesterBlock(version) {
1375
+ return {
1376
+ version,
1377
+ hooks: {
1378
+ PreToolUse: [
1379
+ {
1380
+ matcher: "Read|Glob|Grep",
1381
+ hooks: [{ type: "command", command: "npx maester skill runtime preread" }]
1382
+ }
1383
+ ]
1384
+ }
1385
+ };
1386
+ }
1387
+ function interpolate(template, opts) {
1388
+ return template.replace(/\{\{baseDir\}\}/g, opts.baseDir);
1389
+ }
1390
+
1391
+ // src/core/skill/targets/claude-code.ts
1392
+ var SKILL_MD_PATH = ".claude/skills/grand-maester/SKILL.md";
1393
+ var SETTINGS_JSON_PATH = ".claude/settings.json";
1394
+ var claudeCodeTarget = {
1395
+ id: "claude-code",
1396
+ label: "Claude Code",
1397
+ artifactPaths: [SKILL_MD_PATH, SETTINGS_JSON_PATH],
1398
+ writerKey: "claude-code",
1399
+ write: writeClaudeCode,
1400
+ readInstalledVersion
1401
+ };
1402
+ async function writeClaudeCode(input) {
1403
+ const skillResult = await writeSkillMd(input);
1404
+ const settingsResult = await writeSettingsJson(input);
1405
+ const action = combineActions(skillResult.action, settingsResult.action);
1406
+ return {
1407
+ action,
1408
+ installedVersion: input.skillVersion
1409
+ };
1410
+ }
1411
+ async function writeSkillMd(input) {
1412
+ const filePath = path.join(input.repoRoot, SKILL_MD_PATH);
1413
+ await promises.mkdir(path.dirname(filePath), { recursive: true });
1414
+ const existing = await readTextOrUndefined(filePath);
1415
+ const previousVersion = existing ? extractMarkdownRegion(existing)?.version : void 0;
1416
+ const body = renderClaudeSkillBody({ baseDir: input.citadelBaseDir });
1417
+ const managedRegion = replaceMarkdownRegion(void 0, body, input.skillVersion).trimEnd();
1418
+ const fileContent = existing ? replaceMarkdownRegion(existing, body, input.skillVersion) : `${renderClaudeSkillFile(managedRegion)}
1419
+ `;
1420
+ if (existing === fileContent) return { action: "unchanged" };
1421
+ await promises.writeFile(filePath, fileContent, "utf8");
1422
+ if (existing === void 0) return { action: "installed" };
1423
+ if (previousVersion === void 0) return { action: "installed" };
1424
+ return { action: "upgraded" };
1425
+ }
1426
+ async function writeSettingsJson(input) {
1427
+ const filePath = path.join(input.repoRoot, SETTINGS_JSON_PATH);
1428
+ await promises.mkdir(path.dirname(filePath), { recursive: true });
1429
+ const existing = await readTextOrUndefined(filePath);
1430
+ const previousBlock = readJsonMaesterKey(existing);
1431
+ const previousVersion = typeof previousBlock?.version === "string" ? previousBlock.version : void 0;
1432
+ const block = buildClaudeMaesterBlock(input.skillVersion);
1433
+ const next = replaceJsonMaesterKey(existing, block);
1434
+ if (existing === next) return { action: "unchanged" };
1435
+ await promises.writeFile(filePath, next, "utf8");
1436
+ if (existing === void 0 || previousBlock === void 0) return { action: "installed" };
1437
+ if (previousVersion !== input.skillVersion) return { action: "upgraded" };
1438
+ return { action: "upgraded" };
1439
+ }
1440
+ async function readInstalledVersion(repoRoot) {
1441
+ const skillPath = path.join(repoRoot, SKILL_MD_PATH);
1442
+ const text = await readTextOrUndefined(skillPath);
1443
+ if (!text) return void 0;
1444
+ return extractMarkdownRegion(text)?.version;
1445
+ }
1446
+ async function readTextOrUndefined(filePath) {
1447
+ try {
1448
+ return await promises.readFile(filePath, "utf8");
1449
+ } catch (err) {
1450
+ if (err.code === "ENOENT") return void 0;
1451
+ throw err;
1452
+ }
1453
+ }
1454
+ function combineActions(a, b) {
1455
+ if (a === "failed" || b === "failed") return "failed";
1456
+ if (a === "installed" || b === "installed") return "installed";
1457
+ if (a === "upgraded" || b === "upgraded") return "upgraded";
1458
+ return "unchanged";
1459
+ }
1460
+
1461
+ // src/core/skill/templates/shells/agents-md.ts
1462
+ var PREAMBLE = `# AGENTS.md
1463
+
1464
+ This file contains agent instructions for working in this repository. The
1465
+ section between the maester managed-region markers is generated by
1466
+ \`maester skill install\` and refreshed by \`maester skill upgrade\`. Anything
1467
+ you write outside that region is preserved across upgrades.
1468
+ `;
1469
+ function renderAgentsMdBody(opts) {
1470
+ return [
1471
+ "# Grand Maester guidance",
1472
+ "",
1473
+ "This repository is set up to aggregate documentation from remote sources",
1474
+ "into a local citadel. When you reason about citadel content, follow the",
1475
+ "guidance below.",
1476
+ "",
1477
+ interpolate2(citadel_awareness_default, opts),
1478
+ "",
1479
+ interpolate2(state_awareness_default, opts),
1480
+ "",
1481
+ interpolate2(freshness_awareness_default, opts)
1482
+ ].join("\n");
1483
+ }
1484
+ function agentsMdPreamble() {
1485
+ return PREAMBLE;
1486
+ }
1487
+ function interpolate2(template, opts) {
1488
+ return template.replace(/\{\{baseDir\}\}/g, opts.baseDir);
1489
+ }
1490
+
1491
+ // src/core/skill/targets/agents-md-writer.ts
1492
+ var AGENTS_MD_ARTIFACT_PATH = "AGENTS.md";
1493
+ async function writeAgentsMd(input) {
1494
+ const filePath = path.join(input.repoRoot, AGENTS_MD_ARTIFACT_PATH);
1495
+ const existingText = await readTextOrUndefined2(filePath);
1496
+ const previousVersion = existingText ? extractMarkdownRegion(existingText)?.version : void 0;
1497
+ const body = renderAgentsMdBody({ baseDir: input.citadelBaseDir });
1498
+ const next = replaceMarkdownRegion(existingText, body, input.skillVersion, agentsMdPreamble());
1499
+ const action = decideAction(existingText, previousVersion, input.skillVersion, next);
1500
+ if (action === "unchanged") {
1501
+ return previousVersion !== void 0 ? { action, installedVersion: previousVersion } : { action };
1502
+ }
1503
+ await promises.writeFile(filePath, next, "utf8");
1504
+ return { action, installedVersion: input.skillVersion };
1505
+ }
1506
+ async function readAgentsMdInstalledVersion(repoRoot) {
1507
+ const filePath = path.join(repoRoot, AGENTS_MD_ARTIFACT_PATH);
1508
+ const text = await readTextOrUndefined2(filePath);
1509
+ if (!text) return void 0;
1510
+ return extractMarkdownRegion(text)?.version;
1511
+ }
1512
+ async function readTextOrUndefined2(filePath) {
1513
+ try {
1514
+ return await promises.readFile(filePath, "utf8");
1515
+ } catch (err) {
1516
+ if (err.code === "ENOENT") return void 0;
1517
+ throw err;
1518
+ }
1519
+ }
1520
+ function decideAction(existing, previousVersion, newVersion, newContent) {
1521
+ if (existing === void 0) return "installed";
1522
+ if (existing === newContent) return "unchanged";
1523
+ if (previousVersion === void 0) return "installed";
1524
+ if (previousVersion !== newVersion) return "upgraded";
1525
+ return "upgraded";
1526
+ }
1527
+
1528
+ // src/core/skill/targets/codex.ts
1529
+ var codexTarget = {
1530
+ id: "codex",
1531
+ label: "Codex CLI",
1532
+ artifactPaths: [AGENTS_MD_ARTIFACT_PATH],
1533
+ writerKey: "agents-md",
1534
+ write: writeAgentsMd,
1535
+ readInstalledVersion: readAgentsMdInstalledVersion
1536
+ };
1537
+
1538
+ // src/core/skill/templates/shells/cursor.ts
1539
+ var DESCRIPTION = "Citadel-aware guidance for reading aggregated documentation under the citadel base directory.";
1540
+ function renderCursorRuleBody(opts) {
1541
+ return [
1542
+ "# Grand Maester (Cursor rule)",
1543
+ "",
1544
+ "This rule applies when the user asks about content under the citadel",
1545
+ `base directory (\`${opts.baseDir}/\`).`,
1546
+ "",
1547
+ interpolate3(citadel_awareness_default, opts),
1548
+ "",
1549
+ interpolate3(state_awareness_default, opts),
1550
+ "",
1551
+ interpolate3(freshness_awareness_default, opts)
1552
+ ].join("\n");
1553
+ }
1554
+ function renderCursorRuleFile(body, opts) {
1555
+ return [
1556
+ "---",
1557
+ `description: ${DESCRIPTION}`,
1558
+ `globs: ["${opts.baseDir}/**/*"]`,
1559
+ "alwaysApply: false",
1560
+ "---",
1561
+ "",
1562
+ body
1563
+ ].join("\n");
1564
+ }
1565
+ function interpolate3(template, opts) {
1566
+ return template.replace(/\{\{baseDir\}\}/g, opts.baseDir);
1567
+ }
1568
+
1569
+ // src/core/skill/targets/cursor.ts
1570
+ var CURSOR_RULE_PATH = ".cursor/rules/grand-maester.mdc";
1571
+ var cursorTarget = {
1572
+ id: "cursor",
1573
+ label: "Cursor",
1574
+ artifactPaths: [CURSOR_RULE_PATH],
1575
+ writerKey: "cursor",
1576
+ write: writeCursor,
1577
+ readInstalledVersion: readInstalledVersion2
1578
+ };
1579
+ async function writeCursor(input) {
1580
+ const filePath = path.join(input.repoRoot, CURSOR_RULE_PATH);
1581
+ await promises.mkdir(path.dirname(filePath), { recursive: true });
1582
+ const existing = await readTextOrUndefined3(filePath);
1583
+ const previousVersion = existing ? extractMarkdownRegion(existing)?.version : void 0;
1584
+ const body = renderCursorRuleBody({ baseDir: input.citadelBaseDir });
1585
+ const next = existing ? replaceMarkdownRegion(existing, body, input.skillVersion) : `${renderCursorRuleFile(
1586
+ replaceMarkdownRegion(void 0, body, input.skillVersion).trimEnd(),
1587
+ {
1588
+ baseDir: input.citadelBaseDir
1589
+ }
1590
+ )}
1591
+ `;
1592
+ const action = decideAction2(existing, previousVersion, input.skillVersion, next);
1593
+ if (action === "unchanged") {
1594
+ return previousVersion !== void 0 ? { action, installedVersion: previousVersion } : { action };
1595
+ }
1596
+ await promises.writeFile(filePath, next, "utf8");
1597
+ return { action, installedVersion: input.skillVersion };
1598
+ }
1599
+ async function readInstalledVersion2(repoRoot) {
1600
+ const filePath = path.join(repoRoot, CURSOR_RULE_PATH);
1601
+ const text = await readTextOrUndefined3(filePath);
1602
+ if (!text) return void 0;
1603
+ return extractMarkdownRegion(text)?.version;
1604
+ }
1605
+ async function readTextOrUndefined3(filePath) {
1606
+ try {
1607
+ return await promises.readFile(filePath, "utf8");
1608
+ } catch (err) {
1609
+ if (err.code === "ENOENT") return void 0;
1610
+ throw err;
1611
+ }
1612
+ }
1613
+ function decideAction2(existing, previousVersion, newVersion, newContent) {
1614
+ if (existing === void 0) return "installed";
1615
+ if (existing === newContent) return "unchanged";
1616
+ if (previousVersion === void 0) return "installed";
1617
+ if (previousVersion !== newVersion) return "upgraded";
1618
+ return "upgraded";
1619
+ }
1620
+
1621
+ // src/core/skill/targets/generic.ts
1622
+ var genericTarget = {
1623
+ id: "agents-md",
1624
+ label: "Generic AGENTS.md",
1625
+ artifactPaths: [AGENTS_MD_ARTIFACT_PATH],
1626
+ writerKey: "agents-md",
1627
+ write: writeAgentsMd,
1628
+ readInstalledVersion: readAgentsMdInstalledVersion
1629
+ };
1630
+
1631
+ // src/core/skill/targets/index.ts
1632
+ var REGISTRY = [
1633
+ claudeCodeTarget,
1634
+ codexTarget,
1635
+ cursorTarget,
1636
+ genericTarget
1637
+ ];
1638
+ function listSkillTargets() {
1639
+ return REGISTRY;
1640
+ }
1641
+ function getTarget(id) {
1642
+ const found = REGISTRY.find((t) => t.id === id);
1643
+ if (!found) {
1644
+ throw new Error(
1645
+ `Unknown skill target '${id}'. Supported: ${REGISTRY.map((t) => t.id).join(", ")}`
1646
+ );
1647
+ }
1648
+ return found;
1649
+ }
1650
+ function dedupeTargets(targets) {
1651
+ const groups = /* @__PURE__ */ new Map();
1652
+ for (const target of targets) {
1653
+ const existing = groups.get(target.writerKey);
1654
+ if (existing) {
1655
+ existing.ids.push(target.id);
1656
+ existing.labels.push(target.label);
1657
+ } else {
1658
+ groups.set(target.writerKey, {
1659
+ writerKey: target.writerKey,
1660
+ primary: target,
1661
+ ids: [target.id],
1662
+ labels: [target.label],
1663
+ artifactPaths: target.artifactPaths
1664
+ });
1665
+ }
1666
+ }
1667
+ return [...groups.values()];
1668
+ }
1669
+
1670
+ // package.json
1671
+ var package_default = {
1672
+ version: "0.1.0"};
1673
+ var PACKAGE_VERSION = package_default.version;
1674
+
1675
+ // src/core/skill/version.ts
1676
+ var SKILL_VERSION = PACKAGE_VERSION;
1677
+
1678
+ // src/core/skill/runner.ts
1679
+ async function runSkillInstall(repoRoot, opts) {
1680
+ if (opts.targets.length === 0) {
1681
+ throw new Error("At least one target id must be supplied.");
1682
+ }
1683
+ const targets = opts.targets.map((id) => getTarget(id));
1684
+ const groups = dedupeTargets(targets);
1685
+ const outcomes = [];
1686
+ for (const group of groups) {
1687
+ const writeOutcome = await safeWrite(group.primary.write, {
1688
+ repoRoot,
1689
+ skillVersion: SKILL_VERSION,
1690
+ citadelBaseDir: opts.citadelBaseDir
1691
+ });
1692
+ for (let i = 0; i < group.ids.length; i += 1) {
1693
+ const idValue = group.ids[i];
1694
+ const labelValue = group.labels[i];
1695
+ if (idValue === void 0 || labelValue === void 0) continue;
1696
+ outcomes.push({
1697
+ id: idValue,
1698
+ label: labelValue,
1699
+ artifactPaths: group.artifactPaths,
1700
+ action: writeOutcome.action,
1701
+ ...writeOutcome.installedVersion !== void 0 ? { installedVersion: writeOutcome.installedVersion } : {},
1702
+ ...writeOutcome.error !== void 0 ? { error: writeOutcome.error } : {}
1703
+ });
1704
+ }
1705
+ }
1706
+ return { outcomes, counts: countOutcomes(outcomes) };
1707
+ }
1708
+ async function runSkillUpgrade(repoRoot, opts) {
1709
+ const installedGroups = await findInstalledGroups(repoRoot);
1710
+ if (installedGroups.length === 0) {
1711
+ return { outcomes: [], counts: countOutcomes([]) };
1712
+ }
1713
+ const outcomes = [];
1714
+ for (const group of installedGroups) {
1715
+ const installedVersion = await group.primary.readInstalledVersion(repoRoot);
1716
+ const isOutdated = installedVersion !== SKILL_VERSION;
1717
+ if (opts.check === true) {
1718
+ const action = isOutdated ? "upgraded" : "unchanged";
1719
+ for (let i = 0; i < group.ids.length; i += 1) {
1720
+ const idValue = group.ids[i];
1721
+ const labelValue = group.labels[i];
1722
+ if (idValue === void 0 || labelValue === void 0) continue;
1723
+ outcomes.push({
1724
+ id: idValue,
1725
+ label: labelValue,
1726
+ artifactPaths: group.artifactPaths,
1727
+ action,
1728
+ ...installedVersion !== void 0 ? { installedVersion } : {}
1729
+ });
1730
+ }
1731
+ continue;
1732
+ }
1733
+ const writeOutcome = await safeWrite(group.primary.write, {
1734
+ repoRoot,
1735
+ skillVersion: SKILL_VERSION,
1736
+ citadelBaseDir: opts.citadelBaseDir
1737
+ });
1738
+ for (let i = 0; i < group.ids.length; i += 1) {
1739
+ const idValue = group.ids[i];
1740
+ const labelValue = group.labels[i];
1741
+ if (idValue === void 0 || labelValue === void 0) continue;
1742
+ outcomes.push({
1743
+ id: idValue,
1744
+ label: labelValue,
1745
+ artifactPaths: group.artifactPaths,
1746
+ action: writeOutcome.action,
1747
+ ...writeOutcome.installedVersion !== void 0 ? { installedVersion: writeOutcome.installedVersion } : {},
1748
+ ...writeOutcome.error !== void 0 ? { error: writeOutcome.error } : {}
1749
+ });
1750
+ }
1751
+ }
1752
+ return { outcomes, counts: countOutcomes(outcomes) };
1753
+ }
1754
+ async function runSkillStatus(repoRoot) {
1755
+ const outcomes = [];
1756
+ let upToDate = 0;
1757
+ let outdated = 0;
1758
+ let notInstalled = 0;
1759
+ for (const target of listSkillTargets()) {
1760
+ const installedVersion = await target.readInstalledVersion(repoRoot);
1761
+ let state;
1762
+ if (installedVersion === void 0) {
1763
+ state = "not-installed";
1764
+ notInstalled += 1;
1765
+ } else if (installedVersion === SKILL_VERSION) {
1766
+ state = "up-to-date";
1767
+ upToDate += 1;
1768
+ } else {
1769
+ state = "outdated";
1770
+ outdated += 1;
1771
+ }
1772
+ outcomes.push({
1773
+ id: target.id,
1774
+ label: target.label,
1775
+ artifactPaths: target.artifactPaths,
1776
+ state,
1777
+ ...installedVersion !== void 0 ? { installedVersion } : {},
1778
+ currentVersion: SKILL_VERSION
1779
+ });
1780
+ }
1781
+ return {
1782
+ outcomes,
1783
+ counts: { upToDate, outdated, notInstalled }
1784
+ };
1785
+ }
1786
+ async function safeWrite(write6, input) {
1787
+ try {
1788
+ return await write6(input);
1789
+ } catch (err) {
1790
+ const message = err instanceof Error ? err.message : String(err);
1791
+ return { action: "failed", error: message };
1792
+ }
1793
+ }
1794
+ async function findInstalledGroups(repoRoot) {
1795
+ const targets = listSkillTargets();
1796
+ const installed = [];
1797
+ for (const target of targets) {
1798
+ const installedVersion = await target.readInstalledVersion(repoRoot);
1799
+ if (installedVersion !== void 0) installed.push(target);
1800
+ }
1801
+ return dedupeTargets(installed);
1802
+ }
1803
+ function countOutcomes(outcomes) {
1804
+ let installed = 0;
1805
+ let upgraded = 0;
1806
+ let unchanged = 0;
1807
+ let failed = 0;
1808
+ for (const o of outcomes) {
1809
+ if (o.action === "installed") installed += 1;
1810
+ else if (o.action === "upgraded") upgraded += 1;
1811
+ else if (o.action === "unchanged") unchanged += 1;
1812
+ else if (o.action === "failed") failed += 1;
1813
+ }
1814
+ return { installed, upgraded, unchanged, failed };
1815
+ }
1816
+
1817
+ export { AuthError, ConfigError, DestinationBlockedError, MaesterError, RefNotFoundError, SKILL_VERSION, listSkillTargets, loadCitadelConfig, loadMaesterConfig, runSkillInstall, runSkillStatus, runSkillUpgrade, runStatus, runSync };
1818
+ //# sourceMappingURL=index.js.map
1819
+ //# sourceMappingURL=index.js.map