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/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/bin/maester.mjs +7 -0
- package/dist/cli/main.d.ts +4 -0
- package/dist/cli/main.js +3760 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/index.d.ts +546 -0
- package/dist/index.js +1819 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
package/dist/cli/main.js
ADDED
|
@@ -0,0 +1,3760 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { existsSync, promises } from 'fs';
|
|
3
|
+
import path4, { resolve, dirname, relative, extname } from 'path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import * as clack from '@clack/prompts';
|
|
6
|
+
import { Chalk } from 'chalk';
|
|
7
|
+
import { readFile, mkdir, writeFile, mkdtemp, rm, readdir, cp, rename } from 'fs/promises';
|
|
8
|
+
import { parseDocument, isMap, stringify } from 'yaml';
|
|
9
|
+
import { globby } from 'globby';
|
|
10
|
+
import { execFile as execFile$1 } from 'child_process';
|
|
11
|
+
import { promisify } from 'util';
|
|
12
|
+
import { simpleGit } from 'simple-git';
|
|
13
|
+
import { createConsola } from 'consola';
|
|
14
|
+
import picomatch from 'picomatch';
|
|
15
|
+
import matter from 'gray-matter';
|
|
16
|
+
|
|
17
|
+
var __defProp = Object.defineProperty;
|
|
18
|
+
var __export = (target, all) => {
|
|
19
|
+
for (var name in all)
|
|
20
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
21
|
+
};
|
|
22
|
+
var STATE_VALUES = ["draft", "canon"];
|
|
23
|
+
var StateSchema = z.enum(STATE_VALUES);
|
|
24
|
+
var DEFAULT_STATE = "draft";
|
|
25
|
+
function parseState(value) {
|
|
26
|
+
if (value === void 0 || value === null) return { kind: "absent" };
|
|
27
|
+
const raw = typeof value === "string" ? value : String(value);
|
|
28
|
+
if (raw.length === 0) return { kind: "absent" };
|
|
29
|
+
const result = StateSchema.safeParse(raw);
|
|
30
|
+
if (result.success) return { kind: "valid", value: result.data };
|
|
31
|
+
return { kind: "invalid", raw };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/schemas/citadel.ts
|
|
35
|
+
var SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
36
|
+
var ENV_VAR_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
37
|
+
var URL_FORMS = [/^https:\/\/\S+$/i, /^ssh:\/\/\S+$/i, /^git@[^\s:]+:\S+$/, /^file:\/\/\S+$/i];
|
|
38
|
+
function isValidGitUrl(url) {
|
|
39
|
+
if (/\s/.test(url)) return false;
|
|
40
|
+
return URL_FORMS.some((re) => re.test(url));
|
|
41
|
+
}
|
|
42
|
+
function isSafeRelativePath(value) {
|
|
43
|
+
if (value.length === 0) return false;
|
|
44
|
+
if (value.startsWith("/")) return false;
|
|
45
|
+
if (value.split(/[\\/]+/).some((seg) => seg === "..")) return false;
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
function isSafeIncludesEntry(value) {
|
|
49
|
+
if (value.length === 0 || /^\s+$/.test(value)) return false;
|
|
50
|
+
if (value.startsWith("/")) return false;
|
|
51
|
+
if (value.split(/[\\/]+/).some((seg) => seg === "..")) return false;
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
var AuthRefNoneSchema = z.object({
|
|
55
|
+
type: z.literal("none")
|
|
56
|
+
}).strict();
|
|
57
|
+
var AuthRefTokenSchema = z.object({
|
|
58
|
+
type: z.literal("token"),
|
|
59
|
+
envVar: z.string().min(1, "envVar is required for token auth").regex(ENV_VAR_RE, "envVar must be uppercase letters, digits, and underscores")
|
|
60
|
+
}).strict();
|
|
61
|
+
var AuthRefSchema = z.discriminatedUnion("type", [AuthRefNoneSchema, AuthRefTokenSchema]);
|
|
62
|
+
var IncludesPathSchema = z.string().min(1).refine(
|
|
63
|
+
isSafeIncludesEntry,
|
|
64
|
+
"includes entry must be a repo-relative path or glob; no leading '/' and no '..'"
|
|
65
|
+
);
|
|
66
|
+
var IncludeEntryObjectSchema = z.object({
|
|
67
|
+
path: IncludesPathSchema,
|
|
68
|
+
state: StateSchema.optional()
|
|
69
|
+
}).strict();
|
|
70
|
+
var IncludeEntrySchema = z.union([IncludesPathSchema, IncludeEntryObjectSchema]);
|
|
71
|
+
function normalizeIncludeEntry(entry) {
|
|
72
|
+
if (typeof entry === "string") return { path: entry };
|
|
73
|
+
if (entry.state === void 0) return { path: entry.path };
|
|
74
|
+
return { path: entry.path, state: entry.state };
|
|
75
|
+
}
|
|
76
|
+
var SourceSchema = z.object({
|
|
77
|
+
name: z.string().min(1).regex(SLUG_RE, "name must be a kebab-case slug starting with a letter or digit"),
|
|
78
|
+
url: z.string().refine(isValidGitUrl, "url must be https://, ssh://, or git@host:path"),
|
|
79
|
+
ref: z.string().min(1).optional(),
|
|
80
|
+
includes: z.array(IncludeEntrySchema).min(1, "includes must declare at least one entry when present").optional(),
|
|
81
|
+
auth: AuthRefSchema.optional(),
|
|
82
|
+
destination: z.string().min(1).refine(isSafeRelativePath, "destination must be a repo-relative path with no '..' segments").optional(),
|
|
83
|
+
description: z.string().min(1).optional(),
|
|
84
|
+
tags: z.array(z.string().min(1).regex(SLUG_RE, "tags must be slugs")).optional()
|
|
85
|
+
}).strict();
|
|
86
|
+
var DEFAULT_BASE_DIR = "citadel";
|
|
87
|
+
function applyCombinedInvariants(data, ctx) {
|
|
88
|
+
if (data.sources.length === 0) {
|
|
89
|
+
ctx.addIssue({
|
|
90
|
+
code: z.ZodIssueCode.custom,
|
|
91
|
+
message: "citadel must declare at least one source",
|
|
92
|
+
path: ["sources"]
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const baseDir = data.baseDir ?? DEFAULT_BASE_DIR;
|
|
97
|
+
const namesSeen = /* @__PURE__ */ new Map();
|
|
98
|
+
const destsSeen = /* @__PURE__ */ new Map();
|
|
99
|
+
for (let i = 0; i < data.sources.length; i++) {
|
|
100
|
+
const entry = data.sources[i];
|
|
101
|
+
if (!entry?.name) continue;
|
|
102
|
+
const priorIndex = namesSeen.get(entry.name);
|
|
103
|
+
if (priorIndex !== void 0) {
|
|
104
|
+
ctx.addIssue({
|
|
105
|
+
code: z.ZodIssueCode.custom,
|
|
106
|
+
message: `duplicate name '${entry.name}' \u2014 also used by sources[${priorIndex}]`,
|
|
107
|
+
path: ["sources", i, "name"]
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
namesSeen.set(entry.name, i);
|
|
111
|
+
}
|
|
112
|
+
const resolved = entry.destination ? resolve("/_citadel_root_", entry.destination) : resolve("/_citadel_root_", baseDir, entry.name);
|
|
113
|
+
const prior = destsSeen.get(resolved);
|
|
114
|
+
if (prior !== void 0) {
|
|
115
|
+
ctx.addIssue({
|
|
116
|
+
code: z.ZodIssueCode.custom,
|
|
117
|
+
message: `destination collision: sources '${entry.name}' and '${prior.name}' (sources[${prior.index}]) both resolve to the same path`,
|
|
118
|
+
path: ["sources", i, "destination"]
|
|
119
|
+
});
|
|
120
|
+
} else {
|
|
121
|
+
destsSeen.set(resolved, { index: i, name: entry.name });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
var CitadelConfigSchema = z.object({
|
|
126
|
+
schemaVersion: z.literal(1),
|
|
127
|
+
baseDir: z.string().min(1).refine(isSafeRelativePath, "baseDir must be a repo-relative path with no '..' segments").optional(),
|
|
128
|
+
sources: z.array(SourceSchema).optional().default([])
|
|
129
|
+
}).strict().superRefine((data, ctx) => {
|
|
130
|
+
applyCombinedInvariants(data, ctx);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// src/core/config/paths.ts
|
|
134
|
+
var CITADEL_CONFIG_FILENAME = "citadel.yaml";
|
|
135
|
+
var MAESTER_CONFIG_FILENAME = "maester.yaml";
|
|
136
|
+
var CACHE_DIR_NAME = ".maester";
|
|
137
|
+
var CACHE_SUBDIR = ".maester/cache";
|
|
138
|
+
function citadelConfigPath(repoRoot) {
|
|
139
|
+
return resolve(repoRoot, CITADEL_CONFIG_FILENAME);
|
|
140
|
+
}
|
|
141
|
+
function maesterConfigPath(repoRoot) {
|
|
142
|
+
return resolve(repoRoot, MAESTER_CONFIG_FILENAME);
|
|
143
|
+
}
|
|
144
|
+
function cachePathForSource(repoRoot, sourceName) {
|
|
145
|
+
return resolve(repoRoot, CACHE_SUBDIR, sourceName);
|
|
146
|
+
}
|
|
147
|
+
function defaultDestinationFor(repoRoot, sourceName, baseDir) {
|
|
148
|
+
return resolve(repoRoot, baseDir ?? DEFAULT_BASE_DIR, sourceName);
|
|
149
|
+
}
|
|
150
|
+
function detectRoles(repoRoot) {
|
|
151
|
+
return {
|
|
152
|
+
hasCitadel: existsSync(citadelConfigPath(repoRoot)),
|
|
153
|
+
hasMaester: existsSync(maesterConfigPath(repoRoot))
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/core/errors.ts
|
|
158
|
+
var MaesterError = class extends Error {
|
|
159
|
+
code;
|
|
160
|
+
cause;
|
|
161
|
+
constructor(code, message, options) {
|
|
162
|
+
super(message);
|
|
163
|
+
this.name = "MaesterError";
|
|
164
|
+
this.code = code;
|
|
165
|
+
if (options?.cause !== void 0) {
|
|
166
|
+
this.cause = options.cause;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
var ConfigError = class extends MaesterError {
|
|
171
|
+
filePath;
|
|
172
|
+
line;
|
|
173
|
+
column;
|
|
174
|
+
constructor(message, detail = {}) {
|
|
175
|
+
super("CONFIG_ERROR", message, detail.cause !== void 0 ? { cause: detail.cause } : {});
|
|
176
|
+
this.name = "ConfigError";
|
|
177
|
+
this.filePath = detail.filePath;
|
|
178
|
+
if (detail.line !== void 0) this.line = detail.line;
|
|
179
|
+
if (detail.column !== void 0) this.column = detail.column;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
var AuthError = class extends MaesterError {
|
|
183
|
+
envVar;
|
|
184
|
+
constructor(envVar, message) {
|
|
185
|
+
super("AUTH_ERROR", message ?? `Environment variable ${envVar} is not set.`);
|
|
186
|
+
this.name = "AuthError";
|
|
187
|
+
this.envVar = envVar;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
var RefNotFoundError = class extends MaesterError {
|
|
191
|
+
ref;
|
|
192
|
+
url;
|
|
193
|
+
constructor(ref, url, cause) {
|
|
194
|
+
super(
|
|
195
|
+
"REF_NOT_FOUND",
|
|
196
|
+
`ref \`${ref}\` not found on \`${url}\``,
|
|
197
|
+
cause !== void 0 ? { cause } : {}
|
|
198
|
+
);
|
|
199
|
+
this.name = "RefNotFoundError";
|
|
200
|
+
this.ref = ref;
|
|
201
|
+
this.url = url;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
var DestinationBlockedError = class extends MaesterError {
|
|
205
|
+
destination;
|
|
206
|
+
constructor(destination, message) {
|
|
207
|
+
super("DESTINATION_BLOCKED", message);
|
|
208
|
+
this.name = "DestinationBlockedError";
|
|
209
|
+
this.destination = destination;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// src/ui/components/banner.ts
|
|
214
|
+
var FULL = `\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
215
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2585\u2584\u2583\u2582\u2581\u2581\u2581\u2582\u2582\u2581\u2581\u2583\u2583\u2585\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
216
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2585\u2585\u2584\u2582 \u2581\u2582\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
217
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2585\u2584\u2586\u2584\u2585\u2582\u2582\u2581 \u2582\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
218
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2584\u2587\u2588\u2586\u2586\u2582\u2582\u2583\u2583\u2581 \u2581 \u2581\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
219
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2585\u2585\u2588\u2588\u2588\u2588\u2587\u2585\u2585\u2584\u2583\u2582\u2582\u2581 \u2581\u2581\u2581\u2581\u2582\u2582\u2582\u2582\u2582\u2581 \u2583\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
220
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2585\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2587\u2587\u2585\u2582\u2582 \u2581\u2582\u2582\u2581\u2583\u2583\u2583\u2583\u2581\u2581\u2581\u2581 \u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
221
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2581\u2581\u2582\u2583\u2583\u2582\u2583 \u2581\u2581 \u2581\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
222
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584\u2586\u2587\u2588\u2588\u2588\u2588\u2588\u2587\u2582 \u2581 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
223
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2583\u2586\u2588\u2588\u2588\u2588\u2588\u2588\u2582\u2582\u2582\u2581 \u2585\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
224
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2582\u2585\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2585\u2582\u2581 \u2581 \u2581 \u2581\u2586\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
225
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2585\u2587\u2588\u2588\u2588\u2587\u2588\u2587\u2585\u2582\u2583\u2584\u2582\u2581 \u2581\u2581\u2582 \u2582\u2581\u2581 \u2582\u2581\u2581\u2583\u2583\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
226
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2585\u2588\u2588\u2588\u2586\u2584\u2583\u2588\u2588\u2588\u2586\u2584\u2583\u2582\u2581\u2582\u2583\u2583\u2582\u2582\u2581\u2581\u2582\u2584\u2586\u2587\u2587\u2586\u2581\u2581\u2581\u2582\u2586\u2585\u2585\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
227
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2586\u2587\u2588\u2588\u2583\u2585\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2582\u2587\u2588\u2588\u2588\u2588\u2588\u2587\u2585\u2584\u2587\u2585\u2581\u2581\u2587\u2584\u2583\u2583\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
228
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2584\u2588\u2587\u2588\u2588\u2584\u2586\u2588\u2588\u2588\u2588\u2587\u2587\u2588\u2588\u2588\u2588\u2583\u2581\u2581\u2587\u2588\u2588\u2587\u2585\u2583\u2584\u2583\u2582 \u2581\u2582\u2588\u2583\u2583\u2582\u2586\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
229
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2587\u2588\u2588\u2588\u2588\u2586\u2583\u2588\u2587\u2586\u2588\u2587\u2587\u2586\u2587\u2588\u2588\u2584 \u2581\u2582\u2581\u2583\u2586\u2586\u2584\u2582\u2581 \u2582\u2583\u2584\u2583\u2586\u2582\u2582\u2583\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
230
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2584\u2587\u2588\u2588\u2588\u2588\u2585\u2588\u2587\u2588\u2586\u2584\u2584\u2581\u2582\u2588\u2588\u2588\u2583 \u2581\u2582\u2581 \u2581\u2582\u2584\u2585\u2586\u2588\u2582\u2584\u2584\u2588\u2582\u2582\u2582\u2585\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
231
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2587\u2585\u2586\u2588\u2588\u2588\u2588\u2585\u2588\u2588\u2588\u2588\u2588\u2587\u2581\u2586\u2588\u2585\u2588\u2581 \u2581\u2582\u2587\u2583 \u2585\u2588\u2588\u2584 \u2587\u2588\u2588\u2582\u2583\u2581\u2587\u2586\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
232
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2586\u2586\u2586\u2588\u2588\u2588\u2588\u2585\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2586\u2587\u2588\u2586\u2581\u2583\u2583 \u2581\u2586\u2585\u2581 \u2587\u2588\u2586\u2582\u2581\u2584\u2584\u2586\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
233
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2586\u2588\u2588\u2586\u2588\u2588\u2588\u2588\u2585\u2588\u2588\u2588\u2588\u2588\u2588\u2584\u2585\u2588\u2588\u2588\u2588\u2588\u2585\u2581 \u2581\u2582 \u2582 \u2581\u2588\u2587\u2583\u2581\u2581\u2587\u2588\u2585\u2583\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
234
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2586\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2585\u2588\u2588\u2588\u2587\u2586\u2585\u2588\u2588\u2588\u2588\u2588\u2587\u2588\u2587\u2587\u2585\u2585\u2582\u2581\u2581 \u2585\u2588\u2585\u2583\u2582\u2584\u2585\u2588\u2588\u2582\u2583\u2585\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
235
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584\u2585\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2588\u2588\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2584\u2584\u2582\u2581\u2582\u2588\u2582 \u2581\u2585\u2588\u2588\u2585\u2583\u2583\u2585\u2586\u2588\u2588\u2584\u2585\u2582\u2581\u2586\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
236
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2585\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2586\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2585\u2584\u2585\u2587\u2587\u2583\u2583\u2584\u2583\u2581\u2582\u2587\u2588\u2588\u2588\u2588\u2586\u2584\u2584\u2587\u2588\u2585\u2588\u2586\u2583\u2584\u2581\u2586\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
237
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2585\u2585\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2586\u2586\u2584\u2582\u2582\u2581 \u2581\u2582\u2581 \u2581\u2583\u2588\u2587\u2587\u2588\u2588\u2588\u2587\u2585\u2586\u2588\u2587\u2587\u2586\u2587\u2586\u2586\u2584\u2585\u2583\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
238
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2587\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584\u2583\u2581\u2582\u2583\u2581\u2582\u2581 \u2581\u2581\u2582\u2586\u2588\u2586\u2587\u2588\u2588\u2588\u2587\u2587\u2586\u2588\u2587\u2587\u2587\u2587\u2588\u2588\u2587\u2587\u2581 \u2582\u2585\u2587\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
239
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2587\u2586\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2585\u2583\u2582\u2582\u2582\u2582\u2583\u2585\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2587\u2587\u2588\u2588\u2588\u2587\u2587\u2588\u2587\u2588\u2587\u2582\u2581\u2581\u2585\u2586\u2583\u2582\u2584\u2586\u2586\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
240
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2587\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2588\u2585\u2585\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2586\u2588\u2588\u2588\u2588\u2587\u2588\u2588\u2588\u2588\u2588\u2587\u2583\u2581\u2584\u2587\u2583\u2583\u2581\u2582\u2583\u2586\u2586\u2586\u2584\u2586\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
241
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2587\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2586\u2588\u2588\u2588\u2588\u2587\u2587\u2587\u2588\u2588\u2588\u2588\u2585\u2583\u2583\u2587\u2586\u2581 \u2582\u2583\u2581 \u2582\u2585\u2582\u2581\u2581\u2584\u2587\u2586\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
242
|
+
\u2588\u2588\u2588\u2588\u2588\u2587\u2587\u2587\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2586\u2586\u2586\u2588\u2588\u2588\u2588\u2588\u2586\u2587\u2588\u2585\u2588\u2588\u2588\u2588\u2587\u2587\u2588\u2585 \u2581\u2583\u2582\u2582\u2581\u2583\u2582 \u2582\u2583\u2585\u2588\u2588\u2587\u2586\u2586\u2586\u2588\u2588\u2588\u2588\u2588
|
|
243
|
+
\u2588\u2588\u2588\u2588\u2585\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2587\u2588\u2588\u2588\u2588\u2588\u2587\u2586\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2588\u2587\u2586\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2587\u2588\u2586\u2584\u2584\u2581\u2581\u2582\u2583\u2582\u2581 \u2584\u2588\u2587\u2587\u2587\u2585\u2586\u2588\u2588\u2588
|
|
244
|
+
\u2588\u2588\u2588\u2588\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2586\u2587\u2586\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2587\u2588\u2588\u2588\u2587\u2588\u2585\u2585\u2585\u2583\u2581\u2581\u2581\u2583\u2585\u2586\u2585\u2584\u2586\u2588\u2587\u2588\u2588\u2588\u2588
|
|
245
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2587\u2587\u2588\u2586\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2588\u2588\u2588\u2588\u2588\u2587\u2588\u2587\u2586\u2587\u2587\u2587\u2586\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
246
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2587\u2587\u2586\u2588\u2586\u2587\u2586\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2587\u2588\u2588\u2588\u2588\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2587\u2587\u2587\u2586\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
247
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2587\u2587\u2587\u2587\u2588\u2588\u2588\u2588\u2588\u2587\u2588\u2588\u2588\u2588\u2588\u2587\u2586\u2587\u2586\u2588\u2586\u2587\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
248
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2586\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2585\u2585\u2585\u2588\u2588\u2588\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
249
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2587\u2585\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
250
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2588\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
251
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2586\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
252
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2588\u2587\u2586\u2588\u2588\u2588\u2588\u2588\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
253
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2587\u2588\u2586\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
254
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
255
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2587\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
256
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2586\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
257
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588`;
|
|
258
|
+
var COMPACT = `\u2588\u2580\u2584\u2580\u2588 \u2584\u2580\u2588 \u2588\u2580\u2580 \u2588\u2580 \u2580\u2588\u2580 \u2588\u2580\u2580 \u2588\u2580\u2588
|
|
259
|
+
\u2588 \u2580 \u2588 \u2588\u2580\u2588 \u2588\u2588\u2584 \u2584\u2588 \u2588 \u2588\u2588\u2584 \u2588\u2580\u2584`;
|
|
260
|
+
var MIN_WIDTH_FOR_FULL = 100;
|
|
261
|
+
var MIN_WIDTH_FOR_COMPACT = 40;
|
|
262
|
+
function selectBannerVariant(columns, isTTY) {
|
|
263
|
+
if (!isTTY) return "suppressed";
|
|
264
|
+
if (columns < MIN_WIDTH_FOR_COMPACT) return "suppressed";
|
|
265
|
+
if (columns < MIN_WIDTH_FOR_FULL) return "compact";
|
|
266
|
+
return "full";
|
|
267
|
+
}
|
|
268
|
+
function renderBanner(theming, variant, opts = {}) {
|
|
269
|
+
if (variant === "suppressed") return "";
|
|
270
|
+
const art = variant === "full" ? FULL : COMPACT;
|
|
271
|
+
const accentArt = theming.painter.token("accent", art);
|
|
272
|
+
if (!opts.subtitle) return accentArt;
|
|
273
|
+
const subtitle = theming.painter.dim(opts.subtitle);
|
|
274
|
+
return `${accentArt}
|
|
275
|
+
${subtitle}`;
|
|
276
|
+
}
|
|
277
|
+
function bannerForContext(theming, columns, subtitle) {
|
|
278
|
+
const variant = selectBannerVariant(columns, theming.caps.isTTY);
|
|
279
|
+
return renderBanner(theming, variant, subtitle !== void 0 ? { subtitle } : {});
|
|
280
|
+
}
|
|
281
|
+
var PromptCancelledError = class extends Error {
|
|
282
|
+
constructor(message = "Prompt cancelled") {
|
|
283
|
+
super(message);
|
|
284
|
+
this.name = "PromptCancelledError";
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
function ensure(value) {
|
|
288
|
+
if (clack.isCancel(value)) {
|
|
289
|
+
throw new PromptCancelledError();
|
|
290
|
+
}
|
|
291
|
+
return value;
|
|
292
|
+
}
|
|
293
|
+
function buildOption(o) {
|
|
294
|
+
const base = { value: o.value, label: o.label };
|
|
295
|
+
return o.hint === void 0 ? base : { ...base, hint: o.hint };
|
|
296
|
+
}
|
|
297
|
+
function createPrompts(theming) {
|
|
298
|
+
return {
|
|
299
|
+
intro: (title) => clack.intro(title),
|
|
300
|
+
outro: (message) => clack.outro(message),
|
|
301
|
+
note: (title, body) => clack.note(body ?? "", title),
|
|
302
|
+
log: {
|
|
303
|
+
message: (m) => clack.log.message(m),
|
|
304
|
+
success: (m) => clack.log.success(m),
|
|
305
|
+
warning: (m) => clack.log.warning(m),
|
|
306
|
+
error: (m) => clack.log.error(m),
|
|
307
|
+
info: (m) => clack.log.info(m)
|
|
308
|
+
},
|
|
309
|
+
text: async (opts) => {
|
|
310
|
+
const validateAdapter = opts.validate ? (v) => {
|
|
311
|
+
const result2 = opts.validate?.(v ?? "");
|
|
312
|
+
return result2 ?? void 0;
|
|
313
|
+
} : void 0;
|
|
314
|
+
const baseOpts = {
|
|
315
|
+
message: opts.message,
|
|
316
|
+
...opts.placeholder !== void 0 ? { placeholder: opts.placeholder } : {},
|
|
317
|
+
...opts.initialValue !== void 0 ? { initialValue: opts.initialValue } : {},
|
|
318
|
+
...validateAdapter ? { validate: validateAdapter } : {}
|
|
319
|
+
};
|
|
320
|
+
const result = await clack.text(baseOpts);
|
|
321
|
+
const checked = ensure(result);
|
|
322
|
+
return checked ?? "";
|
|
323
|
+
},
|
|
324
|
+
confirm: async (opts) => {
|
|
325
|
+
const result = await clack.confirm({
|
|
326
|
+
message: opts.message,
|
|
327
|
+
...opts.initialValue !== void 0 ? { initialValue: opts.initialValue } : {}
|
|
328
|
+
});
|
|
329
|
+
return ensure(result);
|
|
330
|
+
},
|
|
331
|
+
select: async (opts) => {
|
|
332
|
+
const options = opts.options.map(buildOption);
|
|
333
|
+
const params = {
|
|
334
|
+
message: opts.message,
|
|
335
|
+
options,
|
|
336
|
+
...opts.initialValue !== void 0 ? { initialValue: opts.initialValue } : {}
|
|
337
|
+
};
|
|
338
|
+
const result = await clack.select(params);
|
|
339
|
+
return ensure(result);
|
|
340
|
+
},
|
|
341
|
+
multiselect: async (opts) => {
|
|
342
|
+
const options = opts.options.map(buildOption);
|
|
343
|
+
const params = {
|
|
344
|
+
message: opts.message,
|
|
345
|
+
options,
|
|
346
|
+
...opts.initialValues !== void 0 ? { initialValues: opts.initialValues } : {},
|
|
347
|
+
...opts.required !== void 0 ? { required: opts.required } : {}
|
|
348
|
+
};
|
|
349
|
+
const result = await clack.multiselect(params);
|
|
350
|
+
return ensure(result);
|
|
351
|
+
},
|
|
352
|
+
group: clack.group,
|
|
353
|
+
spinner: () => clack.spinner(),
|
|
354
|
+
raw: clack
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/ui/theme/detect.ts
|
|
359
|
+
function isNonEmpty(value) {
|
|
360
|
+
return typeof value === "string" && value.length > 0;
|
|
361
|
+
}
|
|
362
|
+
function detectColorDepth(env, isTTY) {
|
|
363
|
+
if (isNonEmpty(env.NO_COLOR)) {
|
|
364
|
+
return "none";
|
|
365
|
+
}
|
|
366
|
+
if (isNonEmpty(env.FORCE_COLOR)) {
|
|
367
|
+
const v = env.FORCE_COLOR;
|
|
368
|
+
if (v === "0" || v === "false") return "none";
|
|
369
|
+
if (v === "1") return "ansi16";
|
|
370
|
+
if (v === "2") return "ansi256";
|
|
371
|
+
if (v === "3" || v === "true" || v === "") return "truecolor";
|
|
372
|
+
return "truecolor";
|
|
373
|
+
}
|
|
374
|
+
if (!isTTY) {
|
|
375
|
+
return "none";
|
|
376
|
+
}
|
|
377
|
+
const colorTerm = env.COLORTERM ?? "";
|
|
378
|
+
if (colorTerm === "truecolor" || colorTerm === "24bit") {
|
|
379
|
+
return "truecolor";
|
|
380
|
+
}
|
|
381
|
+
const term = env.TERM ?? "";
|
|
382
|
+
if (term.includes("truecolor") || term.includes("24bit")) {
|
|
383
|
+
return "truecolor";
|
|
384
|
+
}
|
|
385
|
+
if (term.includes("256color")) {
|
|
386
|
+
return "ansi256";
|
|
387
|
+
}
|
|
388
|
+
if (term === "dumb" || term === "") {
|
|
389
|
+
return "none";
|
|
390
|
+
}
|
|
391
|
+
return "ansi16";
|
|
392
|
+
}
|
|
393
|
+
function detectTheme(env, override) {
|
|
394
|
+
if (override) return override;
|
|
395
|
+
const fromEnv = env.MAESTER_THEME?.toLowerCase();
|
|
396
|
+
if (fromEnv === "light" || fromEnv === "dark") return fromEnv;
|
|
397
|
+
const colorFgBg = env.COLORFGBG;
|
|
398
|
+
if (colorFgBg) {
|
|
399
|
+
const parts = colorFgBg.split(";");
|
|
400
|
+
const bg = parts.length > 0 ? parts[parts.length - 1] : void 0;
|
|
401
|
+
if (bg !== void 0 && /^\d+$/.test(bg)) {
|
|
402
|
+
const idx = Number(bg);
|
|
403
|
+
if (idx >= 7 && idx <= 15) return "light";
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return "dark";
|
|
407
|
+
}
|
|
408
|
+
function detectUnicode(env, override) {
|
|
409
|
+
if (override !== void 0) return override;
|
|
410
|
+
const lcCtype = env.LC_CTYPE ?? "";
|
|
411
|
+
const lang = env.LANG ?? "";
|
|
412
|
+
return /UTF-?8/i.test(lcCtype) || /UTF-?8/i.test(lang) || process.platform === "darwin";
|
|
413
|
+
}
|
|
414
|
+
function detectMotion(env) {
|
|
415
|
+
return !isNonEmpty(env.MAESTER_NO_MOTION) && !isNonEmpty(env.NO_MOTION);
|
|
416
|
+
}
|
|
417
|
+
function detect(opts = {}) {
|
|
418
|
+
const env = opts.env ?? process.env;
|
|
419
|
+
const isTTY = opts.isTTY ?? Boolean(process.stdout.isTTY);
|
|
420
|
+
const force = opts.forceColor ?? "auto";
|
|
421
|
+
let colorDepth;
|
|
422
|
+
if (force === "never") colorDepth = "none";
|
|
423
|
+
else if (force === "always")
|
|
424
|
+
colorDepth = detectColorDepth({ ...env, FORCE_COLOR: env.FORCE_COLOR ?? "3" }, isTTY);
|
|
425
|
+
else colorDepth = detectColorDepth(env, isTTY);
|
|
426
|
+
return {
|
|
427
|
+
isTTY,
|
|
428
|
+
colorDepth,
|
|
429
|
+
theme: detectTheme(env, opts.themeOverride),
|
|
430
|
+
unicode: detectUnicode(env, opts.unicodeOverride),
|
|
431
|
+
motion: detectMotion(env)
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// src/ui/theme/glyphs.ts
|
|
436
|
+
var CATALOG = {
|
|
437
|
+
cursor: { unicode: "\u25B8", ascii: ">", token: "accent" },
|
|
438
|
+
expand: { unicode: "\u25BE", ascii: "v", token: "accent" },
|
|
439
|
+
collapse: { unicode: "\u25B4", ascii: "^", token: "accent" },
|
|
440
|
+
checkOff: { unicode: "\u25EF", ascii: "[ ]", token: "fgMuted" },
|
|
441
|
+
checkOn: { unicode: "\u25C9", ascii: "[x]", token: "accent" },
|
|
442
|
+
success: { unicode: "\u2713", ascii: "[ok]", token: "success" },
|
|
443
|
+
warning: { unicode: "!", ascii: "!", token: "warning" },
|
|
444
|
+
error: { unicode: "\u2717", ascii: "[X]", token: "error" },
|
|
445
|
+
info: { unicode: "\u203A", ascii: ">", token: "info" },
|
|
446
|
+
bullet: { unicode: "\xB7", ascii: "-", token: "fgMuted" },
|
|
447
|
+
ellipsis: { unicode: "\u2026", ascii: "...", token: void 0 },
|
|
448
|
+
separator: { unicode: " \xB7 ", ascii: " - ", token: "fgMuted" },
|
|
449
|
+
progressFill: { unicode: "\u2588", ascii: "#", token: "accent" },
|
|
450
|
+
progressTrack: { unicode: "\u2591", ascii: ".", token: "fgFaint" }
|
|
451
|
+
};
|
|
452
|
+
var SPINNER_FRAMES_UNICODE = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
453
|
+
var SPINNER_FRAMES_ASCII = ["|", "/", "-", "\\"];
|
|
454
|
+
function glyph(role, unicode) {
|
|
455
|
+
const def = CATALOG[role];
|
|
456
|
+
return { text: unicode ? def.unicode : def.ascii, token: def.token };
|
|
457
|
+
}
|
|
458
|
+
function spinnerFrames(unicode) {
|
|
459
|
+
return unicode ? SPINNER_FRAMES_UNICODE : SPINNER_FRAMES_ASCII;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/ui/theme/tokens.ts
|
|
463
|
+
var PALETTE = {
|
|
464
|
+
accent: { hex: "#7CE7C7", ansi256: 121 },
|
|
465
|
+
accentStrong: { hex: "#2BA88A", ansi256: 36 },
|
|
466
|
+
accentAlt: { hex: "#B197FC", ansi256: 141 },
|
|
467
|
+
success: { hex: "#6ED49B", ansi256: 78 },
|
|
468
|
+
warning: { hex: "#F0C674", ansi256: 222 },
|
|
469
|
+
error: { hex: "#FF6B6B", ansi256: 203 },
|
|
470
|
+
info: { hex: "#74A9F0", ansi256: 111 },
|
|
471
|
+
fgMuted: { hex: "#8A8A8A", ansi256: 245 },
|
|
472
|
+
fgFaint: { hex: "#5A5A5A", ansi256: 240 }
|
|
473
|
+
};
|
|
474
|
+
var ANSI16_BY_TOKEN = {
|
|
475
|
+
accent: "cyanBright",
|
|
476
|
+
accentStrong: "cyan",
|
|
477
|
+
accentAlt: "magentaBright",
|
|
478
|
+
success: "greenBright",
|
|
479
|
+
warning: "yellow",
|
|
480
|
+
error: "redBright",
|
|
481
|
+
info: "blueBright",
|
|
482
|
+
fgMuted: "gray",
|
|
483
|
+
fgFaint: "gray"
|
|
484
|
+
};
|
|
485
|
+
function resolveToken(token, theme) {
|
|
486
|
+
const effective = token === "accent" && theme === "light" ? "accentStrong" : token;
|
|
487
|
+
const palette = PALETTE[effective];
|
|
488
|
+
return {
|
|
489
|
+
hex: palette.hex,
|
|
490
|
+
ansi256: palette.ansi256,
|
|
491
|
+
ansi16: ANSI16_BY_TOKEN[effective] ?? "white"
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/ui/theme/resolver.ts
|
|
496
|
+
var COLOR_LEVEL_BY_DEPTH = {
|
|
497
|
+
none: 0,
|
|
498
|
+
ansi16: 1,
|
|
499
|
+
ansi256: 2,
|
|
500
|
+
truecolor: 3
|
|
501
|
+
};
|
|
502
|
+
function createPainter(depth, theme) {
|
|
503
|
+
const level = COLOR_LEVEL_BY_DEPTH[depth];
|
|
504
|
+
const instance = new Chalk({ level });
|
|
505
|
+
function paint(token, text2) {
|
|
506
|
+
if (level === 0) return text2;
|
|
507
|
+
const resolved = resolveToken(token, theme);
|
|
508
|
+
if (level >= 3) {
|
|
509
|
+
return instance.hex(resolved.hex)(text2);
|
|
510
|
+
}
|
|
511
|
+
if (level === 2) {
|
|
512
|
+
return instance.ansi256(resolved.ansi256)(text2);
|
|
513
|
+
}
|
|
514
|
+
const named = resolved.ansi16;
|
|
515
|
+
const fn = instance[named];
|
|
516
|
+
if (typeof fn === "function") {
|
|
517
|
+
return fn(text2);
|
|
518
|
+
}
|
|
519
|
+
return text2;
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
token: paint,
|
|
523
|
+
bold: (text2) => level === 0 ? text2 : instance.bold(text2),
|
|
524
|
+
dim: (text2) => level === 0 ? text2 : instance.dim(text2),
|
|
525
|
+
italic: (text2) => level === 0 ? text2 : instance.italic(text2),
|
|
526
|
+
underline: (text2) => level === 0 ? text2 : instance.underline(text2),
|
|
527
|
+
inverse: (text2) => level === 0 ? text2 : instance.inverse(text2),
|
|
528
|
+
raw: (text2) => text2
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/ui/theme/index.ts
|
|
533
|
+
function createTheming(opts = {}) {
|
|
534
|
+
const caps = detect(opts);
|
|
535
|
+
const painter = createPainter(caps.colorDepth, caps.theme);
|
|
536
|
+
return {
|
|
537
|
+
caps,
|
|
538
|
+
painter,
|
|
539
|
+
glyph: (role) => glyph(role, caps.unicode),
|
|
540
|
+
paintedGlyph: (role) => {
|
|
541
|
+
const g = glyph(role, caps.unicode);
|
|
542
|
+
return g.token ? painter.token(g.token, g.text) : g.text;
|
|
543
|
+
},
|
|
544
|
+
spinnerFrames: () => spinnerFrames(caps.unicode)
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// src/ui/width.ts
|
|
549
|
+
var DEFAULT_FALLBACK = 80;
|
|
550
|
+
function readColumns(stream = process.stdout) {
|
|
551
|
+
if (typeof stream.columns === "number" && stream.columns > 0) return stream.columns;
|
|
552
|
+
return DEFAULT_FALLBACK;
|
|
553
|
+
}
|
|
554
|
+
var CITADEL_HEADER = `# citadel.yaml
|
|
555
|
+
#
|
|
556
|
+
# This file declares the remote knowledge sources this repository pulls into
|
|
557
|
+
# a local destination directory.
|
|
558
|
+
#
|
|
559
|
+
# Each source is a git repository. By default, the citadel fetches whatever
|
|
560
|
+
# the source publishes in its own \`maester.yaml\` manifest. If a source does
|
|
561
|
+
# not publish a manifest (or you want to override what gets pulled), declare
|
|
562
|
+
# an \`includes\` list and the citadel will materialize exactly those paths
|
|
563
|
+
# or globs instead.
|
|
564
|
+
#
|
|
565
|
+
# By default, every source is surfaced at \`<baseDir>/<source-name>/\` from the
|
|
566
|
+
# repository root. The optional top-level \`baseDir\` field changes that parent
|
|
567
|
+
# folder once for every source; when omitted, the default is \`citadel\`. A
|
|
568
|
+
# per-source \`destination\` always wins over the configured base.
|
|
569
|
+
#
|
|
570
|
+
# Run \`maester sync\` (or \`npm run maester:sync\`) to refresh every source in
|
|
571
|
+
# one pass. Generated by \`npx maester init\` and safe to commit. Secret
|
|
572
|
+
# values are never stored here \u2014 only the names of environment variables
|
|
573
|
+
# that hold them.
|
|
574
|
+
|
|
575
|
+
`;
|
|
576
|
+
var MAESTER_HEADER = `# maester.yaml
|
|
577
|
+
#
|
|
578
|
+
# This file declares the documents this repository publishes to any citadel
|
|
579
|
+
# that pulls from it. It is a manifest only \u2014 the documents themselves live
|
|
580
|
+
# wherever the \`path\` fields point, and \`maester\` does not modify them.
|
|
581
|
+
#
|
|
582
|
+
# Generated by \`npx maester publish\` and safe to commit.
|
|
583
|
+
|
|
584
|
+
`;
|
|
585
|
+
async function writeCitadelConfig(repoRoot, config) {
|
|
586
|
+
const path5 = citadelConfigPath(repoRoot);
|
|
587
|
+
const ordered = {
|
|
588
|
+
schemaVersion: config.schemaVersion,
|
|
589
|
+
...config.baseDir ? { baseDir: config.baseDir } : {},
|
|
590
|
+
sources: config.sources
|
|
591
|
+
};
|
|
592
|
+
const body = stringify(ordered, { indent: 2, lineWidth: 100, singleQuote: false });
|
|
593
|
+
await writeFile(path5, `${CITADEL_HEADER}${body}`, "utf8");
|
|
594
|
+
return path5;
|
|
595
|
+
}
|
|
596
|
+
async function writeMaesterConfig(repoRoot, config) {
|
|
597
|
+
const path5 = maesterConfigPath(repoRoot);
|
|
598
|
+
const body = stringify(config, { indent: 2, lineWidth: 100, singleQuote: false });
|
|
599
|
+
await writeFile(path5, `${MAESTER_HEADER}${body}`, "utf8");
|
|
600
|
+
return path5;
|
|
601
|
+
}
|
|
602
|
+
async function appendMissingGitignoreEntries(repoRoot, entries) {
|
|
603
|
+
const path5 = resolve(repoRoot, ".gitignore");
|
|
604
|
+
let existing = "";
|
|
605
|
+
if (existsSync(path5)) {
|
|
606
|
+
existing = await readFile(path5, "utf8");
|
|
607
|
+
}
|
|
608
|
+
const existingLines = new Set(
|
|
609
|
+
existing.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0)
|
|
610
|
+
);
|
|
611
|
+
const added = [];
|
|
612
|
+
const alreadyPresent = [];
|
|
613
|
+
for (const entry of entries) {
|
|
614
|
+
const normalized = entry.trim();
|
|
615
|
+
if (normalized.length === 0) continue;
|
|
616
|
+
if (existingLines.has(normalized)) {
|
|
617
|
+
alreadyPresent.push(normalized);
|
|
618
|
+
} else {
|
|
619
|
+
added.push(normalized);
|
|
620
|
+
existingLines.add(normalized);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (added.length === 0) {
|
|
624
|
+
return { added, alreadyPresent };
|
|
625
|
+
}
|
|
626
|
+
const needsTrailingNewline = existing.length > 0 && !existing.endsWith("\n");
|
|
627
|
+
const appendBlock = `${needsTrailingNewline ? "\n" : ""}${added.join("\n")}
|
|
628
|
+
`;
|
|
629
|
+
await writeFile(path5, `${existing}${appendBlock}`, "utf8");
|
|
630
|
+
return { added, alreadyPresent };
|
|
631
|
+
}
|
|
632
|
+
async function ensureScript(repoRoot, scriptName, command) {
|
|
633
|
+
const path5 = resolve(repoRoot, "package.json");
|
|
634
|
+
if (!existsSync(path5)) {
|
|
635
|
+
return { added: false, reason: "no-package-json" };
|
|
636
|
+
}
|
|
637
|
+
const raw = await readFile(path5, "utf8");
|
|
638
|
+
const trailingNewline = raw.endsWith("\n");
|
|
639
|
+
const parsed = JSON.parse(raw);
|
|
640
|
+
const scripts = parsed.scripts ?? {};
|
|
641
|
+
if (scripts[scriptName] === command) {
|
|
642
|
+
return { added: false, reason: "already-set" };
|
|
643
|
+
}
|
|
644
|
+
if (typeof scripts[scriptName] === "string" && scripts[scriptName] !== command) {
|
|
645
|
+
return { added: false, reason: "already-set" };
|
|
646
|
+
}
|
|
647
|
+
scripts[scriptName] = command;
|
|
648
|
+
parsed.scripts = scripts;
|
|
649
|
+
const serialized = JSON.stringify(parsed, null, 2) + (trailingNewline ? "\n" : "");
|
|
650
|
+
await writeFile(path5, serialized, "utf8");
|
|
651
|
+
return { added: true, reason: "added" };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/core/init/finalize.ts
|
|
655
|
+
async function finalizeCitadel(repoRoot, input) {
|
|
656
|
+
detectDestinationCollisions(repoRoot, input);
|
|
657
|
+
const config = {
|
|
658
|
+
schemaVersion: 1,
|
|
659
|
+
...input.baseDir ? { baseDir: input.baseDir } : {},
|
|
660
|
+
sources: input.sources
|
|
661
|
+
};
|
|
662
|
+
const citadelPath = await writeCitadelConfig(repoRoot, config);
|
|
663
|
+
const gitignore = await appendMissingGitignoreEntries(repoRoot, [`${CACHE_DIR_NAME}/`]);
|
|
664
|
+
const script = await ensureScript(repoRoot, "maester:sync", "maester sync");
|
|
665
|
+
return {
|
|
666
|
+
citadelPath,
|
|
667
|
+
gitignoreAdded: gitignore.added,
|
|
668
|
+
packageJsonScript: script.reason
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function detectDestinationCollisions(repoRoot, input, baseDirArg) {
|
|
672
|
+
const baseDir = Array.isArray(input) ? baseDirArg : input.baseDir;
|
|
673
|
+
const entries = Array.isArray(input) ? input : input.sources.map((s) => ({ name: s.name, destination: s.destination }));
|
|
674
|
+
const byDest = /* @__PURE__ */ new Map();
|
|
675
|
+
for (const entry of entries) {
|
|
676
|
+
const dest = entry.destination ? resolve(repoRoot, entry.destination) : defaultDestinationFor(repoRoot, entry.name, baseDir);
|
|
677
|
+
const prior = byDest.get(dest);
|
|
678
|
+
if (prior) {
|
|
679
|
+
throw new Error(
|
|
680
|
+
`sources '${entry.name}' and '${prior.name}' both resolve to destination '${dest}'. Set a unique destination for one of them.`
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
byDest.set(dest, { name: entry.name });
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/core/init/validators.ts
|
|
688
|
+
function validateSourceName(value) {
|
|
689
|
+
if (!value || value.length === 0) return { ok: false, reason: "Name cannot be empty." };
|
|
690
|
+
if (!SLUG_RE.test(value)) {
|
|
691
|
+
return {
|
|
692
|
+
ok: false,
|
|
693
|
+
reason: "Name must be a kebab-case slug (lowercase letters, digits, and hyphens)."
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
return { ok: true };
|
|
697
|
+
}
|
|
698
|
+
function validateGitUrl(value) {
|
|
699
|
+
if (!value || value.length === 0) return { ok: false, reason: "URL cannot be empty." };
|
|
700
|
+
if (/\s/.test(value)) return { ok: false, reason: "URL cannot contain whitespace." };
|
|
701
|
+
if (value.startsWith("https://") || value.startsWith("ssh://") || value.startsWith("file://")) {
|
|
702
|
+
return { ok: true };
|
|
703
|
+
}
|
|
704
|
+
if (/^git@[^\s:]+:\S+$/.test(value)) return { ok: true };
|
|
705
|
+
return {
|
|
706
|
+
ok: false,
|
|
707
|
+
reason: "URL must start with https://, ssh://, file://, or use the git@host:path form."
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
function validateEnvVarName(value) {
|
|
711
|
+
if (!value || value.length === 0) {
|
|
712
|
+
return { ok: false, reason: "Environment variable name cannot be empty." };
|
|
713
|
+
}
|
|
714
|
+
if (/\s/.test(value)) return { ok: false, reason: "Whitespace is not allowed." };
|
|
715
|
+
if (!ENV_VAR_RE.test(value)) {
|
|
716
|
+
return {
|
|
717
|
+
ok: false,
|
|
718
|
+
reason: "Environment variable name must be UPPER_SNAKE_CASE (e.g. MAESTER_DOCS_TOKEN)."
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
if (value.length >= 32 && !value.includes("_")) {
|
|
722
|
+
return {
|
|
723
|
+
ok: true,
|
|
724
|
+
warning: "That looks unusually long and has no underscores \u2014 make sure you entered the NAME of the env var, not its value."
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
return { ok: true };
|
|
728
|
+
}
|
|
729
|
+
function validateDestination(value) {
|
|
730
|
+
if (!value || value.length === 0) return { ok: true };
|
|
731
|
+
if (value.startsWith("/"))
|
|
732
|
+
return { ok: false, reason: "Destination must be repo-relative (no leading '/')." };
|
|
733
|
+
if (value.split(/[\\/]+/).some((seg) => seg === "..")) {
|
|
734
|
+
return { ok: false, reason: "Destination cannot contain '..' segments." };
|
|
735
|
+
}
|
|
736
|
+
return { ok: true };
|
|
737
|
+
}
|
|
738
|
+
function validateBaseDir(value) {
|
|
739
|
+
if (!value || value.length === 0) return { ok: true };
|
|
740
|
+
if (value.startsWith("/"))
|
|
741
|
+
return { ok: false, reason: "Base directory must be repo-relative (no leading '/')." };
|
|
742
|
+
if (value.split(/[\\/]+/).some((seg) => seg === "..")) {
|
|
743
|
+
return { ok: false, reason: "Base directory cannot contain '..' segments." };
|
|
744
|
+
}
|
|
745
|
+
return { ok: true };
|
|
746
|
+
}
|
|
747
|
+
function validateIncludesEntry(value) {
|
|
748
|
+
if (!value || value.length === 0) return { ok: false, reason: "Includes entry cannot be empty." };
|
|
749
|
+
if (/^\s+$/.test(value)) return { ok: false, reason: "Includes entry cannot be whitespace." };
|
|
750
|
+
if (value.startsWith("/")) {
|
|
751
|
+
return { ok: false, reason: "Includes entry must be repo-relative (no leading '/')." };
|
|
752
|
+
}
|
|
753
|
+
if (value.split(/[\\/]+/).some((seg) => seg === "..")) {
|
|
754
|
+
return { ok: false, reason: "Includes entry cannot contain '..' segments." };
|
|
755
|
+
}
|
|
756
|
+
return { ok: true };
|
|
757
|
+
}
|
|
758
|
+
function validateTag(value) {
|
|
759
|
+
if (!value || value.length === 0) return { ok: false, reason: "Tag cannot be empty." };
|
|
760
|
+
if (!SLUG_RE.test(value)) {
|
|
761
|
+
return { ok: false, reason: "Tag must be a kebab-case slug." };
|
|
762
|
+
}
|
|
763
|
+
return { ok: true };
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// src/core/skill/managed-region.ts
|
|
767
|
+
var BEGIN_MARKER_RE = /<!--\s*maester:skill:begin(?:\s+v=([^\s>]+))?\s*-->/;
|
|
768
|
+
var END_MARKER_LITERAL = "<!-- maester:skill:end -->";
|
|
769
|
+
function extractMarkdownRegion(text2) {
|
|
770
|
+
const beginMatch = BEGIN_MARKER_RE.exec(text2);
|
|
771
|
+
if (!beginMatch) return void 0;
|
|
772
|
+
const beginIdx = beginMatch.index;
|
|
773
|
+
const afterBegin = beginIdx + beginMatch[0].length;
|
|
774
|
+
const endIdx = text2.indexOf(END_MARKER_LITERAL, afterBegin);
|
|
775
|
+
if (endIdx < 0) return void 0;
|
|
776
|
+
const suffixStart = endIdx + END_MARKER_LITERAL.length;
|
|
777
|
+
return {
|
|
778
|
+
prefix: text2.slice(0, beginIdx),
|
|
779
|
+
suffix: text2.slice(suffixStart),
|
|
780
|
+
version: beginMatch[1]
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
function replaceMarkdownRegion(existing, body, version, preambleWhenAbsent = "") {
|
|
784
|
+
const region = renderManagedRegion(body, version);
|
|
785
|
+
if (existing === void 0) {
|
|
786
|
+
if (preambleWhenAbsent.length === 0) {
|
|
787
|
+
return `${region}
|
|
788
|
+
`;
|
|
789
|
+
}
|
|
790
|
+
return `${preambleWhenAbsent}${preambleWhenAbsent.endsWith("\n") ? "" : "\n"}${region}
|
|
791
|
+
`;
|
|
792
|
+
}
|
|
793
|
+
const found = extractMarkdownRegion(existing);
|
|
794
|
+
if (!found) {
|
|
795
|
+
const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
796
|
+
return `${existing}${sep}${region}
|
|
797
|
+
`;
|
|
798
|
+
}
|
|
799
|
+
return `${found.prefix}${region}${found.suffix}`;
|
|
800
|
+
}
|
|
801
|
+
function renderManagedRegion(body, version) {
|
|
802
|
+
const inner = body.endsWith("\n") ? body.slice(0, -1) : body;
|
|
803
|
+
return `<!-- maester:skill:begin v=${version} -->
|
|
804
|
+
${inner}
|
|
805
|
+
${END_MARKER_LITERAL}`;
|
|
806
|
+
}
|
|
807
|
+
function replaceJsonMaesterKey(existingText, maesterBlock) {
|
|
808
|
+
const parsed = existingText && existingText.trim().length > 0 ? JSON.parse(existingText) : {};
|
|
809
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
810
|
+
throw new Error("Expected .claude/settings.json to be a JSON object at the top level.");
|
|
811
|
+
}
|
|
812
|
+
const rebuilt = {};
|
|
813
|
+
let placed = false;
|
|
814
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
815
|
+
if (key === "maester") {
|
|
816
|
+
rebuilt[key] = maesterBlock;
|
|
817
|
+
placed = true;
|
|
818
|
+
} else {
|
|
819
|
+
rebuilt[key] = value;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (!placed) {
|
|
823
|
+
rebuilt.maester = maesterBlock;
|
|
824
|
+
}
|
|
825
|
+
return `${JSON.stringify(rebuilt, null, 2)}
|
|
826
|
+
`;
|
|
827
|
+
}
|
|
828
|
+
function readJsonMaesterKey(existingText) {
|
|
829
|
+
if (!existingText || existingText.trim().length === 0) return void 0;
|
|
830
|
+
let parsed;
|
|
831
|
+
try {
|
|
832
|
+
parsed = JSON.parse(existingText);
|
|
833
|
+
} catch {
|
|
834
|
+
return void 0;
|
|
835
|
+
}
|
|
836
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
837
|
+
const block = parsed.maester;
|
|
838
|
+
if (typeof block !== "object" || block === null || Array.isArray(block)) return void 0;
|
|
839
|
+
return block;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/core/skill/templates/content/citadel-awareness.md
|
|
843
|
+
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';
|
|
844
|
+
|
|
845
|
+
// src/core/skill/templates/content/freshness-awareness.md
|
|
846
|
+
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';
|
|
847
|
+
|
|
848
|
+
// src/core/skill/templates/content/state-awareness.md
|
|
849
|
+
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';
|
|
850
|
+
|
|
851
|
+
// src/core/skill/templates/shells/claude-code.ts
|
|
852
|
+
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.";
|
|
853
|
+
function renderClaudeSkillBody(opts) {
|
|
854
|
+
return [
|
|
855
|
+
"# Grand Maester (Claude Code skill)",
|
|
856
|
+
"",
|
|
857
|
+
"Use this guidance whenever you read files under the citadel base directory",
|
|
858
|
+
`(\`${opts.baseDir}/\`) in this repository.`,
|
|
859
|
+
"",
|
|
860
|
+
interpolate(citadel_awareness_default, opts),
|
|
861
|
+
"",
|
|
862
|
+
interpolate(state_awareness_default, opts),
|
|
863
|
+
"",
|
|
864
|
+
interpolate(freshness_awareness_default, opts)
|
|
865
|
+
].join("\n");
|
|
866
|
+
}
|
|
867
|
+
function renderClaudeSkillFile(body) {
|
|
868
|
+
return [
|
|
869
|
+
"---",
|
|
870
|
+
"name: grand-maester",
|
|
871
|
+
`description: ${SKILL_FRONTMATTER_DESCRIPTION}`,
|
|
872
|
+
"---",
|
|
873
|
+
"",
|
|
874
|
+
body
|
|
875
|
+
].join("\n");
|
|
876
|
+
}
|
|
877
|
+
function buildClaudeMaesterBlock(version) {
|
|
878
|
+
return {
|
|
879
|
+
version,
|
|
880
|
+
hooks: {
|
|
881
|
+
PreToolUse: [
|
|
882
|
+
{
|
|
883
|
+
matcher: "Read|Glob|Grep",
|
|
884
|
+
hooks: [{ type: "command", command: "npx maester skill runtime preread" }]
|
|
885
|
+
}
|
|
886
|
+
]
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
function interpolate(template, opts) {
|
|
891
|
+
return template.replace(/\{\{baseDir\}\}/g, opts.baseDir);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/core/skill/targets/claude-code.ts
|
|
895
|
+
var SKILL_MD_PATH = ".claude/skills/grand-maester/SKILL.md";
|
|
896
|
+
var SETTINGS_JSON_PATH = ".claude/settings.json";
|
|
897
|
+
var claudeCodeTarget = {
|
|
898
|
+
id: "claude-code",
|
|
899
|
+
label: "Claude Code",
|
|
900
|
+
artifactPaths: [SKILL_MD_PATH, SETTINGS_JSON_PATH],
|
|
901
|
+
writerKey: "claude-code",
|
|
902
|
+
write: writeClaudeCode,
|
|
903
|
+
readInstalledVersion
|
|
904
|
+
};
|
|
905
|
+
async function writeClaudeCode(input) {
|
|
906
|
+
const skillResult = await writeSkillMd(input);
|
|
907
|
+
const settingsResult = await writeSettingsJson(input);
|
|
908
|
+
const action = combineActions(skillResult.action, settingsResult.action);
|
|
909
|
+
return {
|
|
910
|
+
action,
|
|
911
|
+
installedVersion: input.skillVersion
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
async function writeSkillMd(input) {
|
|
915
|
+
const filePath = path4.join(input.repoRoot, SKILL_MD_PATH);
|
|
916
|
+
await promises.mkdir(path4.dirname(filePath), { recursive: true });
|
|
917
|
+
const existing = await readTextOrUndefined(filePath);
|
|
918
|
+
const previousVersion = existing ? extractMarkdownRegion(existing)?.version : void 0;
|
|
919
|
+
const body = renderClaudeSkillBody({ baseDir: input.citadelBaseDir });
|
|
920
|
+
const managedRegion = replaceMarkdownRegion(void 0, body, input.skillVersion).trimEnd();
|
|
921
|
+
const fileContent = existing ? replaceMarkdownRegion(existing, body, input.skillVersion) : `${renderClaudeSkillFile(managedRegion)}
|
|
922
|
+
`;
|
|
923
|
+
if (existing === fileContent) return { action: "unchanged" };
|
|
924
|
+
await promises.writeFile(filePath, fileContent, "utf8");
|
|
925
|
+
if (existing === void 0) return { action: "installed" };
|
|
926
|
+
if (previousVersion === void 0) return { action: "installed" };
|
|
927
|
+
return { action: "upgraded" };
|
|
928
|
+
}
|
|
929
|
+
async function writeSettingsJson(input) {
|
|
930
|
+
const filePath = path4.join(input.repoRoot, SETTINGS_JSON_PATH);
|
|
931
|
+
await promises.mkdir(path4.dirname(filePath), { recursive: true });
|
|
932
|
+
const existing = await readTextOrUndefined(filePath);
|
|
933
|
+
const previousBlock = readJsonMaesterKey(existing);
|
|
934
|
+
const previousVersion = typeof previousBlock?.version === "string" ? previousBlock.version : void 0;
|
|
935
|
+
const block = buildClaudeMaesterBlock(input.skillVersion);
|
|
936
|
+
const next = replaceJsonMaesterKey(existing, block);
|
|
937
|
+
if (existing === next) return { action: "unchanged" };
|
|
938
|
+
await promises.writeFile(filePath, next, "utf8");
|
|
939
|
+
if (existing === void 0 || previousBlock === void 0) return { action: "installed" };
|
|
940
|
+
if (previousVersion !== input.skillVersion) return { action: "upgraded" };
|
|
941
|
+
return { action: "upgraded" };
|
|
942
|
+
}
|
|
943
|
+
async function readInstalledVersion(repoRoot) {
|
|
944
|
+
const skillPath = path4.join(repoRoot, SKILL_MD_PATH);
|
|
945
|
+
const text2 = await readTextOrUndefined(skillPath);
|
|
946
|
+
if (!text2) return void 0;
|
|
947
|
+
return extractMarkdownRegion(text2)?.version;
|
|
948
|
+
}
|
|
949
|
+
async function readTextOrUndefined(filePath) {
|
|
950
|
+
try {
|
|
951
|
+
return await promises.readFile(filePath, "utf8");
|
|
952
|
+
} catch (err) {
|
|
953
|
+
if (err.code === "ENOENT") return void 0;
|
|
954
|
+
throw err;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
function combineActions(a, b) {
|
|
958
|
+
if (a === "failed" || b === "failed") return "failed";
|
|
959
|
+
if (a === "installed" || b === "installed") return "installed";
|
|
960
|
+
if (a === "upgraded" || b === "upgraded") return "upgraded";
|
|
961
|
+
return "unchanged";
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// src/core/skill/templates/shells/agents-md.ts
|
|
965
|
+
var PREAMBLE = `# AGENTS.md
|
|
966
|
+
|
|
967
|
+
This file contains agent instructions for working in this repository. The
|
|
968
|
+
section between the maester managed-region markers is generated by
|
|
969
|
+
\`maester skill install\` and refreshed by \`maester skill upgrade\`. Anything
|
|
970
|
+
you write outside that region is preserved across upgrades.
|
|
971
|
+
`;
|
|
972
|
+
function renderAgentsMdBody(opts) {
|
|
973
|
+
return [
|
|
974
|
+
"# Grand Maester guidance",
|
|
975
|
+
"",
|
|
976
|
+
"This repository is set up to aggregate documentation from remote sources",
|
|
977
|
+
"into a local citadel. When you reason about citadel content, follow the",
|
|
978
|
+
"guidance below.",
|
|
979
|
+
"",
|
|
980
|
+
interpolate2(citadel_awareness_default, opts),
|
|
981
|
+
"",
|
|
982
|
+
interpolate2(state_awareness_default, opts),
|
|
983
|
+
"",
|
|
984
|
+
interpolate2(freshness_awareness_default, opts)
|
|
985
|
+
].join("\n");
|
|
986
|
+
}
|
|
987
|
+
function agentsMdPreamble() {
|
|
988
|
+
return PREAMBLE;
|
|
989
|
+
}
|
|
990
|
+
function interpolate2(template, opts) {
|
|
991
|
+
return template.replace(/\{\{baseDir\}\}/g, opts.baseDir);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// src/core/skill/targets/agents-md-writer.ts
|
|
995
|
+
var AGENTS_MD_ARTIFACT_PATH = "AGENTS.md";
|
|
996
|
+
async function writeAgentsMd(input) {
|
|
997
|
+
const filePath = path4.join(input.repoRoot, AGENTS_MD_ARTIFACT_PATH);
|
|
998
|
+
const existingText = await readTextOrUndefined2(filePath);
|
|
999
|
+
const previousVersion = existingText ? extractMarkdownRegion(existingText)?.version : void 0;
|
|
1000
|
+
const body = renderAgentsMdBody({ baseDir: input.citadelBaseDir });
|
|
1001
|
+
const next = replaceMarkdownRegion(existingText, body, input.skillVersion, agentsMdPreamble());
|
|
1002
|
+
const action = decideAction(existingText, previousVersion, input.skillVersion, next);
|
|
1003
|
+
if (action === "unchanged") {
|
|
1004
|
+
return previousVersion !== void 0 ? { action, installedVersion: previousVersion } : { action };
|
|
1005
|
+
}
|
|
1006
|
+
await promises.writeFile(filePath, next, "utf8");
|
|
1007
|
+
return { action, installedVersion: input.skillVersion };
|
|
1008
|
+
}
|
|
1009
|
+
async function readAgentsMdInstalledVersion(repoRoot) {
|
|
1010
|
+
const filePath = path4.join(repoRoot, AGENTS_MD_ARTIFACT_PATH);
|
|
1011
|
+
const text2 = await readTextOrUndefined2(filePath);
|
|
1012
|
+
if (!text2) return void 0;
|
|
1013
|
+
return extractMarkdownRegion(text2)?.version;
|
|
1014
|
+
}
|
|
1015
|
+
async function readTextOrUndefined2(filePath) {
|
|
1016
|
+
try {
|
|
1017
|
+
return await promises.readFile(filePath, "utf8");
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
if (err.code === "ENOENT") return void 0;
|
|
1020
|
+
throw err;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function decideAction(existing, previousVersion, newVersion, newContent) {
|
|
1024
|
+
if (existing === void 0) return "installed";
|
|
1025
|
+
if (existing === newContent) return "unchanged";
|
|
1026
|
+
if (previousVersion === void 0) return "installed";
|
|
1027
|
+
if (previousVersion !== newVersion) return "upgraded";
|
|
1028
|
+
return "upgraded";
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// src/core/skill/targets/codex.ts
|
|
1032
|
+
var codexTarget = {
|
|
1033
|
+
id: "codex",
|
|
1034
|
+
label: "Codex CLI",
|
|
1035
|
+
artifactPaths: [AGENTS_MD_ARTIFACT_PATH],
|
|
1036
|
+
writerKey: "agents-md",
|
|
1037
|
+
write: writeAgentsMd,
|
|
1038
|
+
readInstalledVersion: readAgentsMdInstalledVersion
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
// src/core/skill/templates/shells/cursor.ts
|
|
1042
|
+
var DESCRIPTION = "Citadel-aware guidance for reading aggregated documentation under the citadel base directory.";
|
|
1043
|
+
function renderCursorRuleBody(opts) {
|
|
1044
|
+
return [
|
|
1045
|
+
"# Grand Maester (Cursor rule)",
|
|
1046
|
+
"",
|
|
1047
|
+
"This rule applies when the user asks about content under the citadel",
|
|
1048
|
+
`base directory (\`${opts.baseDir}/\`).`,
|
|
1049
|
+
"",
|
|
1050
|
+
interpolate3(citadel_awareness_default, opts),
|
|
1051
|
+
"",
|
|
1052
|
+
interpolate3(state_awareness_default, opts),
|
|
1053
|
+
"",
|
|
1054
|
+
interpolate3(freshness_awareness_default, opts)
|
|
1055
|
+
].join("\n");
|
|
1056
|
+
}
|
|
1057
|
+
function renderCursorRuleFile(body, opts) {
|
|
1058
|
+
return [
|
|
1059
|
+
"---",
|
|
1060
|
+
`description: ${DESCRIPTION}`,
|
|
1061
|
+
`globs: ["${opts.baseDir}/**/*"]`,
|
|
1062
|
+
"alwaysApply: false",
|
|
1063
|
+
"---",
|
|
1064
|
+
"",
|
|
1065
|
+
body
|
|
1066
|
+
].join("\n");
|
|
1067
|
+
}
|
|
1068
|
+
function interpolate3(template, opts) {
|
|
1069
|
+
return template.replace(/\{\{baseDir\}\}/g, opts.baseDir);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// src/core/skill/targets/cursor.ts
|
|
1073
|
+
var CURSOR_RULE_PATH = ".cursor/rules/grand-maester.mdc";
|
|
1074
|
+
var cursorTarget = {
|
|
1075
|
+
id: "cursor",
|
|
1076
|
+
label: "Cursor",
|
|
1077
|
+
artifactPaths: [CURSOR_RULE_PATH],
|
|
1078
|
+
writerKey: "cursor",
|
|
1079
|
+
write: writeCursor,
|
|
1080
|
+
readInstalledVersion: readInstalledVersion2
|
|
1081
|
+
};
|
|
1082
|
+
async function writeCursor(input) {
|
|
1083
|
+
const filePath = path4.join(input.repoRoot, CURSOR_RULE_PATH);
|
|
1084
|
+
await promises.mkdir(path4.dirname(filePath), { recursive: true });
|
|
1085
|
+
const existing = await readTextOrUndefined3(filePath);
|
|
1086
|
+
const previousVersion = existing ? extractMarkdownRegion(existing)?.version : void 0;
|
|
1087
|
+
const body = renderCursorRuleBody({ baseDir: input.citadelBaseDir });
|
|
1088
|
+
const next = existing ? replaceMarkdownRegion(existing, body, input.skillVersion) : `${renderCursorRuleFile(
|
|
1089
|
+
replaceMarkdownRegion(void 0, body, input.skillVersion).trimEnd(),
|
|
1090
|
+
{
|
|
1091
|
+
baseDir: input.citadelBaseDir
|
|
1092
|
+
}
|
|
1093
|
+
)}
|
|
1094
|
+
`;
|
|
1095
|
+
const action = decideAction2(existing, previousVersion, input.skillVersion, next);
|
|
1096
|
+
if (action === "unchanged") {
|
|
1097
|
+
return previousVersion !== void 0 ? { action, installedVersion: previousVersion } : { action };
|
|
1098
|
+
}
|
|
1099
|
+
await promises.writeFile(filePath, next, "utf8");
|
|
1100
|
+
return { action, installedVersion: input.skillVersion };
|
|
1101
|
+
}
|
|
1102
|
+
async function readInstalledVersion2(repoRoot) {
|
|
1103
|
+
const filePath = path4.join(repoRoot, CURSOR_RULE_PATH);
|
|
1104
|
+
const text2 = await readTextOrUndefined3(filePath);
|
|
1105
|
+
if (!text2) return void 0;
|
|
1106
|
+
return extractMarkdownRegion(text2)?.version;
|
|
1107
|
+
}
|
|
1108
|
+
async function readTextOrUndefined3(filePath) {
|
|
1109
|
+
try {
|
|
1110
|
+
return await promises.readFile(filePath, "utf8");
|
|
1111
|
+
} catch (err) {
|
|
1112
|
+
if (err.code === "ENOENT") return void 0;
|
|
1113
|
+
throw err;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
function decideAction2(existing, previousVersion, newVersion, newContent) {
|
|
1117
|
+
if (existing === void 0) return "installed";
|
|
1118
|
+
if (existing === newContent) return "unchanged";
|
|
1119
|
+
if (previousVersion === void 0) return "installed";
|
|
1120
|
+
if (previousVersion !== newVersion) return "upgraded";
|
|
1121
|
+
return "upgraded";
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// src/core/skill/targets/generic.ts
|
|
1125
|
+
var genericTarget = {
|
|
1126
|
+
id: "agents-md",
|
|
1127
|
+
label: "Generic AGENTS.md",
|
|
1128
|
+
artifactPaths: [AGENTS_MD_ARTIFACT_PATH],
|
|
1129
|
+
writerKey: "agents-md",
|
|
1130
|
+
write: writeAgentsMd,
|
|
1131
|
+
readInstalledVersion: readAgentsMdInstalledVersion
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
// src/core/skill/targets/index.ts
|
|
1135
|
+
var REGISTRY = [
|
|
1136
|
+
claudeCodeTarget,
|
|
1137
|
+
codexTarget,
|
|
1138
|
+
cursorTarget,
|
|
1139
|
+
genericTarget
|
|
1140
|
+
];
|
|
1141
|
+
function listSkillTargets() {
|
|
1142
|
+
return REGISTRY;
|
|
1143
|
+
}
|
|
1144
|
+
function getTarget(id) {
|
|
1145
|
+
const found = REGISTRY.find((t) => t.id === id);
|
|
1146
|
+
if (!found) {
|
|
1147
|
+
throw new Error(
|
|
1148
|
+
`Unknown skill target '${id}'. Supported: ${REGISTRY.map((t) => t.id).join(", ")}`
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
return found;
|
|
1152
|
+
}
|
|
1153
|
+
function dedupeTargets(targets) {
|
|
1154
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1155
|
+
for (const target of targets) {
|
|
1156
|
+
const existing = groups.get(target.writerKey);
|
|
1157
|
+
if (existing) {
|
|
1158
|
+
existing.ids.push(target.id);
|
|
1159
|
+
existing.labels.push(target.label);
|
|
1160
|
+
} else {
|
|
1161
|
+
groups.set(target.writerKey, {
|
|
1162
|
+
writerKey: target.writerKey,
|
|
1163
|
+
primary: target,
|
|
1164
|
+
ids: [target.id],
|
|
1165
|
+
labels: [target.label],
|
|
1166
|
+
artifactPaths: target.artifactPaths
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return [...groups.values()];
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// package.json
|
|
1174
|
+
var package_default = {
|
|
1175
|
+
version: "0.1.0"};
|
|
1176
|
+
var PACKAGE_VERSION = package_default.version;
|
|
1177
|
+
|
|
1178
|
+
// src/core/skill/version.ts
|
|
1179
|
+
var SKILL_VERSION = PACKAGE_VERSION;
|
|
1180
|
+
|
|
1181
|
+
// src/core/skill/runner.ts
|
|
1182
|
+
async function runSkillInstall(repoRoot, opts) {
|
|
1183
|
+
if (opts.targets.length === 0) {
|
|
1184
|
+
throw new Error("At least one target id must be supplied.");
|
|
1185
|
+
}
|
|
1186
|
+
const targets = opts.targets.map((id) => getTarget(id));
|
|
1187
|
+
const groups = dedupeTargets(targets);
|
|
1188
|
+
const outcomes = [];
|
|
1189
|
+
for (const group2 of groups) {
|
|
1190
|
+
const writeOutcome = await safeWrite(group2.primary.write, {
|
|
1191
|
+
repoRoot,
|
|
1192
|
+
skillVersion: SKILL_VERSION,
|
|
1193
|
+
citadelBaseDir: opts.citadelBaseDir
|
|
1194
|
+
});
|
|
1195
|
+
for (let i = 0; i < group2.ids.length; i += 1) {
|
|
1196
|
+
const idValue = group2.ids[i];
|
|
1197
|
+
const labelValue = group2.labels[i];
|
|
1198
|
+
if (idValue === void 0 || labelValue === void 0) continue;
|
|
1199
|
+
outcomes.push({
|
|
1200
|
+
id: idValue,
|
|
1201
|
+
label: labelValue,
|
|
1202
|
+
artifactPaths: group2.artifactPaths,
|
|
1203
|
+
action: writeOutcome.action,
|
|
1204
|
+
...writeOutcome.installedVersion !== void 0 ? { installedVersion: writeOutcome.installedVersion } : {},
|
|
1205
|
+
...writeOutcome.error !== void 0 ? { error: writeOutcome.error } : {}
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
return { outcomes, counts: countOutcomes(outcomes) };
|
|
1210
|
+
}
|
|
1211
|
+
async function runSkillUpgrade(repoRoot, opts) {
|
|
1212
|
+
const installedGroups = await findInstalledGroups(repoRoot);
|
|
1213
|
+
if (installedGroups.length === 0) {
|
|
1214
|
+
return { outcomes: [], counts: countOutcomes([]) };
|
|
1215
|
+
}
|
|
1216
|
+
const outcomes = [];
|
|
1217
|
+
for (const group2 of installedGroups) {
|
|
1218
|
+
const installedVersion = await group2.primary.readInstalledVersion(repoRoot);
|
|
1219
|
+
const isOutdated = installedVersion !== SKILL_VERSION;
|
|
1220
|
+
if (opts.check === true) {
|
|
1221
|
+
const action = isOutdated ? "upgraded" : "unchanged";
|
|
1222
|
+
for (let i = 0; i < group2.ids.length; i += 1) {
|
|
1223
|
+
const idValue = group2.ids[i];
|
|
1224
|
+
const labelValue = group2.labels[i];
|
|
1225
|
+
if (idValue === void 0 || labelValue === void 0) continue;
|
|
1226
|
+
outcomes.push({
|
|
1227
|
+
id: idValue,
|
|
1228
|
+
label: labelValue,
|
|
1229
|
+
artifactPaths: group2.artifactPaths,
|
|
1230
|
+
action,
|
|
1231
|
+
...installedVersion !== void 0 ? { installedVersion } : {}
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
const writeOutcome = await safeWrite(group2.primary.write, {
|
|
1237
|
+
repoRoot,
|
|
1238
|
+
skillVersion: SKILL_VERSION,
|
|
1239
|
+
citadelBaseDir: opts.citadelBaseDir
|
|
1240
|
+
});
|
|
1241
|
+
for (let i = 0; i < group2.ids.length; i += 1) {
|
|
1242
|
+
const idValue = group2.ids[i];
|
|
1243
|
+
const labelValue = group2.labels[i];
|
|
1244
|
+
if (idValue === void 0 || labelValue === void 0) continue;
|
|
1245
|
+
outcomes.push({
|
|
1246
|
+
id: idValue,
|
|
1247
|
+
label: labelValue,
|
|
1248
|
+
artifactPaths: group2.artifactPaths,
|
|
1249
|
+
action: writeOutcome.action,
|
|
1250
|
+
...writeOutcome.installedVersion !== void 0 ? { installedVersion: writeOutcome.installedVersion } : {},
|
|
1251
|
+
...writeOutcome.error !== void 0 ? { error: writeOutcome.error } : {}
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return { outcomes, counts: countOutcomes(outcomes) };
|
|
1256
|
+
}
|
|
1257
|
+
async function runSkillStatus(repoRoot) {
|
|
1258
|
+
const outcomes = [];
|
|
1259
|
+
let upToDate = 0;
|
|
1260
|
+
let outdated = 0;
|
|
1261
|
+
let notInstalled = 0;
|
|
1262
|
+
for (const target of listSkillTargets()) {
|
|
1263
|
+
const installedVersion = await target.readInstalledVersion(repoRoot);
|
|
1264
|
+
let state;
|
|
1265
|
+
if (installedVersion === void 0) {
|
|
1266
|
+
state = "not-installed";
|
|
1267
|
+
notInstalled += 1;
|
|
1268
|
+
} else if (installedVersion === SKILL_VERSION) {
|
|
1269
|
+
state = "up-to-date";
|
|
1270
|
+
upToDate += 1;
|
|
1271
|
+
} else {
|
|
1272
|
+
state = "outdated";
|
|
1273
|
+
outdated += 1;
|
|
1274
|
+
}
|
|
1275
|
+
outcomes.push({
|
|
1276
|
+
id: target.id,
|
|
1277
|
+
label: target.label,
|
|
1278
|
+
artifactPaths: target.artifactPaths,
|
|
1279
|
+
state,
|
|
1280
|
+
...installedVersion !== void 0 ? { installedVersion } : {},
|
|
1281
|
+
currentVersion: SKILL_VERSION
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
return {
|
|
1285
|
+
outcomes,
|
|
1286
|
+
counts: { upToDate, outdated, notInstalled }
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
async function safeWrite(write6, input) {
|
|
1290
|
+
try {
|
|
1291
|
+
return await write6(input);
|
|
1292
|
+
} catch (err) {
|
|
1293
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1294
|
+
return { action: "failed", error: message };
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
async function findInstalledGroups(repoRoot) {
|
|
1298
|
+
const targets = listSkillTargets();
|
|
1299
|
+
const installed = [];
|
|
1300
|
+
for (const target of targets) {
|
|
1301
|
+
const installedVersion = await target.readInstalledVersion(repoRoot);
|
|
1302
|
+
if (installedVersion !== void 0) installed.push(target);
|
|
1303
|
+
}
|
|
1304
|
+
return dedupeTargets(installed);
|
|
1305
|
+
}
|
|
1306
|
+
function countOutcomes(outcomes) {
|
|
1307
|
+
let installed = 0;
|
|
1308
|
+
let upgraded = 0;
|
|
1309
|
+
let unchanged = 0;
|
|
1310
|
+
let failed = 0;
|
|
1311
|
+
for (const o of outcomes) {
|
|
1312
|
+
if (o.action === "installed") installed += 1;
|
|
1313
|
+
else if (o.action === "upgraded") upgraded += 1;
|
|
1314
|
+
else if (o.action === "unchanged") unchanged += 1;
|
|
1315
|
+
else if (o.action === "failed") failed += 1;
|
|
1316
|
+
}
|
|
1317
|
+
return { installed, upgraded, unchanged, failed };
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// src/cli/commands/init.ts
|
|
1321
|
+
function registerInit(program, getContext) {
|
|
1322
|
+
program.command("init").description("Initialize this repository as a citadel (walkthrough).").action(async () => {
|
|
1323
|
+
await runInit(getContext());
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
async function runInit(ctx) {
|
|
1327
|
+
const roles = detectRoles(ctx.repoRoot.path);
|
|
1328
|
+
if (roles.hasCitadel) {
|
|
1329
|
+
ctx.prompts.intro("Citadel already initialized");
|
|
1330
|
+
ctx.prompts.log.info(`A citadel config already exists at ${ctx.repoRoot.path}/citadel.yaml.`);
|
|
1331
|
+
ctx.prompts.log.message("Re-running this flow will not overwrite the existing file.");
|
|
1332
|
+
ctx.prompts.log.message(
|
|
1333
|
+
"To edit the citadel, open citadel.yaml directly (edit verbs coming in a later release)."
|
|
1334
|
+
);
|
|
1335
|
+
ctx.prompts.outro("Nothing changed.");
|
|
1336
|
+
return 0;
|
|
1337
|
+
}
|
|
1338
|
+
ctx.prompts.intro("Initialize a citadel");
|
|
1339
|
+
try {
|
|
1340
|
+
const shouldProceed = await ctx.prompts.confirm({
|
|
1341
|
+
message: `Create a citadel.yaml at ${ctx.repoRoot.path}?`,
|
|
1342
|
+
initialValue: true
|
|
1343
|
+
});
|
|
1344
|
+
if (!shouldProceed) {
|
|
1345
|
+
ctx.prompts.outro("Cancelled \u2014 no files written.");
|
|
1346
|
+
return 0;
|
|
1347
|
+
}
|
|
1348
|
+
const baseDir = await collectBaseDir(ctx);
|
|
1349
|
+
const effectiveBaseDir = baseDir ?? DEFAULT_BASE_DIR;
|
|
1350
|
+
const sources = [];
|
|
1351
|
+
const reservedNames = () => new Set(sources.map((s) => s.name));
|
|
1352
|
+
while (true) {
|
|
1353
|
+
const source = await collectOneSource(ctx, reservedNames(), effectiveBaseDir);
|
|
1354
|
+
sources.push(source);
|
|
1355
|
+
const addAnother = await ctx.prompts.confirm({
|
|
1356
|
+
message: "Add another source?",
|
|
1357
|
+
initialValue: false
|
|
1358
|
+
});
|
|
1359
|
+
if (!addAnother) break;
|
|
1360
|
+
}
|
|
1361
|
+
try {
|
|
1362
|
+
detectDestinationCollisions(ctx.repoRoot.path, {
|
|
1363
|
+
sources,
|
|
1364
|
+
...baseDir ? { baseDir } : {}
|
|
1365
|
+
});
|
|
1366
|
+
} catch (err) {
|
|
1367
|
+
ctx.prompts.log.error(err.message);
|
|
1368
|
+
ctx.prompts.outro("Cancelled due to destination collision. Re-run when resolved.");
|
|
1369
|
+
return 1;
|
|
1370
|
+
}
|
|
1371
|
+
const confirmWrite = await ctx.prompts.confirm({
|
|
1372
|
+
message: `Write ${sources.length} source(s) to citadel.yaml?`,
|
|
1373
|
+
initialValue: true
|
|
1374
|
+
});
|
|
1375
|
+
if (!confirmWrite) {
|
|
1376
|
+
ctx.prompts.outro("Cancelled \u2014 no files written.");
|
|
1377
|
+
return 0;
|
|
1378
|
+
}
|
|
1379
|
+
const result = await finalizeCitadel(ctx.repoRoot.path, {
|
|
1380
|
+
sources,
|
|
1381
|
+
...baseDir ? { baseDir } : {}
|
|
1382
|
+
});
|
|
1383
|
+
ctx.prompts.log.success(`Wrote ${result.citadelPath}`);
|
|
1384
|
+
if (result.gitignoreAdded.length > 0) {
|
|
1385
|
+
ctx.prompts.log.success(`Appended to .gitignore: ${result.gitignoreAdded.join(", ")}`);
|
|
1386
|
+
}
|
|
1387
|
+
if (result.packageJsonScript === "added") {
|
|
1388
|
+
ctx.prompts.log.success('Added "maester:sync" script to package.json.');
|
|
1389
|
+
} else if (result.packageJsonScript === "no-package-json") {
|
|
1390
|
+
ctx.prompts.log.info("No package.json found \u2014 skipping script wire-up.");
|
|
1391
|
+
}
|
|
1392
|
+
const tokenEntries = [];
|
|
1393
|
+
for (const s of sources) {
|
|
1394
|
+
if (s.auth?.type === "token") {
|
|
1395
|
+
tokenEntries.push({ name: s.name, envVar: s.auth.envVar });
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
if (tokenEntries.length > 0) {
|
|
1399
|
+
const summary = tokenEntries.map((t) => `${t.name} -> ${t.envVar}`).join(", ");
|
|
1400
|
+
ctx.prompts.log.info(`Remember to set these env vars before syncing: ${summary}`);
|
|
1401
|
+
}
|
|
1402
|
+
await maybeInstallSkill(ctx, baseDir ?? DEFAULT_BASE_DIR);
|
|
1403
|
+
ctx.prompts.outro("Next: run `npx maester sync` to fetch your sources.");
|
|
1404
|
+
return 0;
|
|
1405
|
+
} catch (err) {
|
|
1406
|
+
if (err instanceof PromptCancelledError) {
|
|
1407
|
+
ctx.prompts.outro("Cancelled \u2014 no files written.");
|
|
1408
|
+
return 130;
|
|
1409
|
+
}
|
|
1410
|
+
throw err;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
async function maybeInstallSkill(ctx, baseDir) {
|
|
1414
|
+
let wantsSkill;
|
|
1415
|
+
try {
|
|
1416
|
+
wantsSkill = await ctx.prompts.confirm({
|
|
1417
|
+
message: "Install the Grand Maester agent skill? (Recommended)",
|
|
1418
|
+
initialValue: true
|
|
1419
|
+
});
|
|
1420
|
+
} catch (err) {
|
|
1421
|
+
if (err instanceof PromptCancelledError) {
|
|
1422
|
+
ctx.prompts.log.info(
|
|
1423
|
+
"Skill install cancelled \u2014 citadel is configured. Run `maester skill install` later if you change your mind."
|
|
1424
|
+
);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
throw err;
|
|
1428
|
+
}
|
|
1429
|
+
if (!wantsSkill) return;
|
|
1430
|
+
let targets;
|
|
1431
|
+
try {
|
|
1432
|
+
targets = await ctx.prompts.multiselect({
|
|
1433
|
+
message: "Which agent(s) should the skill be installed for?",
|
|
1434
|
+
options: listSkillTargets().map((t) => ({ value: t.id, label: t.label })),
|
|
1435
|
+
initialValues: ["claude-code", "codex"],
|
|
1436
|
+
required: true
|
|
1437
|
+
});
|
|
1438
|
+
} catch (err) {
|
|
1439
|
+
if (err instanceof PromptCancelledError) {
|
|
1440
|
+
ctx.prompts.log.info(
|
|
1441
|
+
"Skill install cancelled \u2014 citadel is configured. Run `maester skill install` later if you change your mind."
|
|
1442
|
+
);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
throw err;
|
|
1446
|
+
}
|
|
1447
|
+
if (targets.length === 0) return;
|
|
1448
|
+
try {
|
|
1449
|
+
const result = await runSkillInstall(ctx.repoRoot.path, {
|
|
1450
|
+
targets,
|
|
1451
|
+
mode: "install",
|
|
1452
|
+
citadelBaseDir: baseDir
|
|
1453
|
+
});
|
|
1454
|
+
for (const outcome of result.outcomes) {
|
|
1455
|
+
const artifacts = outcome.artifactPaths.join(", ");
|
|
1456
|
+
if (outcome.action === "failed") {
|
|
1457
|
+
ctx.prompts.log.error(
|
|
1458
|
+
`${outcome.label}: failed${outcome.error ? ` \u2014 ${outcome.error}` : ""}`
|
|
1459
|
+
);
|
|
1460
|
+
} else if (outcome.action === "installed" || outcome.action === "upgraded") {
|
|
1461
|
+
ctx.prompts.log.success(`${outcome.label}: ${outcome.action} \u2192 ${artifacts}`);
|
|
1462
|
+
} else {
|
|
1463
|
+
ctx.prompts.log.info(`${outcome.label}: already up to date (${artifacts})`);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
const total = result.counts.installed + result.counts.upgraded + result.counts.unchanged;
|
|
1467
|
+
if (result.counts.failed === 0 && total > 0) {
|
|
1468
|
+
ctx.prompts.log.success(
|
|
1469
|
+
`Grand Maester installed for ${total} target(s) at v${SKILL_VERSION}.`
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
} catch (err) {
|
|
1473
|
+
ctx.prompts.log.error(
|
|
1474
|
+
`Skill install failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
async function collectOneSource(ctx, reservedNames, effectiveBaseDir) {
|
|
1479
|
+
const name = await ctx.prompts.text({
|
|
1480
|
+
message: "Source name (short, kebab-case, e.g. 'design-system')",
|
|
1481
|
+
validate: (value) => {
|
|
1482
|
+
const trimmed = value.trim();
|
|
1483
|
+
const result = validateSourceName(trimmed);
|
|
1484
|
+
if (!result.ok) return result.reason;
|
|
1485
|
+
if (reservedNames.has(trimmed)) {
|
|
1486
|
+
return `Name '${trimmed}' is already used in this citadel.`;
|
|
1487
|
+
}
|
|
1488
|
+
return void 0;
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
const url = await ctx.prompts.text({
|
|
1492
|
+
message: "Git URL (https://, ssh://, or git@host:path)",
|
|
1493
|
+
validate: (value) => {
|
|
1494
|
+
const result = validateGitUrl(value.trim());
|
|
1495
|
+
return result.ok ? void 0 : result.reason;
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1498
|
+
const ref = await ctx.prompts.text({
|
|
1499
|
+
message: "Ref to pin (branch, tag, or commit SHA) \u2014 leave blank for the remote's default branch",
|
|
1500
|
+
placeholder: "main"
|
|
1501
|
+
});
|
|
1502
|
+
const includes = await collectIncludes(ctx);
|
|
1503
|
+
const auth = await collectAuth(ctx);
|
|
1504
|
+
const destination = await collectDestination(ctx, name.trim(), effectiveBaseDir);
|
|
1505
|
+
const description = await ctx.prompts.text({
|
|
1506
|
+
message: "Description (optional \u2014 short free text)",
|
|
1507
|
+
placeholder: ""
|
|
1508
|
+
});
|
|
1509
|
+
const tagsRaw = await ctx.prompts.text({
|
|
1510
|
+
message: "Tags (optional \u2014 comma-separated slugs)",
|
|
1511
|
+
placeholder: "docs, upstream",
|
|
1512
|
+
validate: (value) => {
|
|
1513
|
+
const trimmed = value.trim();
|
|
1514
|
+
if (trimmed.length === 0) return void 0;
|
|
1515
|
+
const tags2 = parseTagsEntries(trimmed);
|
|
1516
|
+
for (const tag of tags2) {
|
|
1517
|
+
const result = validateTag(tag);
|
|
1518
|
+
if (!result.ok) return `'${tag}': ${result.reason}`;
|
|
1519
|
+
}
|
|
1520
|
+
return void 0;
|
|
1521
|
+
}
|
|
1522
|
+
});
|
|
1523
|
+
const tags = parseTagsEntries(tagsRaw);
|
|
1524
|
+
const trimmedRef = ref.trim();
|
|
1525
|
+
const trimmedDest = destination.trim();
|
|
1526
|
+
const trimmedDesc = description.trim();
|
|
1527
|
+
const source = {
|
|
1528
|
+
name: name.trim(),
|
|
1529
|
+
url: url.trim(),
|
|
1530
|
+
...trimmedRef ? { ref: trimmedRef } : {},
|
|
1531
|
+
...includes.length > 0 ? { includes } : {},
|
|
1532
|
+
...auth ? { auth } : {},
|
|
1533
|
+
...trimmedDest ? { destination: trimmedDest } : {},
|
|
1534
|
+
...trimmedDesc ? { description: trimmedDesc } : {},
|
|
1535
|
+
...tags.length > 0 ? { tags } : {}
|
|
1536
|
+
};
|
|
1537
|
+
return source;
|
|
1538
|
+
}
|
|
1539
|
+
async function collectIncludes(ctx) {
|
|
1540
|
+
const useExplicit = await ctx.prompts.confirm({
|
|
1541
|
+
message: "Declare an explicit `includes` list? (Skip to let the source's own maester.yaml manifest drive what gets pulled.)",
|
|
1542
|
+
initialValue: false
|
|
1543
|
+
});
|
|
1544
|
+
if (!useExplicit) return [];
|
|
1545
|
+
const raw = await ctx.prompts.text({
|
|
1546
|
+
message: "Includes \u2014 repo-relative paths or globs, comma- or whitespace-separated. At least one required.",
|
|
1547
|
+
placeholder: "docs/**/*.md, README.md",
|
|
1548
|
+
validate: (value) => {
|
|
1549
|
+
const entries = parseIncludesEntries(value);
|
|
1550
|
+
if (entries.length === 0) return "At least one includes entry is required.";
|
|
1551
|
+
for (const entry of entries) {
|
|
1552
|
+
const result = validateIncludesEntry(entry);
|
|
1553
|
+
if (!result.ok) return `'${entry}': ${result.reason}`;
|
|
1554
|
+
}
|
|
1555
|
+
return void 0;
|
|
1556
|
+
}
|
|
1557
|
+
});
|
|
1558
|
+
return parseIncludesEntries(raw);
|
|
1559
|
+
}
|
|
1560
|
+
async function collectAuth(ctx) {
|
|
1561
|
+
const authType = await ctx.prompts.select({
|
|
1562
|
+
message: "How should this source authenticate?",
|
|
1563
|
+
initialValue: "public",
|
|
1564
|
+
options: [
|
|
1565
|
+
{ value: "public", label: "No auth required (public repo)" },
|
|
1566
|
+
{
|
|
1567
|
+
value: "delegated",
|
|
1568
|
+
label: "Delegate to my local git (SSH key, credential helper, gh auth)"
|
|
1569
|
+
},
|
|
1570
|
+
{ value: "token", label: "Token via environment variable" }
|
|
1571
|
+
]
|
|
1572
|
+
});
|
|
1573
|
+
if (authType !== "token") return void 0;
|
|
1574
|
+
while (true) {
|
|
1575
|
+
const envVar = await ctx.prompts.text({
|
|
1576
|
+
message: "Enter the NAME of the environment variable (not the token itself)",
|
|
1577
|
+
placeholder: "MAESTER_DOCS_TOKEN",
|
|
1578
|
+
validate: (value) => {
|
|
1579
|
+
const result = validateEnvVarName(value.trim());
|
|
1580
|
+
return result.ok ? void 0 : result.reason;
|
|
1581
|
+
}
|
|
1582
|
+
});
|
|
1583
|
+
const trimmed = envVar.trim();
|
|
1584
|
+
const check = validateEnvVarName(trimmed);
|
|
1585
|
+
if (check.ok && check.warning) {
|
|
1586
|
+
const proceed = await ctx.prompts.confirm({
|
|
1587
|
+
message: `${check.warning}
|
|
1588
|
+
Continue with '${trimmed}' as the env-var name?`,
|
|
1589
|
+
initialValue: false
|
|
1590
|
+
});
|
|
1591
|
+
if (!proceed) continue;
|
|
1592
|
+
}
|
|
1593
|
+
return { type: "token", envVar: trimmed };
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
async function collectDestination(ctx, name, effectiveBaseDir) {
|
|
1597
|
+
return ctx.prompts.text({
|
|
1598
|
+
message: `Destination override (optional, relative to repo root). Default is ${effectiveBaseDir}/${name}`,
|
|
1599
|
+
placeholder: "",
|
|
1600
|
+
validate: (value) => {
|
|
1601
|
+
const result = validateDestination(value.trim());
|
|
1602
|
+
return result.ok ? void 0 : result.reason;
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
async function collectBaseDir(ctx) {
|
|
1607
|
+
const raw = await ctx.prompts.text({
|
|
1608
|
+
message: `Base directory for synced sources (each defaults to <baseDir>/<name>). Press Enter to accept the default '${DEFAULT_BASE_DIR}'.`,
|
|
1609
|
+
placeholder: DEFAULT_BASE_DIR,
|
|
1610
|
+
validate: (value) => {
|
|
1611
|
+
const result = validateBaseDir(value.trim());
|
|
1612
|
+
return result.ok ? void 0 : result.reason;
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
const trimmed = raw.trim();
|
|
1616
|
+
if (trimmed.length === 0 || trimmed === DEFAULT_BASE_DIR) return void 0;
|
|
1617
|
+
return trimmed;
|
|
1618
|
+
}
|
|
1619
|
+
function parseIncludesEntries(raw) {
|
|
1620
|
+
return raw.split(/[\n,]/).map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
1621
|
+
}
|
|
1622
|
+
function parseTagsEntries(raw) {
|
|
1623
|
+
return raw.split(/[,\s]+/).map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// src/core/publish/finalize.ts
|
|
1627
|
+
async function finalizeMaesterManifest(repoRoot, documents) {
|
|
1628
|
+
detectDuplicatePaths(documents);
|
|
1629
|
+
const config = { schemaVersion: 1, documents };
|
|
1630
|
+
const path5 = await writeMaesterConfig(repoRoot, config);
|
|
1631
|
+
return { maesterPath: path5, documentCount: documents.length };
|
|
1632
|
+
}
|
|
1633
|
+
function detectDuplicatePaths(documents) {
|
|
1634
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1635
|
+
for (let i = 0; i < documents.length; i++) {
|
|
1636
|
+
const p = documents[i]?.path;
|
|
1637
|
+
if (!p) continue;
|
|
1638
|
+
if (seen.has(p)) {
|
|
1639
|
+
throw new Error(`Duplicate path '${p}' (also at index ${seen.get(p)}).`);
|
|
1640
|
+
}
|
|
1641
|
+
seen.set(p, i);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// src/core/publish/validators.ts
|
|
1646
|
+
function validateDocumentPath(value) {
|
|
1647
|
+
if (!value || value.length === 0) return { ok: false, reason: "Path cannot be empty." };
|
|
1648
|
+
if (/^\s+$/.test(value)) return { ok: false, reason: "Path cannot be whitespace only." };
|
|
1649
|
+
if (value.startsWith("/")) {
|
|
1650
|
+
return { ok: false, reason: "Path must be repo-relative (no leading '/')." };
|
|
1651
|
+
}
|
|
1652
|
+
if (value.split(/[\\/]+/).some((seg) => seg === "..")) {
|
|
1653
|
+
return { ok: false, reason: "Path cannot contain '..' segments." };
|
|
1654
|
+
}
|
|
1655
|
+
return { ok: true };
|
|
1656
|
+
}
|
|
1657
|
+
function isGlobPath(value) {
|
|
1658
|
+
return /[*?[\]{}]/.test(value);
|
|
1659
|
+
}
|
|
1660
|
+
function validateCategory(value) {
|
|
1661
|
+
if (!value || value.length === 0) return { ok: true };
|
|
1662
|
+
if (!SLUG_RE.test(value)) {
|
|
1663
|
+
return { ok: false, reason: "Category must be a kebab-case slug." };
|
|
1664
|
+
}
|
|
1665
|
+
return { ok: true };
|
|
1666
|
+
}
|
|
1667
|
+
function validateTags(value) {
|
|
1668
|
+
if (!value || value.length === 0) return { ok: true };
|
|
1669
|
+
const tags = value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
1670
|
+
for (const tag of tags) {
|
|
1671
|
+
if (!SLUG_RE.test(tag)) {
|
|
1672
|
+
return { ok: false, reason: `Tag '${tag}' must be a kebab-case slug.` };
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
return { ok: true };
|
|
1676
|
+
}
|
|
1677
|
+
function parseTags(value) {
|
|
1678
|
+
return value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// src/cli/commands/publish.ts
|
|
1682
|
+
function registerPublish(program, getContext) {
|
|
1683
|
+
program.command("publish").description("Configure this repository as a maester (walkthrough).").action(async () => {
|
|
1684
|
+
await runPublish(getContext());
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
async function runPublish(ctx) {
|
|
1688
|
+
const roles = detectRoles(ctx.repoRoot.path);
|
|
1689
|
+
if (roles.hasMaester) {
|
|
1690
|
+
ctx.prompts.intro("Maester manifest already configured");
|
|
1691
|
+
ctx.prompts.log.info(`A maester.yaml already exists at ${ctx.repoRoot.path}/maester.yaml.`);
|
|
1692
|
+
ctx.prompts.log.message("Re-running this flow will not overwrite the existing file.");
|
|
1693
|
+
ctx.prompts.outro("Nothing changed.");
|
|
1694
|
+
return 0;
|
|
1695
|
+
}
|
|
1696
|
+
ctx.prompts.intro("Configure this repo as a maester");
|
|
1697
|
+
const documents = [];
|
|
1698
|
+
const readmePath = resolve(ctx.repoRoot.path, "README.md");
|
|
1699
|
+
if (existsSync(readmePath)) {
|
|
1700
|
+
const useReadme = await ctx.prompts.confirm({
|
|
1701
|
+
message: "Publish README.md (the most common starter document)?",
|
|
1702
|
+
initialValue: true
|
|
1703
|
+
});
|
|
1704
|
+
if (useReadme) {
|
|
1705
|
+
documents.push({ path: "README.md", category: "readme" });
|
|
1706
|
+
ctx.prompts.log.success("Added README.md");
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
try {
|
|
1710
|
+
while (true) {
|
|
1711
|
+
const addMore = await ctx.prompts.confirm({
|
|
1712
|
+
message: documents.length === 0 ? "Add a published document?" : "Add another document?",
|
|
1713
|
+
initialValue: documents.length === 0
|
|
1714
|
+
});
|
|
1715
|
+
if (!addMore) break;
|
|
1716
|
+
const doc = await collectOneDocument(ctx, ctx.repoRoot.path, documents);
|
|
1717
|
+
documents.push(doc);
|
|
1718
|
+
}
|
|
1719
|
+
} catch (err) {
|
|
1720
|
+
if (err instanceof PromptCancelledError) {
|
|
1721
|
+
ctx.prompts.outro("Cancelled \u2014 no files written.");
|
|
1722
|
+
return 130;
|
|
1723
|
+
}
|
|
1724
|
+
throw err;
|
|
1725
|
+
}
|
|
1726
|
+
if (documents.length === 0) {
|
|
1727
|
+
ctx.prompts.log.error("A maester manifest must declare at least one document.");
|
|
1728
|
+
ctx.prompts.outro("Cancelled \u2014 no files written.");
|
|
1729
|
+
return 1;
|
|
1730
|
+
}
|
|
1731
|
+
try {
|
|
1732
|
+
detectDuplicatePaths(documents);
|
|
1733
|
+
} catch (err) {
|
|
1734
|
+
ctx.prompts.log.error(err.message);
|
|
1735
|
+
ctx.prompts.outro("Cancelled.");
|
|
1736
|
+
return 1;
|
|
1737
|
+
}
|
|
1738
|
+
const confirmWrite = await ctx.prompts.confirm({
|
|
1739
|
+
message: `Write ${documents.length} document(s) to maester.yaml?`,
|
|
1740
|
+
initialValue: true
|
|
1741
|
+
});
|
|
1742
|
+
if (!confirmWrite) {
|
|
1743
|
+
ctx.prompts.outro("Cancelled \u2014 no files written.");
|
|
1744
|
+
return 0;
|
|
1745
|
+
}
|
|
1746
|
+
const result = await finalizeMaesterManifest(ctx.repoRoot.path, documents);
|
|
1747
|
+
ctx.prompts.log.success(`Wrote ${result.maesterPath} (${result.documentCount} document(s))`);
|
|
1748
|
+
ctx.prompts.outro("Consuming citadels will pick up this manifest at their next `maester sync`.");
|
|
1749
|
+
return 0;
|
|
1750
|
+
}
|
|
1751
|
+
async function collectOneDocument(ctx, repoRoot, existing) {
|
|
1752
|
+
const path5 = await ctx.prompts.text({
|
|
1753
|
+
message: "Path or glob (relative to the repo root)",
|
|
1754
|
+
placeholder: "docs/runbooks/**/*.md",
|
|
1755
|
+
validate: (value) => {
|
|
1756
|
+
const trimmed2 = value.trim();
|
|
1757
|
+
const result = validateDocumentPath(trimmed2);
|
|
1758
|
+
if (!result.ok) return result.reason;
|
|
1759
|
+
if (existing.some((d) => d.path === trimmed2)) {
|
|
1760
|
+
return `Path '${trimmed2}' is already declared in this manifest.`;
|
|
1761
|
+
}
|
|
1762
|
+
return void 0;
|
|
1763
|
+
}
|
|
1764
|
+
});
|
|
1765
|
+
const trimmed = path5.trim();
|
|
1766
|
+
if (isGlobPath(trimmed)) {
|
|
1767
|
+
const matches = await globby(trimmed, { cwd: repoRoot, dot: false, gitignore: false });
|
|
1768
|
+
ctx.prompts.log.info(`Glob matches ${matches.length} file(s) currently.`);
|
|
1769
|
+
} else {
|
|
1770
|
+
if (!existsSync(resolve(repoRoot, trimmed))) {
|
|
1771
|
+
ctx.prompts.log.warning(`'${trimmed}' does not yet exist in this repo. Saving anyway.`);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
const description = await ctx.prompts.text({
|
|
1775
|
+
message: "Description (optional)",
|
|
1776
|
+
placeholder: ""
|
|
1777
|
+
});
|
|
1778
|
+
const category = await ctx.prompts.text({
|
|
1779
|
+
message: "Category slug (optional, e.g. 'readme', 'adr', 'runbook')",
|
|
1780
|
+
placeholder: "",
|
|
1781
|
+
validate: (value) => {
|
|
1782
|
+
const result = validateCategory(value.trim());
|
|
1783
|
+
return result.ok ? void 0 : result.reason;
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
const tagsRaw = await ctx.prompts.text({
|
|
1787
|
+
message: "Tags (comma-separated, optional)",
|
|
1788
|
+
placeholder: "",
|
|
1789
|
+
validate: (value) => {
|
|
1790
|
+
const result = validateTags(value.trim());
|
|
1791
|
+
return result.ok ? void 0 : result.reason;
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
const trimmedDesc = description.trim();
|
|
1795
|
+
const trimmedCat = category.trim();
|
|
1796
|
+
const tags = parseTags(tagsRaw.trim());
|
|
1797
|
+
const doc = {
|
|
1798
|
+
path: trimmed,
|
|
1799
|
+
...trimmedDesc ? { description: trimmedDesc } : {},
|
|
1800
|
+
...trimmedCat ? { category: trimmedCat } : {},
|
|
1801
|
+
...tags.length > 0 ? { tags } : {}
|
|
1802
|
+
};
|
|
1803
|
+
return doc;
|
|
1804
|
+
}
|
|
1805
|
+
function isSafeRepoRelative(value) {
|
|
1806
|
+
if (value.length === 0 || /^\s+$/.test(value)) return false;
|
|
1807
|
+
if (value.startsWith("/")) return false;
|
|
1808
|
+
if (value.split(/[\\/]+/).some((seg) => seg === "..")) return false;
|
|
1809
|
+
return true;
|
|
1810
|
+
}
|
|
1811
|
+
var PublishedDocumentSchema = z.object({
|
|
1812
|
+
path: z.string().min(1).refine(
|
|
1813
|
+
isSafeRepoRelative,
|
|
1814
|
+
"path must be a repo-relative file or glob; no leading '/' and no '..'"
|
|
1815
|
+
),
|
|
1816
|
+
description: z.string().min(1).optional(),
|
|
1817
|
+
category: z.string().min(1).regex(SLUG_RE, "category must be a kebab-case slug").optional(),
|
|
1818
|
+
tags: z.array(z.string().min(1).regex(SLUG_RE, "tags must be slugs")).optional(),
|
|
1819
|
+
state: StateSchema.optional()
|
|
1820
|
+
}).strict();
|
|
1821
|
+
var MaesterConfigSchema = z.object({
|
|
1822
|
+
schemaVersion: z.literal(1),
|
|
1823
|
+
documents: z.array(PublishedDocumentSchema).min(1, "at least one published document must be declared").superRefine((docs, ctx) => {
|
|
1824
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1825
|
+
for (let i = 0; i < docs.length; i++) {
|
|
1826
|
+
const p = docs[i]?.path;
|
|
1827
|
+
if (!p) continue;
|
|
1828
|
+
const prior = seen.get(p);
|
|
1829
|
+
if (prior !== void 0) {
|
|
1830
|
+
ctx.addIssue({
|
|
1831
|
+
code: z.ZodIssueCode.custom,
|
|
1832
|
+
message: `duplicate path '${p}' (also at index ${prior})`,
|
|
1833
|
+
path: [i, "path"]
|
|
1834
|
+
});
|
|
1835
|
+
} else {
|
|
1836
|
+
seen.set(p, i);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
})
|
|
1840
|
+
}).strict();
|
|
1841
|
+
|
|
1842
|
+
// src/core/config/loader.ts
|
|
1843
|
+
async function loadCitadelConfig(repoRoot) {
|
|
1844
|
+
const path5 = citadelConfigPath(repoRoot);
|
|
1845
|
+
if (!existsSync(path5)) {
|
|
1846
|
+
throw new ConfigError(
|
|
1847
|
+
"No citadel.yaml found at the repository root. Run `npx maester init` to create one.",
|
|
1848
|
+
{ filePath: path5 }
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
const raw = await readFile(path5, "utf8");
|
|
1852
|
+
return parseAndValidate(raw, CitadelConfigSchema, path5);
|
|
1853
|
+
}
|
|
1854
|
+
function parseAndValidate(raw, schema, filePath) {
|
|
1855
|
+
const data = parseYaml(raw, filePath);
|
|
1856
|
+
return runSchema(data, schema, filePath);
|
|
1857
|
+
}
|
|
1858
|
+
function parseYaml(raw, filePath) {
|
|
1859
|
+
const doc = parseDocument(raw, { keepSourceTokens: false });
|
|
1860
|
+
const yamlErrors = doc.errors;
|
|
1861
|
+
if (yamlErrors.length > 0) {
|
|
1862
|
+
const first = yamlErrors[0];
|
|
1863
|
+
const pos = positionFromError(first, raw);
|
|
1864
|
+
throw new ConfigError(`YAML parse error: ${first.message}`, {
|
|
1865
|
+
filePath,
|
|
1866
|
+
line: pos.line,
|
|
1867
|
+
column: pos.column,
|
|
1868
|
+
cause: first
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
return doc.toJS({ maxAliasCount: -1 });
|
|
1872
|
+
}
|
|
1873
|
+
function runSchema(data, schema, filePath) {
|
|
1874
|
+
const result = schema.safeParse(data);
|
|
1875
|
+
if (!result.success) {
|
|
1876
|
+
const issue = result.error.issues[0];
|
|
1877
|
+
const where = issue?.path?.length ? ` at \`${issue.path.join(".")}\`` : "";
|
|
1878
|
+
throw new ConfigError(`${filePath}: ${issue?.message ?? "validation failed"}${where}`, {
|
|
1879
|
+
filePath,
|
|
1880
|
+
cause: result.error
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
return result.data;
|
|
1884
|
+
}
|
|
1885
|
+
function positionFromError(err, raw) {
|
|
1886
|
+
const pos = err.pos;
|
|
1887
|
+
if (!pos) return { line: 1, column: 1 };
|
|
1888
|
+
const offset = pos[0];
|
|
1889
|
+
let line = 1;
|
|
1890
|
+
let lastLineStart = 0;
|
|
1891
|
+
for (let i = 0; i < offset && i < raw.length; i++) {
|
|
1892
|
+
if (raw[i] === "\n") {
|
|
1893
|
+
line++;
|
|
1894
|
+
lastLineStart = i + 1;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
return { line, column: offset - lastLineStart + 1 };
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// src/core/auth/resolver.ts
|
|
1901
|
+
function resolveAuth(auth, env = process.env) {
|
|
1902
|
+
if (!auth || auth.type === "none") return { type: "delegated" };
|
|
1903
|
+
const value = env[auth.envVar];
|
|
1904
|
+
if (value === void 0 || value.length === 0) {
|
|
1905
|
+
throw new AuthError(
|
|
1906
|
+
auth.envVar,
|
|
1907
|
+
`${auth.envVar} is not set. Define it in your shell, .env loader, or CI secret manager before syncing.`
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
return { type: "token", value };
|
|
1911
|
+
}
|
|
1912
|
+
var PROVENANCE_FILENAME = ".maester-source.json";
|
|
1913
|
+
async function writeProvenanceMarker(destination, marker) {
|
|
1914
|
+
const path5 = resolve(destination, PROVENANCE_FILENAME);
|
|
1915
|
+
const body = `${JSON.stringify(marker, null, 2)}
|
|
1916
|
+
`;
|
|
1917
|
+
await writeFile(path5, body, "utf8");
|
|
1918
|
+
return path5;
|
|
1919
|
+
}
|
|
1920
|
+
async function readProvenanceMarker(destination) {
|
|
1921
|
+
const path5 = resolve(destination, PROVENANCE_FILENAME);
|
|
1922
|
+
if (!existsSync(path5)) return void 0;
|
|
1923
|
+
try {
|
|
1924
|
+
const text2 = await readFile(path5, "utf8");
|
|
1925
|
+
const parsed = JSON.parse(text2);
|
|
1926
|
+
if (typeof parsed.sourceName !== "string" || typeof parsed.sourceUrl !== "string" || typeof parsed.commitSha !== "string" || !Array.isArray(parsed.filterSet)) {
|
|
1927
|
+
return void 0;
|
|
1928
|
+
}
|
|
1929
|
+
return {
|
|
1930
|
+
sourceName: parsed.sourceName,
|
|
1931
|
+
sourceUrl: parsed.sourceUrl,
|
|
1932
|
+
ref: typeof parsed.ref === "string" ? parsed.ref : void 0,
|
|
1933
|
+
commitSha: parsed.commitSha,
|
|
1934
|
+
filterSet: parsed.filterSet,
|
|
1935
|
+
syncedAt: typeof parsed.syncedAt === "string" ? parsed.syncedAt : (/* @__PURE__ */ new Date(0)).toISOString()
|
|
1936
|
+
};
|
|
1937
|
+
} catch {
|
|
1938
|
+
return void 0;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
function filterSetMatches(a, b) {
|
|
1942
|
+
if (a.length !== b.length) return false;
|
|
1943
|
+
for (let i = 0; i < a.length; i++) {
|
|
1944
|
+
if (a[i] !== b[i]) return false;
|
|
1945
|
+
}
|
|
1946
|
+
return true;
|
|
1947
|
+
}
|
|
1948
|
+
var execFile = promisify(execFile$1);
|
|
1949
|
+
var cachedCapabilities;
|
|
1950
|
+
async function detectGitCapabilities() {
|
|
1951
|
+
if (cachedCapabilities) return cachedCapabilities;
|
|
1952
|
+
const { stdout } = await execFile("git", ["--version"]);
|
|
1953
|
+
const match = stdout.match(/git version (\d+)\.(\d+)(?:\.(\d+))?/);
|
|
1954
|
+
if (!match) {
|
|
1955
|
+
cachedCapabilities = { version: stdout.trim(), supportsPartialClone: false };
|
|
1956
|
+
return cachedCapabilities;
|
|
1957
|
+
}
|
|
1958
|
+
const major = Number(match[1] ?? 0);
|
|
1959
|
+
const minor = Number(match[2] ?? 0);
|
|
1960
|
+
const supportsPartialClone = major > 2 || major === 2 && minor >= 27;
|
|
1961
|
+
cachedCapabilities = { version: `${major}.${minor}`, supportsPartialClone };
|
|
1962
|
+
return cachedCapabilities;
|
|
1963
|
+
}
|
|
1964
|
+
async function shallowSparseClone(input) {
|
|
1965
|
+
await mkdir(input.destination, { recursive: true });
|
|
1966
|
+
const caps = await detectGitCapabilities();
|
|
1967
|
+
const url = input.useTokenInUrl ? injectToken(input.url, input.useTokenInUrl) : input.url;
|
|
1968
|
+
const git = simpleGit(input.destination);
|
|
1969
|
+
if (caps.supportsPartialClone) {
|
|
1970
|
+
const cloneArgs = ["--filter=blob:none", "--depth=1", "--no-checkout", "--sparse"];
|
|
1971
|
+
if (input.ref) cloneArgs.push("--branch", input.ref);
|
|
1972
|
+
try {
|
|
1973
|
+
await git.clone(url, ".", cloneArgs);
|
|
1974
|
+
} catch (err) {
|
|
1975
|
+
if (input.ref && refNotFoundFromError(err)) {
|
|
1976
|
+
throw new RefNotFoundError(input.ref, input.url, err);
|
|
1977
|
+
}
|
|
1978
|
+
throw err;
|
|
1979
|
+
}
|
|
1980
|
+
} else {
|
|
1981
|
+
const cloneArgs = ["--depth=1"];
|
|
1982
|
+
if (input.ref) cloneArgs.push("--branch", input.ref);
|
|
1983
|
+
try {
|
|
1984
|
+
await git.clone(url, ".", cloneArgs);
|
|
1985
|
+
} catch (err) {
|
|
1986
|
+
if (input.ref && refNotFoundFromError(err)) {
|
|
1987
|
+
throw new RefNotFoundError(input.ref, input.url, err);
|
|
1988
|
+
}
|
|
1989
|
+
throw err;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
return git;
|
|
1993
|
+
}
|
|
1994
|
+
async function setSparsePatterns(workDir, patterns) {
|
|
1995
|
+
const git = simpleGit(workDir);
|
|
1996
|
+
if (patterns.length === 0) {
|
|
1997
|
+
await git.raw(["sparse-checkout", "disable"]);
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
await git.raw(["sparse-checkout", "set", "--no-cone", ...patterns]);
|
|
2001
|
+
}
|
|
2002
|
+
async function checkoutRef(workDir, ref) {
|
|
2003
|
+
const git = simpleGit(workDir);
|
|
2004
|
+
try {
|
|
2005
|
+
if (ref) await git.checkout(ref);
|
|
2006
|
+
else await git.checkout(["HEAD"]);
|
|
2007
|
+
} catch (err) {
|
|
2008
|
+
if (ref && refNotFoundFromError(err)) {
|
|
2009
|
+
throw new RefNotFoundError(ref, await tryReadOriginUrl(workDir), err);
|
|
2010
|
+
}
|
|
2011
|
+
throw err;
|
|
2012
|
+
}
|
|
2013
|
+
const sha = (await git.revparse(["HEAD"])).trim();
|
|
2014
|
+
return sha;
|
|
2015
|
+
}
|
|
2016
|
+
async function fetchHead(workDir, ref) {
|
|
2017
|
+
const git = simpleGit(workDir);
|
|
2018
|
+
const args = ref ? ["--depth=1", "origin", ref] : ["--depth=1", "origin"];
|
|
2019
|
+
try {
|
|
2020
|
+
await git.fetch(args);
|
|
2021
|
+
} catch (err) {
|
|
2022
|
+
if (ref && refNotFoundFromError(err)) {
|
|
2023
|
+
throw new RefNotFoundError(ref, await tryReadOriginUrl(workDir), err);
|
|
2024
|
+
}
|
|
2025
|
+
throw err;
|
|
2026
|
+
}
|
|
2027
|
+
await git.raw(["reset", "--hard", "FETCH_HEAD"]);
|
|
2028
|
+
return (await git.revparse(["HEAD"])).trim();
|
|
2029
|
+
}
|
|
2030
|
+
async function clearWorktree(workDir) {
|
|
2031
|
+
await rm(workDir, { recursive: true, force: true });
|
|
2032
|
+
}
|
|
2033
|
+
async function listRemoteRef(input) {
|
|
2034
|
+
const url = input.useTokenInUrl ? injectToken(input.url, input.useTokenInUrl) : input.url;
|
|
2035
|
+
const git = simpleGit();
|
|
2036
|
+
const target = input.ref ?? "HEAD";
|
|
2037
|
+
let output;
|
|
2038
|
+
try {
|
|
2039
|
+
output = await git.listRemote([url, target]);
|
|
2040
|
+
} catch (err) {
|
|
2041
|
+
throw new RefNotFoundError(target, input.url, err);
|
|
2042
|
+
}
|
|
2043
|
+
const sha = parseListRemoteOutput(output, target);
|
|
2044
|
+
if (!sha) {
|
|
2045
|
+
throw new RefNotFoundError(target, input.url);
|
|
2046
|
+
}
|
|
2047
|
+
return sha;
|
|
2048
|
+
}
|
|
2049
|
+
function parseListRemoteOutput(output, target) {
|
|
2050
|
+
const lines = output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
2051
|
+
if (lines.length === 0) return void 0;
|
|
2052
|
+
let primary;
|
|
2053
|
+
let peeled;
|
|
2054
|
+
for (const line of lines) {
|
|
2055
|
+
const match = line.match(/^([0-9a-f]{40})\s+(\S+)$/);
|
|
2056
|
+
if (!match) continue;
|
|
2057
|
+
const sha = match[1];
|
|
2058
|
+
const refName = match[2];
|
|
2059
|
+
if (!sha || !refName) continue;
|
|
2060
|
+
if (refName === target) {
|
|
2061
|
+
primary = sha;
|
|
2062
|
+
continue;
|
|
2063
|
+
}
|
|
2064
|
+
if (matchesRefName(refName, target)) {
|
|
2065
|
+
primary = sha;
|
|
2066
|
+
}
|
|
2067
|
+
if (refName.endsWith("^{}") && matchesRefName(refName.slice(0, -3), target)) {
|
|
2068
|
+
peeled = sha;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
return peeled ?? primary;
|
|
2072
|
+
}
|
|
2073
|
+
function matchesRefName(refName, target) {
|
|
2074
|
+
if (refName === target) return true;
|
|
2075
|
+
if (refName === `refs/heads/${target}`) return true;
|
|
2076
|
+
if (refName === `refs/tags/${target}`) return true;
|
|
2077
|
+
if (refName === `refs/remotes/origin/${target}`) return true;
|
|
2078
|
+
return false;
|
|
2079
|
+
}
|
|
2080
|
+
function refNotFoundFromError(err) {
|
|
2081
|
+
const msg = err instanceof Error ? `${err.message}
|
|
2082
|
+
${err.stack ?? ""}` : String(err);
|
|
2083
|
+
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);
|
|
2084
|
+
}
|
|
2085
|
+
function injectToken(url, token) {
|
|
2086
|
+
if (!url.startsWith("https://")) return url;
|
|
2087
|
+
const after = url.slice("https://".length);
|
|
2088
|
+
return `https://x-access-token:${token}@${after}`;
|
|
2089
|
+
}
|
|
2090
|
+
async function tryReadOriginUrl(workDir) {
|
|
2091
|
+
try {
|
|
2092
|
+
const git = simpleGit(workDir);
|
|
2093
|
+
const remotes = await git.getRemotes(true);
|
|
2094
|
+
return remotes.find((r) => r.name === "origin")?.refs.fetch ?? "";
|
|
2095
|
+
} catch {
|
|
2096
|
+
return "";
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
var MAESTER_MANIFEST_FILENAME = "maester.yaml";
|
|
2100
|
+
async function fetchSource(entry, ctx) {
|
|
2101
|
+
if (entry.includes && entry.includes.length > 0) {
|
|
2102
|
+
const normalized = entry.includes.map(normalizeIncludeEntry);
|
|
2103
|
+
return fetchWithExplicitIncludes(entry, ctx, normalized);
|
|
2104
|
+
}
|
|
2105
|
+
return fetchWithRemoteManifest(entry, ctx);
|
|
2106
|
+
}
|
|
2107
|
+
async function fetchWithRemoteManifest(entry, ctx) {
|
|
2108
|
+
if (!ctx.cacheExists) {
|
|
2109
|
+
await shallowSparseClone({
|
|
2110
|
+
url: entry.url,
|
|
2111
|
+
destination: ctx.cacheDir,
|
|
2112
|
+
ref: entry.ref,
|
|
2113
|
+
...ctx.tokenForUrl ? { useTokenInUrl: ctx.tokenForUrl } : {}
|
|
2114
|
+
});
|
|
2115
|
+
await setSparsePatterns(ctx.cacheDir, [MAESTER_MANIFEST_FILENAME]);
|
|
2116
|
+
await checkoutRef(ctx.cacheDir, entry.ref);
|
|
2117
|
+
} else {
|
|
2118
|
+
await fetchHead(ctx.cacheDir, entry.ref);
|
|
2119
|
+
}
|
|
2120
|
+
const discovery = await discoverManifestFromCache(ctx.cacheDir);
|
|
2121
|
+
if (discovery.mode === "no-manifest") {
|
|
2122
|
+
throw manifestError(entry.name, discovery.reason);
|
|
2123
|
+
}
|
|
2124
|
+
await setSparsePatterns(ctx.cacheDir, discovery.patterns);
|
|
2125
|
+
const commitSha = await checkoutRef(ctx.cacheDir, entry.ref);
|
|
2126
|
+
return {
|
|
2127
|
+
name: entry.name,
|
|
2128
|
+
cacheDir: ctx.cacheDir,
|
|
2129
|
+
commitSha,
|
|
2130
|
+
filterSet: discovery.patterns,
|
|
2131
|
+
rules: discovery.rules,
|
|
2132
|
+
warnings: []
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
async function fetchWithExplicitIncludes(entry, ctx, normalized) {
|
|
2136
|
+
if (!ctx.cacheExists) {
|
|
2137
|
+
await shallowSparseClone({
|
|
2138
|
+
url: entry.url,
|
|
2139
|
+
destination: ctx.cacheDir,
|
|
2140
|
+
ref: entry.ref,
|
|
2141
|
+
...ctx.tokenForUrl ? { useTokenInUrl: ctx.tokenForUrl } : {}
|
|
2142
|
+
});
|
|
2143
|
+
} else {
|
|
2144
|
+
await fetchHead(ctx.cacheDir, entry.ref);
|
|
2145
|
+
}
|
|
2146
|
+
const patterns = normalized.map((entry2) => entry2.path);
|
|
2147
|
+
await setSparsePatterns(ctx.cacheDir, patterns);
|
|
2148
|
+
const commitSha = await checkoutRef(ctx.cacheDir, entry.ref);
|
|
2149
|
+
const matchedFileCount = await countMaterializedFiles(ctx.cacheDir);
|
|
2150
|
+
const warnings = [];
|
|
2151
|
+
if (matchedFileCount === 0) {
|
|
2152
|
+
warnings.push({ type: "no-matches", name: entry.name, includes: patterns });
|
|
2153
|
+
}
|
|
2154
|
+
const rules = normalized.map(
|
|
2155
|
+
(entry2) => entry2.state === void 0 ? { pattern: entry2.path } : { pattern: entry2.path, state: entry2.state }
|
|
2156
|
+
);
|
|
2157
|
+
return {
|
|
2158
|
+
name: entry.name,
|
|
2159
|
+
cacheDir: ctx.cacheDir,
|
|
2160
|
+
commitSha,
|
|
2161
|
+
filterSet: patterns,
|
|
2162
|
+
rules,
|
|
2163
|
+
warnings
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
function manifestError(name, reason) {
|
|
2167
|
+
if (reason === "absent") {
|
|
2168
|
+
return new MaesterError(
|
|
2169
|
+
"MAESTER_MANIFEST_MISSING",
|
|
2170
|
+
`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.`
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
2173
|
+
return new MaesterError(
|
|
2174
|
+
"MAESTER_MANIFEST_INVALID",
|
|
2175
|
+
`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.`
|
|
2176
|
+
);
|
|
2177
|
+
}
|
|
2178
|
+
async function discoverManifestFromCache(cacheDir) {
|
|
2179
|
+
const path5 = resolve(cacheDir, MAESTER_MANIFEST_FILENAME);
|
|
2180
|
+
if (!existsSync(path5)) return { mode: "no-manifest", reason: "absent" };
|
|
2181
|
+
try {
|
|
2182
|
+
const raw = await readFile(path5, "utf8");
|
|
2183
|
+
const doc = parseDocument(raw);
|
|
2184
|
+
if (doc.errors.length > 0) return { mode: "no-manifest", reason: "invalid" };
|
|
2185
|
+
const parsed = MaesterConfigSchema.safeParse(doc.toJS({ maxAliasCount: -1 }));
|
|
2186
|
+
if (!parsed.success) return { mode: "no-manifest", reason: "invalid" };
|
|
2187
|
+
const rules = parsed.data.documents.map(
|
|
2188
|
+
(d) => d.state === void 0 ? { pattern: d.path } : { pattern: d.path, state: d.state }
|
|
2189
|
+
);
|
|
2190
|
+
const patterns = parsed.data.documents.map((d) => d.path);
|
|
2191
|
+
if (patterns.length === 0) return { mode: "no-manifest", reason: "invalid" };
|
|
2192
|
+
if (!patterns.includes(MAESTER_MANIFEST_FILENAME)) patterns.unshift(MAESTER_MANIFEST_FILENAME);
|
|
2193
|
+
return { mode: "manifest", patterns, rules };
|
|
2194
|
+
} catch {
|
|
2195
|
+
return { mode: "no-manifest", reason: "invalid" };
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
async function countMaterializedFiles(cacheDir) {
|
|
2199
|
+
let count = 0;
|
|
2200
|
+
async function walk2(dir) {
|
|
2201
|
+
let entries;
|
|
2202
|
+
try {
|
|
2203
|
+
entries = await readdir(dir, {
|
|
2204
|
+
withFileTypes: true,
|
|
2205
|
+
encoding: "utf8"
|
|
2206
|
+
});
|
|
2207
|
+
} catch {
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
for (const entry of entries) {
|
|
2211
|
+
if (entry.name === ".git") continue;
|
|
2212
|
+
const fullPath = resolve(dir, entry.name);
|
|
2213
|
+
if (entry.isDirectory()) {
|
|
2214
|
+
await walk2(fullPath);
|
|
2215
|
+
} else if (entry.isFile()) {
|
|
2216
|
+
count++;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
await walk2(cacheDir);
|
|
2221
|
+
return count;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
// src/core/status/probe.ts
|
|
2225
|
+
var SHA_RE = /^[0-9a-f]{40}$/;
|
|
2226
|
+
var MAESTER_MANIFEST_FILENAME2 = "maester.yaml";
|
|
2227
|
+
var STATUS_TEMP_PREFIX = ".status-";
|
|
2228
|
+
async function probeCommitSha(source, ctx) {
|
|
2229
|
+
if (source.ref && SHA_RE.test(source.ref)) {
|
|
2230
|
+
return source.ref;
|
|
2231
|
+
}
|
|
2232
|
+
return listRemoteRef({
|
|
2233
|
+
url: source.url,
|
|
2234
|
+
ref: source.ref,
|
|
2235
|
+
...ctx.tokenForUrl ? { useTokenInUrl: ctx.tokenForUrl } : {}
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
async function probeManifest(source, ctx) {
|
|
2239
|
+
const tempRoot = resolve(ctx.repoRoot, CACHE_DIR_NAME);
|
|
2240
|
+
await mkdir(tempRoot, { recursive: true });
|
|
2241
|
+
const tempDir = await mkdtemp(resolve(tempRoot, STATUS_TEMP_PREFIX));
|
|
2242
|
+
try {
|
|
2243
|
+
await shallowSparseClone({
|
|
2244
|
+
url: source.url,
|
|
2245
|
+
destination: tempDir,
|
|
2246
|
+
ref: source.ref,
|
|
2247
|
+
...ctx.tokenForUrl ? { useTokenInUrl: ctx.tokenForUrl } : {}
|
|
2248
|
+
});
|
|
2249
|
+
await setSparsePatterns(tempDir, [MAESTER_MANIFEST_FILENAME2]);
|
|
2250
|
+
await checkoutRef(tempDir, source.ref);
|
|
2251
|
+
const discovery = await discoverManifestFromCache(tempDir);
|
|
2252
|
+
if (discovery.mode === "no-manifest") {
|
|
2253
|
+
throw manifestError2(source.name, discovery.reason);
|
|
2254
|
+
}
|
|
2255
|
+
return { filterSet: discovery.patterns };
|
|
2256
|
+
} finally {
|
|
2257
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
function manifestError2(name, reason) {
|
|
2261
|
+
if (reason === "absent") {
|
|
2262
|
+
return new MaesterError(
|
|
2263
|
+
"MAESTER_MANIFEST_MISSING",
|
|
2264
|
+
`source '${name}' does not publish a maester.yaml manifest at the configured ref.`
|
|
2265
|
+
);
|
|
2266
|
+
}
|
|
2267
|
+
return new MaesterError(
|
|
2268
|
+
"MAESTER_MANIFEST_INVALID",
|
|
2269
|
+
`source '${name}' publishes a maester.yaml that failed schema validation.`
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
// src/core/status/runner.ts
|
|
2274
|
+
var DEFAULT_CONCURRENCY = 4;
|
|
2275
|
+
async function runStatus(config, options) {
|
|
2276
|
+
const env = options.env ?? process.env;
|
|
2277
|
+
const scope = options.scope?.length ? new Set(options.scope) : void 0;
|
|
2278
|
+
const baseDir = options.baseDir ?? config.baseDir;
|
|
2279
|
+
if (scope) {
|
|
2280
|
+
const known = new Set(config.sources.map((s) => s.name));
|
|
2281
|
+
for (const name of scope) {
|
|
2282
|
+
if (!known.has(name)) {
|
|
2283
|
+
throw new MaesterError(
|
|
2284
|
+
"UNKNOWN_SOURCE",
|
|
2285
|
+
`Unknown source '${name}' \u2014 not declared in citadel.yaml.`
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
const entries = config.sources.filter((s) => !scope || scope.has(s.name));
|
|
2291
|
+
if (entries.length === 0) {
|
|
2292
|
+
return { outcomes: [], counts: { upToDate: 0, behind: 0, failed: 0 } };
|
|
2293
|
+
}
|
|
2294
|
+
const limit = Math.min(Math.max(1, options.concurrency ?? DEFAULT_CONCURRENCY), entries.length);
|
|
2295
|
+
const outcomes = new Array(entries.length);
|
|
2296
|
+
let cursor = 0;
|
|
2297
|
+
const workers = [];
|
|
2298
|
+
for (let i = 0; i < limit; i++) {
|
|
2299
|
+
workers.push(
|
|
2300
|
+
(async () => {
|
|
2301
|
+
while (true) {
|
|
2302
|
+
const index = cursor++;
|
|
2303
|
+
if (index >= entries.length) return;
|
|
2304
|
+
const entry = entries[index];
|
|
2305
|
+
if (!entry) return;
|
|
2306
|
+
outcomes[index] = await checkSource(entry, options, env, baseDir);
|
|
2307
|
+
}
|
|
2308
|
+
})()
|
|
2309
|
+
);
|
|
2310
|
+
}
|
|
2311
|
+
await Promise.all(workers);
|
|
2312
|
+
const counts = { upToDate: 0, behind: 0, failed: 0 };
|
|
2313
|
+
for (const outcome of outcomes) {
|
|
2314
|
+
if (outcome.verdict === "up-to-date") counts.upToDate++;
|
|
2315
|
+
else if (outcome.verdict === "behind") counts.behind++;
|
|
2316
|
+
else counts.failed++;
|
|
2317
|
+
}
|
|
2318
|
+
return { outcomes, counts };
|
|
2319
|
+
}
|
|
2320
|
+
async function checkSource(source, options, env, baseDir) {
|
|
2321
|
+
const destination = source.destination ? resolve(options.repoRoot, source.destination) : defaultDestinationFor(options.repoRoot, source.name, baseDir);
|
|
2322
|
+
const marker = await readProvenanceMarker(destination);
|
|
2323
|
+
if (!marker) {
|
|
2324
|
+
return {
|
|
2325
|
+
name: source.name,
|
|
2326
|
+
verdict: "behind",
|
|
2327
|
+
reasons: ["never-synced"]
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
try {
|
|
2331
|
+
const auth = resolveAuth(source.auth, env);
|
|
2332
|
+
const tokenForUrl = auth.type === "token" ? auth.value : void 0;
|
|
2333
|
+
const probeCtx = { repoRoot: options.repoRoot, tokenForUrl };
|
|
2334
|
+
const resolvedSha = await probeCommitSha(source, probeCtx);
|
|
2335
|
+
const reasons = [];
|
|
2336
|
+
if (resolvedSha !== marker.commitSha) {
|
|
2337
|
+
reasons.push("remote-ref-advanced");
|
|
2338
|
+
}
|
|
2339
|
+
const isManifestDriven = !source.includes || source.includes.length === 0;
|
|
2340
|
+
if (isManifestDriven) {
|
|
2341
|
+
const { filterSet } = await probeManifest(source, probeCtx);
|
|
2342
|
+
if (!filterSetsEqual(filterSet, marker.filterSet)) {
|
|
2343
|
+
reasons.push("manifest-changed");
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
if (reasons.length === 0) {
|
|
2347
|
+
return {
|
|
2348
|
+
name: source.name,
|
|
2349
|
+
verdict: "up-to-date",
|
|
2350
|
+
commitSha: resolvedSha
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
return {
|
|
2354
|
+
name: source.name,
|
|
2355
|
+
verdict: "behind",
|
|
2356
|
+
reasons,
|
|
2357
|
+
commitSha: marker.commitSha,
|
|
2358
|
+
resolvedSha
|
|
2359
|
+
};
|
|
2360
|
+
} catch (err) {
|
|
2361
|
+
return {
|
|
2362
|
+
name: source.name,
|
|
2363
|
+
verdict: "failed",
|
|
2364
|
+
error: errorMessage(err)
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
function filterSetsEqual(a, b) {
|
|
2369
|
+
const left = [...new Set(a)].sort();
|
|
2370
|
+
const right = [...new Set(b)].sort();
|
|
2371
|
+
if (left.length !== right.length) return false;
|
|
2372
|
+
for (let i = 0; i < left.length; i++) {
|
|
2373
|
+
if (left[i] !== right[i]) return false;
|
|
2374
|
+
}
|
|
2375
|
+
return true;
|
|
2376
|
+
}
|
|
2377
|
+
function errorMessage(err) {
|
|
2378
|
+
if (err instanceof AuthError) return err.message;
|
|
2379
|
+
if (err instanceof RefNotFoundError) return err.message;
|
|
2380
|
+
if (err instanceof MaesterError) return err.message;
|
|
2381
|
+
if (err instanceof Error) return err.message;
|
|
2382
|
+
return String(err);
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
// src/core/skill/runtime.ts
|
|
2386
|
+
var CACHE_RELATIVE_PATH = ".maester/.skill-cache.json";
|
|
2387
|
+
var DEFAULT_TTL_SECONDS = 300;
|
|
2388
|
+
async function runtimePreread(stdinJson, opts) {
|
|
2389
|
+
const envelope = parseEnvelopeSafe(stdinJson);
|
|
2390
|
+
if (!envelope) return "";
|
|
2391
|
+
const filePath = extractTargetPath(envelope, opts.repoRoot);
|
|
2392
|
+
if (!filePath) return "";
|
|
2393
|
+
let baseDir;
|
|
2394
|
+
let configForStatus;
|
|
2395
|
+
try {
|
|
2396
|
+
configForStatus = await loadCitadelConfig(opts.repoRoot);
|
|
2397
|
+
baseDir = configForStatus.baseDir ?? DEFAULT_BASE_DIR;
|
|
2398
|
+
} catch {
|
|
2399
|
+
return "";
|
|
2400
|
+
}
|
|
2401
|
+
if (!isUnderBaseDir(filePath, opts.repoRoot, baseDir)) return "";
|
|
2402
|
+
const ttlSeconds = readTtlSeconds(opts.env);
|
|
2403
|
+
const now = opts.now ?? (() => Date.now());
|
|
2404
|
+
const cached = await readCache(opts.repoRoot);
|
|
2405
|
+
let verdict;
|
|
2406
|
+
if (cached && now() - cached.ts < ttlSeconds * 1e3) {
|
|
2407
|
+
verdict = cached;
|
|
2408
|
+
} else {
|
|
2409
|
+
const runner = opts.runStatus ?? runStatus;
|
|
2410
|
+
try {
|
|
2411
|
+
const result = await runner(configForStatus, { repoRoot: opts.repoRoot });
|
|
2412
|
+
verdict = {
|
|
2413
|
+
ts: now(),
|
|
2414
|
+
verdict: classifyVerdict(result),
|
|
2415
|
+
summary: summarize(result)
|
|
2416
|
+
};
|
|
2417
|
+
await writeCache(opts.repoRoot, verdict);
|
|
2418
|
+
} catch (err) {
|
|
2419
|
+
verdict = {
|
|
2420
|
+
ts: now(),
|
|
2421
|
+
verdict: "failed",
|
|
2422
|
+
summary: err instanceof Error ? err.message : String(err)
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
if (verdict.verdict === "up-to-date") return "";
|
|
2427
|
+
return buildHookResponse(verdict);
|
|
2428
|
+
}
|
|
2429
|
+
async function runtimeStatusSummary(opts) {
|
|
2430
|
+
let config;
|
|
2431
|
+
try {
|
|
2432
|
+
config = await loadCitadelConfig(opts.repoRoot);
|
|
2433
|
+
} catch (err) {
|
|
2434
|
+
return {
|
|
2435
|
+
summary: err instanceof Error ? err.message : String(err),
|
|
2436
|
+
exitCode: 2
|
|
2437
|
+
};
|
|
2438
|
+
}
|
|
2439
|
+
const runner = opts.runStatus ?? runStatus;
|
|
2440
|
+
try {
|
|
2441
|
+
const result = await runner(config, { repoRoot: opts.repoRoot });
|
|
2442
|
+
const summary = summarize(result);
|
|
2443
|
+
let exitCode = 0;
|
|
2444
|
+
if (result.counts.failed > 0) exitCode = 2;
|
|
2445
|
+
else if (result.counts.behind > 0) exitCode = 1;
|
|
2446
|
+
return { summary, exitCode };
|
|
2447
|
+
} catch (err) {
|
|
2448
|
+
return {
|
|
2449
|
+
summary: err instanceof Error ? err.message : String(err),
|
|
2450
|
+
exitCode: 2
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
function parseEnvelopeSafe(raw) {
|
|
2455
|
+
if (!raw || raw.trim().length === 0) return void 0;
|
|
2456
|
+
try {
|
|
2457
|
+
const parsed = JSON.parse(raw);
|
|
2458
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
2459
|
+
return parsed;
|
|
2460
|
+
} catch {
|
|
2461
|
+
return void 0;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
function extractTargetPath(envelope, repoRoot) {
|
|
2465
|
+
const input = envelope.tool_input;
|
|
2466
|
+
if (!input) return void 0;
|
|
2467
|
+
const raw = input.file_path ?? input.path ?? input.pattern;
|
|
2468
|
+
if (typeof raw !== "string" || raw.length === 0) return void 0;
|
|
2469
|
+
const cwd = typeof envelope.cwd === "string" ? envelope.cwd : repoRoot;
|
|
2470
|
+
return path4.isAbsolute(raw) ? raw : path4.resolve(cwd, raw);
|
|
2471
|
+
}
|
|
2472
|
+
function isUnderBaseDir(targetPath, repoRoot, baseDir) {
|
|
2473
|
+
const base = path4.resolve(repoRoot, baseDir);
|
|
2474
|
+
const rel = path4.relative(base, targetPath);
|
|
2475
|
+
if (rel === "") return true;
|
|
2476
|
+
return !rel.startsWith("..") && !path4.isAbsolute(rel);
|
|
2477
|
+
}
|
|
2478
|
+
function readTtlSeconds(env) {
|
|
2479
|
+
const raw = (env ?? process.env).MAESTER_SKILL_STATUS_TTL;
|
|
2480
|
+
if (!raw) return DEFAULT_TTL_SECONDS;
|
|
2481
|
+
const parsed = Number.parseInt(raw, 10);
|
|
2482
|
+
if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_TTL_SECONDS;
|
|
2483
|
+
return parsed;
|
|
2484
|
+
}
|
|
2485
|
+
async function readCache(repoRoot) {
|
|
2486
|
+
try {
|
|
2487
|
+
const raw = await promises.readFile(path4.join(repoRoot, CACHE_RELATIVE_PATH), "utf8");
|
|
2488
|
+
const parsed = JSON.parse(raw);
|
|
2489
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
2490
|
+
const candidate = parsed;
|
|
2491
|
+
if (typeof candidate.ts !== "number" || candidate.verdict !== "up-to-date" && candidate.verdict !== "behind" && candidate.verdict !== "failed" || typeof candidate.summary !== "string") {
|
|
2492
|
+
return void 0;
|
|
2493
|
+
}
|
|
2494
|
+
return {
|
|
2495
|
+
ts: candidate.ts,
|
|
2496
|
+
verdict: candidate.verdict,
|
|
2497
|
+
summary: candidate.summary
|
|
2498
|
+
};
|
|
2499
|
+
} catch {
|
|
2500
|
+
return void 0;
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
async function writeCache(repoRoot, verdict) {
|
|
2504
|
+
const finalPath = path4.join(repoRoot, CACHE_RELATIVE_PATH);
|
|
2505
|
+
await promises.mkdir(path4.dirname(finalPath), { recursive: true });
|
|
2506
|
+
const tempPath = `${finalPath}.tmp-${Math.floor(Math.random() * 1e9)}`;
|
|
2507
|
+
await promises.writeFile(tempPath, `${JSON.stringify(verdict)}
|
|
2508
|
+
`, "utf8");
|
|
2509
|
+
await promises.rename(tempPath, finalPath);
|
|
2510
|
+
}
|
|
2511
|
+
function classifyVerdict(result) {
|
|
2512
|
+
if (result.counts.failed > 0) return "failed";
|
|
2513
|
+
if (result.counts.behind > 0) return "behind";
|
|
2514
|
+
return "up-to-date";
|
|
2515
|
+
}
|
|
2516
|
+
function summarize(result) {
|
|
2517
|
+
const { upToDate, behind, failed } = result.counts;
|
|
2518
|
+
const total = upToDate + behind + failed;
|
|
2519
|
+
if (total === 0) return "no sources configured";
|
|
2520
|
+
if (behind === 0 && failed === 0) return `all ${upToDate} sources up to date`;
|
|
2521
|
+
const parts = [];
|
|
2522
|
+
if (behind > 0) parts.push(`${behind} behind`);
|
|
2523
|
+
if (failed > 0) parts.push(`${failed} failed`);
|
|
2524
|
+
if (upToDate > 0) parts.push(`${upToDate} up to date`);
|
|
2525
|
+
return parts.join(", ");
|
|
2526
|
+
}
|
|
2527
|
+
function buildHookResponse(verdict) {
|
|
2528
|
+
const detail = verdict.verdict === "failed" ? `Citadel status check failed: ${verdict.summary}. Proceed with the read and flag that cited content may be stale; do not retry sync in a loop.` : `Citadel is behind (${verdict.summary}). Auto-sync policy: run \`npx maester sync\`, then \`rm -f .maester/.skill-cache.json\` to invalidate this hook's cache, then proceed with the read. Do not prompt the user \u2014 sync is read-only against the configured remotes.`;
|
|
2529
|
+
const response = {
|
|
2530
|
+
hookSpecificOutput: {
|
|
2531
|
+
hookEventName: "PreToolUse",
|
|
2532
|
+
additionalContext: detail
|
|
2533
|
+
}
|
|
2534
|
+
};
|
|
2535
|
+
return `${JSON.stringify(response)}
|
|
2536
|
+
`;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// src/cli/commands/skill.ts
|
|
2540
|
+
var EXIT_OK = 0;
|
|
2541
|
+
var EXIT_OUTDATED_OR_BEHIND = 1;
|
|
2542
|
+
var EXIT_FAILED = 2;
|
|
2543
|
+
var SUPPORTED_IDS = ["claude-code", "codex", "cursor", "agents-md"];
|
|
2544
|
+
function registerSkill(program, getContext) {
|
|
2545
|
+
const group2 = program.command("skill").description("Install and manage the Grand Maester agent skill in this repository.");
|
|
2546
|
+
group2.command("install").description("Install the Grand Maester skill for one or more agent targets.").option(
|
|
2547
|
+
"--target <id>",
|
|
2548
|
+
"Agent target id (repeatable). Skips the interactive picker.",
|
|
2549
|
+
collectTarget,
|
|
2550
|
+
[]
|
|
2551
|
+
).action(async (options) => {
|
|
2552
|
+
process.exitCode = await runSkillInstallCommand(getContext(), options.target, "install");
|
|
2553
|
+
});
|
|
2554
|
+
group2.command("upgrade").description("Refresh every installed target's content to match the running maester version.").option("--check", "Report which targets are outdated without writing.").action(async (options) => {
|
|
2555
|
+
process.exitCode = await runSkillUpgradeCommand(getContext(), options.check === true);
|
|
2556
|
+
});
|
|
2557
|
+
group2.command("add-target <id>").description("Install the skill for an additional agent target.").action(async (id) => {
|
|
2558
|
+
if (!isSupportedId(id)) {
|
|
2559
|
+
const ctx = getContext();
|
|
2560
|
+
ctx.logger.error(`Unknown target '${id}'. Supported: ${SUPPORTED_IDS.join(", ")}`);
|
|
2561
|
+
process.exitCode = EXIT_FAILED;
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
process.exitCode = await runSkillInstallCommand(getContext(), [id], "add-target");
|
|
2565
|
+
});
|
|
2566
|
+
group2.command("status").description(
|
|
2567
|
+
"Show which agent targets have the Grand Maester installed and whether each is up to date."
|
|
2568
|
+
).action(async () => {
|
|
2569
|
+
process.exitCode = await runSkillStatusCommand(getContext());
|
|
2570
|
+
});
|
|
2571
|
+
const runtime = group2.command("runtime").description("Internal helpers invoked by installed agent hooks.");
|
|
2572
|
+
runtime.command("preread").description(
|
|
2573
|
+
"Read a Claude Code PreToolUse hook envelope from stdin; emit a hook response. Always exits 0."
|
|
2574
|
+
).action(async () => {
|
|
2575
|
+
process.exitCode = await runRuntimePrereadCommand(getContext());
|
|
2576
|
+
});
|
|
2577
|
+
runtime.command("status-summary").description("Print a one-line citadel-status summary. Exit ladder mirrors `maester status`.").action(async () => {
|
|
2578
|
+
process.exitCode = await runRuntimeStatusSummaryCommand(getContext());
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
async function runSkillInstallCommand(ctx, flagTargets, mode) {
|
|
2582
|
+
const baseDir = await loadBaseDir(ctx);
|
|
2583
|
+
if (baseDir === null) return EXIT_FAILED;
|
|
2584
|
+
let targets;
|
|
2585
|
+
if (flagTargets.length > 0) {
|
|
2586
|
+
targets = [...flagTargets];
|
|
2587
|
+
} else {
|
|
2588
|
+
try {
|
|
2589
|
+
targets = await pickTargetsInteractively(ctx);
|
|
2590
|
+
} catch (err) {
|
|
2591
|
+
if (err instanceof PromptCancelledError) {
|
|
2592
|
+
ctx.prompts.outro("Cancelled \u2014 no skill artifacts written.");
|
|
2593
|
+
return 130;
|
|
2594
|
+
}
|
|
2595
|
+
throw err;
|
|
2596
|
+
}
|
|
2597
|
+
if (targets.length === 0) {
|
|
2598
|
+
ctx.logger.warning("No targets selected \u2014 nothing to install.");
|
|
2599
|
+
return EXIT_OK;
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
let result;
|
|
2603
|
+
try {
|
|
2604
|
+
result = await runSkillInstall(ctx.repoRoot.path, {
|
|
2605
|
+
targets,
|
|
2606
|
+
mode,
|
|
2607
|
+
citadelBaseDir: baseDir
|
|
2608
|
+
});
|
|
2609
|
+
} catch (err) {
|
|
2610
|
+
ctx.logger.error(err instanceof Error ? err.message : String(err));
|
|
2611
|
+
return EXIT_FAILED;
|
|
2612
|
+
}
|
|
2613
|
+
renderInstallResult(ctx, result, mode);
|
|
2614
|
+
return result.counts.failed > 0 ? EXIT_FAILED : EXIT_OK;
|
|
2615
|
+
}
|
|
2616
|
+
async function runSkillUpgradeCommand(ctx, check) {
|
|
2617
|
+
const baseDir = await loadBaseDir(ctx);
|
|
2618
|
+
if (baseDir === null) return EXIT_FAILED;
|
|
2619
|
+
let result;
|
|
2620
|
+
try {
|
|
2621
|
+
result = await runSkillUpgrade(ctx.repoRoot.path, { check, citadelBaseDir: baseDir });
|
|
2622
|
+
} catch (err) {
|
|
2623
|
+
ctx.logger.error(err instanceof Error ? err.message : String(err));
|
|
2624
|
+
return EXIT_FAILED;
|
|
2625
|
+
}
|
|
2626
|
+
if (result.outcomes.length === 0) {
|
|
2627
|
+
ctx.logger.info("No Grand Maester targets are installed. Run `maester skill install` first.");
|
|
2628
|
+
return check ? EXIT_OK : EXIT_OK;
|
|
2629
|
+
}
|
|
2630
|
+
for (const outcome of result.outcomes) {
|
|
2631
|
+
renderInstallOutcome(ctx, outcome);
|
|
2632
|
+
}
|
|
2633
|
+
ctx.logger.blank();
|
|
2634
|
+
const upgradedOrPending = result.counts.upgraded;
|
|
2635
|
+
if (result.counts.failed > 0) {
|
|
2636
|
+
ctx.logger.error(`${result.counts.failed} target(s) failed. See errors above.`);
|
|
2637
|
+
return EXIT_FAILED;
|
|
2638
|
+
}
|
|
2639
|
+
if (check && upgradedOrPending > 0) {
|
|
2640
|
+
ctx.logger.warning(
|
|
2641
|
+
`${upgradedOrPending} target(s) are outdated. Run \`maester skill upgrade\` to refresh.`
|
|
2642
|
+
);
|
|
2643
|
+
return EXIT_OUTDATED_OR_BEHIND;
|
|
2644
|
+
}
|
|
2645
|
+
if (upgradedOrPending > 0) {
|
|
2646
|
+
ctx.logger.success(`Upgraded ${upgradedOrPending} target(s) to v${SKILL_VERSION}.`);
|
|
2647
|
+
} else {
|
|
2648
|
+
ctx.logger.success(`All ${result.outcomes.length} installed target(s) up to date.`);
|
|
2649
|
+
}
|
|
2650
|
+
return EXIT_OK;
|
|
2651
|
+
}
|
|
2652
|
+
async function runSkillStatusCommand(ctx) {
|
|
2653
|
+
const result = await runSkillStatus(ctx.repoRoot.path);
|
|
2654
|
+
if (ctx.flags.json) {
|
|
2655
|
+
for (const outcome of result.outcomes) {
|
|
2656
|
+
process.stdout.write(`${JSON.stringify(outcome)}
|
|
2657
|
+
`);
|
|
2658
|
+
}
|
|
2659
|
+
process.stdout.write(`${JSON.stringify({ type: "summary", ...result.counts })}
|
|
2660
|
+
`);
|
|
2661
|
+
} else {
|
|
2662
|
+
renderStatusResult(ctx, result);
|
|
2663
|
+
}
|
|
2664
|
+
if (result.counts.upToDate + result.counts.outdated === 0) return EXIT_FAILED;
|
|
2665
|
+
if (result.counts.outdated > 0) return EXIT_OUTDATED_OR_BEHIND;
|
|
2666
|
+
return EXIT_OK;
|
|
2667
|
+
}
|
|
2668
|
+
async function runRuntimePrereadCommand(ctx) {
|
|
2669
|
+
const stdin = await readAllStdin();
|
|
2670
|
+
const out = await runtimePreread(stdin, { repoRoot: ctx.repoRoot.path });
|
|
2671
|
+
if (out.length > 0) process.stdout.write(out);
|
|
2672
|
+
return EXIT_OK;
|
|
2673
|
+
}
|
|
2674
|
+
async function runRuntimeStatusSummaryCommand(ctx) {
|
|
2675
|
+
const { summary, exitCode } = await runtimeStatusSummary({ repoRoot: ctx.repoRoot.path });
|
|
2676
|
+
process.stdout.write(`${summary}
|
|
2677
|
+
`);
|
|
2678
|
+
return exitCode;
|
|
2679
|
+
}
|
|
2680
|
+
async function pickTargetsInteractively(ctx) {
|
|
2681
|
+
ctx.prompts.intro("Install the Grand Maester");
|
|
2682
|
+
const choices = listSkillTargets().map((t) => ({
|
|
2683
|
+
value: t.id,
|
|
2684
|
+
label: t.label
|
|
2685
|
+
}));
|
|
2686
|
+
const picked = await ctx.prompts.multiselect({
|
|
2687
|
+
message: "Which agent(s) should the skill be installed for?",
|
|
2688
|
+
options: choices,
|
|
2689
|
+
initialValues: ["claude-code", "codex"],
|
|
2690
|
+
required: true
|
|
2691
|
+
});
|
|
2692
|
+
return picked;
|
|
2693
|
+
}
|
|
2694
|
+
async function loadBaseDir(ctx) {
|
|
2695
|
+
try {
|
|
2696
|
+
const config = await loadCitadelConfig(ctx.repoRoot.path);
|
|
2697
|
+
return config.baseDir ?? DEFAULT_BASE_DIR;
|
|
2698
|
+
} catch (err) {
|
|
2699
|
+
const message = err instanceof MaesterError ? err.message : err instanceof Error ? err.message : String(err);
|
|
2700
|
+
ctx.logger.error(message);
|
|
2701
|
+
return null;
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
function renderInstallResult(ctx, result, mode) {
|
|
2705
|
+
if (ctx.flags.json) {
|
|
2706
|
+
for (const outcome of result.outcomes) {
|
|
2707
|
+
process.stdout.write(`${JSON.stringify(outcome)}
|
|
2708
|
+
`);
|
|
2709
|
+
}
|
|
2710
|
+
process.stdout.write(`${JSON.stringify({ type: "summary", ...result.counts })}
|
|
2711
|
+
`);
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
for (const outcome of result.outcomes) {
|
|
2715
|
+
renderInstallOutcome(ctx, outcome);
|
|
2716
|
+
}
|
|
2717
|
+
ctx.logger.blank();
|
|
2718
|
+
const action = mode === "add-target" ? "Added" : "Installed";
|
|
2719
|
+
const total = result.counts.installed + result.counts.upgraded + result.counts.unchanged;
|
|
2720
|
+
if (result.counts.failed > 0) {
|
|
2721
|
+
ctx.logger.error(`${result.counts.failed} target(s) failed. See errors above.`);
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
ctx.logger.success(`${action} Grand Maester for ${total} target(s) at v${SKILL_VERSION}.`);
|
|
2725
|
+
}
|
|
2726
|
+
function renderInstallOutcome(ctx, outcome) {
|
|
2727
|
+
const artifacts = outcome.artifactPaths.join(", ");
|
|
2728
|
+
switch (outcome.action) {
|
|
2729
|
+
case "installed":
|
|
2730
|
+
ctx.logger.success(`${outcome.label}: installed \u2192 ${artifacts}`);
|
|
2731
|
+
break;
|
|
2732
|
+
case "upgraded":
|
|
2733
|
+
ctx.logger.success(`${outcome.label}: upgraded \u2192 ${artifacts}`);
|
|
2734
|
+
break;
|
|
2735
|
+
case "unchanged":
|
|
2736
|
+
ctx.logger.info(`${outcome.label}: already up to date (${artifacts})`);
|
|
2737
|
+
break;
|
|
2738
|
+
case "failed":
|
|
2739
|
+
ctx.logger.error(`${outcome.label}: failed${outcome.error ? ` \u2014 ${outcome.error}` : ""}`);
|
|
2740
|
+
break;
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
function renderStatusResult(ctx, result) {
|
|
2744
|
+
if (result.outcomes.length === 0) {
|
|
2745
|
+
ctx.logger.info("No skill targets registered.");
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
for (const outcome of result.outcomes) {
|
|
2749
|
+
switch (outcome.state) {
|
|
2750
|
+
case "up-to-date":
|
|
2751
|
+
ctx.logger.success(`${outcome.label}: v${outcome.installedVersion ?? "?"} (up to date)`);
|
|
2752
|
+
break;
|
|
2753
|
+
case "outdated":
|
|
2754
|
+
ctx.logger.warning(
|
|
2755
|
+
`${outcome.label}: v${outcome.installedVersion ?? "?"} (latest v${outcome.currentVersion})`
|
|
2756
|
+
);
|
|
2757
|
+
break;
|
|
2758
|
+
case "not-installed":
|
|
2759
|
+
ctx.logger.info(`${outcome.label}: not installed`);
|
|
2760
|
+
break;
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
ctx.logger.blank();
|
|
2764
|
+
const { upToDate, outdated, notInstalled } = result.counts;
|
|
2765
|
+
if (upToDate + outdated === 0) {
|
|
2766
|
+
ctx.logger.info("No Grand Maester targets installed. Run `maester skill install` to add one.");
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
if (outdated > 0) {
|
|
2770
|
+
ctx.logger.warning(`${outdated} target(s) outdated. Run \`maester skill upgrade\`.`);
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
ctx.logger.success(`${upToDate} target(s) up to date; ${notInstalled} available to install.`);
|
|
2774
|
+
}
|
|
2775
|
+
function collectTarget(value, prev) {
|
|
2776
|
+
if (!isSupportedId(value)) {
|
|
2777
|
+
throw new Error(`Unknown target '${value}'. Supported: ${SUPPORTED_IDS.join(", ")}`);
|
|
2778
|
+
}
|
|
2779
|
+
return [...prev, value];
|
|
2780
|
+
}
|
|
2781
|
+
function isSupportedId(id) {
|
|
2782
|
+
return SUPPORTED_IDS.includes(id);
|
|
2783
|
+
}
|
|
2784
|
+
async function readAllStdin() {
|
|
2785
|
+
if (process.stdin.isTTY) return "";
|
|
2786
|
+
const chunks = [];
|
|
2787
|
+
for await (const chunk of process.stdin) {
|
|
2788
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
2789
|
+
}
|
|
2790
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2791
|
+
}
|
|
2792
|
+
var SEVERITY_TO_GLYPH = {
|
|
2793
|
+
success: "success",
|
|
2794
|
+
warning: "warning",
|
|
2795
|
+
error: "error",
|
|
2796
|
+
info: "info",
|
|
2797
|
+
verbose: "info"
|
|
2798
|
+
};
|
|
2799
|
+
var LEVEL_RANK = {
|
|
2800
|
+
silent: -1,
|
|
2801
|
+
error: 0,
|
|
2802
|
+
warn: 1,
|
|
2803
|
+
info: 2,
|
|
2804
|
+
verbose: 3
|
|
2805
|
+
};
|
|
2806
|
+
function severityMeetsLevel(sev, level) {
|
|
2807
|
+
if (level === "silent") return false;
|
|
2808
|
+
const need = sev === "error" ? "error" : sev === "warning" ? "warn" : sev === "verbose" ? "verbose" : "info";
|
|
2809
|
+
return LEVEL_RANK[need] <= LEVEL_RANK[level];
|
|
2810
|
+
}
|
|
2811
|
+
function createLogger(opts) {
|
|
2812
|
+
const level = opts.level ?? "info";
|
|
2813
|
+
const json = opts.json ?? false;
|
|
2814
|
+
const stdout = opts.stdout ?? process.stdout;
|
|
2815
|
+
const stderr = opts.stderr ?? process.stderr;
|
|
2816
|
+
const { theming } = opts;
|
|
2817
|
+
createConsola({
|
|
2818
|
+
level: level === "silent" ? -1 : LEVEL_RANK[level],
|
|
2819
|
+
formatOptions: { colors: false, date: false, compact: true }
|
|
2820
|
+
});
|
|
2821
|
+
function writeJson(sev, message, meta) {
|
|
2822
|
+
const payload = { level: sev, message, ...meta ?? {} };
|
|
2823
|
+
const stream = sev === "error" ? stderr : stdout;
|
|
2824
|
+
stream.write(`${JSON.stringify(payload)}
|
|
2825
|
+
`);
|
|
2826
|
+
}
|
|
2827
|
+
function writeHuman(sev, message) {
|
|
2828
|
+
const role = SEVERITY_TO_GLYPH[sev];
|
|
2829
|
+
const g = theming.glyph(role);
|
|
2830
|
+
const paintedGlyph = g.token ? theming.painter.token(g.token, g.text) : g.text;
|
|
2831
|
+
const line = `${paintedGlyph} ${message}`;
|
|
2832
|
+
const stream = sev === "error" ? stderr : stdout;
|
|
2833
|
+
stream.write(`${line}
|
|
2834
|
+
`);
|
|
2835
|
+
}
|
|
2836
|
+
function emit(sev, message, meta) {
|
|
2837
|
+
if (!severityMeetsLevel(sev, level)) return;
|
|
2838
|
+
if (json) {
|
|
2839
|
+
writeJson(sev, message, meta);
|
|
2840
|
+
} else {
|
|
2841
|
+
writeHuman(sev, message);
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
return {
|
|
2845
|
+
level,
|
|
2846
|
+
json,
|
|
2847
|
+
success: (m, meta) => emit("success", m, meta),
|
|
2848
|
+
warning: (m, meta) => emit("warning", m, meta),
|
|
2849
|
+
error: (m, meta) => emit("error", m, meta),
|
|
2850
|
+
info: (m, meta) => emit("info", m, meta),
|
|
2851
|
+
verbose: (m, meta) => emit("verbose", m, meta),
|
|
2852
|
+
log: (message) => {
|
|
2853
|
+
if (level === "silent") return;
|
|
2854
|
+
if (json) {
|
|
2855
|
+
stdout.write(`${JSON.stringify({ level: "info", message })}
|
|
2856
|
+
`);
|
|
2857
|
+
} else {
|
|
2858
|
+
stdout.write(`${message}
|
|
2859
|
+
`);
|
|
2860
|
+
}
|
|
2861
|
+
},
|
|
2862
|
+
blank: () => {
|
|
2863
|
+
if (level === "silent" || json) return;
|
|
2864
|
+
stdout.write("\n");
|
|
2865
|
+
}
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
function redactUrl(value) {
|
|
2869
|
+
return value.replace(/https?:\/\/([^:@/\s]+):([^@\s]+)@/g, (_, user) => `https://${user}:***@`);
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
// src/cli/commands/status.ts
|
|
2873
|
+
var EXIT_OK2 = 0;
|
|
2874
|
+
var EXIT_BEHIND = 1;
|
|
2875
|
+
var EXIT_FAILED2 = 2;
|
|
2876
|
+
function registerStatus(program, getContext) {
|
|
2877
|
+
program.command("status").description(
|
|
2878
|
+
"Check whether configured sources are up to date with their remotes. Exit codes: 0 (all up to date), 1 (any behind), 2 (any failed or config error)."
|
|
2879
|
+
).argument("[names...]", "Optional source names to scope the run").option(
|
|
2880
|
+
"--concurrency <n>",
|
|
2881
|
+
"Override the per-run source concurrency (default: 4)",
|
|
2882
|
+
(v) => Number(v)
|
|
2883
|
+
).action(async (names, options) => {
|
|
2884
|
+
const code = await runStatusCommand(getContext(), names, options.concurrency);
|
|
2885
|
+
process.exitCode = code;
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
async function runStatusCommand(ctx, scope, concurrency) {
|
|
2889
|
+
let config;
|
|
2890
|
+
try {
|
|
2891
|
+
config = await loadCitadelConfig(ctx.repoRoot.path);
|
|
2892
|
+
} catch (err) {
|
|
2893
|
+
const message = err instanceof MaesterError ? err.message : err instanceof Error ? err.message : String(err);
|
|
2894
|
+
ctx.logger.error(message);
|
|
2895
|
+
return EXIT_FAILED2;
|
|
2896
|
+
}
|
|
2897
|
+
let result;
|
|
2898
|
+
try {
|
|
2899
|
+
result = await runStatus(config, {
|
|
2900
|
+
repoRoot: ctx.repoRoot.path,
|
|
2901
|
+
...scope.length > 0 ? { scope } : {},
|
|
2902
|
+
...concurrency !== void 0 && Number.isFinite(concurrency) ? { concurrency } : {}
|
|
2903
|
+
});
|
|
2904
|
+
} catch (err) {
|
|
2905
|
+
const message = err instanceof MaesterError ? err.message : err instanceof Error ? err.message : String(err);
|
|
2906
|
+
ctx.logger.error(message);
|
|
2907
|
+
return EXIT_FAILED2;
|
|
2908
|
+
}
|
|
2909
|
+
if (ctx.flags.json) {
|
|
2910
|
+
for (const outcome of result.outcomes) {
|
|
2911
|
+
process.stdout.write(`${JSON.stringify(buildJsonOutcome(outcome))}
|
|
2912
|
+
`);
|
|
2913
|
+
}
|
|
2914
|
+
process.stdout.write(`${JSON.stringify({ type: "summary", ...result.counts })}
|
|
2915
|
+
`);
|
|
2916
|
+
} else {
|
|
2917
|
+
renderHumanSummary(ctx, result);
|
|
2918
|
+
}
|
|
2919
|
+
if (result.counts.failed > 0) return EXIT_FAILED2;
|
|
2920
|
+
if (result.counts.behind > 0) return EXIT_BEHIND;
|
|
2921
|
+
return EXIT_OK2;
|
|
2922
|
+
}
|
|
2923
|
+
function buildJsonOutcome(outcome) {
|
|
2924
|
+
if (outcome.verdict === "up-to-date") {
|
|
2925
|
+
return { name: outcome.name, verdict: outcome.verdict, commitSha: outcome.commitSha };
|
|
2926
|
+
}
|
|
2927
|
+
if (outcome.verdict === "behind") {
|
|
2928
|
+
const base = {
|
|
2929
|
+
name: outcome.name,
|
|
2930
|
+
verdict: outcome.verdict,
|
|
2931
|
+
reasons: outcome.reasons
|
|
2932
|
+
};
|
|
2933
|
+
if (outcome.commitSha !== void 0) base.commitSha = outcome.commitSha;
|
|
2934
|
+
if (outcome.resolvedSha !== void 0) base.resolvedSha = outcome.resolvedSha;
|
|
2935
|
+
return base;
|
|
2936
|
+
}
|
|
2937
|
+
return { name: outcome.name, verdict: outcome.verdict, error: redactUrl(outcome.error) };
|
|
2938
|
+
}
|
|
2939
|
+
function renderHumanSummary(ctx, result) {
|
|
2940
|
+
if (result.outcomes.length === 0) {
|
|
2941
|
+
ctx.logger.info("No sources configured.");
|
|
2942
|
+
return;
|
|
2943
|
+
}
|
|
2944
|
+
for (const outcome of result.outcomes) {
|
|
2945
|
+
switch (outcome.verdict) {
|
|
2946
|
+
case "up-to-date":
|
|
2947
|
+
ctx.logger.success(`${outcome.name}: up to date (${shortSha(outcome.commitSha)})`);
|
|
2948
|
+
break;
|
|
2949
|
+
case "behind":
|
|
2950
|
+
ctx.logger.warning(`${outcome.name}: behind \u2014 ${formatReasons(outcome)}`);
|
|
2951
|
+
break;
|
|
2952
|
+
case "failed":
|
|
2953
|
+
ctx.logger.error(`${outcome.name}: failed \u2014 ${redactUrl(outcome.error)}`);
|
|
2954
|
+
break;
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
ctx.logger.blank();
|
|
2958
|
+
renderSummaryLine(ctx, result);
|
|
2959
|
+
}
|
|
2960
|
+
function renderSummaryLine(ctx, result) {
|
|
2961
|
+
const { upToDate, behind, failed } = result.counts;
|
|
2962
|
+
if (failed > 0) {
|
|
2963
|
+
ctx.logger.error(
|
|
2964
|
+
`${failed} source(s) failed, ${behind} behind, ${upToDate} up to date. See errors above.`
|
|
2965
|
+
);
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2968
|
+
if (behind > 0) {
|
|
2969
|
+
ctx.logger.warning(
|
|
2970
|
+
`${behind} of ${result.outcomes.length} source(s) behind. Run \`maester sync\` to refresh.`
|
|
2971
|
+
);
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
ctx.logger.success(`All ${result.outcomes.length} source(s) up to date.`);
|
|
2975
|
+
}
|
|
2976
|
+
function formatReasons(outcome) {
|
|
2977
|
+
const parts = [];
|
|
2978
|
+
for (const reason of outcome.reasons) {
|
|
2979
|
+
parts.push(formatReason(reason, outcome));
|
|
2980
|
+
}
|
|
2981
|
+
return parts.join("; ");
|
|
2982
|
+
}
|
|
2983
|
+
function formatReason(reason, outcome) {
|
|
2984
|
+
switch (reason) {
|
|
2985
|
+
case "never-synced":
|
|
2986
|
+
return "never synced (run `maester sync` to populate this destination)";
|
|
2987
|
+
case "remote-ref-advanced": {
|
|
2988
|
+
const recorded = outcome.commitSha ? shortSha(outcome.commitSha) : "\u2014";
|
|
2989
|
+
const remote = outcome.resolvedSha ? shortSha(outcome.resolvedSha) : "\u2014";
|
|
2990
|
+
return `remote ref advanced (recorded ${recorded}, remote ${remote})`;
|
|
2991
|
+
}
|
|
2992
|
+
case "manifest-changed":
|
|
2993
|
+
return "remote maester.yaml publish surface changed";
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
function shortSha(sha) {
|
|
2997
|
+
return sha.slice(0, 7);
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
// src/core/state/html.ts
|
|
3001
|
+
var html_exports = {};
|
|
3002
|
+
__export(html_exports, {
|
|
3003
|
+
parse: () => parse,
|
|
3004
|
+
write: () => write
|
|
3005
|
+
});
|
|
3006
|
+
var STATE_COMMENT_RE = /^<!--\s*state:\s*([^\s-]+)\s*-->$/;
|
|
3007
|
+
function splitFirstLine(text2) {
|
|
3008
|
+
const crlfIdx = text2.indexOf("\r\n");
|
|
3009
|
+
const lfIdx = text2.indexOf("\n");
|
|
3010
|
+
if (crlfIdx !== -1 && (lfIdx === -1 || crlfIdx <= lfIdx)) {
|
|
3011
|
+
return { firstLine: text2.slice(0, crlfIdx), rest: text2.slice(crlfIdx + 2), eol: "\r\n" };
|
|
3012
|
+
}
|
|
3013
|
+
if (lfIdx !== -1) {
|
|
3014
|
+
return { firstLine: text2.slice(0, lfIdx), rest: text2.slice(lfIdx + 1), eol: "\n" };
|
|
3015
|
+
}
|
|
3016
|
+
return { firstLine: text2, rest: "", eol: "\n" };
|
|
3017
|
+
}
|
|
3018
|
+
function parse(buf) {
|
|
3019
|
+
const text2 = buf.toString("utf8");
|
|
3020
|
+
const { firstLine } = splitFirstLine(text2);
|
|
3021
|
+
const match = firstLine.match(STATE_COMMENT_RE);
|
|
3022
|
+
if (!match) return { kind: "absent" };
|
|
3023
|
+
return parseState(match[1]);
|
|
3024
|
+
}
|
|
3025
|
+
function write(buf, state) {
|
|
3026
|
+
const text2 = buf.toString("utf8");
|
|
3027
|
+
const { firstLine, rest, eol } = splitFirstLine(text2);
|
|
3028
|
+
const desiredLine = `<!-- state: ${state} -->`;
|
|
3029
|
+
const existingMatch = firstLine.match(STATE_COMMENT_RE);
|
|
3030
|
+
if (existingMatch) {
|
|
3031
|
+
if (firstLine === desiredLine) return buf;
|
|
3032
|
+
return Buffer.from(`${desiredLine}${eol}${rest}`, "utf8");
|
|
3033
|
+
}
|
|
3034
|
+
const prefix = `${desiredLine}
|
|
3035
|
+
`;
|
|
3036
|
+
return Buffer.from(`${prefix}${text2}`, "utf8");
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
// src/core/state/json.ts
|
|
3040
|
+
var json_exports = {};
|
|
3041
|
+
__export(json_exports, {
|
|
3042
|
+
parse: () => parse2,
|
|
3043
|
+
write: () => write2
|
|
3044
|
+
});
|
|
3045
|
+
function detectIndent(text2) {
|
|
3046
|
+
const lines = text2.split("\n");
|
|
3047
|
+
for (const line of lines) {
|
|
3048
|
+
if (line.startsWith(" ")) return " ";
|
|
3049
|
+
const match = line.match(/^( +)\S/);
|
|
3050
|
+
if (match) return match[1]?.length ?? 2;
|
|
3051
|
+
}
|
|
3052
|
+
return 2;
|
|
3053
|
+
}
|
|
3054
|
+
function detectTrailingNewline(text2) {
|
|
3055
|
+
return text2.endsWith("\n") ? "\n" : "";
|
|
3056
|
+
}
|
|
3057
|
+
function parse2(buf) {
|
|
3058
|
+
let value;
|
|
3059
|
+
try {
|
|
3060
|
+
value = JSON.parse(buf.toString("utf8"));
|
|
3061
|
+
} catch {
|
|
3062
|
+
return { kind: "absent" };
|
|
3063
|
+
}
|
|
3064
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
3065
|
+
return { kind: "absent" };
|
|
3066
|
+
}
|
|
3067
|
+
return parseState(value.state);
|
|
3068
|
+
}
|
|
3069
|
+
function write2(buf, state) {
|
|
3070
|
+
const text2 = buf.toString("utf8");
|
|
3071
|
+
let value;
|
|
3072
|
+
try {
|
|
3073
|
+
value = JSON.parse(text2);
|
|
3074
|
+
} catch {
|
|
3075
|
+
return buf;
|
|
3076
|
+
}
|
|
3077
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
3078
|
+
return buf;
|
|
3079
|
+
}
|
|
3080
|
+
const obj = value;
|
|
3081
|
+
if (obj.state === state) return buf;
|
|
3082
|
+
obj.state = state;
|
|
3083
|
+
const indent = detectIndent(text2);
|
|
3084
|
+
const trailing = detectTrailingNewline(text2);
|
|
3085
|
+
const serialized = JSON.stringify(obj, null, indent);
|
|
3086
|
+
return Buffer.from(`${serialized}${trailing}`, "utf8");
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
// src/core/state/markdown.ts
|
|
3090
|
+
var markdown_exports = {};
|
|
3091
|
+
__export(markdown_exports, {
|
|
3092
|
+
parse: () => parse3,
|
|
3093
|
+
write: () => write3
|
|
3094
|
+
});
|
|
3095
|
+
function parse3(buf) {
|
|
3096
|
+
let parsed;
|
|
3097
|
+
try {
|
|
3098
|
+
parsed = matter(buf.toString("utf8"));
|
|
3099
|
+
} catch {
|
|
3100
|
+
return { kind: "absent" };
|
|
3101
|
+
}
|
|
3102
|
+
const raw = parsed.data.state;
|
|
3103
|
+
return parseState(raw);
|
|
3104
|
+
}
|
|
3105
|
+
function write3(buf, state) {
|
|
3106
|
+
let parsed;
|
|
3107
|
+
try {
|
|
3108
|
+
parsed = matter(buf.toString("utf8"));
|
|
3109
|
+
} catch {
|
|
3110
|
+
return buf;
|
|
3111
|
+
}
|
|
3112
|
+
const sourceData = parsed.data;
|
|
3113
|
+
if (sourceData.state === state) return buf;
|
|
3114
|
+
const nextData = { ...sourceData, state };
|
|
3115
|
+
const next = matter.stringify(parsed.content, nextData);
|
|
3116
|
+
return Buffer.from(next, "utf8");
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
// src/core/state/plaintext.ts
|
|
3120
|
+
var plaintext_exports = {};
|
|
3121
|
+
__export(plaintext_exports, {
|
|
3122
|
+
parse: () => parse4,
|
|
3123
|
+
write: () => write4
|
|
3124
|
+
});
|
|
3125
|
+
var STATE_LINE_RE = /^state:\s+(\S+)\s*$/;
|
|
3126
|
+
function splitFirstLine2(text2) {
|
|
3127
|
+
const crlfIdx = text2.indexOf("\r\n");
|
|
3128
|
+
const lfIdx = text2.indexOf("\n");
|
|
3129
|
+
if (crlfIdx !== -1 && (lfIdx === -1 || crlfIdx <= lfIdx)) {
|
|
3130
|
+
return { firstLine: text2.slice(0, crlfIdx), rest: text2.slice(crlfIdx + 2), eol: "\r\n" };
|
|
3131
|
+
}
|
|
3132
|
+
if (lfIdx !== -1) {
|
|
3133
|
+
return { firstLine: text2.slice(0, lfIdx), rest: text2.slice(lfIdx + 1), eol: "\n" };
|
|
3134
|
+
}
|
|
3135
|
+
return { firstLine: text2, rest: "", eol: "\n" };
|
|
3136
|
+
}
|
|
3137
|
+
function parse4(buf) {
|
|
3138
|
+
const text2 = buf.toString("utf8");
|
|
3139
|
+
const { firstLine } = splitFirstLine2(text2);
|
|
3140
|
+
const match = firstLine.match(STATE_LINE_RE);
|
|
3141
|
+
if (!match) return { kind: "absent" };
|
|
3142
|
+
return parseState(match[1]);
|
|
3143
|
+
}
|
|
3144
|
+
function write4(buf, state) {
|
|
3145
|
+
const text2 = buf.toString("utf8");
|
|
3146
|
+
const { firstLine, rest, eol } = splitFirstLine2(text2);
|
|
3147
|
+
const desiredLine = `state: ${state}`;
|
|
3148
|
+
const existingMatch = firstLine.match(STATE_LINE_RE);
|
|
3149
|
+
if (existingMatch) {
|
|
3150
|
+
if (firstLine === desiredLine) return buf;
|
|
3151
|
+
return Buffer.from(`${desiredLine}${eol}${rest}`, "utf8");
|
|
3152
|
+
}
|
|
3153
|
+
return Buffer.from(`${desiredLine}
|
|
3154
|
+
${text2}`, "utf8");
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
// src/core/state/yaml.ts
|
|
3158
|
+
var yaml_exports = {};
|
|
3159
|
+
__export(yaml_exports, {
|
|
3160
|
+
parse: () => parse5,
|
|
3161
|
+
write: () => write5
|
|
3162
|
+
});
|
|
3163
|
+
function parse5(buf) {
|
|
3164
|
+
let doc;
|
|
3165
|
+
try {
|
|
3166
|
+
doc = parseDocument(buf.toString("utf8"));
|
|
3167
|
+
} catch {
|
|
3168
|
+
return { kind: "absent" };
|
|
3169
|
+
}
|
|
3170
|
+
if (doc.errors.length > 0) return { kind: "absent" };
|
|
3171
|
+
if (!isMap(doc.contents)) return { kind: "absent" };
|
|
3172
|
+
const raw = doc.get("state");
|
|
3173
|
+
return parseState(raw);
|
|
3174
|
+
}
|
|
3175
|
+
function write5(buf, state) {
|
|
3176
|
+
const text2 = buf.toString("utf8");
|
|
3177
|
+
let doc;
|
|
3178
|
+
try {
|
|
3179
|
+
doc = parseDocument(text2);
|
|
3180
|
+
} catch {
|
|
3181
|
+
return buf;
|
|
3182
|
+
}
|
|
3183
|
+
if (doc.errors.length > 0) return buf;
|
|
3184
|
+
if (!isMap(doc.contents)) return buf;
|
|
3185
|
+
if (doc.get("state") === state) return buf;
|
|
3186
|
+
doc.set("state", state);
|
|
3187
|
+
return Buffer.from(doc.toString(), "utf8");
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
// src/core/state/format.ts
|
|
3191
|
+
var HANDLERS = {
|
|
3192
|
+
".md": markdown_exports,
|
|
3193
|
+
".markdown": markdown_exports,
|
|
3194
|
+
".html": html_exports,
|
|
3195
|
+
".htm": html_exports,
|
|
3196
|
+
".yaml": yaml_exports,
|
|
3197
|
+
".yml": yaml_exports,
|
|
3198
|
+
".json": json_exports,
|
|
3199
|
+
".txt": plaintext_exports
|
|
3200
|
+
};
|
|
3201
|
+
function handlerFor(filePath) {
|
|
3202
|
+
const ext = extname(filePath).toLowerCase();
|
|
3203
|
+
return HANDLERS[ext];
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
// src/core/state/applier.ts
|
|
3207
|
+
var MATCHER_OPTIONS = { dot: false, nocase: false };
|
|
3208
|
+
function compileMatchers(rules) {
|
|
3209
|
+
return rules.map((r) => picomatch(r.pattern, MATCHER_OPTIONS));
|
|
3210
|
+
}
|
|
3211
|
+
function findRuleState(filePath, rules, matchers) {
|
|
3212
|
+
for (let i = 0; i < rules.length; i++) {
|
|
3213
|
+
const match = matchers[i];
|
|
3214
|
+
const rule = rules[i];
|
|
3215
|
+
if (!match || !rule) continue;
|
|
3216
|
+
if (!match(filePath)) continue;
|
|
3217
|
+
if (rule.state !== void 0) return rule.state;
|
|
3218
|
+
return void 0;
|
|
3219
|
+
}
|
|
3220
|
+
return void 0;
|
|
3221
|
+
}
|
|
3222
|
+
async function* walk(dir, root) {
|
|
3223
|
+
let entries;
|
|
3224
|
+
try {
|
|
3225
|
+
entries = await readdir(dir, { withFileTypes: true, encoding: "utf8" });
|
|
3226
|
+
} catch {
|
|
3227
|
+
return;
|
|
3228
|
+
}
|
|
3229
|
+
const isRoot = dir === root;
|
|
3230
|
+
for (const entry of entries) {
|
|
3231
|
+
if (entry.name === ".git") continue;
|
|
3232
|
+
if (entry.name === ".maester-source.json") continue;
|
|
3233
|
+
if (isRoot && entry.name === "maester.yaml") continue;
|
|
3234
|
+
const full = resolve(dir, entry.name);
|
|
3235
|
+
if (entry.isDirectory()) {
|
|
3236
|
+
yield* walk(full, root);
|
|
3237
|
+
} else if (entry.isFile()) {
|
|
3238
|
+
yield relative(root, full);
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
async function applyState(stagedDir, rules) {
|
|
3243
|
+
const matchers = compileMatchers(rules);
|
|
3244
|
+
const breakdown = { canon: 0, draft: 0, untagged: 0 };
|
|
3245
|
+
const warnings = [];
|
|
3246
|
+
const details = [];
|
|
3247
|
+
for await (const relPath of walk(stagedDir, stagedDir)) {
|
|
3248
|
+
const handler = handlerFor(relPath);
|
|
3249
|
+
if (!handler) {
|
|
3250
|
+
breakdown.untagged++;
|
|
3251
|
+
details.push({ file: relPath, state: "untagged", sourceOfTruth: "untagged" });
|
|
3252
|
+
continue;
|
|
3253
|
+
}
|
|
3254
|
+
const fullPath = resolve(stagedDir, relPath);
|
|
3255
|
+
const buf = await readFile(fullPath);
|
|
3256
|
+
const parsed = handler.parse(buf);
|
|
3257
|
+
let resolved;
|
|
3258
|
+
let sourceOfTruth;
|
|
3259
|
+
if (parsed.kind === "valid") {
|
|
3260
|
+
resolved = parsed.value;
|
|
3261
|
+
sourceOfTruth = "inline";
|
|
3262
|
+
const ruleState = findRuleState(relPath, rules, matchers);
|
|
3263
|
+
if (ruleState !== void 0 && ruleState !== resolved) {
|
|
3264
|
+
warnings.push({
|
|
3265
|
+
type: "disagreement",
|
|
3266
|
+
file: relPath,
|
|
3267
|
+
inline: resolved,
|
|
3268
|
+
rule: ruleState
|
|
3269
|
+
});
|
|
3270
|
+
}
|
|
3271
|
+
} else {
|
|
3272
|
+
if (parsed.kind === "invalid") {
|
|
3273
|
+
warnings.push({ type: "bad-inline-state", file: relPath, raw: parsed.raw });
|
|
3274
|
+
}
|
|
3275
|
+
const ruleState = findRuleState(relPath, rules, matchers);
|
|
3276
|
+
if (ruleState !== void 0) {
|
|
3277
|
+
resolved = ruleState;
|
|
3278
|
+
sourceOfTruth = "rule";
|
|
3279
|
+
} else {
|
|
3280
|
+
resolved = DEFAULT_STATE;
|
|
3281
|
+
sourceOfTruth = "default";
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
const next = handler.write(buf, resolved);
|
|
3285
|
+
if (next !== buf) {
|
|
3286
|
+
await writeFile(fullPath, next);
|
|
3287
|
+
}
|
|
3288
|
+
breakdown[resolved]++;
|
|
3289
|
+
details.push({ file: relPath, state: resolved, sourceOfTruth });
|
|
3290
|
+
}
|
|
3291
|
+
return { breakdown, warnings, details };
|
|
3292
|
+
}
|
|
3293
|
+
async function stageDestination(input) {
|
|
3294
|
+
await assertDestinationSafe(input.destination, input.marker.sourceName);
|
|
3295
|
+
const tempDir = `${input.destination}.tmp-${Math.random().toString(36).slice(2, 10)}`;
|
|
3296
|
+
await mkdir(dirname(input.destination), { recursive: true });
|
|
3297
|
+
await mkdir(tempDir, { recursive: true });
|
|
3298
|
+
await copyCacheToTemp(input.cacheDir, tempDir);
|
|
3299
|
+
const beforePromoteResult = input.beforePromote ? await input.beforePromote(tempDir) : void 0;
|
|
3300
|
+
await writeProvenanceMarker(tempDir, input.marker);
|
|
3301
|
+
await promoteTempToDestination(tempDir, input.destination);
|
|
3302
|
+
if (beforePromoteResult !== void 0) {
|
|
3303
|
+
return { staged: true, finalPath: input.destination, beforePromoteResult };
|
|
3304
|
+
}
|
|
3305
|
+
return { staged: true, finalPath: input.destination };
|
|
3306
|
+
}
|
|
3307
|
+
async function copyCacheToTemp(cacheDir, tempDir) {
|
|
3308
|
+
if (!existsSync(cacheDir)) return;
|
|
3309
|
+
const entries = await readdir(cacheDir, { withFileTypes: true });
|
|
3310
|
+
for (const entry of entries) {
|
|
3311
|
+
if (entry.name === ".git") continue;
|
|
3312
|
+
const src = resolve(cacheDir, entry.name);
|
|
3313
|
+
const dst = resolve(tempDir, entry.name);
|
|
3314
|
+
await cp(src, dst, { recursive: true, force: true, errorOnExist: false });
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
async function promoteTempToDestination(tempDir, destination) {
|
|
3318
|
+
if (existsSync(destination)) {
|
|
3319
|
+
const backup = `${destination}.old-${Math.random().toString(36).slice(2, 10)}`;
|
|
3320
|
+
await rename(destination, backup);
|
|
3321
|
+
try {
|
|
3322
|
+
await rename(tempDir, destination);
|
|
3323
|
+
} catch (err) {
|
|
3324
|
+
await rename(backup, destination).catch(() => void 0);
|
|
3325
|
+
throw err;
|
|
3326
|
+
}
|
|
3327
|
+
await rm(backup, { recursive: true, force: true });
|
|
3328
|
+
return;
|
|
3329
|
+
}
|
|
3330
|
+
await rename(tempDir, destination);
|
|
3331
|
+
}
|
|
3332
|
+
async function assertDestinationSafe(destination, expectedSourceName) {
|
|
3333
|
+
if (!existsSync(destination)) return;
|
|
3334
|
+
const entries = await readdir(destination);
|
|
3335
|
+
if (entries.length === 0) return;
|
|
3336
|
+
if (entries.length === 1 && entries[0] === PROVENANCE_FILENAME) {
|
|
3337
|
+
const marker2 = await readProvenanceMarker(destination);
|
|
3338
|
+
if (!marker2 || marker2.sourceName === expectedSourceName) return;
|
|
3339
|
+
}
|
|
3340
|
+
const marker = await readProvenanceMarker(destination);
|
|
3341
|
+
if (marker && marker.sourceName === expectedSourceName) return;
|
|
3342
|
+
throw new DestinationBlockedError(
|
|
3343
|
+
destination,
|
|
3344
|
+
`Refusing to overwrite ${destination}: contains content that was not produced by '${expectedSourceName}'. Remove the directory or choose a different destination.`
|
|
3345
|
+
);
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
// src/core/sync/runner.ts
|
|
3349
|
+
var DEFAULT_CONCURRENCY2 = 4;
|
|
3350
|
+
async function runSync(config, options) {
|
|
3351
|
+
const env = options.env ?? process.env;
|
|
3352
|
+
const scope = options.scope?.length ? new Set(options.scope) : void 0;
|
|
3353
|
+
const baseDir = options.baseDir ?? config.baseDir;
|
|
3354
|
+
if (scope) {
|
|
3355
|
+
const known = new Set(config.sources.map((s) => s.name));
|
|
3356
|
+
for (const name of scope) {
|
|
3357
|
+
if (!known.has(name)) {
|
|
3358
|
+
throw new MaesterError(
|
|
3359
|
+
"UNKNOWN_SOURCE",
|
|
3360
|
+
`Unknown source '${name}' \u2014 not declared in citadel.yaml.`
|
|
3361
|
+
);
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
const entries = config.sources.filter((s) => !scope || scope.has(s.name));
|
|
3366
|
+
const limit = Math.min(
|
|
3367
|
+
Math.max(1, options.concurrency ?? DEFAULT_CONCURRENCY2),
|
|
3368
|
+
entries.length || 1
|
|
3369
|
+
);
|
|
3370
|
+
const outcomes = new Array(entries.length);
|
|
3371
|
+
await mkdir(resolve(options.repoRoot, CACHE_SUBDIR), { recursive: true });
|
|
3372
|
+
let cursor = 0;
|
|
3373
|
+
const workers = [];
|
|
3374
|
+
for (let i = 0; i < limit; i++) {
|
|
3375
|
+
workers.push(
|
|
3376
|
+
(async () => {
|
|
3377
|
+
while (true) {
|
|
3378
|
+
const index = cursor++;
|
|
3379
|
+
if (index >= entries.length) return;
|
|
3380
|
+
const entry = entries[index];
|
|
3381
|
+
if (!entry) return;
|
|
3382
|
+
outcomes[index] = await processEntry(entry, options, env, baseDir);
|
|
3383
|
+
}
|
|
3384
|
+
})()
|
|
3385
|
+
);
|
|
3386
|
+
}
|
|
3387
|
+
await Promise.all(workers);
|
|
3388
|
+
const failed = outcomes.filter((o) => o.status === "failed").length;
|
|
3389
|
+
return { outcomes, failed };
|
|
3390
|
+
}
|
|
3391
|
+
async function processEntry(source, options, env, baseDir) {
|
|
3392
|
+
const cacheDir = cachePathForSource(options.repoRoot, source.name);
|
|
3393
|
+
const destination = source.destination ? resolve(options.repoRoot, source.destination) : defaultDestinationFor(options.repoRoot, source.name, baseDir);
|
|
3394
|
+
options.onProgress?.({ type: "start", name: source.name });
|
|
3395
|
+
try {
|
|
3396
|
+
const auth = resolveAuth(source.auth, env);
|
|
3397
|
+
const tokenForUrl = auth.type === "token" ? auth.value : void 0;
|
|
3398
|
+
const cacheExists = existsSync(cacheDir);
|
|
3399
|
+
const tree = await fetchSource(source, {
|
|
3400
|
+
cacheDir,
|
|
3401
|
+
cacheExists,
|
|
3402
|
+
tokenForUrl
|
|
3403
|
+
});
|
|
3404
|
+
options.onProgress?.({ type: "fetched", name: source.name, commitSha: tree.commitSha });
|
|
3405
|
+
for (const warning of tree.warnings) {
|
|
3406
|
+
options.onProgress?.({ type: "warning", name: source.name, warning });
|
|
3407
|
+
}
|
|
3408
|
+
const existingMarker = await readProvenanceMarker(destination);
|
|
3409
|
+
const wasUnchanged = !!existingMarker && existingMarker.commitSha === tree.commitSha && existingMarker.sourceName === source.name && filterSetMatches(existingMarker.filterSet, tree.filterSet) && existsSync(destination);
|
|
3410
|
+
if (wasUnchanged) {
|
|
3411
|
+
options.onProgress?.({ type: "staged", name: source.name, status: "unchanged" });
|
|
3412
|
+
return {
|
|
3413
|
+
name: source.name,
|
|
3414
|
+
status: "unchanged",
|
|
3415
|
+
destination,
|
|
3416
|
+
ref: source.ref,
|
|
3417
|
+
commitSha: tree.commitSha,
|
|
3418
|
+
warnings: tree.warnings
|
|
3419
|
+
};
|
|
3420
|
+
}
|
|
3421
|
+
const stageResult = await stageDestination({
|
|
3422
|
+
cacheDir: tree.cacheDir,
|
|
3423
|
+
destination,
|
|
3424
|
+
marker: {
|
|
3425
|
+
sourceName: tree.name,
|
|
3426
|
+
sourceUrl: source.url,
|
|
3427
|
+
ref: source.ref,
|
|
3428
|
+
commitSha: tree.commitSha,
|
|
3429
|
+
filterSet: tree.filterSet,
|
|
3430
|
+
syncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3431
|
+
},
|
|
3432
|
+
beforePromote: (stagedDir) => applyState(stagedDir, tree.rules)
|
|
3433
|
+
});
|
|
3434
|
+
const stateResult = stageResult.beforePromoteResult;
|
|
3435
|
+
const status = existingMarker ? "updated" : "added";
|
|
3436
|
+
options.onProgress?.({ type: "staged", name: source.name, status });
|
|
3437
|
+
return {
|
|
3438
|
+
name: source.name,
|
|
3439
|
+
status,
|
|
3440
|
+
destination,
|
|
3441
|
+
ref: source.ref,
|
|
3442
|
+
commitSha: tree.commitSha,
|
|
3443
|
+
warnings: tree.warnings,
|
|
3444
|
+
...stateResult ? {
|
|
3445
|
+
stateBreakdown: stateResult.breakdown,
|
|
3446
|
+
stateWarnings: stateResult.warnings,
|
|
3447
|
+
stateDetails: stateResult.details
|
|
3448
|
+
} : {}
|
|
3449
|
+
};
|
|
3450
|
+
} catch (err) {
|
|
3451
|
+
const message = errorMessage2(err);
|
|
3452
|
+
options.onProgress?.({ type: "failed", name: source.name, error: message });
|
|
3453
|
+
try {
|
|
3454
|
+
await clearWorktree(cacheDir);
|
|
3455
|
+
} catch {
|
|
3456
|
+
}
|
|
3457
|
+
return {
|
|
3458
|
+
name: source.name,
|
|
3459
|
+
status: "failed",
|
|
3460
|
+
destination,
|
|
3461
|
+
ref: source.ref,
|
|
3462
|
+
warnings: [],
|
|
3463
|
+
error: message
|
|
3464
|
+
};
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
function errorMessage2(err) {
|
|
3468
|
+
if (err instanceof AuthError) return err.message;
|
|
3469
|
+
if (err instanceof RefNotFoundError) return err.message;
|
|
3470
|
+
if (err instanceof MaesterError) return err.message;
|
|
3471
|
+
if (err instanceof Error) return err.message;
|
|
3472
|
+
return String(err);
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
// src/cli/commands/sync.ts
|
|
3476
|
+
function registerSync(program, getContext) {
|
|
3477
|
+
program.command("sync").description("Fetch all configured sources into the local citadel.").argument("[names...]", "Optional source names to scope the run").option(
|
|
3478
|
+
"--concurrency <n>",
|
|
3479
|
+
"Override the per-run source concurrency (default: 4)",
|
|
3480
|
+
(v) => Number(v)
|
|
3481
|
+
).action(async (names, options) => {
|
|
3482
|
+
const code = await runSyncCommand(getContext(), names, options.concurrency);
|
|
3483
|
+
process.exitCode = code;
|
|
3484
|
+
});
|
|
3485
|
+
}
|
|
3486
|
+
async function runSyncCommand(ctx, scope, concurrency) {
|
|
3487
|
+
let config;
|
|
3488
|
+
try {
|
|
3489
|
+
config = await loadCitadelConfig(ctx.repoRoot.path);
|
|
3490
|
+
} catch (err) {
|
|
3491
|
+
const message = err instanceof MaesterError ? err.message : err instanceof Error ? err.message : String(err);
|
|
3492
|
+
ctx.logger.error(message);
|
|
3493
|
+
return 1;
|
|
3494
|
+
}
|
|
3495
|
+
if (!ctx.flags.json) {
|
|
3496
|
+
const totalConfigured = config.sources.length;
|
|
3497
|
+
ctx.logger.info(`Syncing ${scope.length > 0 ? scope.length : totalConfigured} source(s)\u2026`);
|
|
3498
|
+
}
|
|
3499
|
+
const result = await runSync(config, {
|
|
3500
|
+
repoRoot: ctx.repoRoot.path,
|
|
3501
|
+
...scope.length > 0 ? { scope } : {},
|
|
3502
|
+
...concurrency !== void 0 && Number.isFinite(concurrency) ? { concurrency } : {},
|
|
3503
|
+
onProgress: (event) => {
|
|
3504
|
+
if (!ctx.flags.json) return;
|
|
3505
|
+
process.stdout.write(`${JSON.stringify({ event: event.type, ...event })}
|
|
3506
|
+
`);
|
|
3507
|
+
}
|
|
3508
|
+
});
|
|
3509
|
+
if (ctx.flags.json) {
|
|
3510
|
+
for (const outcome of result.outcomes) {
|
|
3511
|
+
process.stdout.write(`${JSON.stringify(buildJsonOutcome2(outcome))}
|
|
3512
|
+
`);
|
|
3513
|
+
}
|
|
3514
|
+
} else {
|
|
3515
|
+
renderHumanSummary2(ctx, result);
|
|
3516
|
+
}
|
|
3517
|
+
return result.failed > 0 ? 1 : 0;
|
|
3518
|
+
}
|
|
3519
|
+
function buildJsonOutcome2(outcome) {
|
|
3520
|
+
const base = {
|
|
3521
|
+
name: outcome.name,
|
|
3522
|
+
status: outcome.status,
|
|
3523
|
+
destination: outcome.destination
|
|
3524
|
+
};
|
|
3525
|
+
if (outcome.ref !== void 0) base.ref = outcome.ref;
|
|
3526
|
+
if (outcome.commitSha !== void 0) base.commitSha = outcome.commitSha;
|
|
3527
|
+
if (outcome.warnings.length > 0) {
|
|
3528
|
+
base.warnings = outcome.warnings.map(serializeWarning);
|
|
3529
|
+
}
|
|
3530
|
+
if (outcome.stateBreakdown !== void 0) {
|
|
3531
|
+
base.stateBreakdown = outcome.stateBreakdown;
|
|
3532
|
+
}
|
|
3533
|
+
if (outcome.stateWarnings && outcome.stateWarnings.length > 0) {
|
|
3534
|
+
base.stateWarnings = outcome.stateWarnings;
|
|
3535
|
+
}
|
|
3536
|
+
if (outcome.error !== void 0) base.error = redactUrl(outcome.error);
|
|
3537
|
+
return base;
|
|
3538
|
+
}
|
|
3539
|
+
function serializeWarning(warning) {
|
|
3540
|
+
return {
|
|
3541
|
+
type: warning.type,
|
|
3542
|
+
name: warning.name,
|
|
3543
|
+
includes: warning.includes
|
|
3544
|
+
};
|
|
3545
|
+
}
|
|
3546
|
+
function renderHumanSummary2(ctx, result) {
|
|
3547
|
+
for (const outcome of result.outcomes) {
|
|
3548
|
+
const shortSha2 = outcome.commitSha?.slice(0, 7) ?? "\u2014";
|
|
3549
|
+
const stateSuffix = formatStateBreakdownSuffix(outcome);
|
|
3550
|
+
switch (outcome.status) {
|
|
3551
|
+
case "added":
|
|
3552
|
+
ctx.logger.success(`${outcome.name}: added (${shortSha2})${stateSuffix}`);
|
|
3553
|
+
break;
|
|
3554
|
+
case "updated":
|
|
3555
|
+
ctx.logger.success(`${outcome.name}: updated (${shortSha2})${stateSuffix}`);
|
|
3556
|
+
break;
|
|
3557
|
+
case "unchanged":
|
|
3558
|
+
ctx.logger.info(`${outcome.name}: unchanged`);
|
|
3559
|
+
break;
|
|
3560
|
+
case "failed":
|
|
3561
|
+
ctx.logger.error(
|
|
3562
|
+
`${outcome.name}: failed \u2014 ${redactUrl(outcome.error ?? "unknown error")}`
|
|
3563
|
+
);
|
|
3564
|
+
break;
|
|
3565
|
+
}
|
|
3566
|
+
for (const warning of outcome.warnings) {
|
|
3567
|
+
ctx.logger.warning(` ${formatWarning(warning)}`);
|
|
3568
|
+
}
|
|
3569
|
+
for (const stateWarning of outcome.stateWarnings ?? []) {
|
|
3570
|
+
ctx.logger.warning(` ${formatStateWarning(stateWarning)}`);
|
|
3571
|
+
}
|
|
3572
|
+
if (ctx.flags.verbose && outcome.stateDetails && outcome.stateDetails.length > 0) {
|
|
3573
|
+
for (const detail of outcome.stateDetails) {
|
|
3574
|
+
ctx.logger.verbose(` ${detail.file} \u2014 state=${detail.state} (${detail.sourceOfTruth})`);
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
ctx.logger.blank();
|
|
3579
|
+
if (result.failed > 0) {
|
|
3580
|
+
ctx.logger.error(`${result.failed} source(s) failed.`);
|
|
3581
|
+
return;
|
|
3582
|
+
}
|
|
3583
|
+
ctx.logger.success(`All ${result.outcomes.length} source(s) up to date.`);
|
|
3584
|
+
}
|
|
3585
|
+
function formatWarning(warning) {
|
|
3586
|
+
return `no files matched includes \u2014 citadel-side filter set may need updating (${warning.includes.join(", ")})`;
|
|
3587
|
+
}
|
|
3588
|
+
function formatStateBreakdownSuffix(outcome) {
|
|
3589
|
+
const b = outcome.stateBreakdown;
|
|
3590
|
+
if (!b) return "";
|
|
3591
|
+
return ` canon: ${b.canon} \xB7 draft: ${b.draft} \xB7 untagged: ${b.untagged}`;
|
|
3592
|
+
}
|
|
3593
|
+
function formatStateWarning(warning) {
|
|
3594
|
+
if (warning.type === "bad-inline-state") {
|
|
3595
|
+
return `${warning.file}: inline state '${warning.raw}' is not in {draft, canon} \u2014 treated as missing`;
|
|
3596
|
+
}
|
|
3597
|
+
return `${warning.file}: inline state '${warning.inline}' overrides rule state '${warning.rule}'`;
|
|
3598
|
+
}
|
|
3599
|
+
function getRepoRoot(start = process.cwd()) {
|
|
3600
|
+
const path5 = resolve(start);
|
|
3601
|
+
return {
|
|
3602
|
+
path: path5,
|
|
3603
|
+
hasGit: existsSync(resolve(path5, ".git")),
|
|
3604
|
+
hasPackageJson: existsSync(resolve(path5, "package.json"))
|
|
3605
|
+
};
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
// src/cli/context.ts
|
|
3609
|
+
function buildContext(flags, cwd = process.cwd()) {
|
|
3610
|
+
const theming = createTheming({
|
|
3611
|
+
forceColor: flags.color ?? "auto",
|
|
3612
|
+
...flags.theme !== void 0 ? { themeOverride: flags.theme } : {}
|
|
3613
|
+
});
|
|
3614
|
+
const level = flags.quiet ? "error" : flags.verbose ? "verbose" : "info";
|
|
3615
|
+
const logger = createLogger({ theming, level, json: flags.json ?? false });
|
|
3616
|
+
const prompts = createPrompts();
|
|
3617
|
+
const repoRoot = getRepoRoot(cwd);
|
|
3618
|
+
return { flags, theming, logger, prompts, repoRoot };
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
// src/cli/menu.ts
|
|
3622
|
+
async function showTopLevelMenu(ctx) {
|
|
3623
|
+
const roles = detectRoles(ctx.repoRoot.path);
|
|
3624
|
+
const citadelLabel = roles.hasCitadel ? "View citadel" : "Initialize a citadel";
|
|
3625
|
+
const citadelHint = roles.hasCitadel ? "summary of the existing config" : "this repo pulls from remote knowledge sources";
|
|
3626
|
+
const maesterLabel = roles.hasMaester ? "View maester manifest" : "Configure this repo as a maester";
|
|
3627
|
+
const maesterHint = roles.hasMaester ? "summary of the existing manifest" : "this repo publishes documents";
|
|
3628
|
+
return ctx.prompts.select({
|
|
3629
|
+
message: "What would you like to do?",
|
|
3630
|
+
options: [
|
|
3631
|
+
{ value: "init", label: citadelLabel, hint: citadelHint },
|
|
3632
|
+
{ value: "publish", label: maesterLabel, hint: maesterHint },
|
|
3633
|
+
{
|
|
3634
|
+
value: "status",
|
|
3635
|
+
label: "Show status",
|
|
3636
|
+
hint: "check whether configured sources are up to date"
|
|
3637
|
+
},
|
|
3638
|
+
{ value: "exit", label: "Exit" }
|
|
3639
|
+
]
|
|
3640
|
+
});
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
// src/cli/main.ts
|
|
3644
|
+
async function run(argv = process.argv) {
|
|
3645
|
+
const code = await runMain(argv);
|
|
3646
|
+
process.exitCode = code;
|
|
3647
|
+
}
|
|
3648
|
+
async function runMain(argv = process.argv) {
|
|
3649
|
+
maybeRenderHelpVersionBanner(argv);
|
|
3650
|
+
const program = buildProgram();
|
|
3651
|
+
try {
|
|
3652
|
+
await program.parseAsync(argv);
|
|
3653
|
+
return toExitCode(process.exitCode);
|
|
3654
|
+
} catch (err) {
|
|
3655
|
+
if (err instanceof PromptCancelledError) return 130;
|
|
3656
|
+
if (err instanceof MaesterError) {
|
|
3657
|
+
process.stderr.write(`${err.message}
|
|
3658
|
+
`);
|
|
3659
|
+
return 1;
|
|
3660
|
+
}
|
|
3661
|
+
process.stderr.write(`${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
3662
|
+
`);
|
|
3663
|
+
return 1;
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
function maybeRenderHelpVersionBanner(argv) {
|
|
3667
|
+
if (!process.stdout.isTTY) return;
|
|
3668
|
+
const tail = argv.slice(2);
|
|
3669
|
+
const wantsHelp = tail.includes("--help") || tail.includes("-h");
|
|
3670
|
+
const wantsVersion = tail.includes("--version") || tail.includes("-V");
|
|
3671
|
+
if (!wantsHelp && !wantsVersion) return;
|
|
3672
|
+
const theming = createTheming();
|
|
3673
|
+
const subtitle = wantsVersion ? "v0.1.0 \xB7 living specs" : "living specs \xB7 v0.1.0";
|
|
3674
|
+
const banner = bannerForContext(theming, readColumns(), subtitle);
|
|
3675
|
+
if (banner.length > 0) {
|
|
3676
|
+
process.stdout.write(`${banner}
|
|
3677
|
+
|
|
3678
|
+
`);
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
function toExitCode(value) {
|
|
3682
|
+
if (typeof value === "number") return value;
|
|
3683
|
+
if (typeof value === "string") {
|
|
3684
|
+
const parsed = Number.parseInt(value, 10);
|
|
3685
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
3686
|
+
}
|
|
3687
|
+
return 0;
|
|
3688
|
+
}
|
|
3689
|
+
function buildProgram() {
|
|
3690
|
+
const program = new Command();
|
|
3691
|
+
program.name("maester").description("Aggregate documentation from many sources into one citadel.").version("0.1.0", "-V, --version", "Print the maester version.").option("--verbose", "Show verbose output").option("--quiet", "Suppress all output except errors").option("--json", "Emit machine-readable JSON output (one object per line)").option("--color", "Force colored output (overrides auto-detection)").option("--no-color", "Disable colored output (overrides auto-detection)").option("--theme <theme>", "Theme override: 'dark' or 'light'").option("--no-welcome", "Suppress the first-run welcome banner").enablePositionalOptions(false).allowExcessArguments(false);
|
|
3692
|
+
registerInit(program, () => buildContext(extractFlags(program.opts())));
|
|
3693
|
+
registerPublish(program, () => buildContext(extractFlags(program.opts())));
|
|
3694
|
+
registerSkill(program, () => buildContext(extractFlags(program.opts())));
|
|
3695
|
+
registerStatus(program, () => buildContext(extractFlags(program.opts())));
|
|
3696
|
+
registerSync(program, () => buildContext(extractFlags(program.opts())));
|
|
3697
|
+
program.action(async () => {
|
|
3698
|
+
const ctx = buildContext(extractFlags(program.opts()));
|
|
3699
|
+
const code = await runNoArgs(ctx);
|
|
3700
|
+
process.exitCode = code;
|
|
3701
|
+
});
|
|
3702
|
+
return program;
|
|
3703
|
+
}
|
|
3704
|
+
function extractFlags(opts) {
|
|
3705
|
+
let color = "auto";
|
|
3706
|
+
if (opts.color === true) color = "always";
|
|
3707
|
+
if (opts.color === false) color = "never";
|
|
3708
|
+
const theme = typeof opts.theme === "string" && (opts.theme === "light" || opts.theme === "dark") ? opts.theme : void 0;
|
|
3709
|
+
const envNoWelcome = typeof process.env.MAESTER_NO_WELCOME === "string" && process.env.MAESTER_NO_WELCOME.length > 0;
|
|
3710
|
+
return {
|
|
3711
|
+
verbose: opts.verbose === true,
|
|
3712
|
+
quiet: opts.quiet === true,
|
|
3713
|
+
json: opts.json === true,
|
|
3714
|
+
color,
|
|
3715
|
+
...theme ? { theme } : {},
|
|
3716
|
+
noWelcome: opts.welcome === false || envNoWelcome
|
|
3717
|
+
};
|
|
3718
|
+
}
|
|
3719
|
+
async function runNoArgs(ctx) {
|
|
3720
|
+
if (!process.stdout.isTTY) {
|
|
3721
|
+
process.stdout.write("Run `maester --help` to see available commands.\n");
|
|
3722
|
+
return 0;
|
|
3723
|
+
}
|
|
3724
|
+
const roles = detectRoles(ctx.repoRoot.path);
|
|
3725
|
+
const showWelcome = !roles.hasCitadel && !roles.hasMaester && !ctx.flags.noWelcome;
|
|
3726
|
+
if (showWelcome) {
|
|
3727
|
+
const banner = bannerForContext(ctx.theming, readColumns(), "living specs \xB7 v0.1.0");
|
|
3728
|
+
if (banner.length > 0) {
|
|
3729
|
+
process.stdout.write(`${banner}
|
|
3730
|
+
|
|
3731
|
+
`);
|
|
3732
|
+
}
|
|
3733
|
+
ctx.prompts.intro("Welcome to maester");
|
|
3734
|
+
ctx.prompts.log.message("This repository has no citadel or maester config yet.");
|
|
3735
|
+
}
|
|
3736
|
+
try {
|
|
3737
|
+
const choice = await showTopLevelMenu(ctx);
|
|
3738
|
+
switch (choice) {
|
|
3739
|
+
case "init":
|
|
3740
|
+
return runInit(ctx);
|
|
3741
|
+
case "publish":
|
|
3742
|
+
return runPublish(ctx);
|
|
3743
|
+
case "status":
|
|
3744
|
+
return runStatusCommand(ctx, []);
|
|
3745
|
+
case "exit":
|
|
3746
|
+
ctx.prompts.outro("Goodbye.");
|
|
3747
|
+
return 0;
|
|
3748
|
+
}
|
|
3749
|
+
} catch (err) {
|
|
3750
|
+
if (err instanceof PromptCancelledError) {
|
|
3751
|
+
process.stdout.write("\n");
|
|
3752
|
+
return 130;
|
|
3753
|
+
}
|
|
3754
|
+
throw err;
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
export { run, runMain };
|
|
3759
|
+
//# sourceMappingURL=main.js.map
|
|
3760
|
+
//# sourceMappingURL=main.js.map
|