@tryaura/aura-cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/aura.js +3 -3
- package/dist/index.js +1 -1
- package/dist/plugins/index.js +1 -1
- package/dist/{plugins-SZegBVXF.js → plugins-CL4yWDig.js} +88 -604
- package/dist/{run.boundary-BtMqQAEQ.js → run.boundary-DtkI7ZeG.js} +5815 -3586
- package/dist/{shared-link-plan-DVgOyqP9.js → shared-link-plan-C57dSoid.js} +188 -853
- package/package.json +5 -5
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { advanceMarkdownFence, defineOwnProperty, definePlugin, jsonPropertyPath, mcpServerNameProblem, parseMcpServerDefinition, pluginContentUrl, splitSourceLines as splitSourceLines$1 } from "@tryaura/aura-sdk";
|
|
2
2
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
-
import { Buffer, isUtf8 } from "node:buffer";
|
|
4
3
|
import { createHash } from "node:crypto";
|
|
5
|
-
import { createTwoFilesPatch } from "diff";
|
|
6
4
|
import { gt, valid } from "semver";
|
|
7
5
|
import { isDeepStrictEqual } from "node:util";
|
|
8
6
|
//#region ../core/src/display-path.ts
|
|
@@ -157,6 +155,8 @@ var AuraManifestError = class extends Error {
|
|
|
157
155
|
};
|
|
158
156
|
//#endregion
|
|
159
157
|
//#region ../core/src/manifest/protocol.ts
|
|
158
|
+
/** Home-relative protocol path shared by every Aura distribution. */
|
|
159
|
+
const AURA_MANIFEST_PATH = "~/agents/aura.json";
|
|
160
160
|
/** Resolves the manifest protocol path against one captured home directory. */
|
|
161
161
|
function resolveAuraManifestPath(homeDir) {
|
|
162
162
|
return join(homeDir, "agents", "aura.json");
|
|
@@ -353,6 +353,48 @@ function targetApps(value, path, name, scope, claimed) {
|
|
|
353
353
|
}
|
|
354
354
|
return entries;
|
|
355
355
|
}
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region ../core/src/manifest/schema-snippets.ts
|
|
358
|
+
/**
|
|
359
|
+
* The same grammar `CONTENT_ID_PATTERN` admits for a preset's snippet ids.
|
|
360
|
+
*
|
|
361
|
+
* Deliberately not stricter: a preset may name any id this pattern accepts, and a manifest that
|
|
362
|
+
* refused one would fail on a selection the preset was allowed to make.
|
|
363
|
+
*/
|
|
364
|
+
const SNIPPET_ID_PATTERN = /^[a-z0-9][a-z0-9._/-]*$/u;
|
|
365
|
+
/**
|
|
366
|
+
* Reads the install record for every snippet appended to the shared instructions.
|
|
367
|
+
*
|
|
368
|
+
* Two older shapes name an install that has to survive the read: the released record carried a
|
|
369
|
+
* `pinned` revision Aura no longer holds content at, and an early install-once build wrote a bare
|
|
370
|
+
* id. Both normalize to `{ id, hash? }` — the dead revision keys are dropped rather than carried
|
|
371
|
+
* forward, and a record with no hash stays installed rather than being discarded for the shape.
|
|
372
|
+
*/
|
|
373
|
+
function snippets(value) {
|
|
374
|
+
if (!Array.isArray(value)) throw invalid("$.snippets", "must be an array");
|
|
375
|
+
const seen = /* @__PURE__ */ new Set();
|
|
376
|
+
const installed = [];
|
|
377
|
+
for (const [index, candidate] of value.entries()) {
|
|
378
|
+
const path = `$.snippets[${String(index)}]`;
|
|
379
|
+
const record = typeof candidate === "string" ? { id: candidate } : requiredObject(candidate, path);
|
|
380
|
+
const id = requiredString(record, "id", path);
|
|
381
|
+
if (!SNIPPET_ID_PATTERN.test(id)) throw invalid(`${path}.id`, "must be a lowercase snippet id such as \"acme/rules\"");
|
|
382
|
+
const hash = optionalSnippetHash(record, path);
|
|
383
|
+
if (seen.has(id)) continue;
|
|
384
|
+
seen.add(id);
|
|
385
|
+
installed.push(Object.freeze({
|
|
386
|
+
...hash === void 0 ? {} : { hash },
|
|
387
|
+
id
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
return Object.freeze(installed);
|
|
391
|
+
}
|
|
392
|
+
function optionalSnippetHash(record, path) {
|
|
393
|
+
const hash = record["hash"];
|
|
394
|
+
if (hash === void 0) return;
|
|
395
|
+
if (typeof hash !== "string" || !SHA256_PATTERN.test(hash)) throw invalid(`${path}.hash`, "must be a lowercase SHA-256 hash");
|
|
396
|
+
return hash;
|
|
397
|
+
}
|
|
356
398
|
const MAX_TRUSTED_PATH_LENGTH = 1024;
|
|
357
399
|
/**
|
|
358
400
|
* Reads `trustedRepoPresets`, the repository presets the user accepted during setup.
|
|
@@ -360,28 +402,44 @@ const MAX_TRUSTED_PATH_LENGTH = 1024;
|
|
|
360
402
|
* Each entry binds an absolute preset path to a hash of the exact contents that were reviewed, so
|
|
361
403
|
* a file edited after acceptance is untrusted again until someone looks at the new contents. Only
|
|
362
404
|
* acceptances appear here: declining records nothing, and the next interactive setup asks again.
|
|
405
|
+
*
|
|
406
|
+
* `mainWorktreePath` names the same file in the repository's primary Git checkout and carries no
|
|
407
|
+
* uniqueness of its own: every worktree of one repository shares it, so a repository accumulates
|
|
408
|
+
* one entry per distinct set of contents its user accepted rather than one per working directory.
|
|
363
409
|
*/
|
|
364
410
|
function optionalTrustedRepoPresets(value) {
|
|
365
411
|
if (value === void 0) return {};
|
|
366
412
|
if (!Array.isArray(value)) throw invalid("$.trustedRepoPresets", "must be an array");
|
|
367
413
|
if (value.length > 64) throw invalid("$.trustedRepoPresets", `must contain at most ${String(64)} entries`);
|
|
368
|
-
const
|
|
414
|
+
const records = /* @__PURE__ */ new Set();
|
|
369
415
|
return { trustedRepoPresets: Object.freeze(value.map((candidate, index) => {
|
|
370
416
|
const path = `$.trustedRepoPresets[${String(index)}]`;
|
|
371
417
|
const entry = requiredObject(candidate, path);
|
|
372
|
-
const presetPath = requiredString(entry, "path", path);
|
|
373
|
-
if (presetPath.length === 0 || presetPath.length > MAX_TRUSTED_PATH_LENGTH) throw invalid(`${path}.path`, `must be a non-empty path of at most ${String(MAX_TRUSTED_PATH_LENGTH)} characters`);
|
|
374
|
-
if (paths.has(presetPath)) throw invalid(`${path}.path`, "must not duplicate another trusted preset path");
|
|
375
|
-
paths.add(presetPath);
|
|
418
|
+
const presetPath = trustedPath(requiredString(entry, "path", path), `${path}.path`);
|
|
376
419
|
const hash = requiredString(entry, "hash", path);
|
|
377
420
|
if (!SHA256_PATTERN.test(hash)) throw invalid(`${path}.hash`, "must be a lowercase SHA-256 hash");
|
|
421
|
+
const record = `${presetPath}\0${hash}`;
|
|
422
|
+
if (records.has(record)) throw invalid(`${path}.hash`, "must not duplicate another trusted preset path and hash");
|
|
423
|
+
records.add(record);
|
|
424
|
+
const mainWorktree = entry["mainWorktreePath"];
|
|
425
|
+
if (mainWorktree === void 0) return Object.freeze({
|
|
426
|
+
...entry,
|
|
427
|
+
hash,
|
|
428
|
+
path: presetPath
|
|
429
|
+
});
|
|
430
|
+
if (typeof mainWorktree !== "string") throw invalid(`${path}.mainWorktreePath`, "must be a string");
|
|
378
431
|
return Object.freeze({
|
|
379
432
|
...entry,
|
|
380
433
|
hash,
|
|
434
|
+
mainWorktreePath: trustedPath(mainWorktree, `${path}.mainWorktreePath`),
|
|
381
435
|
path: presetPath
|
|
382
436
|
});
|
|
383
437
|
})) };
|
|
384
438
|
}
|
|
439
|
+
function trustedPath(value, jsonPath) {
|
|
440
|
+
if (value.length === 0 || value.length > MAX_TRUSTED_PATH_LENGTH) throw invalid(jsonPath, `must be a non-empty path of at most ${String(MAX_TRUSTED_PATH_LENGTH)} characters`);
|
|
441
|
+
return value;
|
|
442
|
+
}
|
|
385
443
|
//#endregion
|
|
386
444
|
//#region ../core/src/manifest/schema.ts
|
|
387
445
|
const APP_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/u;
|
|
@@ -505,22 +563,6 @@ function apps(value) {
|
|
|
505
563
|
}
|
|
506
564
|
return Object.freeze(result);
|
|
507
565
|
}
|
|
508
|
-
function snippets(value) {
|
|
509
|
-
if (!Array.isArray(value)) throw invalid("$.snippets", "must be an array");
|
|
510
|
-
return Object.freeze(value.map((candidate, index) => {
|
|
511
|
-
const path = `$.snippets[${String(index)}]`;
|
|
512
|
-
const snippet = requiredObject(candidate, path);
|
|
513
|
-
const hash = requiredString(snippet, "hash", path);
|
|
514
|
-
if (!SHA256_PATTERN.test(hash)) throw invalid(`${path}.hash`, "must be a lowercase SHA-256 hash");
|
|
515
|
-
return Object.freeze({
|
|
516
|
-
...snippet,
|
|
517
|
-
hash,
|
|
518
|
-
id: requiredString(snippet, "id", path),
|
|
519
|
-
pinned: requiredBoolean(snippet, "pinned", path),
|
|
520
|
-
version: requiredString(snippet, "version", path)
|
|
521
|
-
});
|
|
522
|
-
}));
|
|
523
|
-
}
|
|
524
566
|
function ownership(value) {
|
|
525
567
|
const source = requiredObject(value, "$.ownership");
|
|
526
568
|
const result = {};
|
|
@@ -689,228 +731,49 @@ function createAuraManifestWriteOperation(state, manifest) {
|
|
|
689
731
|
});
|
|
690
732
|
}
|
|
691
733
|
//#endregion
|
|
692
|
-
//#region ../core/src/
|
|
693
|
-
/**
|
|
694
|
-
|
|
695
|
-
*
|
|
696
|
-
* A mutation is only reversible if its previous contents were captured, so this doubles as the
|
|
697
|
-
* ceiling on what one operation retains in memory. Agent configuration is measured in kilobytes;
|
|
698
|
-
* anything approaching this size is not something a fix should be rewriting unattended.
|
|
699
|
-
*/
|
|
700
|
-
const MAX_MUTABLE_FILE_BYTES = 4194304;
|
|
701
|
-
/**
|
|
702
|
-
* The most file content Aura retains across a whole plan.
|
|
703
|
-
*
|
|
704
|
-
* {@link MAX_MUTABLE_FILE_BYTES} bounds one operation; without a plan-wide budget a long plan still
|
|
705
|
-
* multiplies it by the operation count.
|
|
706
|
-
*/
|
|
707
|
-
const MAX_RETAINED_PLAN_BYTES = 67108864;
|
|
708
|
-
/**
|
|
709
|
-
* The combined before/after size above which a preview summarizes instead of rendering a patch.
|
|
710
|
-
*
|
|
711
|
-
* A unified diff is larger than the inputs that produced it, and a preview no human can read is not
|
|
712
|
-
* worth the memory it costs.
|
|
713
|
-
*/
|
|
714
|
-
const MAX_DIFF_BYTES = 262144;
|
|
715
|
-
/**
|
|
716
|
-
* Every mode a plan may request.
|
|
717
|
-
*
|
|
718
|
-
* `FileMode` already closes this set, but a type is erased at runtime and plugins ship as compiled
|
|
719
|
-
* JavaScript. Since the mode reaches `chmod` unchanged, checking it here is what actually keeps a
|
|
720
|
-
* plan from asking for a world-writable or setuid file.
|
|
721
|
-
*/
|
|
722
|
-
const FILE_MODES = /* @__PURE__ */ new Set([
|
|
723
|
-
384,
|
|
724
|
-
420,
|
|
725
|
-
448,
|
|
726
|
-
493
|
|
727
|
-
]);
|
|
728
|
-
//#endregion
|
|
729
|
-
//#region ../core/src/fix-plan/diff.ts
|
|
730
|
-
const NULL_PATH = "/dev/null";
|
|
731
|
-
function renderWriteDiff(path, before, content, requestedMode, mode) {
|
|
732
|
-
const note = modeNote(before, requestedMode, mode);
|
|
733
|
-
const previous = textOf(before);
|
|
734
|
-
if (previous === void 0) return renderSummary("write", path, "Binary or oversized file would change.") + note;
|
|
735
|
-
if (exceedsDiffBudget(previous, content)) return renderSummary("write", path, "File is too large to diff; contents would change.") + note;
|
|
736
|
-
return renderPatch("write", path, before.kind === "missing" ? NULL_PATH : path, path, previous, content) + note;
|
|
737
|
-
}
|
|
738
|
-
/** Fail-closed write preview used when a semantic redactor cannot safely project both sides. */
|
|
739
|
-
function renderWriteSummary(path, before, requestedMode, mode) {
|
|
740
|
-
return renderSummary("write", path, "Sensitive MCP configuration would change.") + modeNote(before, requestedMode, mode);
|
|
741
|
-
}
|
|
742
|
-
function renderRemoveDiff(path, before) {
|
|
743
|
-
if (before.kind === "directory") return renderSummary("remove", path, "Remove empty directory.");
|
|
744
|
-
const previous = textOf(before);
|
|
745
|
-
if (previous === void 0) return renderSummary("remove", path, "Binary or oversized file would be removed.");
|
|
746
|
-
if (exceedsDiffBudget(previous, "")) return renderSummary("remove", path, "File is too large to diff; it would be removed.");
|
|
747
|
-
return renderPatch("remove", path, path, NULL_PATH, previous, "");
|
|
748
|
-
}
|
|
749
|
-
function renderMoveDiff(sourcePath, destinationPath) {
|
|
750
|
-
return [
|
|
751
|
-
`diff --aura move ${sourcePath} ${destinationPath}`,
|
|
752
|
-
`rename from ${sourcePath}`,
|
|
753
|
-
`rename to ${destinationPath}`,
|
|
754
|
-
""
|
|
755
|
-
].join("\n");
|
|
756
|
-
}
|
|
757
|
-
function renderArchiveDiff(path, relativePath, before, replacement, replacementMode) {
|
|
758
|
-
const archive = renderSummary("archive", path, `Preserve the original at <backup>/consolidation/${relativePath}.`);
|
|
759
|
-
if (replacement === void 0) return `${archive}${renderRemoveDiff(path, before)}`;
|
|
760
|
-
return replacement.type === "symlink" ? `${archive}${renderSymlinkDiff(path, before, replacement.target)}` : `${archive}${renderWriteDiff(path, before, replacement.content, replacement.mode, replacementMode ?? 420)}`;
|
|
761
|
-
}
|
|
762
|
-
function renderSymlinkDiff(path, before, target) {
|
|
763
|
-
const previous = textOf(before);
|
|
764
|
-
if (previous === void 0) return renderSummary("symlink", path, `Binary or oversized file would become a link.`);
|
|
765
|
-
if (exceedsDiffBudget(previous, target)) return renderSummary("symlink", path, "File is too large to diff; it would become a link.");
|
|
766
|
-
return `${renderPatch("symlink", path, before.kind === "missing" ? NULL_PATH : path, path, previous, `${target}\n`)}link target ${target}\n`;
|
|
767
|
-
}
|
|
768
|
-
/** Describes a conflict in the same shape as a diff, so a renderer can treat previews uniformly. */
|
|
769
|
-
function renderConflict(operation, path, reason) {
|
|
770
|
-
return renderSummary(operation, path, `Blocked: ${reason}.`);
|
|
771
|
-
}
|
|
772
|
-
function renderPatch(operation, displayPath, oldPath, newPath, oldContent, newContent) {
|
|
773
|
-
return `diff --aura ${operation} ${displayPath}\n${createTwoFilesPatch(oldPath, newPath, oldContent, newContent, "before", "after", { context: 3 })}`;
|
|
774
|
-
}
|
|
775
|
-
function renderSummary(operation, path, detail) {
|
|
776
|
-
return [
|
|
777
|
-
`diff --aura ${operation} ${path}`,
|
|
778
|
-
detail,
|
|
779
|
-
""
|
|
780
|
-
].join("\n");
|
|
781
|
-
}
|
|
782
|
-
/**
|
|
783
|
-
* Says what happens to an existing file's permissions.
|
|
784
|
-
*
|
|
785
|
-
* Two cases are worth a line. A mode that changes is a change the diff itself cannot show, and core
|
|
786
|
-
* only does that to files it owns as protocol. A `mode` a plan asked for and will not get is the
|
|
787
|
-
* commoner one: an existing file keeps whatever the user set, and saying so is the difference
|
|
788
|
-
* between a deliberate choice and a silent one.
|
|
789
|
-
*/
|
|
790
|
-
function modeNote(before, requestedMode, mode) {
|
|
791
|
-
if (before.kind !== "file") return "";
|
|
792
|
-
if (before.mode !== mode) return `mode ${formatMode(before.mode)} changes to ${formatMode(mode)}\n`;
|
|
793
|
-
if (requestedMode !== void 0 && requestedMode !== before.mode) return `mode ${formatMode(requestedMode)} requested; existing mode ${formatMode(before.mode)} is preserved\n`;
|
|
794
|
-
return "";
|
|
795
|
-
}
|
|
796
|
-
function formatMode(mode) {
|
|
797
|
-
return `0o${mode.toString(8).padStart(3, "0")}`;
|
|
798
|
-
}
|
|
799
|
-
function exceedsDiffBudget(before, after) {
|
|
800
|
-
return Buffer.byteLength(before, "utf8") + Buffer.byteLength(after, "utf8") > MAX_DIFF_BYTES;
|
|
801
|
-
}
|
|
802
|
-
function textOf(state) {
|
|
803
|
-
switch (state.kind) {
|
|
804
|
-
case "directory":
|
|
805
|
-
case "unsupported": return;
|
|
806
|
-
case "file": return state.content !== void 0 && isUtf8(state.content) ? state.content.toString("utf8") : void 0;
|
|
807
|
-
case "missing": return "";
|
|
808
|
-
case "symlink": return `${state.target}\n`;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
//#endregion
|
|
812
|
-
//#region ../core/src/fix-plan/write-redaction.ts
|
|
813
|
-
const REDACTORS = /* @__PURE__ */ new WeakMap();
|
|
734
|
+
//#region ../core/src/content-hash.ts
|
|
735
|
+
/** Shape of every hash this module produces, for validators that must recognize one. */
|
|
736
|
+
const CONTENT_HASH_PATTERN = /^[0-9a-f]{64}$/u;
|
|
814
737
|
/**
|
|
815
|
-
*
|
|
738
|
+
* Normalizes Markdown to LF and exactly one trailing newline.
|
|
816
739
|
*
|
|
817
|
-
*
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
740
|
+
* Line endings and trailing blank lines are what a checkout, an editor, or an append seam change
|
|
741
|
+
* without changing what the text says, so a hash that counted them would report drift on every
|
|
742
|
+
* machine that rewrote them — and a fragment appended in this form is the same text the hash
|
|
743
|
+
* covers. Trailing newlines are trimmed by scanning rather than with `/\n+$/`, whose backtracking
|
|
744
|
+
* is quadratic when a long newline run does not reach the end of the string.
|
|
822
745
|
*/
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
REDACTED_PATHS.add(operation.path);
|
|
746
|
+
function canonicalizeContent(content) {
|
|
747
|
+
const normalized = content.replace(/\r\n?/gu, "\n");
|
|
748
|
+
let end = normalized.length;
|
|
749
|
+
while (end > 0 && normalized[end - 1] === "\n") end -= 1;
|
|
750
|
+
return `${normalized.slice(0, end)}\n`;
|
|
829
751
|
}
|
|
830
752
|
/**
|
|
831
|
-
*
|
|
753
|
+
* Fingerprints Markdown Aura records but does not own: an installed snippet, a trusted preset.
|
|
832
754
|
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
* and still holds the credential.
|
|
755
|
+
* One function for both because the two records answer the same question — "is this still the text
|
|
756
|
+
* the user accepted?" — and two spellings of the canonical form would answer it differently on the
|
|
757
|
+
* first file whose line endings a checkout rewrote.
|
|
837
758
|
*/
|
|
838
|
-
function
|
|
839
|
-
return (content)
|
|
840
|
-
const redaction = transform.redact({
|
|
841
|
-
content,
|
|
842
|
-
sightings
|
|
843
|
-
});
|
|
844
|
-
return redaction === void 0 || redaction.unresolved.length > 0 ? void 0 : redaction.content;
|
|
845
|
-
};
|
|
846
|
-
}
|
|
847
|
-
/** Renders a write only after every registered semantic masker succeeds on both diff sides. */
|
|
848
|
-
function renderRedactedWriteDiff(operations, path, before, content, requestedMode, mode) {
|
|
849
|
-
const redactors = operations.flatMap((operation) => REDACTORS.get(operation) ?? []);
|
|
850
|
-
if (redactors.length === 0) return REDACTED_PATHS.has(path) ? renderWriteSummary(path, before, requestedMode, mode) : renderWriteDiff(path, before, content, requestedMode, mode);
|
|
851
|
-
const next = redactAll(redactors, content);
|
|
852
|
-
if (next === void 0) return renderWriteSummary(path, before, requestedMode, mode);
|
|
853
|
-
if (before.kind !== "file" || before.content === void 0) return renderWriteDiff(path, before, next, requestedMode, mode);
|
|
854
|
-
const previous = redactAll(redactors, before.content.toString("utf8"));
|
|
855
|
-
if (previous === void 0) return renderWriteSummary(path, before, requestedMode, mode);
|
|
856
|
-
return renderWriteDiff(path, {
|
|
857
|
-
...before,
|
|
858
|
-
content: Buffer.from(previous, "utf8")
|
|
859
|
-
}, next, requestedMode, mode);
|
|
860
|
-
}
|
|
861
|
-
function redactAll(redactors, content) {
|
|
862
|
-
let projected = content;
|
|
863
|
-
for (const redact of redactors) {
|
|
864
|
-
const result = safeRedact(redact, projected);
|
|
865
|
-
if (result === void 0) return;
|
|
866
|
-
projected = result;
|
|
867
|
-
}
|
|
868
|
-
return projected;
|
|
759
|
+
function hashContent(content) {
|
|
760
|
+
return createHash("sha256").update(canonicalizeContent(content), "utf8").digest("hex");
|
|
869
761
|
}
|
|
870
|
-
function safeRedact(redact, content) {
|
|
871
|
-
try {
|
|
872
|
-
return redact(content);
|
|
873
|
-
} catch {
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
//#endregion
|
|
878
|
-
//#region ../core/src/managed-block/protocol.ts
|
|
879
|
-
/** Distribution-independent outer marker opening Aura-managed content. */
|
|
880
|
-
const AURA_MANAGED_BLOCK_BEGIN = "<!-- aura:begin -->";
|
|
881
|
-
/** Distribution-independent outer marker closing Aura-managed content. */
|
|
882
|
-
const AURA_MANAGED_BLOCK_END = "<!-- aura:end -->";
|
|
883
|
-
/** Prefix shared by every Aura snippet opening marker. */
|
|
884
|
-
const AURA_MANAGED_SNIPPET_BEGIN_PREFIX = "<!-- aura:begin id=";
|
|
885
|
-
/** Prefix shared by every Aura snippet closing marker. */
|
|
886
|
-
const AURA_MANAGED_SNIPPET_END_PREFIX = "<!-- aura:end id=";
|
|
887
|
-
/** Human-facing ownership warning rendered once inside the outer block. */
|
|
888
|
-
const AURA_MANAGED_BLOCK_NOTICE = "Managed by Aura. Edit via the Aura CLI; manual edits to this block are overwritten.";
|
|
889
762
|
const MANAGED_SNIPPET_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
890
|
-
const MANAGED_SNIPPET_HASH_PATTERN =
|
|
763
|
+
const MANAGED_SNIPPET_HASH_PATTERN = CONTENT_HASH_PATTERN;
|
|
891
764
|
/** Whether an id is both line-safe and legal inside an HTML comment. */
|
|
892
765
|
function isManagedSnippetId(id) {
|
|
893
766
|
return MANAGED_SNIPPET_ID_PATTERN.test(id) && !id.includes("--");
|
|
894
767
|
}
|
|
895
768
|
/**
|
|
896
|
-
*
|
|
769
|
+
* Computes the protocol hash for a snippet's canonical UTF-8 contents.
|
|
897
770
|
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
771
|
+
* The legacy block's hash is {@link hashContent} under another name: a marked section read back
|
|
772
|
+
* from an older file has to compare equal to the same text hashed anywhere else in Aura, or a
|
|
773
|
+
* migration would report drift on content nobody touched.
|
|
900
774
|
*/
|
|
901
|
-
function canonicalizeManagedSnippet(content) {
|
|
902
|
-
const normalized = content.replace(/\r\n?/g, "\n");
|
|
903
|
-
let end = normalized.length;
|
|
904
|
-
while (end > 0 && normalized[end - 1] === "\n") end -= 1;
|
|
905
|
-
return `${normalized.slice(0, end)}\n`;
|
|
906
|
-
}
|
|
907
|
-
/** Computes the protocol hash for text already in {@link canonicalizeManagedSnippet} form. */
|
|
908
|
-
function hashCanonicalManagedSnippet(canonical) {
|
|
909
|
-
return createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
910
|
-
}
|
|
911
|
-
/** Computes the protocol hash for a snippet's canonical UTF-8 contents. */
|
|
912
775
|
function hashManagedSnippet(content) {
|
|
913
|
-
return
|
|
776
|
+
return hashContent(content);
|
|
914
777
|
}
|
|
915
778
|
//#endregion
|
|
916
779
|
//#region ../core/src/managed-block/parse-state.ts
|
|
@@ -1132,324 +995,23 @@ function collectPlainLine(source, line, protectedByFence, block, snippet, notes)
|
|
|
1132
995
|
}));
|
|
1133
996
|
}
|
|
1134
997
|
//#endregion
|
|
1135
|
-
//#region ../core/src/managed-block/
|
|
1136
|
-
/**
|
|
1137
|
-
* Finds protocol markers the reader would honour, applying the same fence rules as
|
|
1138
|
-
* {@link readManagedBlock} so fenced marker examples stay ordinary text.
|
|
1139
|
-
*/
|
|
1140
|
-
function scanForMarkers(text) {
|
|
1141
|
-
let fence;
|
|
1142
|
-
let markerLine;
|
|
1143
|
-
for (const line of splitSourceLines$1(text)) {
|
|
1144
|
-
const previousFence = fence;
|
|
1145
|
-
fence = advanceMarkdownFence(line.text, fence);
|
|
1146
|
-
if (previousFence !== void 0 || fence !== void 0) continue;
|
|
1147
|
-
if (markerLine === void 0 && parseMarker(line.text) !== void 0) markerLine = line.number;
|
|
1148
|
-
}
|
|
1149
|
-
return {
|
|
1150
|
-
markerLine,
|
|
1151
|
-
unterminatedFence: fence !== void 0
|
|
1152
|
-
};
|
|
1153
|
-
}
|
|
998
|
+
//#region ../core/src/managed-block/strip.ts
|
|
1154
999
|
/**
|
|
1155
|
-
*
|
|
1156
|
-
* body reopens or closes the protocol, and the resulting file parses as invalid forever — so the
|
|
1157
|
-
* damage has to be caught before the write, not after.
|
|
1000
|
+
* Removes a legacy Aura-managed block from a source string, keeping everything else byte-for-byte.
|
|
1158
1001
|
*
|
|
1159
|
-
*
|
|
1160
|
-
*
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
const scan = scanForMarkers(canonical);
|
|
1164
|
-
if (scan.markerLine !== void 0) return Object.freeze([Object.freeze({
|
|
1165
|
-
code: "invalid-snippet-content",
|
|
1166
|
-
message: `Snippet "${id}" declares an Aura marker on content line ${String(scan.markerLine)}. Wrap marker examples in a Markdown fence.`
|
|
1167
|
-
})]);
|
|
1168
|
-
if (scan.unterminatedFence) return Object.freeze([Object.freeze({
|
|
1169
|
-
code: "invalid-snippet-content",
|
|
1170
|
-
message: `Snippet "${id}" ends inside an unclosed Markdown fence, which would hide every marker after it. Close the fence.`
|
|
1171
|
-
})]);
|
|
1172
|
-
return Object.freeze([]);
|
|
1173
|
-
}
|
|
1174
|
-
/**
|
|
1175
|
-
* Drops every protocol line the reader honours, plus the notice that follows an opening marker,
|
|
1176
|
-
* leaving handwritten text byte-for-byte. Fenced marker examples survive because the reader never
|
|
1177
|
-
* treated them as protocol in the first place.
|
|
1178
|
-
*/
|
|
1179
|
-
function stripManagedMarkers(source) {
|
|
1180
|
-
const kept = [];
|
|
1181
|
-
let fence;
|
|
1182
|
-
let afterBlockBegin = false;
|
|
1183
|
-
for (const line of splitSourceLines$1(source)) {
|
|
1184
|
-
const previousFence = fence;
|
|
1185
|
-
fence = advanceMarkdownFence(line.text, fence);
|
|
1186
|
-
const marker = previousFence !== void 0 || fence !== void 0 ? void 0 : parseMarker(line.text);
|
|
1187
|
-
if (marker !== void 0) {
|
|
1188
|
-
afterBlockBegin = marker.kind === "block-begin";
|
|
1189
|
-
continue;
|
|
1190
|
-
}
|
|
1191
|
-
if (afterBlockBegin && line.text === "Managed by Aura. Edit via the Aura CLI; manual edits to this block are overwritten.") {
|
|
1192
|
-
afterBlockBegin = false;
|
|
1193
|
-
continue;
|
|
1194
|
-
}
|
|
1195
|
-
afterBlockBegin = false;
|
|
1196
|
-
kept.push(source.slice(line.start, line.end));
|
|
1197
|
-
}
|
|
1198
|
-
return kept.join("");
|
|
1199
|
-
}
|
|
1200
|
-
//#endregion
|
|
1201
|
-
//#region ../core/src/managed-block/reconcile-snippet.ts
|
|
1202
|
-
/** Reconciles exactly one snippet while preserving every other source byte. */
|
|
1203
|
-
function reconcileManagedSnippet(source, snippetId, resolution) {
|
|
1204
|
-
const current = readManagedBlock(source);
|
|
1205
|
-
if (current.status === "invalid") return invalidResult$1(source, current.notes, current.problems);
|
|
1206
|
-
if (current.status === "absent") return missingSnippet(source, current.notes, snippetId, "an Aura-managed block");
|
|
1207
|
-
const snippet = current.block.snippets.find((candidate) => candidate.id === snippetId);
|
|
1208
|
-
if (snippet === void 0) return missingSnippet(source, current.notes, snippetId, "the Aura-managed block");
|
|
1209
|
-
const lineEnding = markerLineEnding(source, snippet.startOffset, snippet.contentStartOffset);
|
|
1210
|
-
const canonical = resolution.kind === "restore" ? canonicalizeManagedSnippet(resolution.content) : void 0;
|
|
1211
|
-
if (canonical !== void 0) {
|
|
1212
|
-
const problems = managedSnippetContentProblems(snippet.id, canonical);
|
|
1213
|
-
if (problems.length > 0) return invalidResult$1(source, current.notes, problems);
|
|
1214
|
-
}
|
|
1215
|
-
const content = canonical === void 0 ? snippet.content : withLineEnding(canonical, lineEnding);
|
|
1216
|
-
const hash = canonical === void 0 ? snippet.computedHash : hashCanonicalManagedSnippet(canonical);
|
|
1217
|
-
const opening = `${AURA_MANAGED_SNIPPET_BEGIN_PREFIX}${snippet.id} sha256=${hash} -->${lineEnding}`;
|
|
1218
|
-
const updated = source.slice(0, snippet.startOffset) + opening + content + source.slice(snippet.contentEndOffset);
|
|
1219
|
-
return updated === source ? Object.freeze({
|
|
1220
|
-
content: source,
|
|
1221
|
-
notes: current.notes,
|
|
1222
|
-
status: "unchanged"
|
|
1223
|
-
}) : Object.freeze({
|
|
1224
|
-
content: updated,
|
|
1225
|
-
notes: current.notes,
|
|
1226
|
-
status: "updated"
|
|
1227
|
-
});
|
|
1228
|
-
}
|
|
1229
|
-
/** Creates the opt-in edited-versus-canonical detail used by the merge choice. */
|
|
1230
|
-
function diffManagedSnippet(path, editedContent, canonicalContent) {
|
|
1231
|
-
return createTwoFilesPatch(`${path} (edited)`, `${path} (canonical)`, canonicalizeManagedSnippet(editedContent), canonicalizeManagedSnippet(canonicalContent), "edited", "canonical", { context: 3 });
|
|
1232
|
-
}
|
|
1233
|
-
function missingSnippet(source, notes, snippetId, location) {
|
|
1234
|
-
return invalidResult$1(source, notes, [Object.freeze({
|
|
1235
|
-
code: "missing-snippet",
|
|
1236
|
-
message: `Snippet "${snippetId}" does not exist in ${location}.`
|
|
1237
|
-
})]);
|
|
1238
|
-
}
|
|
1239
|
-
function invalidResult$1(source, notes, problems) {
|
|
1240
|
-
return Object.freeze({
|
|
1241
|
-
content: source,
|
|
1242
|
-
notes,
|
|
1243
|
-
problems,
|
|
1244
|
-
status: "invalid"
|
|
1245
|
-
});
|
|
1246
|
-
}
|
|
1247
|
-
function markerLineEnding(source, startOffset, contentStartOffset) {
|
|
1248
|
-
return source.slice(startOffset, contentStartOffset).endsWith("\r\n") ? "\r\n" : "\n";
|
|
1249
|
-
}
|
|
1250
|
-
function withLineEnding(content, lineEnding) {
|
|
1251
|
-
return lineEnding === "\n" ? content : content.replaceAll("\n", lineEnding);
|
|
1252
|
-
}
|
|
1253
|
-
//#endregion
|
|
1254
|
-
//#region ../core/src/managed-block/reconcile-desired.ts
|
|
1255
|
-
function prepareDesiredSnippets(snippets, options) {
|
|
1256
|
-
const ids = /* @__PURE__ */ new Set();
|
|
1257
|
-
const preserved = new Set(options.preserveSnippetIds ?? []);
|
|
1258
|
-
const prepared = [];
|
|
1259
|
-
const problems = [];
|
|
1260
|
-
for (const snippet of snippets) {
|
|
1261
|
-
if (!isManagedSnippetId(snippet.id)) problems.push(Object.freeze({
|
|
1262
|
-
code: "invalid-snippet-id",
|
|
1263
|
-
message: `Snippet ID "${snippet.id}" is not safe inside an HTML comment marker.`
|
|
1264
|
-
}));
|
|
1265
|
-
if (ids.has(snippet.id)) problems.push(Object.freeze({
|
|
1266
|
-
code: "duplicate-snippet",
|
|
1267
|
-
message: `Desired snippet ID "${snippet.id}" appears more than once.`
|
|
1268
|
-
}));
|
|
1269
|
-
if (preserved.has(snippet.id)) problems.push(Object.freeze({
|
|
1270
|
-
code: "duplicate-snippet",
|
|
1271
|
-
message: `Snippet ID "${snippet.id}" is both desired and preserved; writing both would duplicate it.`
|
|
1272
|
-
}));
|
|
1273
|
-
ids.add(snippet.id);
|
|
1274
|
-
const canonical = canonicalizeManagedSnippet(snippet.content);
|
|
1275
|
-
problems.push(...managedSnippetContentProblems(snippet.id, canonical));
|
|
1276
|
-
prepared.push({
|
|
1277
|
-
canonical,
|
|
1278
|
-
hash: hashCanonicalManagedSnippet(canonical),
|
|
1279
|
-
id: snippet.id,
|
|
1280
|
-
kind: "desired"
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
return {
|
|
1284
|
-
prepared,
|
|
1285
|
-
problems: Object.freeze(problems)
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
//#endregion
|
|
1289
|
-
//#region ../core/src/managed-block/reconcile-ledger.ts
|
|
1290
|
-
function renderLedgerSnippets(source, block, desired, options) {
|
|
1291
|
-
if (block === void 0 || options.ownedSnippetIds === void 0) return desired;
|
|
1292
|
-
const controlled = controlledSnippetIds(desired, options);
|
|
1293
|
-
const preserved = new Set(options.preserveSnippetIds ?? []);
|
|
1294
|
-
const remaining = [...desired];
|
|
1295
|
-
const rendered = [];
|
|
1296
|
-
for (const snippet of block.snippets) {
|
|
1297
|
-
if (preserved.has(snippet.id) || !controlled.has(snippet.id)) {
|
|
1298
|
-
rendered.push({
|
|
1299
|
-
id: snippet.id,
|
|
1300
|
-
kind: "preserved",
|
|
1301
|
-
raw: source.slice(snippet.startOffset, snippet.endOffset)
|
|
1302
|
-
});
|
|
1303
|
-
continue;
|
|
1304
|
-
}
|
|
1305
|
-
const replacement = remaining.shift();
|
|
1306
|
-
if (replacement !== void 0) rendered.push(replacement);
|
|
1307
|
-
}
|
|
1308
|
-
rendered.push(...remaining);
|
|
1309
|
-
return rendered;
|
|
1310
|
-
}
|
|
1311
|
-
function controlledSnippetIds(desired, options) {
|
|
1312
|
-
return /* @__PURE__ */ new Set([...options.ownedSnippetIds ?? [], ...desired.map((snippet) => snippet.id)]);
|
|
1313
|
-
}
|
|
1314
|
-
function preservedUnownedNotes(block, desired, options) {
|
|
1315
|
-
if (block === void 0 || options.ownedSnippetIds === void 0) return [];
|
|
1316
|
-
const controlled = controlledSnippetIds(desired, options);
|
|
1317
|
-
return block.snippets.filter((snippet) => !controlled.has(snippet.id)).map((snippet) => Object.freeze({
|
|
1318
|
-
code: "preserved-unowned-snippet",
|
|
1319
|
-
line: snippet.startLine,
|
|
1320
|
-
message: `Snippet "${snippet.id}" is not recorded in Aura's manifest; its section was preserved.`
|
|
1321
|
-
}));
|
|
1322
|
-
}
|
|
1323
|
-
//#endregion
|
|
1324
|
-
//#region ../core/src/managed-block/reconcile.ts
|
|
1325
|
-
/**
|
|
1326
|
-
* Reconciles one source string against the complete ordered set of desired snippets.
|
|
1002
|
+
* For a consumer deciding what a file's *user-authored* content is — instruction consolidation
|
|
1003
|
+
* above all — the managed block is Aura's own artifact: merging it elsewhere plants links and
|
|
1004
|
+
* legacy ledger sections in files that must not carry them. Unmanaged lines found inside the block
|
|
1005
|
+
* are user text and are kept.
|
|
1327
1006
|
*
|
|
1328
|
-
*
|
|
1329
|
-
*
|
|
1330
|
-
*
|
|
1331
|
-
* the original source unchanged unless the caller opts into `onInvalid: "repair"`.
|
|
1007
|
+
* A source with no block, and one whose block does not parse, are returned unchanged: a malformed
|
|
1008
|
+
* block cannot be attributed to Aura with confidence, and dropping bytes on that guess would lose
|
|
1009
|
+
* user content.
|
|
1332
1010
|
*/
|
|
1333
|
-
function
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
* {@link reconcileManagedBlock}, for a caller that already parsed `source`.
|
|
1338
|
-
*
|
|
1339
|
-
* `current` must be `readManagedBlock(source)` for the same string; passing a result read from
|
|
1340
|
-
* anything else splices content at offsets that no longer describe the source.
|
|
1341
|
-
*/
|
|
1342
|
-
function reconcileParsedManagedBlock(source, current, desiredSnippets, options = {}) {
|
|
1343
|
-
const desired = prepareDesiredSnippets(desiredSnippets, options);
|
|
1344
|
-
if (desired.problems.length > 0) return invalidResult(source, current.notes, desired.problems);
|
|
1345
|
-
const hiddenMarker = current.notes.find((note) => note.code === "unterminated-fence");
|
|
1346
|
-
if (hiddenMarker !== void 0) return invalidResult(source, current.notes, [Object.freeze({
|
|
1347
|
-
code: "unterminated-fence",
|
|
1348
|
-
line: hiddenMarker.line,
|
|
1349
|
-
message: hiddenMarker.message
|
|
1350
|
-
})]);
|
|
1351
|
-
if (current.status !== "invalid") {
|
|
1352
|
-
const block = current.status === "present" ? current.block : void 0;
|
|
1353
|
-
const rendered = renderLedgerSnippets(source, block, desired.prepared, options);
|
|
1354
|
-
const notes = [
|
|
1355
|
-
...current.notes,
|
|
1356
|
-
...overwrittenNotes(current, desired.prepared, options),
|
|
1357
|
-
...preservedUnownedNotes(block, desired.prepared, options)
|
|
1358
|
-
];
|
|
1359
|
-
return settle(source, buildContent(source, rendered, current), notes);
|
|
1360
|
-
}
|
|
1361
|
-
if (options.onInvalid !== "repair") return invalidResult(source, current.notes, current.problems);
|
|
1362
|
-
if (options.ownedSnippetIds !== void 0) return invalidResult(source, current.notes, current.problems);
|
|
1363
|
-
const stripped = stripManagedMarkers(source);
|
|
1364
|
-
const repaired = readManagedBlock(stripped);
|
|
1365
|
-
if (repaired.status === "invalid") return invalidResult(source, current.notes, current.problems);
|
|
1366
|
-
const notes = [...current.notes, ...repairNotes(current.problems)];
|
|
1367
|
-
return settle(source, buildContent(stripped, desired.prepared, repaired), notes);
|
|
1368
|
-
}
|
|
1369
|
-
function buildContent(source, prepared, current) {
|
|
1370
|
-
if (prepared.length === 0) return current.status === "absent" ? source : source.slice(0, current.block.startOffset) + current.block.unmanagedContent + source.slice(current.block.endOffset);
|
|
1371
|
-
const lineEnding = detectLineEnding$1(source);
|
|
1372
|
-
if (current.status === "absent") return `${source}${source.length === 0 || source.endsWith("\n") ? "" : lineEnding}${renderManagedBlock(prepared, lineEnding, true)}`;
|
|
1373
|
-
const replacement = renderManagedBlock(prepared, lineEnding, source[current.block.endOffset - 1] === "\n" || current.block.unmanagedContent.length > 0);
|
|
1374
|
-
if (current.block.unmanagedContent.length === 0 && replacement === source.slice(current.block.startOffset, current.block.endOffset)) return source;
|
|
1375
|
-
return source.slice(0, current.block.startOffset) + replacement + current.block.unmanagedContent + source.slice(current.block.endOffset);
|
|
1376
|
-
}
|
|
1377
|
-
function renderManagedBlock(snippets, lineEnding, endsWithLineEnding) {
|
|
1378
|
-
const parts = [
|
|
1379
|
-
AURA_MANAGED_BLOCK_BEGIN,
|
|
1380
|
-
lineEnding,
|
|
1381
|
-
AURA_MANAGED_BLOCK_NOTICE,
|
|
1382
|
-
lineEnding
|
|
1383
|
-
];
|
|
1384
|
-
for (const snippet of snippets) {
|
|
1385
|
-
if (snippet.kind === "preserved") {
|
|
1386
|
-
parts.push(snippet.raw);
|
|
1387
|
-
continue;
|
|
1388
|
-
}
|
|
1389
|
-
const content = lineEnding === "\n" ? snippet.canonical : snippet.canonical.replaceAll("\n", lineEnding);
|
|
1390
|
-
parts.push(`${AURA_MANAGED_SNIPPET_BEGIN_PREFIX}${snippet.id} sha256=${snippet.hash} -->`, lineEnding, content, `${AURA_MANAGED_SNIPPET_END_PREFIX}${snippet.id} -->`, lineEnding);
|
|
1391
|
-
}
|
|
1392
|
-
parts.push(AURA_MANAGED_BLOCK_END);
|
|
1393
|
-
if (endsWithLineEnding) parts.push(lineEnding);
|
|
1394
|
-
return parts.join("");
|
|
1395
|
-
}
|
|
1396
|
-
function overwrittenNotes(current, desired, options) {
|
|
1397
|
-
if (current.status === "absent") return [];
|
|
1398
|
-
const controlled = controlledSnippetIds(desired, options);
|
|
1399
|
-
const desiredById = new Map(desired.map((snippet) => [snippet.id, snippet]));
|
|
1400
|
-
const preserved = new Set(options.preserveSnippetIds ?? []);
|
|
1401
|
-
return current.block.snippets.flatMap((snippet) => {
|
|
1402
|
-
if (options.ownedSnippetIds !== void 0 && !controlled.has(snippet.id) || preserved.has(snippet.id) || !handEdited(snippet, options)) return [];
|
|
1403
|
-
const replacement = desiredById.get(snippet.id);
|
|
1404
|
-
if (replacement === void 0) return [Object.freeze({
|
|
1405
|
-
code: "removed-snippet",
|
|
1406
|
-
line: snippet.startLine,
|
|
1407
|
-
message: `Snippet "${snippet.id}" was edited by hand since Aura wrote it; the edit is being removed.`
|
|
1408
|
-
})];
|
|
1409
|
-
if (replacement.hash === snippet.computedHash) return [];
|
|
1410
|
-
return [Object.freeze({
|
|
1411
|
-
code: "overwritten-snippet",
|
|
1412
|
-
line: snippet.startLine,
|
|
1413
|
-
message: `Snippet "${snippet.id}" was edited by hand since Aura wrote it; the edit is being replaced.`
|
|
1414
|
-
})];
|
|
1415
|
-
});
|
|
1416
|
-
}
|
|
1417
|
-
/**
|
|
1418
|
-
* Whether a section differs from what Aura last recorded for it.
|
|
1419
|
-
*
|
|
1420
|
-
* The marker hash is written by whoever wrote the marker, so a re-stamped section certifies itself
|
|
1421
|
-
* and `hashMatches` alone cannot tell a hand edit from a catalog upgrade. The manifest is the only
|
|
1422
|
-
* record the editor did not control; fall back to the marker only when there is no manifest entry.
|
|
1423
|
-
*/
|
|
1424
|
-
function handEdited(snippet, options) {
|
|
1425
|
-
const previousHash = options.previousSnippetHashes?.get(snippet.id);
|
|
1426
|
-
return previousHash === void 0 ? !snippet.hashMatches : previousHash !== snippet.computedHash;
|
|
1427
|
-
}
|
|
1428
|
-
function repairNotes(problems) {
|
|
1429
|
-
return problems.map((problem) => Object.freeze({
|
|
1430
|
-
code: "repaired-invalid-block",
|
|
1431
|
-
line: problem.line,
|
|
1432
|
-
message: `Rebuilt the managed block to repair: ${problem.message}`
|
|
1433
|
-
}));
|
|
1434
|
-
}
|
|
1435
|
-
function invalidResult(source, notes, problems) {
|
|
1436
|
-
return Object.freeze({
|
|
1437
|
-
content: source,
|
|
1438
|
-
notes: Object.freeze([...notes]),
|
|
1439
|
-
problems,
|
|
1440
|
-
status: "invalid"
|
|
1441
|
-
});
|
|
1442
|
-
}
|
|
1443
|
-
function settle(source, content, notes) {
|
|
1444
|
-
return content === source ? Object.freeze({
|
|
1445
|
-
content: source,
|
|
1446
|
-
notes: Object.freeze([...notes]),
|
|
1447
|
-
status: "unchanged"
|
|
1448
|
-
}) : Object.freeze({
|
|
1449
|
-
content,
|
|
1450
|
-
notes: Object.freeze([...notes]),
|
|
1451
|
-
status: "updated"
|
|
1452
|
-
});
|
|
1011
|
+
function stripLegacyManagedBlock(source) {
|
|
1012
|
+
const parsed = readManagedBlock(source);
|
|
1013
|
+
if (parsed.status !== "present") return source;
|
|
1014
|
+
return source.slice(0, parsed.block.startOffset) + parsed.block.unmanagedContent + source.slice(parsed.block.endOffset);
|
|
1453
1015
|
}
|
|
1454
1016
|
//#endregion
|
|
1455
1017
|
//#region ../core/src/managed-content/revision.ts
|
|
@@ -1580,210 +1142,6 @@ function convergedPlan$1(app, skillId) {
|
|
|
1580
1142
|
};
|
|
1581
1143
|
}
|
|
1582
1144
|
//#endregion
|
|
1583
|
-
//#region ../core/src/workspace/mcp-classify.ts
|
|
1584
|
-
/**
|
|
1585
|
-
* Splits desired servers into the ones Aura may write and the collisions a person has to settle.
|
|
1586
|
-
*
|
|
1587
|
-
* Scope is part of identity here. A server named `docs` in a project `.mcp.json` is not the `docs`
|
|
1588
|
-
* the manifest wants in user-level configuration, and treating them as one either blocks a write
|
|
1589
|
-
* that would not have collided or skips one that never happened.
|
|
1590
|
-
*/
|
|
1591
|
-
function classifyDesired(desired, ledgerNames, state) {
|
|
1592
|
-
const ledger = new Set(ledgerNames);
|
|
1593
|
-
const owned = [];
|
|
1594
|
-
const blockers = [];
|
|
1595
|
-
for (const entry of desired) {
|
|
1596
|
-
const blocker = collisionBlocker(entry, ledger, state);
|
|
1597
|
-
if (blocker === "owned") owned.push(entry);
|
|
1598
|
-
else if (blocker !== void 0) blockers.push(blocker);
|
|
1599
|
-
}
|
|
1600
|
-
return {
|
|
1601
|
-
blockers,
|
|
1602
|
-
owned
|
|
1603
|
-
};
|
|
1604
|
-
}
|
|
1605
|
-
/** `owned` to write it, a blocker to refuse, `undefined` when the config already satisfies it. */
|
|
1606
|
-
function collisionBlocker(entry, ledger, state) {
|
|
1607
|
-
if (ledger.has(entry.name)) return "owned";
|
|
1608
|
-
const sameName = (candidate) => candidate.name === entry.name && candidate.scope === entry.scope;
|
|
1609
|
-
const unusable = state.unusable.find(sameName);
|
|
1610
|
-
if (unusable !== void 0) return {
|
|
1611
|
-
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.`,
|
|
1612
|
-
scope: entry.scope,
|
|
1613
|
-
sourceId: unusable.sourceId
|
|
1614
|
-
};
|
|
1615
|
-
const existing = state.servers.filter(sameName);
|
|
1616
|
-
if (existing.length === 0) return "owned";
|
|
1617
|
-
const normalized = normalizeDesired(entry);
|
|
1618
|
-
if ("message" in normalized) return normalized;
|
|
1619
|
-
return existing.every((server) => isDeepStrictEqual(server.transport, normalized.transport)) ? void 0 : {
|
|
1620
|
-
message: `MCP server ${entry.name} already exists outside Aura's ownership ledger and differs from the manifest.`,
|
|
1621
|
-
scope: entry.scope
|
|
1622
|
-
};
|
|
1623
|
-
}
|
|
1624
|
-
/**
|
|
1625
|
-
* Normalizes one desired transport, reporting a manifest Aura refuses to write as a blocker.
|
|
1626
|
-
*
|
|
1627
|
-
* The manifest is a file a person can edit. One that has acquired a credential literal is not a
|
|
1628
|
-
* crash in a check's `detect`; it is something to say out loud and decline to propagate.
|
|
1629
|
-
*/
|
|
1630
|
-
function normalizeDesired(entry) {
|
|
1631
|
-
try {
|
|
1632
|
-
return { transport: normalizeMcpServerDefinition(entry.transport) };
|
|
1633
|
-
} catch (error) {
|
|
1634
|
-
return {
|
|
1635
|
-
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.`,
|
|
1636
|
-
scope: entry.scope
|
|
1637
|
-
};
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
//#endregion
|
|
1641
|
-
//#region ../core/src/workspace/mcp-convergence.ts
|
|
1642
|
-
/** Captures adapter source bytes behind a function, keeping them out of the model entirely. */
|
|
1643
|
-
function createAppMcpConvergence(adapter, files, state) {
|
|
1644
|
-
const writer = adapter.mcpWrite;
|
|
1645
|
-
if (writer === void 0) return;
|
|
1646
|
-
const targets = [...files.values()].filter((file) => file.spec.kind === "mcp");
|
|
1647
|
-
return (desired, ledgerNames) => {
|
|
1648
|
-
const classified = classifyDesired(desired, ledgerNames, state);
|
|
1649
|
-
if (classified.blockers.length > 0) return blocked(classified.blockers);
|
|
1650
|
-
const scopes = ["global", "project"].map((scope) => planScope(adapter, writer, targets, classified.owned, ledgerNames, scope, state.secretSightings));
|
|
1651
|
-
const blockers = scopes.flatMap((result) => result.blockers);
|
|
1652
|
-
const operations = scopes.flatMap((result) => result.operations);
|
|
1653
|
-
return blockers.length > 0 ? blocked(blockers) : {
|
|
1654
|
-
blockers: [],
|
|
1655
|
-
operations,
|
|
1656
|
-
ownedNames: [...new Set(classified.owned.map((entry) => entry.name))].sort()
|
|
1657
|
-
};
|
|
1658
|
-
};
|
|
1659
|
-
}
|
|
1660
|
-
function planScope(adapter, writer, targets, desired, ledgerNames, scope, sightings) {
|
|
1661
|
-
const scopedDesired = desired.filter((entry) => entry.scope === scope);
|
|
1662
|
-
const scopedTargets = targets.filter((target) => target.spec.scope === scope);
|
|
1663
|
-
if (scopedDesired.length > 0 && scopedTargets.length === 0) return blockedScope(`${adapter.displayName} has no ${scope}-scope MCP configuration target.`, scope);
|
|
1664
|
-
if (scopedTargets.length > 1) return blockedScope(`${adapter.displayName} declares more than one ${scope}-scope MCP write target.`, scope);
|
|
1665
|
-
const target = scopedTargets[0];
|
|
1666
|
-
return target === void 0 ? {
|
|
1667
|
-
blockers: [],
|
|
1668
|
-
operations: []
|
|
1669
|
-
} : writeScopeTarget(adapter, writer, target, scopedDesired, ledgerNames, scope, sightings);
|
|
1670
|
-
}
|
|
1671
|
-
function writeScopeTarget(adapter, writer, target, desired, ledgerNames, scope, sightings) {
|
|
1672
|
-
if (!needsTargetWrite(target, desired, ledgerNames)) return {
|
|
1673
|
-
blockers: [],
|
|
1674
|
-
operations: []
|
|
1675
|
-
};
|
|
1676
|
-
const unwritable = targetRefusal(target);
|
|
1677
|
-
if (unwritable !== void 0) return {
|
|
1678
|
-
blockers: [{
|
|
1679
|
-
...unwritable,
|
|
1680
|
-
scope,
|
|
1681
|
-
sourceId: target.spec.id
|
|
1682
|
-
}],
|
|
1683
|
-
operations: []
|
|
1684
|
-
};
|
|
1685
|
-
const written = runWriter(writer, target, desired, ledgerNames);
|
|
1686
|
-
if ("refusal" in written) return {
|
|
1687
|
-
blockers: [{
|
|
1688
|
-
message: written.refusal,
|
|
1689
|
-
path: target.spec.path,
|
|
1690
|
-
scope,
|
|
1691
|
-
sourceId: target.spec.id
|
|
1692
|
-
}],
|
|
1693
|
-
operations: []
|
|
1694
|
-
};
|
|
1695
|
-
if (Buffer.byteLength(written.content, "utf8") > 4194304) return {
|
|
1696
|
-
blockers: [{
|
|
1697
|
-
message: `${adapter.displayName}'s MCP configuration at ${target.spec.path} is larger than Aura will rewrite in one operation, so Aura left it unchanged.`,
|
|
1698
|
-
path: target.spec.path,
|
|
1699
|
-
scope,
|
|
1700
|
-
sourceId: target.spec.id
|
|
1701
|
-
}],
|
|
1702
|
-
operations: []
|
|
1703
|
-
};
|
|
1704
|
-
const operation = {
|
|
1705
|
-
content: written.content,
|
|
1706
|
-
mode: scope === "global" ? 384 : 420,
|
|
1707
|
-
path: target.spec.path,
|
|
1708
|
-
precondition: targetPrecondition(target),
|
|
1709
|
-
type: "write"
|
|
1710
|
-
};
|
|
1711
|
-
rememberMcpRedactor(adapter, operation, target, sightings);
|
|
1712
|
-
return {
|
|
1713
|
-
blockers: [],
|
|
1714
|
-
operations: [operation]
|
|
1715
|
-
};
|
|
1716
|
-
}
|
|
1717
|
-
/**
|
|
1718
|
-
* Registers preview masking only for a target that actually holds an inline credential.
|
|
1719
|
-
*
|
|
1720
|
-
* Registering unconditionally would cost every convergence preview its diff, because masking has
|
|
1721
|
-
* no previous side to project when the fix is creating the file — which is what most MCP-001 fixes
|
|
1722
|
-
* do, on a file that by definition has no credentials in it yet.
|
|
1723
|
-
*/
|
|
1724
|
-
function rememberMcpRedactor(adapter, operation, target, sightings) {
|
|
1725
|
-
const transform = adapter.mcpSecrets;
|
|
1726
|
-
const scoped = sightings.filter((sighting) => sighting.sourceId === target.spec.id);
|
|
1727
|
-
if (transform === void 0 || scoped.length === 0) return;
|
|
1728
|
-
rememberWriteRedactor(operation, mcpSecretRedactor(transform, scoped));
|
|
1729
|
-
}
|
|
1730
|
-
/** Runs a plugin serializer, treating a thrown error as the refusal it forgot to return. */
|
|
1731
|
-
function runWriter(writer, target, desired, ledgerNames) {
|
|
1732
|
-
try {
|
|
1733
|
-
return writer({
|
|
1734
|
-
desired,
|
|
1735
|
-
...target.content === void 0 ? {} : { existingContent: target.content },
|
|
1736
|
-
ledgerNames
|
|
1737
|
-
});
|
|
1738
|
-
} catch {
|
|
1739
|
-
return { refusal: "The MCP configuration serializer failed, so Aura left the file unchanged." };
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
/**
|
|
1743
|
-
* What the file must still look like when the plan is applied.
|
|
1744
|
-
*
|
|
1745
|
-
* The rendered content is this file's own previous bytes with one section replaced, so applying it
|
|
1746
|
-
* against a file the application has since rewritten would silently revert that work. Declaring
|
|
1747
|
-
* what was read turns the race into a reported conflict.
|
|
1748
|
-
*/
|
|
1749
|
-
function targetPrecondition(target) {
|
|
1750
|
-
return target.content === void 0 ? { kind: "absent" } : {
|
|
1751
|
-
digest: createHash("sha256").update(target.content, "utf8").digest("hex"),
|
|
1752
|
-
kind: "sha256"
|
|
1753
|
-
};
|
|
1754
|
-
}
|
|
1755
|
-
function needsTargetWrite(target, desired, ledgerNames) {
|
|
1756
|
-
return (desired.length > 0 || ledgerNames.length > 0) && (target.exists || desired.length > 0);
|
|
1757
|
-
}
|
|
1758
|
-
/** Why this target cannot be rewritten safely, before a serializer is asked to try. */
|
|
1759
|
-
function targetRefusal(target) {
|
|
1760
|
-
const path = target.spec.path;
|
|
1761
|
-
if (target.problem !== void 0 || target.exists && target.content === void 0) return {
|
|
1762
|
-
message: `MCP configuration at ${path} could not be read safely, so Aura left it unchanged.`,
|
|
1763
|
-
path
|
|
1764
|
-
};
|
|
1765
|
-
return (target.size ?? 0) > 4194304 ? {
|
|
1766
|
-
message: `MCP configuration at ${path} is larger than Aura will rewrite in one operation, so Aura left it unchanged.`,
|
|
1767
|
-
path
|
|
1768
|
-
} : void 0;
|
|
1769
|
-
}
|
|
1770
|
-
function blockedScope(message, scope) {
|
|
1771
|
-
return {
|
|
1772
|
-
blockers: [{
|
|
1773
|
-
message,
|
|
1774
|
-
scope
|
|
1775
|
-
}],
|
|
1776
|
-
operations: []
|
|
1777
|
-
};
|
|
1778
|
-
}
|
|
1779
|
-
function blocked(blockers) {
|
|
1780
|
-
return {
|
|
1781
|
-
blockers,
|
|
1782
|
-
operations: [],
|
|
1783
|
-
ownedNames: []
|
|
1784
|
-
};
|
|
1785
|
-
}
|
|
1786
|
-
//#endregion
|
|
1787
1145
|
//#region ../core/src/workspace/mcp-plan-manifest.ts
|
|
1788
1146
|
/**
|
|
1789
1147
|
* Merges preset-required servers under the manifest's own, keyed by scope and name.
|
|
@@ -1842,12 +1200,16 @@ function withoutServer(manifest, server) {
|
|
|
1842
1200
|
* association lives here instead of on the object: reaching a planner takes this module, which
|
|
1843
1201
|
* plugin code cannot import.
|
|
1844
1202
|
*/
|
|
1845
|
-
const PLANNERS
|
|
1203
|
+
const PLANNERS = /* @__PURE__ */ new WeakMap();
|
|
1846
1204
|
/** Per-scan memo of what convergence planning produced, keyed by the model it was computed from. */
|
|
1847
|
-
const PLANS
|
|
1205
|
+
const PLANS = /* @__PURE__ */ new WeakMap();
|
|
1848
1206
|
/** Associates a planner with the app model core just built for it. */
|
|
1849
1207
|
function rememberMcpConvergence(app, convergence) {
|
|
1850
|
-
PLANNERS
|
|
1208
|
+
PLANNERS.set(app, convergence);
|
|
1209
|
+
}
|
|
1210
|
+
/** Forgets one scan's memoized plans, after a refresh replaced the bytes they were built from. */
|
|
1211
|
+
function forgetMcpPlans(model) {
|
|
1212
|
+
PLANS.delete(model);
|
|
1851
1213
|
}
|
|
1852
1214
|
/**
|
|
1853
1215
|
* Reports only why convergence is impossible, without rendering any file.
|
|
@@ -1861,8 +1223,8 @@ function mcpConvergenceBlockers(model, appId) {
|
|
|
1861
1223
|
}
|
|
1862
1224
|
/** Builds application writes and the ownership-ledger update as one atomic fix plan. */
|
|
1863
1225
|
function planManifestMcpConvergence(model, appId) {
|
|
1864
|
-
const memo = PLANS
|
|
1865
|
-
PLANS
|
|
1226
|
+
const memo = PLANS.get(model) ?? /* @__PURE__ */ new Map();
|
|
1227
|
+
PLANS.set(model, memo);
|
|
1866
1228
|
const cached = memo.get(appId);
|
|
1867
1229
|
if (cached !== void 0) return cached;
|
|
1868
1230
|
const computed = computeConvergence(model, appId);
|
|
@@ -1978,103 +1340,53 @@ function ambiguousRemoval(model, server) {
|
|
|
1978
1340
|
function resolvePlanner(model, appId) {
|
|
1979
1341
|
const app = model.apps.find((candidate) => candidate.adapterId === appId);
|
|
1980
1342
|
if (app === void 0) return { blockers: [{ message: `Application ${appId} was not detected.` }] };
|
|
1981
|
-
const convergence = PLANNERS
|
|
1343
|
+
const convergence = PLANNERS.get(app);
|
|
1982
1344
|
return convergence === void 0 ? { blockers: [{ message: `${app.displayName}'s adapter cannot write MCP configuration.` }] } : {
|
|
1983
1345
|
app,
|
|
1984
1346
|
convergence
|
|
1985
1347
|
};
|
|
1986
1348
|
}
|
|
1987
1349
|
//#endregion
|
|
1988
|
-
//#region ../core/src/workspace/
|
|
1989
|
-
const PLANNERS = /* @__PURE__ */ new WeakMap();
|
|
1990
|
-
const PLANS = /* @__PURE__ */ new WeakMap();
|
|
1991
|
-
/** Captures raw MCP bytes for remediation behind the same private boundary as convergence. */
|
|
1992
|
-
function createAppMcpSecretPlanner(adapter, files, sightings) {
|
|
1993
|
-
const transform = adapter.mcpSecrets;
|
|
1994
|
-
if (transform === void 0) return;
|
|
1995
|
-
return {
|
|
1996
|
-
plan: (sourceId) => {
|
|
1997
|
-
const target = files.get(sourceId);
|
|
1998
|
-
const sourceSightings = sightings.filter((sighting) => sighting.sourceId === sourceId);
|
|
1999
|
-
const supported = sourceSightings.filter(transform.supports);
|
|
2000
|
-
if (target === void 0 || target.spec.kind !== "mcp" || target.content === void 0 || targetRefusal(target) !== void 0 || supported.length === 0) return;
|
|
2001
|
-
let rewritten;
|
|
2002
|
-
try {
|
|
2003
|
-
rewritten = transform.rewrite({
|
|
2004
|
-
content: target.content,
|
|
2005
|
-
sightings: supported
|
|
2006
|
-
});
|
|
2007
|
-
} catch {
|
|
2008
|
-
return;
|
|
2009
|
-
}
|
|
2010
|
-
if ("refusal" in rewritten || rewritten.rewrittenFields.length !== supported.length) return;
|
|
2011
|
-
if (Buffer.byteLength(rewritten.content, "utf8") > 4194304) return;
|
|
2012
|
-
const operation = {
|
|
2013
|
-
content: rewritten.content,
|
|
2014
|
-
mode: target.spec.scope === "global" ? 384 : 420,
|
|
2015
|
-
path: target.spec.path,
|
|
2016
|
-
precondition: targetPrecondition(target),
|
|
2017
|
-
type: "write"
|
|
2018
|
-
};
|
|
2019
|
-
rememberWriteRedactor(operation, mcpSecretRedactor(transform, sourceSightings));
|
|
2020
|
-
return {
|
|
2021
|
-
manualSteps: remediationSteps(target.spec.path, supported),
|
|
2022
|
-
operations: [operation],
|
|
2023
|
-
summary: `Replace inline MCP credentials in ${target.spec.path} with environment references.`
|
|
2024
|
-
};
|
|
2025
|
-
},
|
|
2026
|
-
supports: transform.supports
|
|
2027
|
-
};
|
|
2028
|
-
}
|
|
2029
|
-
/** Associates a private planner with the app model that exposes only safe sightings. */
|
|
2030
|
-
function rememberMcpSecretPlanner(app, planner) {
|
|
2031
|
-
PLANNERS.set(app, planner);
|
|
2032
|
-
}
|
|
2033
|
-
/**
|
|
2034
|
-
* Whether this occurrence has a behavior-preserving adapter rewrite that a plan can actually carry.
|
|
2035
|
-
*
|
|
2036
|
-
* `supports` only reads the locator's shape, so it answers "could this kind of field be rewritten",
|
|
2037
|
-
* not "was this one". Planning is what discovers that the document is spelled in a form the writer
|
|
2038
|
-
* cannot edit, and a finding that advertises a guided fix the planner then declines to produce is
|
|
2039
|
-
* a dead end for the user. The plan is memoized, so asking it here costs nothing.
|
|
2040
|
-
*/
|
|
2041
|
-
function canPlanMcpSecretRemediation(model, sighting) {
|
|
2042
|
-
const app = model.apps.find((candidate) => candidate.adapterId === sighting.appId);
|
|
2043
|
-
if ((app === void 0 ? void 0 : PLANNERS.get(app))?.supports(sighting) !== true) return false;
|
|
2044
|
-
return planMcpSecretRemediation(model, sighting) !== void 0;
|
|
2045
|
-
}
|
|
2046
|
-
/** Builds the one whole-file plan shared by every representable sighting in a source file. */
|
|
2047
|
-
function planMcpSecretRemediation(model, sighting) {
|
|
2048
|
-
const key = `${sighting.appId}:${sighting.sourceId}`;
|
|
2049
|
-
const memo = PLANS.get(model) ?? /* @__PURE__ */ new Map();
|
|
2050
|
-
PLANS.set(model, memo);
|
|
2051
|
-
if (memo.has(key)) return memo.get(key);
|
|
2052
|
-
const app = model.apps.find((candidate) => candidate.adapterId === sighting.appId);
|
|
2053
|
-
const plan = (app === void 0 ? void 0 : PLANNERS.get(app))?.plan(sighting.sourceId);
|
|
2054
|
-
memo.set(key, plan);
|
|
2055
|
-
return plan;
|
|
2056
|
-
}
|
|
1350
|
+
//#region ../core/src/workspace/append-instructions.ts
|
|
2057
1351
|
/**
|
|
2058
|
-
*
|
|
1352
|
+
* Appends Markdown fragments while preserving every existing byte.
|
|
2059
1353
|
*
|
|
2060
|
-
*
|
|
2061
|
-
*
|
|
2062
|
-
*
|
|
2063
|
-
*
|
|
1354
|
+
* Only appended fragments are canonicalized, and by the same normalization the content hash
|
|
1355
|
+
* covers — that is what lets a recorded fingerprint answer for text already in the file. They use
|
|
1356
|
+
* the target's line ending and end in one line ending, with one blank-line seam between
|
|
1357
|
+
* consecutive fragments.
|
|
2064
1358
|
*/
|
|
2065
|
-
function
|
|
2066
|
-
|
|
2067
|
-
const
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
1359
|
+
function appendInstructionFragments(source, fragments) {
|
|
1360
|
+
if (fragments.length === 0) return source;
|
|
1361
|
+
const lineEnding = source.includes("\r\n") ? "\r\n" : "\n";
|
|
1362
|
+
let result = source;
|
|
1363
|
+
for (const fragment of fragments) {
|
|
1364
|
+
const canonical = canonicalizeContent(fragment).replaceAll("\n", lineEnding);
|
|
1365
|
+
if (result.length > 0) result += blankLineSeparator(result, lineEnding);
|
|
1366
|
+
result += canonical;
|
|
1367
|
+
}
|
|
1368
|
+
return result;
|
|
1369
|
+
}
|
|
1370
|
+
function blankLineSeparator(source, lineEnding) {
|
|
1371
|
+
let trailingLineEndings = 0;
|
|
1372
|
+
let end = source.length;
|
|
1373
|
+
while (end > 0 && trailingLineEndings < 2) {
|
|
1374
|
+
if (source.endsWith("\r\n", end)) {
|
|
1375
|
+
end -= 2;
|
|
1376
|
+
trailingLineEndings += 1;
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
if (source[end - 1] === "\n" || source[end - 1] === "\r") {
|
|
1380
|
+
end -= 1;
|
|
1381
|
+
trailingLineEndings += 1;
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1386
|
+
return lineEnding.repeat(2 - trailingLineEndings);
|
|
2074
1387
|
}
|
|
2075
1388
|
//#endregion
|
|
2076
1389
|
//#region ../core/src/workspace/shared-link-plan.ts
|
|
2077
|
-
const SHARED_LINK_SNIPPET_ID = "shared-instructions";
|
|
2078
1390
|
/** Builds the same safe shared-instruction link plan for checks and setup. */
|
|
2079
1391
|
function planSharedInstructionLink(app, model, options = {}) {
|
|
2080
1392
|
const link = options.link ?? app.sharedLink;
|
|
@@ -2083,21 +1395,24 @@ function planSharedInstructionLink(app, model, options = {}) {
|
|
|
2083
1395
|
const refusal = refuseBefore(app, status);
|
|
2084
1396
|
if (refusal !== void 0) return { blocked: refusal };
|
|
2085
1397
|
switch (link.kind) {
|
|
2086
|
-
case "import-line": return planImportLine(app,
|
|
2087
|
-
case "native-copy": return planNativeCopy(app,
|
|
1398
|
+
case "import-line": return planImportLine(app, link, status, options.sourceContent);
|
|
1399
|
+
case "native-copy": return planNativeCopy(app, link, status, options.sourceContent);
|
|
2088
1400
|
case "symlink": return planSymlink(app, model, link, status, options);
|
|
2089
1401
|
}
|
|
2090
1402
|
}
|
|
2091
|
-
function planImportLine(app,
|
|
1403
|
+
function planImportLine(app, link, status, sourceContent) {
|
|
2092
1404
|
const source = instructionEntry(app, link.entryPath);
|
|
2093
1405
|
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.` };
|
|
2094
|
-
const
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
1406
|
+
const current = selectedContent(sourceContent, source);
|
|
1407
|
+
const desired = link.content ?? "";
|
|
1408
|
+
if (observedStateHolds(sourceContent) && containsImport(current, desired)) return { plan: convergedPlan(app) };
|
|
1409
|
+
return { plan: writePlan(app, link, appendInstructionFragments(current, [desired])) };
|
|
1410
|
+
}
|
|
1411
|
+
/** Legacy marked imports and new plain imports both contain the exact adapter-rendered line. */
|
|
1412
|
+
function containsImport(source, desired) {
|
|
1413
|
+
const normalized = desired.replace(/\r\n?/gu, "\n");
|
|
1414
|
+
const normalizedDesired = normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
|
|
1415
|
+
return source.replace(/\r\n?/gu, "\n").split("\n").some((line) => line === normalizedDesired);
|
|
2101
1416
|
}
|
|
2102
1417
|
/**
|
|
2103
1418
|
* Whether what the scan saw still describes the file when this plan is applied.
|
|
@@ -2116,19 +1431,19 @@ function unreadableInstructionEntry(status, source, sourceContent) {
|
|
|
2116
1431
|
function selectedContent(sourceContent, source) {
|
|
2117
1432
|
return sourceContent ?? source?.content ?? "";
|
|
2118
1433
|
}
|
|
2119
|
-
function planNativeCopy(app,
|
|
1434
|
+
function planNativeCopy(app, link, status, sourceContent) {
|
|
2120
1435
|
const content = sourceContent ?? instructionEntry(app, link.entryPath)?.content;
|
|
2121
1436
|
const refusal = nativeCopyRefusal(status, sourceContent, content, link.content);
|
|
2122
1437
|
if (refusal !== void 0) return { blocked: refusal };
|
|
2123
1438
|
if (status?.exists === true && content === link.content && observedStateHolds(sourceContent)) return { plan: convergedPlan(app) };
|
|
2124
|
-
return { plan: writePlan(app, link, link.content ?? ""
|
|
1439
|
+
return { plan: writePlan(app, link, link.content ?? "") };
|
|
2125
1440
|
}
|
|
2126
1441
|
function nativeCopyRefusal(status, sourceContent, content, desired) {
|
|
2127
1442
|
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;
|
|
2128
1443
|
}
|
|
2129
1444
|
function planSymlink(app, model, link, status, options) {
|
|
2130
|
-
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." };
|
|
2131
1445
|
const target = options.symlinkTarget ?? model.sharedInstructions.path;
|
|
1446
|
+
if (status?.exists === true && status.pathKind !== "symlink" && options.sourceContent === void 0 && !holdsOnlySharedReference(app, link.entryPath, target)) return { blocked: "The existing file is user-owned and is never replaced automatically. Run `aura setup` and choose instruction consolidation to merge its content into the shared source." };
|
|
2132
1447
|
if (observedStateHolds(options.sourceContent) && pointsAt(status, target)) return { plan: convergedPlan(app) };
|
|
2133
1448
|
return { plan: {
|
|
2134
1449
|
operations: [{
|
|
@@ -2162,9 +1477,16 @@ function refuseBefore(app, status) {
|
|
|
2162
1477
|
if (status?.problem !== void 0) return `Aura could not safely read the instruction entry (${status.problem}) and will not overwrite it.`;
|
|
2163
1478
|
return status?.pathKind === "directory" ? "The instruction entry is a directory and cannot be replaced automatically." : void 0;
|
|
2164
1479
|
}
|
|
2165
|
-
|
|
1480
|
+
/**
|
|
1481
|
+
* Writes one entry, with nothing to warn the user about afterwards.
|
|
1482
|
+
*
|
|
1483
|
+
* There used to be a manual step here telling the user to keep a project entry out of version
|
|
1484
|
+
* control, because a project entry could name the shared source by absolute path. It cannot any
|
|
1485
|
+
* more: a project entry names it relatively, and a home entry through `~`. Both are committable or
|
|
1486
|
+
* personal by construction rather than by advice the user has to remember to follow.
|
|
1487
|
+
*/
|
|
1488
|
+
function writePlan(app, link, content) {
|
|
2166
1489
|
return {
|
|
2167
|
-
...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.`] } : {},
|
|
2168
1490
|
operations: [{
|
|
2169
1491
|
content,
|
|
2170
1492
|
mode: 420,
|
|
@@ -2174,6 +1496,19 @@ function writePlan(app, link, content, homeDir) {
|
|
|
2174
1496
|
summary: `Link ${app.displayName} to the shared instruction source.`
|
|
2175
1497
|
};
|
|
2176
1498
|
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Whether a real file at this entry holds nothing but a legacy block or plain link Aura wrote.
|
|
1501
|
+
*
|
|
1502
|
+
* The refusal above guards the user's own guidance, and an entry Aura wired as an import line holds
|
|
1503
|
+
* none. Without this, an app switching from an import line to a link strands every machine already
|
|
1504
|
+
* wired the old way: consolidation has no source to offer for a file that is Aura's.
|
|
1505
|
+
*/
|
|
1506
|
+
function holdsOnlySharedReference(app, path, target) {
|
|
1507
|
+
const document = instructionEntry(app, path);
|
|
1508
|
+
if (document === void 0) return false;
|
|
1509
|
+
if (stripLegacyManagedBlock(document.content).trim().length === 0) return true;
|
|
1510
|
+
return document.content.replace(/\r\n?/gu, "\n").split("\n").filter((line) => line.trim().length > 0).length === 1 && document.links.some((candidate) => resolve(candidate.targetPath) === resolve(target));
|
|
1511
|
+
}
|
|
2177
1512
|
function instructionEntry(app, path) {
|
|
2178
1513
|
return app.instructionFiles.find((document) => resolve(document.path) === resolve(path));
|
|
2179
1514
|
}
|
|
@@ -2181,4 +1516,4 @@ function entryStatus(app, path) {
|
|
|
2181
1516
|
return app.sourceFiles.find((file) => resolve(file.spec.path) === resolve(path));
|
|
2182
1517
|
}
|
|
2183
1518
|
//#endregion
|
|
2184
|
-
export {
|
|
1519
|
+
export { displayPath as A, isRecord as C, SHARED_INSTRUCTIONS_TEMPLATE as D, AuraManifestError as E, src_default as O, errorMessage as S, resolveAuraManifestPath as T, assertAuraManifestWritable as _, planDesiredMcpConvergence as a, parseAuraManifest as b, rememberMcpConvergence as c, sharedSkillsRoot as d, skillDeploymentStatus as f, hashContent as g, stripLegacyManagedBlock as h, mcpConvergenceBlockers as i, pluralize as k, isAuraOwnedSkillTarget as l, planSharedSkillTreeUpdate as m, appendInstructionFragments as n, planManifestMcpConvergence as o, managedContentRevisionStatus as p, forgetMcpPlans as r, planMcpServerRemoval as s, planSharedInstructionLink as t, planSkillDeployment as u, createAuraManifestWriteOperation as v, AURA_MANIFEST_PATH as w, errorCode as x, createEmptyAuraManifest as y };
|