@tryaura/aura-cli 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/LICENSE +202 -0
- package/README.md +11 -0
- package/content/mcp/atlassian-rovo.json +14 -0
- package/content/mcp/github.json +23 -0
- package/content/mcp/sentry.json +23 -0
- package/content/snippets/ask-before-destructive.md +8 -0
- package/content/snippets/commit-conventions.md +8 -0
- package/content/snippets/confluence-references.md +8 -0
- package/content/snippets/jira-linking.md +8 -0
- package/content/snippets/pr-descriptions.md +8 -0
- package/content/snippets/python-style.md +8 -0
- package/content/snippets/typescript-style.md +8 -0
- package/dist/bin/aura.d.ts +1 -0
- package/dist/bin/aura.js +17 -0
- package/dist/index.d.ts +152 -0
- package/dist/index.js +140 -0
- package/dist/plugins/index.d.ts +14 -0
- package/dist/plugins/index.js +2 -0
- package/dist/plugins-n3DS8XXi.js +5684 -0
- package/dist/run-pMPxJqFQ.js +13367 -0
- package/dist/skill-deployment-plan-C4GTrVSy.js +1996 -0
- package/dist/types-DmBu6g0d.d.ts +84 -0
- package/package.json +73 -0
- package/schema/check-output-v1.schema.json +261 -0
|
@@ -0,0 +1,1996 @@
|
|
|
1
|
+
import { McpWriteError, advanceMarkdownFence, defineOwnProperty, definePlugin, detectLineEnding as detectLineEnding$1, jsonPropertyPath, mcpServerNameProblem, normalizeMcpServerDefinition, parseMcpServerDefinition, pluginContentUrl, splitSourceLines as splitSourceLines$1 } from "@tryaura/aura-sdk";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { Buffer, isUtf8 } from "node:buffer";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { createTwoFilesPatch } from "diff";
|
|
6
|
+
import { isDeepStrictEqual } from "node:util";
|
|
7
|
+
//#region ../core/src/pluralize.ts
|
|
8
|
+
/** Selects the noun or verb form that agrees with a numeric count. */
|
|
9
|
+
function pluralize(count, singular, plural = `${singular}s`) {
|
|
10
|
+
return count === 1 ? singular : plural;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region ../../plugins/content-official/src/index.ts
|
|
14
|
+
/** Neutral starter content used when Aura creates the canonical shared instruction source. */
|
|
15
|
+
const SHARED_INSTRUCTIONS_TEMPLATE = "# Shared agent instructions\n";
|
|
16
|
+
const contentSource = (path) => ({
|
|
17
|
+
type: "file",
|
|
18
|
+
url: pluginContentUrl(import.meta.url, path)
|
|
19
|
+
});
|
|
20
|
+
var src_default = definePlugin({
|
|
21
|
+
apiVersion: 1,
|
|
22
|
+
id: "official",
|
|
23
|
+
name: "Aura Official Content",
|
|
24
|
+
mcpCatalog: [
|
|
25
|
+
{
|
|
26
|
+
description: "Connect Atlassian Rovo to Jira and Confluence through Atlassian OAuth.",
|
|
27
|
+
id: "official/atlassian-rovo",
|
|
28
|
+
kind: "mcp-server",
|
|
29
|
+
name: "Atlassian Rovo",
|
|
30
|
+
source: contentSource("mcp/atlassian-rovo.json"),
|
|
31
|
+
version: "1.0.0"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
description: "Connect GitHub repositories, issues, and pull requests with a personal token.",
|
|
35
|
+
id: "official/github",
|
|
36
|
+
kind: "mcp-server",
|
|
37
|
+
name: "GitHub",
|
|
38
|
+
source: contentSource("mcp/github.json"),
|
|
39
|
+
version: "1.0.0"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
description: "Connect Sentry projects, issues, and telemetry with an auth token.",
|
|
43
|
+
id: "official/sentry",
|
|
44
|
+
kind: "mcp-server",
|
|
45
|
+
name: "Sentry",
|
|
46
|
+
source: contentSource("mcp/sentry.json"),
|
|
47
|
+
version: "1.0.0"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
skillDirectories: [{
|
|
51
|
+
id: "directory:agenticskills",
|
|
52
|
+
kind: "directory",
|
|
53
|
+
name: "agenticskills.io",
|
|
54
|
+
url: "https://agenticskills.io"
|
|
55
|
+
}],
|
|
56
|
+
snippets: [
|
|
57
|
+
{
|
|
58
|
+
category: "git",
|
|
59
|
+
description: "Apply clear, consistent conventions to Git commits.",
|
|
60
|
+
id: "official/commit-conventions",
|
|
61
|
+
kind: "snippet",
|
|
62
|
+
name: "Commit conventions",
|
|
63
|
+
source: contentSource("snippets/commit-conventions.md"),
|
|
64
|
+
version: "1.0.0"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
category: "safety",
|
|
68
|
+
description: "Require confirmation before destructive or irreversible operations.",
|
|
69
|
+
id: "official/ask-before-destructive",
|
|
70
|
+
kind: "snippet",
|
|
71
|
+
name: "Ask before destructive operations",
|
|
72
|
+
source: contentSource("snippets/ask-before-destructive.md"),
|
|
73
|
+
version: "1.0.0"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
category: "git",
|
|
77
|
+
description: "Write pull request descriptions that explain intent and validation.",
|
|
78
|
+
id: "official/pr-descriptions",
|
|
79
|
+
kind: "snippet",
|
|
80
|
+
name: "Pull request descriptions",
|
|
81
|
+
source: contentSource("snippets/pr-descriptions.md"),
|
|
82
|
+
version: "1.0.0"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
category: "atlassian",
|
|
86
|
+
description: "Keep Jira issue references consistent across development artifacts.",
|
|
87
|
+
id: "official/jira-linking",
|
|
88
|
+
kind: "snippet",
|
|
89
|
+
name: "Jira issue linking",
|
|
90
|
+
source: contentSource("snippets/jira-linking.md"),
|
|
91
|
+
version: "1.0.0"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
category: "atlassian",
|
|
95
|
+
description: "Reference Confluence without hiding execution-critical context.",
|
|
96
|
+
id: "official/confluence-references",
|
|
97
|
+
kind: "snippet",
|
|
98
|
+
name: "Confluence references",
|
|
99
|
+
source: contentSource("snippets/confluence-references.md"),
|
|
100
|
+
version: "1.0.0"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
category: "language",
|
|
104
|
+
description: "Apply strict and explicit TypeScript typing conventions.",
|
|
105
|
+
id: "official/typescript-style",
|
|
106
|
+
kind: "snippet",
|
|
107
|
+
name: "TypeScript style",
|
|
108
|
+
source: contentSource("snippets/typescript-style.md"),
|
|
109
|
+
version: "1.0.0"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
category: "language",
|
|
113
|
+
description: "Apply typed Python conventions with consistent Ruff validation.",
|
|
114
|
+
id: "official/python-style",
|
|
115
|
+
kind: "snippet",
|
|
116
|
+
name: "Python style",
|
|
117
|
+
source: contentSource("snippets/python-style.md"),
|
|
118
|
+
version: "1.0.0"
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
version: "0.0.0"
|
|
122
|
+
});
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region ../core/src/manifest/error.ts
|
|
125
|
+
/** A manifest cannot be serialized or written without risking data loss. */
|
|
126
|
+
var AuraManifestError = class extends Error {
|
|
127
|
+
path;
|
|
128
|
+
problem;
|
|
129
|
+
constructor(path, problem, cause) {
|
|
130
|
+
super(problem.message, { cause });
|
|
131
|
+
this.name = "AuraManifestError";
|
|
132
|
+
this.path = path;
|
|
133
|
+
this.problem = problem;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region ../core/src/manifest/protocol.ts
|
|
138
|
+
/** Resolves the manifest protocol path against one captured home directory. */
|
|
139
|
+
function resolveAuraManifestPath(homeDir) {
|
|
140
|
+
return join(homeDir, "agents", "aura.json");
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region ../core/src/values.ts
|
|
144
|
+
/**
|
|
145
|
+
* Reading values back out of unknown data: thrown errors and parsed JSON.
|
|
146
|
+
*
|
|
147
|
+
* These say nothing about any one feature: every module that catches a Node filesystem rejection
|
|
148
|
+
* or narrows a parsed document needs them, so they live in one place rather than accumulating
|
|
149
|
+
* per-directory copies that drift apart.
|
|
150
|
+
*/
|
|
151
|
+
/** The `errno` code of a Node filesystem failure, when the value is one. */
|
|
152
|
+
function errorCode(error) {
|
|
153
|
+
return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
154
|
+
}
|
|
155
|
+
function errorMessage(error) {
|
|
156
|
+
return error instanceof Error ? error.message : String(error);
|
|
157
|
+
}
|
|
158
|
+
/** Narrows an unknown parsed value to a plain object. */
|
|
159
|
+
function isRecord(value) {
|
|
160
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region ../core/src/manifest/schema-values.ts
|
|
164
|
+
var AuraManifestValidationError = class extends Error {
|
|
165
|
+
jsonPath;
|
|
166
|
+
constructor(jsonPath, message) {
|
|
167
|
+
super(`${jsonPath}: ${message}`);
|
|
168
|
+
this.name = "AuraManifestValidationError";
|
|
169
|
+
this.jsonPath = jsonPath;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
function invalid(path, message) {
|
|
173
|
+
return new AuraManifestValidationError(path, message);
|
|
174
|
+
}
|
|
175
|
+
function requiredString(value, key, path) {
|
|
176
|
+
const field = value[key];
|
|
177
|
+
if (typeof field !== "string") throw invalid(`${path}.${key}`, "must be a string");
|
|
178
|
+
return field;
|
|
179
|
+
}
|
|
180
|
+
function requiredBoolean(value, key, path) {
|
|
181
|
+
const field = value[key];
|
|
182
|
+
if (typeof field !== "boolean") throw invalid(`${path}.${key}`, "must be a boolean");
|
|
183
|
+
return field;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Narrows a value the top-level walk has already normalized.
|
|
187
|
+
*
|
|
188
|
+
* The section helpers run over subtrees of that walk, so they check shape and hand the existing
|
|
189
|
+
* frozen object back rather than copying and freezing it a second time.
|
|
190
|
+
*/
|
|
191
|
+
function requiredObject(value, path) {
|
|
192
|
+
if (!isJsonObject(value)) throw invalid(path, "must be an object");
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
function stringArray(value, path) {
|
|
196
|
+
if (!Array.isArray(value)) throw invalid(path, "must be an array");
|
|
197
|
+
return Object.freeze(value.map((candidate, index) => {
|
|
198
|
+
if (typeof candidate !== "string") throw invalid(`${path}[${String(index)}]`, "must be a string");
|
|
199
|
+
return candidate;
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
function isJsonObject(value) {
|
|
203
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region ../core/src/manifest/schema-checks.ts
|
|
207
|
+
const CHECK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/u;
|
|
208
|
+
const MAX_CHECK_IDS = 256;
|
|
209
|
+
function optionalPreset(value) {
|
|
210
|
+
if (value === void 0) return {};
|
|
211
|
+
if (typeof value !== "string" || value.trim().length === 0) throw invalid("$.preset", "must be a non-empty preset reference");
|
|
212
|
+
return { preset: value };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Validates the four recognized check settings, carrying anything else through untouched.
|
|
216
|
+
*
|
|
217
|
+
* Unknown keys survive on purpose, as they do everywhere else in the manifest: the file is written
|
|
218
|
+
* by Aura and read back by whatever version runs next, so a setting a newer release understands
|
|
219
|
+
* must not be destroyed — or rejected — by an older one that does not. Typo protection belongs
|
|
220
|
+
* where a human is authoring by hand, which is the preset schema and the command line, not here.
|
|
221
|
+
*/
|
|
222
|
+
function optionalChecks(value) {
|
|
223
|
+
if (value === void 0) return {};
|
|
224
|
+
const source = requiredObject(value, "$.checks");
|
|
225
|
+
const enabled = optionalStringArray(source["enabled"], "$.checks.enabled");
|
|
226
|
+
const disabled = optionalStringArray(source["disabled"], "$.checks.disabled");
|
|
227
|
+
const conflicts = new Set(enabled ?? []);
|
|
228
|
+
for (const id of disabled ?? []) if (conflicts.has(id)) throw invalid("$.checks", `must not both enable and disable check ${id}`);
|
|
229
|
+
return { checks: Object.freeze({
|
|
230
|
+
...source,
|
|
231
|
+
...disabled === void 0 ? {} : { disabled },
|
|
232
|
+
...enabled === void 0 ? {} : { enabled },
|
|
233
|
+
...optionalSeverity(source["severity"]),
|
|
234
|
+
...optionalThresholds(source["thresholds"])
|
|
235
|
+
}) };
|
|
236
|
+
}
|
|
237
|
+
function optionalStringArray(value, path) {
|
|
238
|
+
if (value === void 0) return;
|
|
239
|
+
const values = stringArray(value, path);
|
|
240
|
+
if (values.length > MAX_CHECK_IDS) throw invalid(path, `must contain at most ${String(MAX_CHECK_IDS)} check IDs`);
|
|
241
|
+
const seen = /* @__PURE__ */ new Set();
|
|
242
|
+
for (const [index, id] of values.entries()) {
|
|
243
|
+
if (!CHECK_ID_PATTERN.test(id)) throw invalid(`${path}[${String(index)}]`, "must be a valid check ID");
|
|
244
|
+
if (seen.has(id)) throw invalid(`${path}[${String(index)}]`, "must not duplicate another check ID");
|
|
245
|
+
seen.add(id);
|
|
246
|
+
}
|
|
247
|
+
return values;
|
|
248
|
+
}
|
|
249
|
+
function optionalSeverity(value) {
|
|
250
|
+
if (value === void 0) return {};
|
|
251
|
+
const source = requiredObject(value, "$.checks.severity");
|
|
252
|
+
if (Object.keys(source).length > MAX_CHECK_IDS) throw invalid("$.checks.severity", `must contain at most ${String(MAX_CHECK_IDS)} entries`);
|
|
253
|
+
const result = {};
|
|
254
|
+
for (const [id, candidate] of Object.entries(source)) {
|
|
255
|
+
if (!CHECK_ID_PATTERN.test(id)) throw invalid(jsonPropertyPath("$.checks.severity", id), "check ID is invalid");
|
|
256
|
+
if (candidate !== "error" && candidate !== "info" && candidate !== "warn") throw invalid(jsonPropertyPath("$.checks.severity", id), "must be error, info, or warn");
|
|
257
|
+
defineOwnProperty(result, id, candidate);
|
|
258
|
+
}
|
|
259
|
+
return { severity: Object.freeze(result) };
|
|
260
|
+
}
|
|
261
|
+
function optionalThresholds(value) {
|
|
262
|
+
if (value === void 0) return {};
|
|
263
|
+
const source = requiredObject(value, "$.checks.thresholds");
|
|
264
|
+
if (Object.keys(source).length > MAX_CHECK_IDS) throw invalid("$.checks.thresholds", `must contain at most ${String(MAX_CHECK_IDS)} entries`);
|
|
265
|
+
const result = {};
|
|
266
|
+
for (const [id, candidate] of Object.entries(source)) {
|
|
267
|
+
if (!CHECK_ID_PATTERN.test(id)) throw invalid(jsonPropertyPath("$.checks.thresholds", id), "check ID is invalid");
|
|
268
|
+
defineOwnProperty(result, id, requiredObject(candidate, jsonPropertyPath("$.checks.thresholds", id)));
|
|
269
|
+
}
|
|
270
|
+
return { thresholds: Object.freeze(result) };
|
|
271
|
+
}
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region ../core/src/manifest/schema-mcp.ts
|
|
274
|
+
/**
|
|
275
|
+
* Validates the desired MCP state, which is the one section that names credentials.
|
|
276
|
+
*
|
|
277
|
+
* The transport is handed to the SDK parser rather than checked here, so a definition Aura writes
|
|
278
|
+
* into an application's configuration and one it reads back out of a manifest pass through exactly
|
|
279
|
+
* the same rules. What stays here is the bookkeeping around it: which applications an entry claims,
|
|
280
|
+
* and the uniqueness that makes "the server named X" mean one thing.
|
|
281
|
+
*/
|
|
282
|
+
function mcpServers(value) {
|
|
283
|
+
if (!Array.isArray(value)) throw invalid("$.mcpServers", "must be an array");
|
|
284
|
+
const claimed = /* @__PURE__ */ new Map();
|
|
285
|
+
return Object.freeze(value.map((candidate, index) => mcpServer(candidate, `$.mcpServers[${String(index)}]`, claimed)));
|
|
286
|
+
}
|
|
287
|
+
function mcpServer(candidate, path, claimed) {
|
|
288
|
+
const server = requiredObject(candidate, path);
|
|
289
|
+
const name = requiredString(server, "name", path);
|
|
290
|
+
const nameProblem = mcpServerNameProblem(name);
|
|
291
|
+
if (nameProblem !== void 0) throw invalid(`${path}.name`, nameProblem);
|
|
292
|
+
const scope = server["scope"];
|
|
293
|
+
if (scope !== "global" && scope !== "project") throw invalid(`${path}.scope`, "must be either \"global\" or \"project\"");
|
|
294
|
+
const apps = targetApps(server["apps"], `${path}.apps`, name, scope, claimed);
|
|
295
|
+
const transport = parseMcpServerDefinition(server["transport"], `${path}.transport`);
|
|
296
|
+
if ("error" in transport) throw invalid(transport.error.path, transport.error.message);
|
|
297
|
+
const catalogId = server["catalogId"];
|
|
298
|
+
if (catalogId !== void 0 && (typeof catalogId !== "string" || catalogId.length === 0)) throw invalid(`${path}.catalogId`, "must be a non-empty string");
|
|
299
|
+
return Object.freeze({
|
|
300
|
+
...server,
|
|
301
|
+
apps,
|
|
302
|
+
...catalogId === void 0 ? {} : { catalogId },
|
|
303
|
+
name,
|
|
304
|
+
scope,
|
|
305
|
+
transport: transport.value
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Reads the applications one entry targets, refusing any that already has this server.
|
|
310
|
+
*
|
|
311
|
+
* `name` is the key written into each application's own configuration, so two entries claiming it
|
|
312
|
+
* for the same application and scope describe two different desired states for one key. Convergence
|
|
313
|
+
* would resolve that by writing whichever came last, which is not a decision a manifest should
|
|
314
|
+
* leave to iteration order. An entry that targets nothing is refused for a plainer reason: it
|
|
315
|
+
* converges nowhere, and the user who wrote it is watching for it to take effect.
|
|
316
|
+
*/
|
|
317
|
+
function targetApps(value, path, name, scope, claimed) {
|
|
318
|
+
const entries = stringArray(value, path);
|
|
319
|
+
if (entries.length === 0) throw invalid(path, "must name at least one application");
|
|
320
|
+
const key = `${scope}${name}`;
|
|
321
|
+
const owners = claimed.get(key) ?? /* @__PURE__ */ new Set();
|
|
322
|
+
claimed.set(key, owners);
|
|
323
|
+
const seen = /* @__PURE__ */ new Set();
|
|
324
|
+
for (const [index, entry] of entries.entries()) {
|
|
325
|
+
if (entry.length === 0) throw invalid(`${path}[${String(index)}]`, "must be non-empty");
|
|
326
|
+
if (seen.has(entry)) throw invalid(`${path}[${String(index)}]`, `duplicates ${entry}`);
|
|
327
|
+
if (owners.has(entry)) throw invalid(`${path}[${String(index)}]`, `duplicates the ${scope} server "${name}" already declared for ${entry}`);
|
|
328
|
+
seen.add(entry);
|
|
329
|
+
owners.add(entry);
|
|
330
|
+
}
|
|
331
|
+
return entries;
|
|
332
|
+
}
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region ../core/src/manifest/schema.ts
|
|
335
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
336
|
+
const SKILL_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
337
|
+
const SKILL_SOURCE_PATTERN = /^(?:directory|driver|plugin):[^\s:]+$/u;
|
|
338
|
+
/**
|
|
339
|
+
* How deep the manifest may nest.
|
|
340
|
+
*
|
|
341
|
+
* The normalizing walk below is recursive, and `JSON.parse` is not: a few kilobytes of `[` parses
|
|
342
|
+
* fine and then overflows the stack on the way back out. A `RangeError` is not one of the failures
|
|
343
|
+
* a manifest can degrade into, so it would escape as an exception and take the whole scan with it.
|
|
344
|
+
* Version 1 nests four levels; this leaves room for extension fields without leaving that open.
|
|
345
|
+
*/
|
|
346
|
+
const MAX_DEPTH = 100;
|
|
347
|
+
/** Validates and normalizes a runtime value into manifest v1 without dropping extension fields. */
|
|
348
|
+
function validateAuraManifest(value) {
|
|
349
|
+
const source = jsonObject(value, "$", "manifest must be an object");
|
|
350
|
+
const version = source["schemaVersion"];
|
|
351
|
+
if (typeof version !== "number" || !Number.isInteger(version)) throw invalid("$.schemaVersion", "must be an integer");
|
|
352
|
+
if (version !== 1) throw new UnsupportedAuraManifestVersionError(version);
|
|
353
|
+
return Object.freeze({
|
|
354
|
+
...source,
|
|
355
|
+
apps: apps(source["apps"]),
|
|
356
|
+
...optionalChecks(source["checks"]),
|
|
357
|
+
mcpServers: mcpServers(source["mcpServers"]),
|
|
358
|
+
ownership: ownership(source["ownership"]),
|
|
359
|
+
...optionalPreset(source["preset"]),
|
|
360
|
+
schemaVersion: 1,
|
|
361
|
+
skills: skills(source["skills"]),
|
|
362
|
+
snippets: snippets(source["snippets"])
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
function skills(value) {
|
|
366
|
+
if (!Array.isArray(value)) throw invalid("$.skills", "must be an array");
|
|
367
|
+
const ids = /* @__PURE__ */ new Set();
|
|
368
|
+
return Object.freeze(value.map((candidate, index) => {
|
|
369
|
+
const path = `$.skills[${String(index)}]`;
|
|
370
|
+
const skill = requiredObject(candidate, path);
|
|
371
|
+
const id = requiredString(skill, "id", path);
|
|
372
|
+
if (id.length > 64 || !SKILL_ID_PATTERN.test(id)) throw invalid(`${path}.id`, "must be a kebab-case skill ID of at most 64 characters");
|
|
373
|
+
if (ids.has(id)) throw invalid(`${path}.id`, "must not duplicate another selected skill ID");
|
|
374
|
+
ids.add(id);
|
|
375
|
+
const source = requiredSkillSource(skill, path);
|
|
376
|
+
const treeHash = requiredString(skill, "treeHash", path);
|
|
377
|
+
if (!SHA256_PATTERN.test(treeHash)) throw invalid(`${path}.treeHash`, "must be a lowercase SHA-256 hash");
|
|
378
|
+
return Object.freeze({
|
|
379
|
+
...skill,
|
|
380
|
+
id,
|
|
381
|
+
pinned: requiredBoolean(skill, "pinned", path),
|
|
382
|
+
source,
|
|
383
|
+
treeHash,
|
|
384
|
+
version: requiredString(skill, "version", path)
|
|
385
|
+
});
|
|
386
|
+
}));
|
|
387
|
+
}
|
|
388
|
+
function requiredSkillSource(value, path) {
|
|
389
|
+
const source = requiredString(value, "source", path);
|
|
390
|
+
if (!SKILL_SOURCE_PATTERN.test(source)) throw invalid(`${path}.source`, "must be a plugin:, directory:, or driver: source ID");
|
|
391
|
+
if (source.startsWith("plugin:")) return `plugin:${source.slice(7)}`;
|
|
392
|
+
if (source.startsWith("directory:")) return `directory:${source.slice(10)}`;
|
|
393
|
+
return `driver:${source.slice(7)}`;
|
|
394
|
+
}
|
|
395
|
+
var UnsupportedAuraManifestVersionError = class extends Error {
|
|
396
|
+
actualVersion;
|
|
397
|
+
constructor(actualVersion) {
|
|
398
|
+
super(`$.schemaVersion: version ${String(actualVersion)} is not supported`);
|
|
399
|
+
this.name = "UnsupportedAuraManifestVersionError";
|
|
400
|
+
this.actualVersion = actualVersion;
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
function apps(value) {
|
|
404
|
+
const source = requiredObject(value, "$.apps");
|
|
405
|
+
const result = {};
|
|
406
|
+
for (const [id, candidate] of Object.entries(source)) {
|
|
407
|
+
const path = jsonPropertyPath("$.apps", id);
|
|
408
|
+
const app = requiredObject(candidate, path);
|
|
409
|
+
defineOwnProperty(result, id, Object.freeze({
|
|
410
|
+
...app,
|
|
411
|
+
managed: requiredBoolean(app, "managed", path)
|
|
412
|
+
}));
|
|
413
|
+
}
|
|
414
|
+
return Object.freeze(result);
|
|
415
|
+
}
|
|
416
|
+
function snippets(value) {
|
|
417
|
+
if (!Array.isArray(value)) throw invalid("$.snippets", "must be an array");
|
|
418
|
+
return Object.freeze(value.map((candidate, index) => {
|
|
419
|
+
const path = `$.snippets[${String(index)}]`;
|
|
420
|
+
const snippet = requiredObject(candidate, path);
|
|
421
|
+
const hash = requiredString(snippet, "hash", path);
|
|
422
|
+
if (!SHA256_PATTERN.test(hash)) throw invalid(`${path}.hash`, "must be a lowercase SHA-256 hash");
|
|
423
|
+
return Object.freeze({
|
|
424
|
+
...snippet,
|
|
425
|
+
hash,
|
|
426
|
+
id: requiredString(snippet, "id", path),
|
|
427
|
+
pinned: requiredBoolean(snippet, "pinned", path),
|
|
428
|
+
version: requiredString(snippet, "version", path)
|
|
429
|
+
});
|
|
430
|
+
}));
|
|
431
|
+
}
|
|
432
|
+
function ownership(value) {
|
|
433
|
+
const source = requiredObject(value, "$.ownership");
|
|
434
|
+
const result = {};
|
|
435
|
+
for (const [id, candidate] of Object.entries(source)) {
|
|
436
|
+
const path = jsonPropertyPath("$.ownership", id);
|
|
437
|
+
const entry = requiredObject(candidate, path);
|
|
438
|
+
defineOwnProperty(result, id, Object.freeze({
|
|
439
|
+
...entry,
|
|
440
|
+
files: stringArray(entry["files"], `${path}.files`),
|
|
441
|
+
mcpServerNames: stringArray(entry["mcpServerNames"], `${path}.mcpServerNames`)
|
|
442
|
+
}));
|
|
443
|
+
}
|
|
444
|
+
return Object.freeze(result);
|
|
445
|
+
}
|
|
446
|
+
function jsonObject(value, path, message) {
|
|
447
|
+
if (!isRecord(value)) throw invalid(path, message);
|
|
448
|
+
return freezeJsonObject(value, path, 0);
|
|
449
|
+
}
|
|
450
|
+
function freezeJsonObject(value, path, depth) {
|
|
451
|
+
const result = {};
|
|
452
|
+
for (const [key, candidate] of Object.entries(value)) defineOwnProperty(result, key, jsonValue(candidate, jsonPropertyPath(path, key), depth + 1));
|
|
453
|
+
return Object.freeze(result);
|
|
454
|
+
}
|
|
455
|
+
function jsonValue(value, path, depth) {
|
|
456
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) return value;
|
|
457
|
+
if (depth > MAX_DEPTH) throw invalid(path, `is nested deeper than the ${String(MAX_DEPTH)} level limit`);
|
|
458
|
+
if (Array.isArray(value)) return Object.freeze(value.map((candidate, index) => jsonValue(candidate, `${path}[${index}]`, depth + 1)));
|
|
459
|
+
if (isRecord(value)) return freezeJsonObject(value, path, depth);
|
|
460
|
+
throw invalid(path, "must be a JSON value");
|
|
461
|
+
}
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region ../core/src/manifest/codec.ts
|
|
464
|
+
/** Creates the writable empty desired state used when no manifest exists yet. */
|
|
465
|
+
function createEmptyAuraManifest() {
|
|
466
|
+
return Object.freeze({
|
|
467
|
+
apps: Object.freeze({}),
|
|
468
|
+
mcpServers: Object.freeze([]),
|
|
469
|
+
ownership: Object.freeze({}),
|
|
470
|
+
schemaVersion: 1,
|
|
471
|
+
skills: Object.freeze([]),
|
|
472
|
+
snippets: Object.freeze([])
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Parses manifest JSON into a ready or read-only model without exposing source contents.
|
|
477
|
+
*
|
|
478
|
+
* `mode` is the file's observed permission bits, carried through so a caller can tell a manifest
|
|
479
|
+
* that only needs its mode repaired from one that needs nothing at all.
|
|
480
|
+
*/
|
|
481
|
+
function parseAuraManifest(text, path, mode) {
|
|
482
|
+
let value;
|
|
483
|
+
try {
|
|
484
|
+
value = JSON.parse(text);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
const location = parseLocation(error, text);
|
|
487
|
+
const message = `${path} is not valid JSON${location === void 0 ? "" : ` at line ${String(location.line)}, column ${String(location.column)}`}. Restore it from an Aura backup or move it aside before writing; Aura left it unchanged.`;
|
|
488
|
+
return readOnly(path, {
|
|
489
|
+
column: location?.column,
|
|
490
|
+
kind: "invalid-json",
|
|
491
|
+
line: location?.line,
|
|
492
|
+
message
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
return Object.freeze({
|
|
497
|
+
exists: true,
|
|
498
|
+
...mode === void 0 ? {} : { mode },
|
|
499
|
+
path,
|
|
500
|
+
status: "ready",
|
|
501
|
+
value: validateAuraManifest(value)
|
|
502
|
+
});
|
|
503
|
+
} catch (error) {
|
|
504
|
+
if (error instanceof UnsupportedAuraManifestVersionError) {
|
|
505
|
+
const newer = error.actualVersion > 1;
|
|
506
|
+
return readOnly(path, {
|
|
507
|
+
actualVersion: error.actualVersion,
|
|
508
|
+
kind: "unsupported-version",
|
|
509
|
+
message: newer ? `${path} uses manifest schema version ${String(error.actualVersion)}, but this Aura release supports version ${String(1)}. Upgrade Aura before making changes; checks will continue in read-only mode.` : `${path} uses unsupported manifest schema version ${String(error.actualVersion)}. Restore a version ${String(1)} backup or move the file aside before writing; Aura left it unchanged.`,
|
|
510
|
+
supportedVersion: 1
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
if (error instanceof AuraManifestValidationError) return readOnly(path, {
|
|
514
|
+
jsonPath: error.jsonPath,
|
|
515
|
+
kind: "invalid-schema",
|
|
516
|
+
message: `${path} has an invalid manifest value at ${error.jsonPath}: ${validationMessage(error)}. Restore it from an Aura backup or repair that value before writing; Aura left it unchanged.`
|
|
517
|
+
});
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/** Serializes one validated manifest using the canonical on-disk formatting. */
|
|
522
|
+
function serializeAuraManifest(manifest, path = "Aura manifest") {
|
|
523
|
+
try {
|
|
524
|
+
return `${JSON.stringify(validateAuraManifest(manifest), void 0, 2)}\n`;
|
|
525
|
+
} catch (error) {
|
|
526
|
+
if (error instanceof UnsupportedAuraManifestVersionError) throw new AuraManifestError(path, {
|
|
527
|
+
actualVersion: error.actualVersion,
|
|
528
|
+
kind: "unsupported-version",
|
|
529
|
+
message: `${path} uses unsupported manifest schema version ${String(error.actualVersion)} and cannot be written.`,
|
|
530
|
+
supportedVersion: 1
|
|
531
|
+
}, error);
|
|
532
|
+
if (error instanceof AuraManifestValidationError) throw new AuraManifestError(path, {
|
|
533
|
+
jsonPath: error.jsonPath,
|
|
534
|
+
kind: "invalid-schema",
|
|
535
|
+
message: `${path} has an invalid manifest value at ${error.jsonPath}: ${validationMessage(error)}.`
|
|
536
|
+
}, error);
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function readOnly(path, problem) {
|
|
541
|
+
return Object.freeze({
|
|
542
|
+
exists: true,
|
|
543
|
+
path,
|
|
544
|
+
problem,
|
|
545
|
+
status: "read-only"
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
function validationMessage(error) {
|
|
549
|
+
const prefix = `${error.jsonPath}: `;
|
|
550
|
+
return error.message.startsWith(prefix) ? error.message.slice(prefix.length) : error.message;
|
|
551
|
+
}
|
|
552
|
+
function parseLocation(error, text) {
|
|
553
|
+
if (!(error instanceof Error)) return;
|
|
554
|
+
const position = errorPosition(error.message);
|
|
555
|
+
return position === void 0 ? errorLineColumn(error.message) : locationAt(text, position);
|
|
556
|
+
}
|
|
557
|
+
function errorPosition(message) {
|
|
558
|
+
const captured = /position (?<position>[0-9]+)/u.exec(message)?.groups?.["position"];
|
|
559
|
+
const position = Number(captured);
|
|
560
|
+
return captured !== void 0 && Number.isInteger(position) && position >= 0 ? position : void 0;
|
|
561
|
+
}
|
|
562
|
+
function errorLineColumn(message) {
|
|
563
|
+
const match = /line (?<line>[0-9]+) column (?<column>[0-9]+)/u.exec(message);
|
|
564
|
+
const line = Number(match?.groups?.["line"]);
|
|
565
|
+
const column = Number(match?.groups?.["column"]);
|
|
566
|
+
return Number.isInteger(line) && Number.isInteger(column) ? {
|
|
567
|
+
column,
|
|
568
|
+
line
|
|
569
|
+
} : void 0;
|
|
570
|
+
}
|
|
571
|
+
function locationAt(text, position) {
|
|
572
|
+
const lines = text.slice(0, position).split("\n");
|
|
573
|
+
return {
|
|
574
|
+
column: (lines.at(-1)?.length ?? 0) + 1,
|
|
575
|
+
line: lines.length
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region ../core/src/manifest/write.ts
|
|
580
|
+
/** Refuses any write based on desired state Aura could not safely understand. */
|
|
581
|
+
function assertAuraManifestWritable(state) {
|
|
582
|
+
if (state.status === "read-only") throw new AuraManifestError(state.path, state.problem);
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Builds the fix-plan operation used to persist a complete manifest.
|
|
586
|
+
*
|
|
587
|
+
* `mode` covers creating the file. Holding an existing one at the same mode is not this function's
|
|
588
|
+
* to ask for: the kernel recognizes the manifest by path and enforces it there.
|
|
589
|
+
*/
|
|
590
|
+
function createAuraManifestWriteOperation(state, manifest) {
|
|
591
|
+
assertAuraManifestWritable(state);
|
|
592
|
+
return Object.freeze({
|
|
593
|
+
content: serializeAuraManifest(manifest, state.path),
|
|
594
|
+
mode: 384,
|
|
595
|
+
path: state.path,
|
|
596
|
+
type: "write"
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region ../core/src/fix-plan/limits.ts
|
|
601
|
+
/**
|
|
602
|
+
* The largest file Aura will write over, remove, or replace with a link.
|
|
603
|
+
*
|
|
604
|
+
* A mutation is only reversible if its previous contents were captured, so this doubles as the
|
|
605
|
+
* ceiling on what one operation retains in memory. Agent configuration is measured in kilobytes;
|
|
606
|
+
* anything approaching this size is not something a fix should be rewriting unattended.
|
|
607
|
+
*/
|
|
608
|
+
const MAX_MUTABLE_FILE_BYTES = 4194304;
|
|
609
|
+
/**
|
|
610
|
+
* The most file content Aura retains across a whole plan.
|
|
611
|
+
*
|
|
612
|
+
* {@link MAX_MUTABLE_FILE_BYTES} bounds one operation; without a plan-wide budget a long plan still
|
|
613
|
+
* multiplies it by the operation count.
|
|
614
|
+
*/
|
|
615
|
+
const MAX_RETAINED_PLAN_BYTES = 67108864;
|
|
616
|
+
/**
|
|
617
|
+
* The combined before/after size above which a preview summarizes instead of rendering a patch.
|
|
618
|
+
*
|
|
619
|
+
* A unified diff is larger than the inputs that produced it, and a preview no human can read is not
|
|
620
|
+
* worth the memory it costs.
|
|
621
|
+
*/
|
|
622
|
+
const MAX_DIFF_BYTES = 262144;
|
|
623
|
+
/**
|
|
624
|
+
* Every mode a plan may request.
|
|
625
|
+
*
|
|
626
|
+
* `FileMode` already closes this set, but a type is erased at runtime and plugins ship as compiled
|
|
627
|
+
* JavaScript. Since the mode reaches `chmod` unchanged, checking it here is what actually keeps a
|
|
628
|
+
* plan from asking for a world-writable or setuid file.
|
|
629
|
+
*/
|
|
630
|
+
const FILE_MODES = /* @__PURE__ */ new Set([
|
|
631
|
+
384,
|
|
632
|
+
420,
|
|
633
|
+
448,
|
|
634
|
+
493
|
|
635
|
+
]);
|
|
636
|
+
//#endregion
|
|
637
|
+
//#region ../core/src/fix-plan/diff.ts
|
|
638
|
+
const NULL_PATH = "/dev/null";
|
|
639
|
+
function renderWriteDiff(path, before, content, requestedMode, mode) {
|
|
640
|
+
const note = modeNote(before, requestedMode, mode);
|
|
641
|
+
const previous = textOf(before);
|
|
642
|
+
if (previous === void 0) return renderSummary("write", path, "Binary or oversized file would change.") + note;
|
|
643
|
+
if (exceedsDiffBudget(previous, content)) return renderSummary("write", path, "File is too large to diff; contents would change.") + note;
|
|
644
|
+
return renderPatch("write", path, before.kind === "missing" ? NULL_PATH : path, path, previous, content) + note;
|
|
645
|
+
}
|
|
646
|
+
/** Fail-closed write preview used when a semantic redactor cannot safely project both sides. */
|
|
647
|
+
function renderWriteSummary(path, before, requestedMode, mode) {
|
|
648
|
+
return renderSummary("write", path, "Sensitive MCP configuration would change.") + modeNote(before, requestedMode, mode);
|
|
649
|
+
}
|
|
650
|
+
function renderRemoveDiff(path, before) {
|
|
651
|
+
if (before.kind === "directory") return renderSummary("remove", path, "Remove empty directory.");
|
|
652
|
+
const previous = textOf(before);
|
|
653
|
+
if (previous === void 0) return renderSummary("remove", path, "Binary or oversized file would be removed.");
|
|
654
|
+
if (exceedsDiffBudget(previous, "")) return renderSummary("remove", path, "File is too large to diff; it would be removed.");
|
|
655
|
+
return renderPatch("remove", path, path, NULL_PATH, previous, "");
|
|
656
|
+
}
|
|
657
|
+
function renderMoveDiff(sourcePath, destinationPath) {
|
|
658
|
+
return [
|
|
659
|
+
`diff --aura move ${sourcePath} ${destinationPath}`,
|
|
660
|
+
`rename from ${sourcePath}`,
|
|
661
|
+
`rename to ${destinationPath}`,
|
|
662
|
+
""
|
|
663
|
+
].join("\n");
|
|
664
|
+
}
|
|
665
|
+
function renderArchiveDiff(path, relativePath, before, replacement, replacementMode) {
|
|
666
|
+
const archive = renderSummary("archive", path, `Preserve the original at <backup>/consolidation/${relativePath}.`);
|
|
667
|
+
if (replacement === void 0) return `${archive}${renderRemoveDiff(path, before)}`;
|
|
668
|
+
return replacement.type === "symlink" ? `${archive}${renderSymlinkDiff(path, before, replacement.target)}` : `${archive}${renderWriteDiff(path, before, replacement.content, replacement.mode, replacementMode ?? 420)}`;
|
|
669
|
+
}
|
|
670
|
+
function renderSymlinkDiff(path, before, target) {
|
|
671
|
+
const previous = textOf(before);
|
|
672
|
+
if (previous === void 0) return renderSummary("symlink", path, `Binary or oversized file would become a link.`);
|
|
673
|
+
if (exceedsDiffBudget(previous, target)) return renderSummary("symlink", path, "File is too large to diff; it would become a link.");
|
|
674
|
+
return `${renderPatch("symlink", path, before.kind === "missing" ? NULL_PATH : path, path, previous, `${target}\n`)}link target ${target}\n`;
|
|
675
|
+
}
|
|
676
|
+
/** Describes a conflict in the same shape as a diff, so a renderer can treat previews uniformly. */
|
|
677
|
+
function renderConflict(operation, path, reason) {
|
|
678
|
+
return renderSummary(operation, path, `Blocked: ${reason}.`);
|
|
679
|
+
}
|
|
680
|
+
function renderPatch(operation, displayPath, oldPath, newPath, oldContent, newContent) {
|
|
681
|
+
return `diff --aura ${operation} ${displayPath}\n${createTwoFilesPatch(oldPath, newPath, oldContent, newContent, "before", "after", { context: 3 })}`;
|
|
682
|
+
}
|
|
683
|
+
function renderSummary(operation, path, detail) {
|
|
684
|
+
return [
|
|
685
|
+
`diff --aura ${operation} ${path}`,
|
|
686
|
+
detail,
|
|
687
|
+
""
|
|
688
|
+
].join("\n");
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Says what happens to an existing file's permissions.
|
|
692
|
+
*
|
|
693
|
+
* Two cases are worth a line. A mode that changes is a change the diff itself cannot show, and core
|
|
694
|
+
* only does that to files it owns as protocol. A `mode` a plan asked for and will not get is the
|
|
695
|
+
* commoner one: an existing file keeps whatever the user set, and saying so is the difference
|
|
696
|
+
* between a deliberate choice and a silent one.
|
|
697
|
+
*/
|
|
698
|
+
function modeNote(before, requestedMode, mode) {
|
|
699
|
+
if (before.kind !== "file") return "";
|
|
700
|
+
if (before.mode !== mode) return `mode ${formatMode(before.mode)} changes to ${formatMode(mode)}\n`;
|
|
701
|
+
if (requestedMode !== void 0 && requestedMode !== before.mode) return `mode ${formatMode(requestedMode)} requested; existing mode ${formatMode(before.mode)} is preserved\n`;
|
|
702
|
+
return "";
|
|
703
|
+
}
|
|
704
|
+
function formatMode(mode) {
|
|
705
|
+
return `0o${mode.toString(8).padStart(3, "0")}`;
|
|
706
|
+
}
|
|
707
|
+
function exceedsDiffBudget(before, after) {
|
|
708
|
+
return Buffer.byteLength(before, "utf8") + Buffer.byteLength(after, "utf8") > MAX_DIFF_BYTES;
|
|
709
|
+
}
|
|
710
|
+
function textOf(state) {
|
|
711
|
+
switch (state.kind) {
|
|
712
|
+
case "directory":
|
|
713
|
+
case "unsupported": return;
|
|
714
|
+
case "file": return state.content !== void 0 && isUtf8(state.content) ? state.content.toString("utf8") : void 0;
|
|
715
|
+
case "missing": return "";
|
|
716
|
+
case "symlink": return `${state.target}\n`;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
//#endregion
|
|
720
|
+
//#region ../core/src/fix-plan/write-redaction.ts
|
|
721
|
+
const REDACTORS = /* @__PURE__ */ new WeakMap();
|
|
722
|
+
/**
|
|
723
|
+
* Paths some operation has asked to have redacted, kept beside the identity-keyed registry above.
|
|
724
|
+
*
|
|
725
|
+
* The registry is keyed on the operation object, so anything that copies an operation between
|
|
726
|
+
* planning and preview silently drops its masker — and the symptom of that would be a credential
|
|
727
|
+
* rendered into a diff rather than an error. Remembering the path as well turns the silent case
|
|
728
|
+
* into the conservative one: a write to a path that has ever needed masking and arrives without a
|
|
729
|
+
* masker gets a summary, not a patch.
|
|
730
|
+
*/
|
|
731
|
+
const REDACTED_PATHS = /* @__PURE__ */ new Set();
|
|
732
|
+
/** Associates a semantic content masker with a write without changing the public operation schema. */
|
|
733
|
+
function rememberWriteRedactor(operation, redactor) {
|
|
734
|
+
const existing = REDACTORS.get(operation) ?? [];
|
|
735
|
+
REDACTORS.set(operation, [...existing, redactor]);
|
|
736
|
+
REDACTED_PATHS.add(operation.path);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Wraps an adapter's transform so a projection that could not account for every field fails closed.
|
|
740
|
+
*
|
|
741
|
+
* `unresolved` names the fields whose server entry this content does not contain. That is not the
|
|
742
|
+
* same as "nothing to mask": a rewritten side legitimately has nothing left to mask and reports no
|
|
743
|
+
* unresolved fields, while a document shaped in a way the adapter cannot navigate reports them all
|
|
744
|
+
* and still holds the credential.
|
|
745
|
+
*/
|
|
746
|
+
function mcpSecretRedactor(transform, sightings) {
|
|
747
|
+
return (content) => {
|
|
748
|
+
const redaction = transform.redact({
|
|
749
|
+
content,
|
|
750
|
+
sightings
|
|
751
|
+
});
|
|
752
|
+
return redaction === void 0 || redaction.unresolved.length > 0 ? void 0 : redaction.content;
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
/** Renders a write only after every registered semantic masker succeeds on both diff sides. */
|
|
756
|
+
function renderRedactedWriteDiff(operations, path, before, content, requestedMode, mode) {
|
|
757
|
+
const redactors = operations.flatMap((operation) => REDACTORS.get(operation) ?? []);
|
|
758
|
+
if (redactors.length === 0) return REDACTED_PATHS.has(path) ? renderWriteSummary(path, before, requestedMode, mode) : renderWriteDiff(path, before, content, requestedMode, mode);
|
|
759
|
+
const next = redactAll(redactors, content);
|
|
760
|
+
if (next === void 0) return renderWriteSummary(path, before, requestedMode, mode);
|
|
761
|
+
if (before.kind !== "file" || before.content === void 0) return renderWriteDiff(path, before, next, requestedMode, mode);
|
|
762
|
+
const previous = redactAll(redactors, before.content.toString("utf8"));
|
|
763
|
+
if (previous === void 0) return renderWriteSummary(path, before, requestedMode, mode);
|
|
764
|
+
return renderWriteDiff(path, {
|
|
765
|
+
...before,
|
|
766
|
+
content: Buffer.from(previous, "utf8")
|
|
767
|
+
}, next, requestedMode, mode);
|
|
768
|
+
}
|
|
769
|
+
function redactAll(redactors, content) {
|
|
770
|
+
let projected = content;
|
|
771
|
+
for (const redact of redactors) {
|
|
772
|
+
const result = safeRedact(redact, projected);
|
|
773
|
+
if (result === void 0) return;
|
|
774
|
+
projected = result;
|
|
775
|
+
}
|
|
776
|
+
return projected;
|
|
777
|
+
}
|
|
778
|
+
function safeRedact(redact, content) {
|
|
779
|
+
try {
|
|
780
|
+
return redact(content);
|
|
781
|
+
} catch {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
//#endregion
|
|
786
|
+
//#region ../core/src/managed-block/protocol.ts
|
|
787
|
+
/** Distribution-independent outer marker opening Aura-managed content. */
|
|
788
|
+
const AURA_MANAGED_BLOCK_BEGIN = "<!-- aura:begin -->";
|
|
789
|
+
/** Distribution-independent outer marker closing Aura-managed content. */
|
|
790
|
+
const AURA_MANAGED_BLOCK_END = "<!-- aura:end -->";
|
|
791
|
+
/** Prefix shared by every Aura snippet opening marker. */
|
|
792
|
+
const AURA_MANAGED_SNIPPET_BEGIN_PREFIX = "<!-- aura:begin id=";
|
|
793
|
+
/** Prefix shared by every Aura snippet closing marker. */
|
|
794
|
+
const AURA_MANAGED_SNIPPET_END_PREFIX = "<!-- aura:end id=";
|
|
795
|
+
/** Human-facing ownership warning rendered once inside the outer block. */
|
|
796
|
+
const AURA_MANAGED_BLOCK_NOTICE = "Managed by Aura. Edit via the Aura CLI; manual edits to this block are overwritten.";
|
|
797
|
+
const MANAGED_SNIPPET_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
798
|
+
const MANAGED_SNIPPET_HASH_PATTERN = /^[0-9a-f]{64}$/;
|
|
799
|
+
/** Whether an id is both line-safe and legal inside an HTML comment. */
|
|
800
|
+
function isManagedSnippetId(id) {
|
|
801
|
+
return MANAGED_SNIPPET_ID_PATTERN.test(id) && !id.includes("--");
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Normalizes snippet text to the bytes covered by the managed-block hash protocol.
|
|
805
|
+
*
|
|
806
|
+
* Trailing newlines are trimmed by scanning rather than with `/\n+$/`, whose backtracking is
|
|
807
|
+
* quadratic when a long newline run does not reach the end of the string.
|
|
808
|
+
*/
|
|
809
|
+
function canonicalizeManagedSnippet(content) {
|
|
810
|
+
const normalized = content.replace(/\r\n?/g, "\n");
|
|
811
|
+
let end = normalized.length;
|
|
812
|
+
while (end > 0 && normalized[end - 1] === "\n") end -= 1;
|
|
813
|
+
return `${normalized.slice(0, end)}\n`;
|
|
814
|
+
}
|
|
815
|
+
/** Computes the protocol hash for text already in {@link canonicalizeManagedSnippet} form. */
|
|
816
|
+
function hashCanonicalManagedSnippet(canonical) {
|
|
817
|
+
return createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
818
|
+
}
|
|
819
|
+
/** Computes the protocol hash for a snippet's canonical UTF-8 contents. */
|
|
820
|
+
function hashManagedSnippet(content) {
|
|
821
|
+
return hashCanonicalManagedSnippet(canonicalizeManagedSnippet(content));
|
|
822
|
+
}
|
|
823
|
+
//#endregion
|
|
824
|
+
//#region ../core/src/managed-block/parse-state.ts
|
|
825
|
+
function collectMarkerValidation(marker, line, problems) {
|
|
826
|
+
if (!isManagedSnippetId(marker.id)) addProblem(problems, "invalid-snippet-id", line.number, `Snippet ID "${marker.id}" is not safe inside an HTML comment marker.`);
|
|
827
|
+
if (marker.kind === "snippet-begin" && !MANAGED_SNIPPET_HASH_PATTERN.test(marker.hash)) addProblem(problems, "invalid-hash", line.number, `Snippet "${marker.id}" must declare a lowercase 64-character SHA-256 hash.`);
|
|
828
|
+
}
|
|
829
|
+
function closeSnippet(snippet, end, source) {
|
|
830
|
+
const content = source.slice(snippet.contentStart, end.start);
|
|
831
|
+
const computedHash = hashManagedSnippet(content);
|
|
832
|
+
return Object.freeze({
|
|
833
|
+
computedHash,
|
|
834
|
+
content,
|
|
835
|
+
contentEndOffset: end.start,
|
|
836
|
+
contentStartOffset: snippet.contentStart,
|
|
837
|
+
endLine: end.number,
|
|
838
|
+
endOffset: end.end,
|
|
839
|
+
hashMatches: computedHash === snippet.storedHash,
|
|
840
|
+
id: snippet.id,
|
|
841
|
+
startLine: snippet.startLine,
|
|
842
|
+
startOffset: snippet.startOffset,
|
|
843
|
+
storedHash: snippet.storedHash
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
function closeBlock(block, end) {
|
|
847
|
+
return Object.freeze({
|
|
848
|
+
endLine: end.number,
|
|
849
|
+
endOffset: end.end,
|
|
850
|
+
snippets: Object.freeze(block.snippets),
|
|
851
|
+
startLine: block.start.number,
|
|
852
|
+
startOffset: block.start.start,
|
|
853
|
+
unmanagedContent: block.unmanaged.join("")
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
function addProblem(problems, code, line, message) {
|
|
857
|
+
problems.push(Object.freeze({
|
|
858
|
+
code,
|
|
859
|
+
line,
|
|
860
|
+
message
|
|
861
|
+
}));
|
|
862
|
+
}
|
|
863
|
+
//#endregion
|
|
864
|
+
//#region ../core/src/managed-block/marker-handlers.ts
|
|
865
|
+
function handleMarkerOutsideBlock(marker, line, blocks, problems) {
|
|
866
|
+
if (marker.kind === "block-begin") {
|
|
867
|
+
if (blocks.length > 0) addProblem(problems, "duplicate-block", line.number, "Aura files may contain only one managed block.");
|
|
868
|
+
return {
|
|
869
|
+
acceptsNotice: true,
|
|
870
|
+
snippets: [],
|
|
871
|
+
snippetIds: /* @__PURE__ */ new Set(),
|
|
872
|
+
start: line,
|
|
873
|
+
unmanaged: []
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
if (marker.kind === "block-end") addProblem(problems, "orphan-outer-end", line.number, "Found an Aura outer end marker without a matching begin marker.");
|
|
877
|
+
else if (marker.kind === "malformed") addProblem(problems, "malformed-marker", line.number, "Found a malformed Aura marker.");
|
|
878
|
+
else {
|
|
879
|
+
collectMarkerValidation(marker, line, problems);
|
|
880
|
+
addProblem(problems, "orphan-snippet-marker", line.number, "Found an Aura snippet marker outside a managed block.");
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
function handleMarkerInsideBlock(marker, line, block, problems) {
|
|
884
|
+
if (marker.kind === "block-end") return { closed: closeBlock(block, line) };
|
|
885
|
+
if (marker.kind === "snippet-begin") {
|
|
886
|
+
collectMarkerValidation(marker, line, problems);
|
|
887
|
+
if (block.snippetIds.has(marker.id)) addProblem(problems, "duplicate-snippet", line.number, `Snippet ID "${marker.id}" appears more than once in the managed block.`);
|
|
888
|
+
block.snippetIds.add(marker.id);
|
|
889
|
+
return { snippet: {
|
|
890
|
+
contentStart: line.end,
|
|
891
|
+
id: marker.id,
|
|
892
|
+
startLine: line.number,
|
|
893
|
+
startOffset: line.start,
|
|
894
|
+
storedHash: marker.hash
|
|
895
|
+
} };
|
|
896
|
+
} else if (marker.kind === "snippet-end") {
|
|
897
|
+
collectMarkerValidation(marker, line, problems);
|
|
898
|
+
addProblem(problems, "orphan-snippet-marker", line.number, "Found a snippet end marker without a matching begin marker.");
|
|
899
|
+
} else addProblem(problems, "malformed-marker", line.number, "Found a malformed Aura marker.");
|
|
900
|
+
return {};
|
|
901
|
+
}
|
|
902
|
+
function handleMarkerInsideSnippet(marker, line, block, snippet, source, problems) {
|
|
903
|
+
if (marker.kind === "snippet-end") {
|
|
904
|
+
collectMarkerValidation(marker, line, problems);
|
|
905
|
+
if (marker.id !== snippet.id) addProblem(problems, "mismatched-snippet-end", line.number, `Snippet "${snippet.id}" ends with marker for "${marker.id}".`);
|
|
906
|
+
block.snippets.push(closeSnippet(snippet, line, source));
|
|
907
|
+
return {};
|
|
908
|
+
}
|
|
909
|
+
if (marker.kind === "block-end") {
|
|
910
|
+
addProblem(problems, "outer-end-before-snippet-end", line.number, `Outer block ends before snippet "${snippet.id}" closes.`);
|
|
911
|
+
return { closedBlock: closeBlock(block, line) };
|
|
912
|
+
}
|
|
913
|
+
if (marker.kind === "snippet-begin") {
|
|
914
|
+
collectMarkerValidation(marker, line, problems);
|
|
915
|
+
addProblem(problems, "nested-snippet", line.number, `Snippet "${marker.id}" begins before snippet "${snippet.id}" ends.`);
|
|
916
|
+
} else addProblem(problems, "malformed-marker", line.number, "Found a malformed Aura marker.");
|
|
917
|
+
return { snippet };
|
|
918
|
+
}
|
|
919
|
+
//#endregion
|
|
920
|
+
//#region ../core/src/managed-block/markers.ts
|
|
921
|
+
/** Parses exact protocol lines; callers suppress this inside Markdown fences. */
|
|
922
|
+
function parseMarker(line) {
|
|
923
|
+
if (line === "<!-- aura:begin -->") return { kind: "block-begin" };
|
|
924
|
+
if (line === "<!-- aura:end -->") return { kind: "block-end" };
|
|
925
|
+
if (line.startsWith("<!-- aura:begin id=")) {
|
|
926
|
+
const match = /^<!-- aura:begin id=(\S+) sha256=(\S+) -->$/.exec(line);
|
|
927
|
+
return match === null || match[1] === void 0 || match[2] === void 0 ? { kind: "malformed" } : {
|
|
928
|
+
hash: match[2],
|
|
929
|
+
id: match[1],
|
|
930
|
+
kind: "snippet-begin"
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
if (line.startsWith("<!-- aura:end id=")) {
|
|
934
|
+
const match = /^<!-- aura:end id=(\S+) -->$/.exec(line);
|
|
935
|
+
return match === null || match[1] === void 0 ? { kind: "malformed" } : {
|
|
936
|
+
id: match[1],
|
|
937
|
+
kind: "snippet-end"
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
if (line.startsWith("<!-- aura:begin") || line.startsWith("<!-- aura:end")) return { kind: "malformed" };
|
|
941
|
+
}
|
|
942
|
+
//#endregion
|
|
943
|
+
//#region ../core/src/managed-block/read.ts
|
|
944
|
+
/** Parses the single Aura-managed block in a source string without touching the filesystem. */
|
|
945
|
+
function readManagedBlock(source) {
|
|
946
|
+
const state = {
|
|
947
|
+
block: void 0,
|
|
948
|
+
blocks: [],
|
|
949
|
+
fence: void 0,
|
|
950
|
+
notes: [],
|
|
951
|
+
problems: [],
|
|
952
|
+
snippet: void 0,
|
|
953
|
+
suppressedMarkerLine: void 0
|
|
954
|
+
};
|
|
955
|
+
for (const line of splitSourceLines$1(source)) processSourceLine(state, line, source);
|
|
956
|
+
return finishRead(state);
|
|
957
|
+
}
|
|
958
|
+
function processSourceLine(state, line, source) {
|
|
959
|
+
const protectedByFence = advanceFenceState(state, line);
|
|
960
|
+
const marker = protectedByFence ? void 0 : parseMarker(line.text);
|
|
961
|
+
if (marker === void 0) {
|
|
962
|
+
collectPlainLine(source, line, protectedByFence, state.block, state.snippet, state.notes);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
if (state.block === void 0) {
|
|
966
|
+
state.block = handleMarkerOutsideBlock(marker, line, state.blocks, state.problems);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
applyMarkerInsideBlock(state, state.block, marker, line, source);
|
|
970
|
+
}
|
|
971
|
+
/** Advances fence state, remembering the first marker the currently open fence is hiding. */
|
|
972
|
+
function advanceFenceState(state, line) {
|
|
973
|
+
const previousFence = state.fence;
|
|
974
|
+
state.fence = advanceMarkdownFence(line.text, state.fence);
|
|
975
|
+
if (state.fence === void 0) {
|
|
976
|
+
state.suppressedMarkerLine = void 0;
|
|
977
|
+
return previousFence !== void 0;
|
|
978
|
+
}
|
|
979
|
+
if (state.suppressedMarkerLine === void 0 && line.text.startsWith("<!-- aura:")) state.suppressedMarkerLine = line.number;
|
|
980
|
+
return true;
|
|
981
|
+
}
|
|
982
|
+
function applyMarkerInsideBlock(state, block, marker, line, source) {
|
|
983
|
+
block.acceptsNotice = false;
|
|
984
|
+
if (marker.kind === "block-begin") {
|
|
985
|
+
addProblem(state.problems, "nested-block", line.number, "Aura managed blocks cannot be nested.");
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (state.snippet === void 0) {
|
|
989
|
+
const outcome = handleMarkerInsideBlock(marker, line, block, state.problems);
|
|
990
|
+
closeBlockIfEnded(state, outcome.closed);
|
|
991
|
+
state.snippet = outcome.snippet;
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const outcome = handleMarkerInsideSnippet(marker, line, block, state.snippet, source, state.problems);
|
|
995
|
+
closeBlockIfEnded(state, outcome.closedBlock);
|
|
996
|
+
state.snippet = outcome.snippet;
|
|
997
|
+
}
|
|
998
|
+
function closeBlockIfEnded(state, closed) {
|
|
999
|
+
if (closed !== void 0) {
|
|
1000
|
+
state.blocks.push(closed);
|
|
1001
|
+
state.block = void 0;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
function finishRead(state) {
|
|
1005
|
+
if (state.fence !== void 0 && state.suppressedMarkerLine !== void 0) state.notes.push(Object.freeze({
|
|
1006
|
+
code: "unterminated-fence",
|
|
1007
|
+
line: state.suppressedMarkerLine,
|
|
1008
|
+
message: "An unclosed Markdown fence made this Aura marker ordinary text. Close the fence if the marker was meant to be read."
|
|
1009
|
+
}));
|
|
1010
|
+
if (state.snippet !== void 0) addProblem(state.problems, "incomplete-snippet", state.snippet.startLine, `Snippet "${state.snippet.id}" has no closing marker.`);
|
|
1011
|
+
if (state.block !== void 0) addProblem(state.problems, "incomplete-outer-block", state.block.start.number, "Aura managed block has no closing marker.");
|
|
1012
|
+
const frozenNotes = Object.freeze(state.notes);
|
|
1013
|
+
if (state.problems.length > 0) return {
|
|
1014
|
+
notes: frozenNotes,
|
|
1015
|
+
problems: Object.freeze(state.problems),
|
|
1016
|
+
status: "invalid"
|
|
1017
|
+
};
|
|
1018
|
+
const managedBlock = state.blocks[0];
|
|
1019
|
+
return managedBlock === void 0 ? {
|
|
1020
|
+
notes: frozenNotes,
|
|
1021
|
+
status: "absent"
|
|
1022
|
+
} : {
|
|
1023
|
+
block: managedBlock,
|
|
1024
|
+
notes: frozenNotes,
|
|
1025
|
+
status: "present"
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
function collectPlainLine(source, line, protectedByFence, block, snippet, notes) {
|
|
1029
|
+
if (block === void 0 || snippet !== void 0) return;
|
|
1030
|
+
if (!protectedByFence && block.acceptsNotice && line.text === "Managed by Aura. Edit via the Aura CLI; manual edits to this block are overwritten.") {
|
|
1031
|
+
block.acceptsNotice = false;
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
block.acceptsNotice = false;
|
|
1035
|
+
block.unmanaged.push(source.slice(line.start, line.end));
|
|
1036
|
+
notes.push(Object.freeze({
|
|
1037
|
+
code: "unmanaged-content",
|
|
1038
|
+
line: line.number,
|
|
1039
|
+
message: "Content outside a tagged snippet will be moved below the Aura block."
|
|
1040
|
+
}));
|
|
1041
|
+
}
|
|
1042
|
+
//#endregion
|
|
1043
|
+
//#region ../core/src/managed-block/scan.ts
|
|
1044
|
+
/**
|
|
1045
|
+
* Finds protocol markers the reader would honour, applying the same fence rules as
|
|
1046
|
+
* {@link readManagedBlock} so fenced marker examples stay ordinary text.
|
|
1047
|
+
*/
|
|
1048
|
+
function scanForMarkers(text) {
|
|
1049
|
+
let fence;
|
|
1050
|
+
let markerLine;
|
|
1051
|
+
for (const line of splitSourceLines$1(text)) {
|
|
1052
|
+
const previousFence = fence;
|
|
1053
|
+
fence = advanceMarkdownFence(line.text, fence);
|
|
1054
|
+
if (previousFence !== void 0 || fence !== void 0) continue;
|
|
1055
|
+
if (markerLine === void 0 && parseMarker(line.text) !== void 0) markerLine = line.number;
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
markerLine,
|
|
1059
|
+
unterminatedFence: fence !== void 0
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Rejects content that would escape its own snippet. Written verbatim, a marker inside a snippet
|
|
1064
|
+
* body reopens or closes the protocol, and the resulting file parses as invalid forever — so the
|
|
1065
|
+
* damage has to be caught before the write, not after.
|
|
1066
|
+
*
|
|
1067
|
+
* Every write path shares this guard. A narrower path that skipped it would be a way to write the
|
|
1068
|
+
* exact bytes the wider one refuses, and the file it corrupts is the same file either way.
|
|
1069
|
+
*/
|
|
1070
|
+
function managedSnippetContentProblems(id, canonical) {
|
|
1071
|
+
const scan = scanForMarkers(canonical);
|
|
1072
|
+
if (scan.markerLine !== void 0) return Object.freeze([Object.freeze({
|
|
1073
|
+
code: "invalid-snippet-content",
|
|
1074
|
+
message: `Snippet "${id}" declares an Aura marker on content line ${String(scan.markerLine)}. Wrap marker examples in a Markdown fence.`
|
|
1075
|
+
})]);
|
|
1076
|
+
if (scan.unterminatedFence) return Object.freeze([Object.freeze({
|
|
1077
|
+
code: "invalid-snippet-content",
|
|
1078
|
+
message: `Snippet "${id}" ends inside an unclosed Markdown fence, which would hide every marker after it. Close the fence.`
|
|
1079
|
+
})]);
|
|
1080
|
+
return Object.freeze([]);
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Drops every protocol line the reader honours, plus the notice that follows an opening marker,
|
|
1084
|
+
* leaving handwritten text byte-for-byte. Fenced marker examples survive because the reader never
|
|
1085
|
+
* treated them as protocol in the first place.
|
|
1086
|
+
*/
|
|
1087
|
+
function stripManagedMarkers(source) {
|
|
1088
|
+
const kept = [];
|
|
1089
|
+
let fence;
|
|
1090
|
+
let afterBlockBegin = false;
|
|
1091
|
+
for (const line of splitSourceLines$1(source)) {
|
|
1092
|
+
const previousFence = fence;
|
|
1093
|
+
fence = advanceMarkdownFence(line.text, fence);
|
|
1094
|
+
const marker = previousFence !== void 0 || fence !== void 0 ? void 0 : parseMarker(line.text);
|
|
1095
|
+
if (marker !== void 0) {
|
|
1096
|
+
afterBlockBegin = marker.kind === "block-begin";
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
if (afterBlockBegin && line.text === "Managed by Aura. Edit via the Aura CLI; manual edits to this block are overwritten.") {
|
|
1100
|
+
afterBlockBegin = false;
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
afterBlockBegin = false;
|
|
1104
|
+
kept.push(source.slice(line.start, line.end));
|
|
1105
|
+
}
|
|
1106
|
+
return kept.join("");
|
|
1107
|
+
}
|
|
1108
|
+
//#endregion
|
|
1109
|
+
//#region ../core/src/managed-block/reconcile-desired.ts
|
|
1110
|
+
function prepareDesiredSnippets(snippets, options) {
|
|
1111
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1112
|
+
const preserved = new Set(options.preserveSnippetIds ?? []);
|
|
1113
|
+
const prepared = [];
|
|
1114
|
+
const problems = [];
|
|
1115
|
+
for (const snippet of snippets) {
|
|
1116
|
+
if (!isManagedSnippetId(snippet.id)) problems.push(Object.freeze({
|
|
1117
|
+
code: "invalid-snippet-id",
|
|
1118
|
+
message: `Snippet ID "${snippet.id}" is not safe inside an HTML comment marker.`
|
|
1119
|
+
}));
|
|
1120
|
+
if (ids.has(snippet.id)) problems.push(Object.freeze({
|
|
1121
|
+
code: "duplicate-snippet",
|
|
1122
|
+
message: `Desired snippet ID "${snippet.id}" appears more than once.`
|
|
1123
|
+
}));
|
|
1124
|
+
if (preserved.has(snippet.id)) problems.push(Object.freeze({
|
|
1125
|
+
code: "duplicate-snippet",
|
|
1126
|
+
message: `Snippet ID "${snippet.id}" is both desired and preserved; writing both would duplicate it.`
|
|
1127
|
+
}));
|
|
1128
|
+
ids.add(snippet.id);
|
|
1129
|
+
const canonical = canonicalizeManagedSnippet(snippet.content);
|
|
1130
|
+
problems.push(...managedSnippetContentProblems(snippet.id, canonical));
|
|
1131
|
+
prepared.push({
|
|
1132
|
+
canonical,
|
|
1133
|
+
hash: hashCanonicalManagedSnippet(canonical),
|
|
1134
|
+
id: snippet.id,
|
|
1135
|
+
kind: "desired"
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
return {
|
|
1139
|
+
prepared,
|
|
1140
|
+
problems: Object.freeze(problems)
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
//#endregion
|
|
1144
|
+
//#region ../core/src/managed-block/reconcile-ledger.ts
|
|
1145
|
+
function renderLedgerSnippets(source, block, desired, options) {
|
|
1146
|
+
if (block === void 0 || options.ownedSnippetIds === void 0) return desired;
|
|
1147
|
+
const controlled = controlledSnippetIds(desired, options);
|
|
1148
|
+
const preserved = new Set(options.preserveSnippetIds ?? []);
|
|
1149
|
+
const remaining = [...desired];
|
|
1150
|
+
const rendered = [];
|
|
1151
|
+
for (const snippet of block.snippets) {
|
|
1152
|
+
if (preserved.has(snippet.id) || !controlled.has(snippet.id)) {
|
|
1153
|
+
rendered.push({
|
|
1154
|
+
id: snippet.id,
|
|
1155
|
+
kind: "preserved",
|
|
1156
|
+
raw: source.slice(snippet.startOffset, snippet.endOffset)
|
|
1157
|
+
});
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
const replacement = remaining.shift();
|
|
1161
|
+
if (replacement !== void 0) rendered.push(replacement);
|
|
1162
|
+
}
|
|
1163
|
+
rendered.push(...remaining);
|
|
1164
|
+
return rendered;
|
|
1165
|
+
}
|
|
1166
|
+
function controlledSnippetIds(desired, options) {
|
|
1167
|
+
return /* @__PURE__ */ new Set([...options.ownedSnippetIds ?? [], ...desired.map((snippet) => snippet.id)]);
|
|
1168
|
+
}
|
|
1169
|
+
function preservedUnownedNotes(block, desired, options) {
|
|
1170
|
+
if (block === void 0 || options.ownedSnippetIds === void 0) return [];
|
|
1171
|
+
const controlled = controlledSnippetIds(desired, options);
|
|
1172
|
+
return block.snippets.filter((snippet) => !controlled.has(snippet.id)).map((snippet) => Object.freeze({
|
|
1173
|
+
code: "preserved-unowned-snippet",
|
|
1174
|
+
line: snippet.startLine,
|
|
1175
|
+
message: `Snippet "${snippet.id}" is not recorded in Aura's manifest; its section was preserved.`
|
|
1176
|
+
}));
|
|
1177
|
+
}
|
|
1178
|
+
//#endregion
|
|
1179
|
+
//#region ../core/src/managed-block/reconcile.ts
|
|
1180
|
+
/**
|
|
1181
|
+
* Reconciles one source string against the complete ordered set of desired snippets.
|
|
1182
|
+
*
|
|
1183
|
+
* Existing hash mismatches remain observable through {@link readManagedBlock}, but reconciliation
|
|
1184
|
+
* deliberately replaces managed content with the desired canonical version, reporting each
|
|
1185
|
+
* discarded hand edit as an `overwritten-snippet` note. Invalid structures fail closed and return
|
|
1186
|
+
* the original source unchanged unless the caller opts into `onInvalid: "repair"`.
|
|
1187
|
+
*/
|
|
1188
|
+
function reconcileManagedBlock(source, desiredSnippets, options = {}) {
|
|
1189
|
+
return reconcileParsedManagedBlock(source, readManagedBlock(source), desiredSnippets, options);
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* {@link reconcileManagedBlock}, for a caller that already parsed `source`.
|
|
1193
|
+
*
|
|
1194
|
+
* `current` must be `readManagedBlock(source)` for the same string; passing a result read from
|
|
1195
|
+
* anything else splices content at offsets that no longer describe the source.
|
|
1196
|
+
*/
|
|
1197
|
+
function reconcileParsedManagedBlock(source, current, desiredSnippets, options = {}) {
|
|
1198
|
+
const desired = prepareDesiredSnippets(desiredSnippets, options);
|
|
1199
|
+
if (desired.problems.length > 0) return invalidResult(source, current.notes, desired.problems);
|
|
1200
|
+
const hiddenMarker = current.notes.find((note) => note.code === "unterminated-fence");
|
|
1201
|
+
if (hiddenMarker !== void 0) return invalidResult(source, current.notes, [Object.freeze({
|
|
1202
|
+
code: "unterminated-fence",
|
|
1203
|
+
line: hiddenMarker.line,
|
|
1204
|
+
message: hiddenMarker.message
|
|
1205
|
+
})]);
|
|
1206
|
+
if (current.status !== "invalid") {
|
|
1207
|
+
const block = current.status === "present" ? current.block : void 0;
|
|
1208
|
+
const rendered = renderLedgerSnippets(source, block, desired.prepared, options);
|
|
1209
|
+
const notes = [
|
|
1210
|
+
...current.notes,
|
|
1211
|
+
...overwrittenNotes(current, desired.prepared, options),
|
|
1212
|
+
...preservedUnownedNotes(block, desired.prepared, options)
|
|
1213
|
+
];
|
|
1214
|
+
return settle(source, buildContent(source, rendered, current), notes);
|
|
1215
|
+
}
|
|
1216
|
+
if (options.onInvalid !== "repair") return invalidResult(source, current.notes, current.problems);
|
|
1217
|
+
if (options.ownedSnippetIds !== void 0) return invalidResult(source, current.notes, current.problems);
|
|
1218
|
+
const stripped = stripManagedMarkers(source);
|
|
1219
|
+
const repaired = readManagedBlock(stripped);
|
|
1220
|
+
if (repaired.status === "invalid") return invalidResult(source, current.notes, current.problems);
|
|
1221
|
+
const notes = [...current.notes, ...repairNotes(current.problems)];
|
|
1222
|
+
return settle(source, buildContent(stripped, desired.prepared, repaired), notes);
|
|
1223
|
+
}
|
|
1224
|
+
function buildContent(source, prepared, current) {
|
|
1225
|
+
if (prepared.length === 0) return current.status === "absent" ? source : source.slice(0, current.block.startOffset) + current.block.unmanagedContent + source.slice(current.block.endOffset);
|
|
1226
|
+
const lineEnding = detectLineEnding$1(source);
|
|
1227
|
+
if (current.status === "absent") return `${source}${source.length === 0 || source.endsWith("\n") ? "" : lineEnding}${renderManagedBlock(prepared, lineEnding, true)}`;
|
|
1228
|
+
const replacement = renderManagedBlock(prepared, lineEnding, source[current.block.endOffset - 1] === "\n" || current.block.unmanagedContent.length > 0);
|
|
1229
|
+
if (current.block.unmanagedContent.length === 0 && replacement === source.slice(current.block.startOffset, current.block.endOffset)) return source;
|
|
1230
|
+
return source.slice(0, current.block.startOffset) + replacement + current.block.unmanagedContent + source.slice(current.block.endOffset);
|
|
1231
|
+
}
|
|
1232
|
+
function renderManagedBlock(snippets, lineEnding, endsWithLineEnding) {
|
|
1233
|
+
const parts = [
|
|
1234
|
+
AURA_MANAGED_BLOCK_BEGIN,
|
|
1235
|
+
lineEnding,
|
|
1236
|
+
AURA_MANAGED_BLOCK_NOTICE,
|
|
1237
|
+
lineEnding
|
|
1238
|
+
];
|
|
1239
|
+
for (const snippet of snippets) {
|
|
1240
|
+
if (snippet.kind === "preserved") {
|
|
1241
|
+
parts.push(snippet.raw);
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
const content = lineEnding === "\n" ? snippet.canonical : snippet.canonical.replaceAll("\n", lineEnding);
|
|
1245
|
+
parts.push(`${AURA_MANAGED_SNIPPET_BEGIN_PREFIX}${snippet.id} sha256=${snippet.hash} -->`, lineEnding, content, `${AURA_MANAGED_SNIPPET_END_PREFIX}${snippet.id} -->`, lineEnding);
|
|
1246
|
+
}
|
|
1247
|
+
parts.push(AURA_MANAGED_BLOCK_END);
|
|
1248
|
+
if (endsWithLineEnding) parts.push(lineEnding);
|
|
1249
|
+
return parts.join("");
|
|
1250
|
+
}
|
|
1251
|
+
function overwrittenNotes(current, desired, options) {
|
|
1252
|
+
if (current.status === "absent") return [];
|
|
1253
|
+
const controlled = controlledSnippetIds(desired, options);
|
|
1254
|
+
const desiredById = new Map(desired.map((snippet) => [snippet.id, snippet]));
|
|
1255
|
+
const preserved = new Set(options.preserveSnippetIds ?? []);
|
|
1256
|
+
return current.block.snippets.flatMap((snippet) => {
|
|
1257
|
+
if (options.ownedSnippetIds !== void 0 && !controlled.has(snippet.id) || preserved.has(snippet.id) || !handEdited(snippet, options)) return [];
|
|
1258
|
+
const replacement = desiredById.get(snippet.id);
|
|
1259
|
+
if (replacement === void 0) return [Object.freeze({
|
|
1260
|
+
code: "removed-snippet",
|
|
1261
|
+
line: snippet.startLine,
|
|
1262
|
+
message: `Snippet "${snippet.id}" was edited by hand since Aura wrote it; the edit is being removed.`
|
|
1263
|
+
})];
|
|
1264
|
+
if (replacement.hash === snippet.computedHash) return [];
|
|
1265
|
+
return [Object.freeze({
|
|
1266
|
+
code: "overwritten-snippet",
|
|
1267
|
+
line: snippet.startLine,
|
|
1268
|
+
message: `Snippet "${snippet.id}" was edited by hand since Aura wrote it; the edit is being replaced.`
|
|
1269
|
+
})];
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Whether a section differs from what Aura last recorded for it.
|
|
1274
|
+
*
|
|
1275
|
+
* The marker hash is written by whoever wrote the marker, so a re-stamped section certifies itself
|
|
1276
|
+
* and `hashMatches` alone cannot tell a hand edit from a catalog upgrade. The manifest is the only
|
|
1277
|
+
* record the editor did not control; fall back to the marker only when there is no manifest entry.
|
|
1278
|
+
*/
|
|
1279
|
+
function handEdited(snippet, options) {
|
|
1280
|
+
const previousHash = options.previousSnippetHashes?.get(snippet.id);
|
|
1281
|
+
return previousHash === void 0 ? !snippet.hashMatches : previousHash !== snippet.computedHash;
|
|
1282
|
+
}
|
|
1283
|
+
function repairNotes(problems) {
|
|
1284
|
+
return problems.map((problem) => Object.freeze({
|
|
1285
|
+
code: "repaired-invalid-block",
|
|
1286
|
+
line: problem.line,
|
|
1287
|
+
message: `Rebuilt the managed block to repair: ${problem.message}`
|
|
1288
|
+
}));
|
|
1289
|
+
}
|
|
1290
|
+
function invalidResult(source, notes, problems) {
|
|
1291
|
+
return Object.freeze({
|
|
1292
|
+
content: source,
|
|
1293
|
+
notes: Object.freeze([...notes]),
|
|
1294
|
+
problems,
|
|
1295
|
+
status: "invalid"
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
function settle(source, content, notes) {
|
|
1299
|
+
return content === source ? Object.freeze({
|
|
1300
|
+
content: source,
|
|
1301
|
+
notes: Object.freeze([...notes]),
|
|
1302
|
+
status: "unchanged"
|
|
1303
|
+
}) : Object.freeze({
|
|
1304
|
+
content,
|
|
1305
|
+
notes: Object.freeze([...notes]),
|
|
1306
|
+
status: "updated"
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
//#endregion
|
|
1310
|
+
//#region ../core/src/workspace/mcp-classify.ts
|
|
1311
|
+
/**
|
|
1312
|
+
* Splits desired servers into the ones Aura may write and the collisions a person has to settle.
|
|
1313
|
+
*
|
|
1314
|
+
* Scope is part of identity here. A server named `docs` in a project `.mcp.json` is not the `docs`
|
|
1315
|
+
* the manifest wants in user-level configuration, and treating them as one either blocks a write
|
|
1316
|
+
* that would not have collided or skips one that never happened.
|
|
1317
|
+
*/
|
|
1318
|
+
function classifyDesired(desired, ledgerNames, state) {
|
|
1319
|
+
const ledger = new Set(ledgerNames);
|
|
1320
|
+
const owned = [];
|
|
1321
|
+
const blockers = [];
|
|
1322
|
+
for (const entry of desired) {
|
|
1323
|
+
const blocker = collisionBlocker(entry, ledger, state);
|
|
1324
|
+
if (blocker === "owned") owned.push(entry);
|
|
1325
|
+
else if (blocker !== void 0) blockers.push(blocker);
|
|
1326
|
+
}
|
|
1327
|
+
return {
|
|
1328
|
+
blockers,
|
|
1329
|
+
owned
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
/** `owned` to write it, a blocker to refuse, `undefined` when the config already satisfies it. */
|
|
1333
|
+
function collisionBlocker(entry, ledger, state) {
|
|
1334
|
+
if (ledger.has(entry.name)) return "owned";
|
|
1335
|
+
const sameName = (candidate) => candidate.name === entry.name && candidate.scope === entry.scope;
|
|
1336
|
+
const unusable = state.unusable.find(sameName);
|
|
1337
|
+
if (unusable !== void 0) return {
|
|
1338
|
+
message: unusable.reason === "disabled" ? `MCP server ${entry.name} is already declared in this application's ${entry.scope} configuration but is turned off there. Remove or enable it, then run the fix again.` : `MCP server ${entry.name} is already declared in this application's ${entry.scope} configuration in a form Aura does not recognize, so Aura left it unchanged.`,
|
|
1339
|
+
scope: entry.scope,
|
|
1340
|
+
sourceId: unusable.sourceId
|
|
1341
|
+
};
|
|
1342
|
+
const existing = state.servers.filter(sameName);
|
|
1343
|
+
if (existing.length === 0) return "owned";
|
|
1344
|
+
const normalized = normalizeDesired(entry);
|
|
1345
|
+
if ("message" in normalized) return normalized;
|
|
1346
|
+
return existing.every((server) => isDeepStrictEqual(server.transport, normalized.transport)) ? void 0 : {
|
|
1347
|
+
message: `MCP server ${entry.name} already exists outside Aura's ownership ledger and differs from the manifest.`,
|
|
1348
|
+
scope: entry.scope
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Normalizes one desired transport, reporting a manifest Aura refuses to write as a blocker.
|
|
1353
|
+
*
|
|
1354
|
+
* The manifest is a file a person can edit. One that has acquired a credential literal is not a
|
|
1355
|
+
* crash in a check's `detect`; it is something to say out loud and decline to propagate.
|
|
1356
|
+
*/
|
|
1357
|
+
function normalizeDesired(entry) {
|
|
1358
|
+
try {
|
|
1359
|
+
return { transport: normalizeMcpServerDefinition(entry.transport) };
|
|
1360
|
+
} catch (error) {
|
|
1361
|
+
return {
|
|
1362
|
+
message: error instanceof McpWriteError ? `MCP server ${entry.name} cannot be written as the manifest defines it: ${error.message}` : `MCP server ${entry.name} has a manifest definition Aura cannot represent.`,
|
|
1363
|
+
scope: entry.scope
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
//#endregion
|
|
1368
|
+
//#region ../core/src/workspace/mcp-convergence.ts
|
|
1369
|
+
/** Captures adapter source bytes behind a function, keeping them out of the model entirely. */
|
|
1370
|
+
function createAppMcpConvergence(adapter, files, state) {
|
|
1371
|
+
const writer = adapter.mcpWrite;
|
|
1372
|
+
if (writer === void 0) return;
|
|
1373
|
+
const targets = [...files.values()].filter((file) => file.spec.kind === "mcp");
|
|
1374
|
+
return (desired, ledgerNames) => {
|
|
1375
|
+
const classified = classifyDesired(desired, ledgerNames, state);
|
|
1376
|
+
if (classified.blockers.length > 0) return blocked(classified.blockers);
|
|
1377
|
+
const scopes = ["global", "project"].map((scope) => planScope(adapter, writer, targets, classified.owned, ledgerNames, scope, state.secretSightings));
|
|
1378
|
+
const blockers = scopes.flatMap((result) => result.blockers);
|
|
1379
|
+
const operations = scopes.flatMap((result) => result.operations);
|
|
1380
|
+
return blockers.length > 0 ? blocked(blockers) : {
|
|
1381
|
+
blockers: [],
|
|
1382
|
+
operations,
|
|
1383
|
+
ownedNames: [...new Set(classified.owned.map((entry) => entry.name))].sort()
|
|
1384
|
+
};
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
function planScope(adapter, writer, targets, desired, ledgerNames, scope, sightings) {
|
|
1388
|
+
const scopedDesired = desired.filter((entry) => entry.scope === scope);
|
|
1389
|
+
const scopedTargets = targets.filter((target) => target.spec.scope === scope);
|
|
1390
|
+
if (scopedDesired.length > 0 && scopedTargets.length === 0) return blockedScope(`${adapter.displayName} has no ${scope}-scope MCP configuration target.`, scope);
|
|
1391
|
+
if (scopedTargets.length > 1) return blockedScope(`${adapter.displayName} declares more than one ${scope}-scope MCP write target.`, scope);
|
|
1392
|
+
const target = scopedTargets[0];
|
|
1393
|
+
return target === void 0 ? {
|
|
1394
|
+
blockers: [],
|
|
1395
|
+
operations: []
|
|
1396
|
+
} : writeScopeTarget(adapter, writer, target, scopedDesired, ledgerNames, scope, sightings);
|
|
1397
|
+
}
|
|
1398
|
+
function writeScopeTarget(adapter, writer, target, desired, ledgerNames, scope, sightings) {
|
|
1399
|
+
if (!needsTargetWrite(target, desired, ledgerNames)) return {
|
|
1400
|
+
blockers: [],
|
|
1401
|
+
operations: []
|
|
1402
|
+
};
|
|
1403
|
+
const unwritable = targetRefusal(target);
|
|
1404
|
+
if (unwritable !== void 0) return {
|
|
1405
|
+
blockers: [{
|
|
1406
|
+
...unwritable,
|
|
1407
|
+
scope,
|
|
1408
|
+
sourceId: target.spec.id
|
|
1409
|
+
}],
|
|
1410
|
+
operations: []
|
|
1411
|
+
};
|
|
1412
|
+
const written = runWriter(writer, target, desired, ledgerNames);
|
|
1413
|
+
if ("refusal" in written) return {
|
|
1414
|
+
blockers: [{
|
|
1415
|
+
message: written.refusal,
|
|
1416
|
+
path: target.spec.path,
|
|
1417
|
+
scope,
|
|
1418
|
+
sourceId: target.spec.id
|
|
1419
|
+
}],
|
|
1420
|
+
operations: []
|
|
1421
|
+
};
|
|
1422
|
+
if (Buffer.byteLength(written.content, "utf8") > 4194304) return {
|
|
1423
|
+
blockers: [{
|
|
1424
|
+
message: `${adapter.displayName}'s MCP configuration at ${target.spec.path} is larger than Aura will rewrite in one operation, so Aura left it unchanged.`,
|
|
1425
|
+
path: target.spec.path,
|
|
1426
|
+
scope,
|
|
1427
|
+
sourceId: target.spec.id
|
|
1428
|
+
}],
|
|
1429
|
+
operations: []
|
|
1430
|
+
};
|
|
1431
|
+
const operation = {
|
|
1432
|
+
content: written.content,
|
|
1433
|
+
mode: scope === "global" ? 384 : 420,
|
|
1434
|
+
path: target.spec.path,
|
|
1435
|
+
precondition: targetPrecondition(target),
|
|
1436
|
+
type: "write"
|
|
1437
|
+
};
|
|
1438
|
+
rememberMcpRedactor(adapter, operation, target, sightings);
|
|
1439
|
+
return {
|
|
1440
|
+
blockers: [],
|
|
1441
|
+
operations: [operation]
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Registers preview masking only for a target that actually holds an inline credential.
|
|
1446
|
+
*
|
|
1447
|
+
* Registering unconditionally would cost every convergence preview its diff, because masking has
|
|
1448
|
+
* no previous side to project when the fix is creating the file — which is what most MCP-001 fixes
|
|
1449
|
+
* do, on a file that by definition has no credentials in it yet.
|
|
1450
|
+
*/
|
|
1451
|
+
function rememberMcpRedactor(adapter, operation, target, sightings) {
|
|
1452
|
+
const transform = adapter.mcpSecrets;
|
|
1453
|
+
const scoped = sightings.filter((sighting) => sighting.sourceId === target.spec.id);
|
|
1454
|
+
if (transform === void 0 || scoped.length === 0) return;
|
|
1455
|
+
rememberWriteRedactor(operation, mcpSecretRedactor(transform, scoped));
|
|
1456
|
+
}
|
|
1457
|
+
/** Runs a plugin serializer, treating a thrown error as the refusal it forgot to return. */
|
|
1458
|
+
function runWriter(writer, target, desired, ledgerNames) {
|
|
1459
|
+
try {
|
|
1460
|
+
return writer({
|
|
1461
|
+
desired,
|
|
1462
|
+
...target.content === void 0 ? {} : { existingContent: target.content },
|
|
1463
|
+
ledgerNames
|
|
1464
|
+
});
|
|
1465
|
+
} catch {
|
|
1466
|
+
return { refusal: "The MCP configuration serializer failed, so Aura left the file unchanged." };
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* What the file must still look like when the plan is applied.
|
|
1471
|
+
*
|
|
1472
|
+
* The rendered content is this file's own previous bytes with one section replaced, so applying it
|
|
1473
|
+
* against a file the application has since rewritten would silently revert that work. Declaring
|
|
1474
|
+
* what was read turns the race into a reported conflict.
|
|
1475
|
+
*/
|
|
1476
|
+
function targetPrecondition(target) {
|
|
1477
|
+
return target.content === void 0 ? { kind: "absent" } : {
|
|
1478
|
+
digest: createHash("sha256").update(target.content, "utf8").digest("hex"),
|
|
1479
|
+
kind: "sha256"
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
function needsTargetWrite(target, desired, ledgerNames) {
|
|
1483
|
+
return (desired.length > 0 || ledgerNames.length > 0) && (target.exists || desired.length > 0);
|
|
1484
|
+
}
|
|
1485
|
+
/** Why this target cannot be rewritten safely, before a serializer is asked to try. */
|
|
1486
|
+
function targetRefusal(target) {
|
|
1487
|
+
const path = target.spec.path;
|
|
1488
|
+
if (target.problem !== void 0 || target.exists && target.content === void 0) return {
|
|
1489
|
+
message: `MCP configuration at ${path} could not be read safely, so Aura left it unchanged.`,
|
|
1490
|
+
path
|
|
1491
|
+
};
|
|
1492
|
+
return (target.size ?? 0) > 4194304 ? {
|
|
1493
|
+
message: `MCP configuration at ${path} is larger than Aura will rewrite in one operation, so Aura left it unchanged.`,
|
|
1494
|
+
path
|
|
1495
|
+
} : void 0;
|
|
1496
|
+
}
|
|
1497
|
+
function blockedScope(message, scope) {
|
|
1498
|
+
return {
|
|
1499
|
+
blockers: [{
|
|
1500
|
+
message,
|
|
1501
|
+
scope
|
|
1502
|
+
}],
|
|
1503
|
+
operations: []
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
function blocked(blockers) {
|
|
1507
|
+
return {
|
|
1508
|
+
blockers,
|
|
1509
|
+
operations: [],
|
|
1510
|
+
ownedNames: []
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
//#endregion
|
|
1514
|
+
//#region ../core/src/workspace/mcp-plan-manifest.ts
|
|
1515
|
+
/**
|
|
1516
|
+
* Merges preset-required servers under the manifest's own, keyed by scope and name.
|
|
1517
|
+
*
|
|
1518
|
+
* The manifest is written last and wins, matching the layer precedence everywhere else. A preset
|
|
1519
|
+
* that requires a server the user already configured must not silently repoint its transport:
|
|
1520
|
+
* a required entry fills a gap, it does not overrule a decision the user recorded.
|
|
1521
|
+
*/
|
|
1522
|
+
function desiredEntries(model, manifest, appId) {
|
|
1523
|
+
const result = /* @__PURE__ */ new Map();
|
|
1524
|
+
for (const server of [...model.requiredMcpServers ?? [], ...manifest.mcpServers]) if (server.apps.includes(appId)) result.set(`${server.scope}\0${server.name}`, {
|
|
1525
|
+
name: server.name,
|
|
1526
|
+
scope: server.scope,
|
|
1527
|
+
transport: server.transport
|
|
1528
|
+
});
|
|
1529
|
+
return Object.freeze([...result.values()]);
|
|
1530
|
+
}
|
|
1531
|
+
/** Restates one application's ownership ledger without disturbing the files it also records. */
|
|
1532
|
+
function withOwnership(manifest, appId, mcpServerNames) {
|
|
1533
|
+
const previous = manifest.ownership[appId];
|
|
1534
|
+
const ownership = {
|
|
1535
|
+
...manifest.ownership,
|
|
1536
|
+
[appId]: {
|
|
1537
|
+
...previous,
|
|
1538
|
+
files: previous?.files ?? [],
|
|
1539
|
+
mcpServerNames
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
return {
|
|
1543
|
+
...manifest,
|
|
1544
|
+
ownership
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
/** Drops one application from a desired server, and the entry entirely once nobody wants it. */
|
|
1548
|
+
function withoutServer(manifest, server) {
|
|
1549
|
+
const mcpServers = manifest.mcpServers.flatMap((entry) => {
|
|
1550
|
+
if (entry.name !== server.name || entry.scope !== server.scope) return [entry];
|
|
1551
|
+
const apps = entry.apps.filter((appId) => appId !== server.appId);
|
|
1552
|
+
return apps.length === 0 ? [] : [{
|
|
1553
|
+
...entry,
|
|
1554
|
+
apps
|
|
1555
|
+
}];
|
|
1556
|
+
});
|
|
1557
|
+
return {
|
|
1558
|
+
...manifest,
|
|
1559
|
+
mcpServers
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
//#endregion
|
|
1563
|
+
//#region ../core/src/workspace/mcp-plan.ts
|
|
1564
|
+
/**
|
|
1565
|
+
* Planners for the applications in one scan, held outside the model they belong to.
|
|
1566
|
+
*
|
|
1567
|
+
* A planner closes over the configuration bytes core read. {@link AppModel} promises those are not
|
|
1568
|
+
* retained beside the documents parsed out of them, and a check holds an `AppModel`, so the
|
|
1569
|
+
* association lives here instead of on the object: reaching a planner takes this module, which
|
|
1570
|
+
* plugin code cannot import.
|
|
1571
|
+
*/
|
|
1572
|
+
const PLANNERS$1 = /* @__PURE__ */ new WeakMap();
|
|
1573
|
+
/** Per-scan memo of what convergence planning produced, keyed by the model it was computed from. */
|
|
1574
|
+
const PLANS$1 = /* @__PURE__ */ new WeakMap();
|
|
1575
|
+
/** Associates a planner with the app model core just built for it. */
|
|
1576
|
+
function rememberMcpConvergence(app, convergence) {
|
|
1577
|
+
PLANNERS$1.set(app, convergence);
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Reports only why convergence is impossible, without rendering any file.
|
|
1581
|
+
*
|
|
1582
|
+
* What a check needs at detect time. The result is shared with {@link planManifestMcpConvergence},
|
|
1583
|
+
* so asking two checks and then a fix costs one evaluation rather than three — which matters
|
|
1584
|
+
* because evaluating it serializes every declared MCP target in full.
|
|
1585
|
+
*/
|
|
1586
|
+
function mcpConvergenceBlockers(model, appId) {
|
|
1587
|
+
return planManifestMcpConvergence(model, appId).blockers;
|
|
1588
|
+
}
|
|
1589
|
+
/** Builds application writes and the ownership-ledger update as one atomic fix plan. */
|
|
1590
|
+
function planManifestMcpConvergence(model, appId) {
|
|
1591
|
+
const memo = PLANS$1.get(model) ?? /* @__PURE__ */ new Map();
|
|
1592
|
+
PLANS$1.set(model, memo);
|
|
1593
|
+
const cached = memo.get(appId);
|
|
1594
|
+
if (cached !== void 0) return cached;
|
|
1595
|
+
const computed = computeConvergence(model, appId);
|
|
1596
|
+
memo.set(appId, computed);
|
|
1597
|
+
return computed;
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Plans one application's MCP configuration without writing the Aura manifest.
|
|
1601
|
+
*
|
|
1602
|
+
* Setup uses this primitive to converge several targets and then fold all returned ownership into
|
|
1603
|
+
* exactly one manifest write.
|
|
1604
|
+
*/
|
|
1605
|
+
function planDesiredMcpConvergence(model, desiredManifest, appId) {
|
|
1606
|
+
const manifestState = model.manifest;
|
|
1607
|
+
if (manifestState.status === "read-only") return {
|
|
1608
|
+
blockers: [{
|
|
1609
|
+
message: manifestState.problem.message,
|
|
1610
|
+
path: manifestState.path
|
|
1611
|
+
}],
|
|
1612
|
+
operations: [],
|
|
1613
|
+
ownedNames: desiredManifest.ownership[appId]?.mcpServerNames ?? []
|
|
1614
|
+
};
|
|
1615
|
+
const target = resolvePlanner(model, appId);
|
|
1616
|
+
if ("blockers" in target) return {
|
|
1617
|
+
blockers: target.blockers,
|
|
1618
|
+
operations: [],
|
|
1619
|
+
ownedNames: desiredManifest.ownership[appId]?.mcpServerNames ?? []
|
|
1620
|
+
};
|
|
1621
|
+
if (desiredManifest.apps[appId]?.managed !== true) return {
|
|
1622
|
+
blockers: [],
|
|
1623
|
+
operations: [],
|
|
1624
|
+
ownedNames: desiredManifest.ownership[appId]?.mcpServerNames ?? []
|
|
1625
|
+
};
|
|
1626
|
+
return target.convergence(desiredEntries(model, desiredManifest, appId), desiredManifest.ownership[appId]?.mcpServerNames ?? []);
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Removes one Aura-owned server from one application while preserving its selection elsewhere.
|
|
1630
|
+
*
|
|
1631
|
+
* The configuration write and updated manifest/ownership ledger share one fix plan. Names outside
|
|
1632
|
+
* the ledger, unmanaged applications, and identities a name-only ledger cannot distinguish are
|
|
1633
|
+
* returned as blockers for a guided check to turn into manual steps.
|
|
1634
|
+
*/
|
|
1635
|
+
function planMcpServerRemoval(model, server) {
|
|
1636
|
+
const state = model.manifest;
|
|
1637
|
+
if (state.status !== "ready") return {
|
|
1638
|
+
blockers: [{
|
|
1639
|
+
message: state.status === "read-only" ? state.problem.message : "The Aura manifest does not exist, so this server is outside Aura's ownership ledger.",
|
|
1640
|
+
path: state.path
|
|
1641
|
+
}],
|
|
1642
|
+
owned: false
|
|
1643
|
+
};
|
|
1644
|
+
const manifest = state.value;
|
|
1645
|
+
const owned = manifest.ownership[server.appId]?.mcpServerNames.includes(server.name) === true;
|
|
1646
|
+
if (!owned) return {
|
|
1647
|
+
blockers: [{
|
|
1648
|
+
message: `MCP server ${server.name} is outside Aura's ownership ledger.`,
|
|
1649
|
+
scope: server.scope,
|
|
1650
|
+
sourceId: server.sourceId
|
|
1651
|
+
}],
|
|
1652
|
+
owned: false
|
|
1653
|
+
};
|
|
1654
|
+
if (manifest.apps[server.appId]?.managed !== true) return {
|
|
1655
|
+
blockers: [{
|
|
1656
|
+
message: `Application ${server.appId} is not managed by Aura, so Aura will not rewrite its MCP configuration.`,
|
|
1657
|
+
scope: server.scope,
|
|
1658
|
+
sourceId: server.sourceId
|
|
1659
|
+
}],
|
|
1660
|
+
owned: true
|
|
1661
|
+
};
|
|
1662
|
+
if (ambiguousRemoval(model, server)) return {
|
|
1663
|
+
blockers: [{
|
|
1664
|
+
message: `MCP server name ${server.name} identifies more than one configured or desired entry for ${server.appId}, and Aura's ledger cannot remove only one safely.`,
|
|
1665
|
+
scope: server.scope,
|
|
1666
|
+
sourceId: server.sourceId
|
|
1667
|
+
}],
|
|
1668
|
+
owned: true
|
|
1669
|
+
};
|
|
1670
|
+
return {
|
|
1671
|
+
...computeConvergence(model, server.appId, withoutServer(manifest, server)),
|
|
1672
|
+
owned
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
function computeConvergence(model, appId, desiredManifest) {
|
|
1676
|
+
const manifestState = model.manifest;
|
|
1677
|
+
if (manifestState.status === "read-only") return { blockers: [{
|
|
1678
|
+
message: manifestState.problem.message,
|
|
1679
|
+
path: manifestState.path
|
|
1680
|
+
}] };
|
|
1681
|
+
if (manifestState.status === "missing") return { blockers: [] };
|
|
1682
|
+
const manifest = desiredManifest ?? manifestState.value;
|
|
1683
|
+
if (manifest.apps[appId]?.managed !== true) return { blockers: [] };
|
|
1684
|
+
const planned = planDesiredMcpConvergence(model, manifest, appId);
|
|
1685
|
+
const displayName = model.apps.find((app) => app.adapterId === appId)?.displayName ?? appId;
|
|
1686
|
+
return planned.blockers.length > 0 ? { blockers: planned.blockers } : {
|
|
1687
|
+
blockers: [],
|
|
1688
|
+
plan: {
|
|
1689
|
+
operations: ledgeredOperations(manifestState, manifest, appId, planned),
|
|
1690
|
+
summary: `Converge ${displayName}'s MCP configuration from the Aura manifest.`
|
|
1691
|
+
}
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
/** The application writes, plus the ledger update when what Aura owns actually changed. */
|
|
1695
|
+
function ledgeredOperations(state, manifest, appId, planned) {
|
|
1696
|
+
const next = withOwnership(manifest, appId, planned.ownedNames);
|
|
1697
|
+
return [...planned.operations, ...isDeepStrictEqual(next, state.value) ? [] : [createAuraManifestWriteOperation(state, next)]];
|
|
1698
|
+
}
|
|
1699
|
+
function ambiguousRemoval(model, server) {
|
|
1700
|
+
const configured = model.mcpServers.filter((candidate) => candidate.appId === server.appId && candidate.name === server.name);
|
|
1701
|
+
if (configured.length > 1 || configured.some((candidate) => candidate.scope !== server.scope || candidate.sourceId !== server.sourceId)) return true;
|
|
1702
|
+
if (model.manifest.status !== "ready") return false;
|
|
1703
|
+
return model.manifest.value.mcpServers.filter((entry) => entry.name === server.name && entry.apps.includes(server.appId)).length > 1;
|
|
1704
|
+
}
|
|
1705
|
+
function resolvePlanner(model, appId) {
|
|
1706
|
+
const app = model.apps.find((candidate) => candidate.adapterId === appId);
|
|
1707
|
+
if (app === void 0) return { blockers: [{ message: `Application ${appId} was not detected.` }] };
|
|
1708
|
+
const convergence = PLANNERS$1.get(app);
|
|
1709
|
+
return convergence === void 0 ? { blockers: [{ message: `${app.displayName}'s adapter cannot write MCP configuration.` }] } : {
|
|
1710
|
+
app,
|
|
1711
|
+
convergence
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
//#endregion
|
|
1715
|
+
//#region ../core/src/workspace/mcp-secret-plan.ts
|
|
1716
|
+
const PLANNERS = /* @__PURE__ */ new WeakMap();
|
|
1717
|
+
const PLANS = /* @__PURE__ */ new WeakMap();
|
|
1718
|
+
/** Captures raw MCP bytes for remediation behind the same private boundary as convergence. */
|
|
1719
|
+
function createAppMcpSecretPlanner(adapter, files, sightings) {
|
|
1720
|
+
const transform = adapter.mcpSecrets;
|
|
1721
|
+
if (transform === void 0) return;
|
|
1722
|
+
return {
|
|
1723
|
+
plan: (sourceId) => {
|
|
1724
|
+
const target = files.get(sourceId);
|
|
1725
|
+
const sourceSightings = sightings.filter((sighting) => sighting.sourceId === sourceId);
|
|
1726
|
+
const supported = sourceSightings.filter(transform.supports);
|
|
1727
|
+
if (target === void 0 || target.spec.kind !== "mcp" || target.content === void 0 || targetRefusal(target) !== void 0 || supported.length === 0) return;
|
|
1728
|
+
let rewritten;
|
|
1729
|
+
try {
|
|
1730
|
+
rewritten = transform.rewrite({
|
|
1731
|
+
content: target.content,
|
|
1732
|
+
sightings: supported
|
|
1733
|
+
});
|
|
1734
|
+
} catch {
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
if ("refusal" in rewritten || rewritten.rewrittenFields.length !== supported.length) return;
|
|
1738
|
+
if (Buffer.byteLength(rewritten.content, "utf8") > 4194304) return;
|
|
1739
|
+
const operation = {
|
|
1740
|
+
content: rewritten.content,
|
|
1741
|
+
mode: target.spec.scope === "global" ? 384 : 420,
|
|
1742
|
+
path: target.spec.path,
|
|
1743
|
+
precondition: targetPrecondition(target),
|
|
1744
|
+
type: "write"
|
|
1745
|
+
};
|
|
1746
|
+
rememberWriteRedactor(operation, mcpSecretRedactor(transform, sourceSightings));
|
|
1747
|
+
return {
|
|
1748
|
+
manualSteps: remediationSteps(target.spec.path, supported),
|
|
1749
|
+
operations: [operation],
|
|
1750
|
+
summary: `Replace inline MCP credentials in ${target.spec.path} with environment references.`
|
|
1751
|
+
};
|
|
1752
|
+
},
|
|
1753
|
+
supports: transform.supports
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
/** Associates a private planner with the app model that exposes only safe sightings. */
|
|
1757
|
+
function rememberMcpSecretPlanner(app, planner) {
|
|
1758
|
+
PLANNERS.set(app, planner);
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Whether this occurrence has a behavior-preserving adapter rewrite that a plan can actually carry.
|
|
1762
|
+
*
|
|
1763
|
+
* `supports` only reads the locator's shape, so it answers "could this kind of field be rewritten",
|
|
1764
|
+
* not "was this one". Planning is what discovers that the document is spelled in a form the writer
|
|
1765
|
+
* cannot edit, and a finding that advertises a guided fix the planner then declines to produce is
|
|
1766
|
+
* a dead end for the user. The plan is memoized, so asking it here costs nothing.
|
|
1767
|
+
*/
|
|
1768
|
+
function canPlanMcpSecretRemediation(model, sighting) {
|
|
1769
|
+
const app = model.apps.find((candidate) => candidate.adapterId === sighting.appId);
|
|
1770
|
+
if ((app === void 0 ? void 0 : PLANNERS.get(app))?.supports(sighting) !== true) return false;
|
|
1771
|
+
return planMcpSecretRemediation(model, sighting) !== void 0;
|
|
1772
|
+
}
|
|
1773
|
+
/** Builds the one whole-file plan shared by every representable sighting in a source file. */
|
|
1774
|
+
function planMcpSecretRemediation(model, sighting) {
|
|
1775
|
+
const key = `${sighting.appId}:${sighting.sourceId}`;
|
|
1776
|
+
const memo = PLANS.get(model) ?? /* @__PURE__ */ new Map();
|
|
1777
|
+
PLANS.set(model, memo);
|
|
1778
|
+
if (memo.has(key)) return memo.get(key);
|
|
1779
|
+
const app = model.apps.find((candidate) => candidate.adapterId === sighting.appId);
|
|
1780
|
+
const plan = (app === void 0 ? void 0 : PLANNERS.get(app))?.plan(sighting.sourceId);
|
|
1781
|
+
memo.set(key, plan);
|
|
1782
|
+
return plan;
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* What the user has to do around the rewrite, in the order the rewrite makes them possible.
|
|
1786
|
+
*
|
|
1787
|
+
* Rotation leads the list because moving a credential out of the file does not undo where it has
|
|
1788
|
+
* already been. A value that sat inline in configuration is a value that may have reached source
|
|
1789
|
+
* control, a shell history, a log, or a copied snippet — which is the reason this check exists, and
|
|
1790
|
+
* the reason the same value put behind an environment reference is still the wrong value to keep.
|
|
1791
|
+
*/
|
|
1792
|
+
function remediationSteps(path, sightings) {
|
|
1793
|
+
const copies = sightings.map((sighting) => `Copy the current value for ${sighting.serverName}.${sighting.field} from ${path} now; Aura will replace it with ${sighting.suggestedEnvName}.`);
|
|
1794
|
+
const exports = [...new Set(sightings.map((sighting) => sighting.suggestedEnvName))].sort().map((name) => `Run \`export ${name}=…\` in the environment that starts the MCP client.`);
|
|
1795
|
+
return [
|
|
1796
|
+
...copies,
|
|
1797
|
+
...exports,
|
|
1798
|
+
"Rotate each credential at its issuer: an inline value may already have been copied elsewhere.",
|
|
1799
|
+
"Run `aura check` again."
|
|
1800
|
+
];
|
|
1801
|
+
}
|
|
1802
|
+
//#endregion
|
|
1803
|
+
//#region ../core/src/workspace/shared-link-plan.ts
|
|
1804
|
+
const SHARED_LINK_SNIPPET_ID = "shared-instructions";
|
|
1805
|
+
/** Builds the same safe shared-instruction link plan for checks and setup. */
|
|
1806
|
+
function planSharedInstructionLink(app, model, options = {}) {
|
|
1807
|
+
const link = options.link ?? app.sharedLink;
|
|
1808
|
+
if (link === void 0) return { blocked: "This adapter does not declare an automatic shared-link mechanism." };
|
|
1809
|
+
const status = entryStatus(app, link.entryPath);
|
|
1810
|
+
const refusal = refuseBefore(app, status);
|
|
1811
|
+
if (refusal !== void 0) return { blocked: refusal };
|
|
1812
|
+
switch (link.kind) {
|
|
1813
|
+
case "import-line": return planImportLine(app, model, link, status, options.sourceContent);
|
|
1814
|
+
case "native-copy": return planNativeCopy(app, model, link, status, options.sourceContent);
|
|
1815
|
+
case "symlink": return planSymlink(app, model, link, status, options);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
function planImportLine(app, model, link, status, sourceContent) {
|
|
1819
|
+
const source = instructionEntry(app, link.entryPath);
|
|
1820
|
+
if (unreadableInstructionEntry(status, source, sourceContent)) return { blocked: `Something is at ${link.entryPath} that the adapter could not read as an instruction file, so Aura will not replace it. Check whether it is a broken symbolic link.` };
|
|
1821
|
+
const reconciled = reconcileManagedBlock(selectedContent(sourceContent, source), [{
|
|
1822
|
+
content: link.content ?? "",
|
|
1823
|
+
id: SHARED_LINK_SNIPPET_ID
|
|
1824
|
+
}]);
|
|
1825
|
+
if (reconciled.status === "invalid") return { blocked: `The Aura-managed block in ${link.entryPath} is malformed, so Aura will not rewrite the file. Repair or delete the block and run check --fix again.` };
|
|
1826
|
+
if (reconciled.status === "unchanged" && observedStateHolds(sourceContent)) return { plan: convergedPlan$1(app) };
|
|
1827
|
+
return { plan: writePlan(app, link, reconciled.content, model.homeDir) };
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Whether what the scan saw still describes the file when this plan is applied.
|
|
1831
|
+
*
|
|
1832
|
+
* A caller that supplies `sourceContent` is telling us it is about to replace the entry — setup
|
|
1833
|
+
* passes `""` for a path it is archiving in the same plan. Reporting "already converged" from the
|
|
1834
|
+
* pre-archive state would drop the operation the archive needs as its replacement, and an archive
|
|
1835
|
+
* with no replacement removes the original outright.
|
|
1836
|
+
*/
|
|
1837
|
+
function observedStateHolds(sourceContent) {
|
|
1838
|
+
return sourceContent === void 0;
|
|
1839
|
+
}
|
|
1840
|
+
function unreadableInstructionEntry(status, source, sourceContent) {
|
|
1841
|
+
return status?.exists === true && source === void 0 && sourceContent === void 0;
|
|
1842
|
+
}
|
|
1843
|
+
function selectedContent(sourceContent, source) {
|
|
1844
|
+
return sourceContent ?? source?.content ?? "";
|
|
1845
|
+
}
|
|
1846
|
+
function planNativeCopy(app, model, link, status, sourceContent) {
|
|
1847
|
+
const content = sourceContent ?? instructionEntry(app, link.entryPath)?.content;
|
|
1848
|
+
const refusal = nativeCopyRefusal(status, sourceContent, content, link.content);
|
|
1849
|
+
if (refusal !== void 0) return { blocked: refusal };
|
|
1850
|
+
if (status?.exists === true && content === link.content && observedStateHolds(sourceContent)) return { plan: convergedPlan$1(app) };
|
|
1851
|
+
return { plan: writePlan(app, link, link.content ?? "", model.homeDir) };
|
|
1852
|
+
}
|
|
1853
|
+
function nativeCopyRefusal(status, sourceContent, content, desired) {
|
|
1854
|
+
return status?.exists === true && sourceContent === void 0 && content !== desired ? "The Aura wrapper path contains different content, so it is treated as user-owned and preserved." : void 0;
|
|
1855
|
+
}
|
|
1856
|
+
function planSymlink(app, model, link, status, options) {
|
|
1857
|
+
if (status?.exists === true && status.pathKind !== "symlink" && options.sourceContent === void 0) return { blocked: "The existing file is user-owned. Consolidate its content before replacing it with a symlink." };
|
|
1858
|
+
const target = options.symlinkTarget ?? model.sharedInstructions.path;
|
|
1859
|
+
if (observedStateHolds(options.sourceContent) && pointsAt(status, target)) return { plan: convergedPlan$1(app) };
|
|
1860
|
+
return { plan: {
|
|
1861
|
+
operations: [{
|
|
1862
|
+
path: link.entryPath,
|
|
1863
|
+
target,
|
|
1864
|
+
type: "symlink"
|
|
1865
|
+
}],
|
|
1866
|
+
summary: `Link ${app.displayName} to the shared instruction source.`
|
|
1867
|
+
} };
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* Whether the entry is already a symbolic link to `target`.
|
|
1871
|
+
*
|
|
1872
|
+
* Both sides are resolved, as everywhere else in this file: `symlinkTarget` is the raw `readlink`
|
|
1873
|
+
* result, so comparing it verbatim would re-plan an identical link whenever the same path happens
|
|
1874
|
+
* to be spelled differently.
|
|
1875
|
+
*/
|
|
1876
|
+
function pointsAt(status, target) {
|
|
1877
|
+
if (status?.pathKind !== "symlink" || status.symlinkTarget === void 0) return false;
|
|
1878
|
+
return resolve(status.symlinkTarget) === resolve(target);
|
|
1879
|
+
}
|
|
1880
|
+
/** No work: the entry already loads the shared source, so the summary says so rather than promising a link. */
|
|
1881
|
+
function convergedPlan$1(app) {
|
|
1882
|
+
return {
|
|
1883
|
+
operations: [],
|
|
1884
|
+
summary: `${app.displayName} already loads the shared instruction source.`
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
function refuseBefore(app, status) {
|
|
1888
|
+
if (app.support.status !== "supported") return "Aura does not modify instruction files for an application version it cannot verify.";
|
|
1889
|
+
if (status?.problem !== void 0) return `Aura could not safely read the instruction entry (${status.problem}) and will not overwrite it.`;
|
|
1890
|
+
return status?.pathKind === "directory" ? "The instruction entry is a directory and cannot be replaced automatically." : void 0;
|
|
1891
|
+
}
|
|
1892
|
+
function writePlan(app, link, content, homeDir) {
|
|
1893
|
+
return {
|
|
1894
|
+
...link.scope === "project" && content.includes(homeDir) ? { manualSteps: [`${link.entryPath} points at the shared source by absolute path, which is specific to this machine and this user. Keep it out of version control — add it to .gitignore or .git/info/exclude.`] } : {},
|
|
1895
|
+
operations: [{
|
|
1896
|
+
content,
|
|
1897
|
+
mode: 420,
|
|
1898
|
+
path: link.entryPath,
|
|
1899
|
+
type: "write"
|
|
1900
|
+
}],
|
|
1901
|
+
summary: `Link ${app.displayName} to the shared instruction source.`
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
function instructionEntry(app, path) {
|
|
1905
|
+
return app.instructionFiles.find((document) => resolve(document.path) === resolve(path));
|
|
1906
|
+
}
|
|
1907
|
+
function entryStatus(app, path) {
|
|
1908
|
+
return app.sourceFiles.find((file) => resolve(file.spec.path) === resolve(path));
|
|
1909
|
+
}
|
|
1910
|
+
//#endregion
|
|
1911
|
+
//#region ../core/src/workspace/skill-deployment-plan.ts
|
|
1912
|
+
/** Plans one manifest skill's link into one adapter-declared directory. */
|
|
1913
|
+
function planSkillDeployment(app, model, skillId, directoryId, options = {}) {
|
|
1914
|
+
const directory = app.skillDirectories?.find((candidate) => candidate.id === directoryId);
|
|
1915
|
+
if (directory === void 0) return {
|
|
1916
|
+
kind: "blocked",
|
|
1917
|
+
path: skillId,
|
|
1918
|
+
reason: "The application no longer declares this skills directory."
|
|
1919
|
+
};
|
|
1920
|
+
const path = join(directory.path, skillId);
|
|
1921
|
+
const target = join(sharedSkillsRoot(model.homeDir), skillId);
|
|
1922
|
+
if (!(model.sharedSkills?.some((skill) => skill.id === skillId) === true) && options.assumeShared !== true) return {
|
|
1923
|
+
kind: "blocked",
|
|
1924
|
+
path,
|
|
1925
|
+
reason: `The shared copy at ${target} is missing. Reinstall skill "${skillId}" before deploying it.`
|
|
1926
|
+
};
|
|
1927
|
+
const status = skillDeploymentStatus(app, directory, skillId);
|
|
1928
|
+
if (status === void 0 || !status.exists) return {
|
|
1929
|
+
kind: "planned",
|
|
1930
|
+
path,
|
|
1931
|
+
plan: symlinkPlan(app, path, target)
|
|
1932
|
+
};
|
|
1933
|
+
if (status.problem !== void 0) return {
|
|
1934
|
+
kind: "blocked",
|
|
1935
|
+
path,
|
|
1936
|
+
reason: `Aura could not safely inspect ${path} (${status.problem}) and will not replace it.`
|
|
1937
|
+
};
|
|
1938
|
+
if (status.pathKind !== "symlink" || status.symlinkTarget === void 0) return {
|
|
1939
|
+
kind: "blocked",
|
|
1940
|
+
path,
|
|
1941
|
+
reason: `${path} is not an Aura-managed skill link. Move it aside before deploying skill "${skillId}" there.`
|
|
1942
|
+
};
|
|
1943
|
+
const actualTarget = absoluteTarget(path, status.symlinkTarget);
|
|
1944
|
+
if (actualTarget === resolve(target)) return {
|
|
1945
|
+
kind: "planned",
|
|
1946
|
+
path,
|
|
1947
|
+
plan: convergedPlan(app, skillId)
|
|
1948
|
+
};
|
|
1949
|
+
if (!isAuraOwnedSkillTarget(model.homeDir, actualTarget)) return {
|
|
1950
|
+
kind: "blocked",
|
|
1951
|
+
path,
|
|
1952
|
+
reason: `${path} is not an Aura-managed skill link because it points outside Aura's shared skills directory. It was preserved.`
|
|
1953
|
+
};
|
|
1954
|
+
return {
|
|
1955
|
+
kind: "planned",
|
|
1956
|
+
path,
|
|
1957
|
+
plan: symlinkPlan(app, path, target)
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
/** Finds the captured status for one deployed skill path. */
|
|
1961
|
+
function skillDeploymentStatus(app, directory, skillId) {
|
|
1962
|
+
const path = resolve(join(directory.path, skillId));
|
|
1963
|
+
return app.sourceFiles.find((file) => resolve(file.spec.path) === path);
|
|
1964
|
+
}
|
|
1965
|
+
/** Whether a lexical target lies within Aura's canonical shared skills root. */
|
|
1966
|
+
function isAuraOwnedSkillTarget(homeDir, targetPath) {
|
|
1967
|
+
const root = resolve(sharedSkillsRoot(homeDir));
|
|
1968
|
+
const target = resolve(targetPath);
|
|
1969
|
+
const fromRoot = relative(root, target);
|
|
1970
|
+
return fromRoot !== "" && fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`);
|
|
1971
|
+
}
|
|
1972
|
+
/** Canonical location Aura uses for shared skill trees. */
|
|
1973
|
+
function sharedSkillsRoot(homeDir) {
|
|
1974
|
+
return join(homeDir, "agents", "skills");
|
|
1975
|
+
}
|
|
1976
|
+
function absoluteTarget(path, target) {
|
|
1977
|
+
return isAbsolute(target) ? resolve(target) : resolve(dirname(path), target);
|
|
1978
|
+
}
|
|
1979
|
+
function symlinkPlan(app, path, target) {
|
|
1980
|
+
return {
|
|
1981
|
+
operations: [{
|
|
1982
|
+
path,
|
|
1983
|
+
target,
|
|
1984
|
+
type: "symlink"
|
|
1985
|
+
}],
|
|
1986
|
+
summary: `Deploy the shared skill to ${app.displayName}.`
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
function convergedPlan(app, skillId) {
|
|
1990
|
+
return {
|
|
1991
|
+
operations: [],
|
|
1992
|
+
summary: `${app.displayName} already loads shared skill "${skillId}".`
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
//#endregion
|
|
1996
|
+
export { MAX_MUTABLE_FILE_BYTES as A, AuraManifestError as B, renderRedactedWriteDiff as C, renderRemoveDiff as D, renderMoveDiff as E, parseAuraManifest as F, src_default as H, errorCode as I, errorMessage as L, assertAuraManifestWritable as M, createAuraManifestWriteOperation as N, renderSymlinkDiff as O, createEmptyAuraManifest as P, isRecord as R, hashManagedSnippet as S, renderConflict as T, pluralize as U, SHARED_INSTRUCTIONS_TEMPLATE as V, managedSnippetContentProblems as _, planSharedInstructionLink as a, canonicalizeManagedSnippet as b, planMcpSecretRemediation as c, planDesiredMcpConvergence as d, planManifestMcpConvergence as f, reconcileParsedManagedBlock as g, createAppMcpConvergence as h, skillDeploymentStatus as i, MAX_RETAINED_PLAN_BYTES as j, FILE_MODES as k, rememberMcpSecretPlanner as l, rememberMcpConvergence as m, planSkillDeployment as n, canPlanMcpSecretRemediation as o, planMcpServerRemoval as p, sharedSkillsRoot as r, createAppMcpSecretPlanner as s, isAuraOwnedSkillTarget as t, mcpConvergenceBlockers as u, readManagedBlock as v, renderArchiveDiff as w, hashCanonicalManagedSnippet as x, AURA_MANAGED_SNIPPET_BEGIN_PREFIX as y, resolveAuraManifestPath as z };
|