@tryaura/aura-cli 0.2.1 → 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-Dbasvr1u.js → plugins-CL4yWDig.js} +58 -580
- package/dist/{run.boundary-CF5kZTH3.js → run.boundary-DtkI7ZeG.js} +5746 -3889
- package/dist/{shared-link-plan-D8fxdFYv.js → shared-link-plan-C57dSoid.js} +167 -848
- package/package.json +6 -6
|
@@ -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.
|
|
@@ -521,22 +563,6 @@ function apps(value) {
|
|
|
521
563
|
}
|
|
522
564
|
return Object.freeze(result);
|
|
523
565
|
}
|
|
524
|
-
function snippets(value) {
|
|
525
|
-
if (!Array.isArray(value)) throw invalid("$.snippets", "must be an array");
|
|
526
|
-
return Object.freeze(value.map((candidate, index) => {
|
|
527
|
-
const path = `$.snippets[${String(index)}]`;
|
|
528
|
-
const snippet = requiredObject(candidate, path);
|
|
529
|
-
const hash = requiredString(snippet, "hash", path);
|
|
530
|
-
if (!SHA256_PATTERN.test(hash)) throw invalid(`${path}.hash`, "must be a lowercase SHA-256 hash");
|
|
531
|
-
return Object.freeze({
|
|
532
|
-
...snippet,
|
|
533
|
-
hash,
|
|
534
|
-
id: requiredString(snippet, "id", path),
|
|
535
|
-
pinned: requiredBoolean(snippet, "pinned", path),
|
|
536
|
-
version: requiredString(snippet, "version", path)
|
|
537
|
-
});
|
|
538
|
-
}));
|
|
539
|
-
}
|
|
540
566
|
function ownership(value) {
|
|
541
567
|
const source = requiredObject(value, "$.ownership");
|
|
542
568
|
const result = {};
|
|
@@ -705,228 +731,49 @@ function createAuraManifestWriteOperation(state, manifest) {
|
|
|
705
731
|
});
|
|
706
732
|
}
|
|
707
733
|
//#endregion
|
|
708
|
-
//#region ../core/src/
|
|
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;
|
|
709
737
|
/**
|
|
710
|
-
*
|
|
738
|
+
* Normalizes Markdown to LF and exactly one trailing newline.
|
|
711
739
|
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
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.
|
|
715
745
|
*/
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
* multiplies it by the operation count.
|
|
722
|
-
*/
|
|
723
|
-
const MAX_RETAINED_PLAN_BYTES = 67108864;
|
|
724
|
-
/**
|
|
725
|
-
* The combined before/after size above which a preview summarizes instead of rendering a patch.
|
|
726
|
-
*
|
|
727
|
-
* A unified diff is larger than the inputs that produced it, and a preview no human can read is not
|
|
728
|
-
* worth the memory it costs.
|
|
729
|
-
*/
|
|
730
|
-
const MAX_DIFF_BYTES = 262144;
|
|
731
|
-
/**
|
|
732
|
-
* Every mode a plan may request.
|
|
733
|
-
*
|
|
734
|
-
* `FileMode` already closes this set, but a type is erased at runtime and plugins ship as compiled
|
|
735
|
-
* JavaScript. Since the mode reaches `chmod` unchanged, checking it here is what actually keeps a
|
|
736
|
-
* plan from asking for a world-writable or setuid file.
|
|
737
|
-
*/
|
|
738
|
-
const FILE_MODES = /* @__PURE__ */ new Set([
|
|
739
|
-
384,
|
|
740
|
-
420,
|
|
741
|
-
448,
|
|
742
|
-
493
|
|
743
|
-
]);
|
|
744
|
-
//#endregion
|
|
745
|
-
//#region ../core/src/fix-plan/diff.ts
|
|
746
|
-
const NULL_PATH = "/dev/null";
|
|
747
|
-
function renderWriteDiff(path, before, content, requestedMode, mode) {
|
|
748
|
-
const note = modeNote(before, requestedMode, mode);
|
|
749
|
-
const previous = textOf(before);
|
|
750
|
-
if (previous === void 0) return renderSummary("write", path, "Binary or oversized file would change.") + note;
|
|
751
|
-
if (exceedsDiffBudget(previous, content)) return renderSummary("write", path, "File is too large to diff; contents would change.") + note;
|
|
752
|
-
return renderPatch("write", path, before.kind === "missing" ? NULL_PATH : path, path, previous, content) + note;
|
|
753
|
-
}
|
|
754
|
-
/** Fail-closed write preview used when a semantic redactor cannot safely project both sides. */
|
|
755
|
-
function renderWriteSummary(path, before, requestedMode, mode) {
|
|
756
|
-
return renderSummary("write", path, "Sensitive MCP configuration would change.") + modeNote(before, requestedMode, mode);
|
|
757
|
-
}
|
|
758
|
-
function renderRemoveDiff(path, before) {
|
|
759
|
-
if (before.kind === "directory") return renderSummary("remove", path, "Remove empty directory.");
|
|
760
|
-
const previous = textOf(before);
|
|
761
|
-
if (previous === void 0) return renderSummary("remove", path, "Binary or oversized file would be removed.");
|
|
762
|
-
if (exceedsDiffBudget(previous, "")) return renderSummary("remove", path, "File is too large to diff; it would be removed.");
|
|
763
|
-
return renderPatch("remove", path, path, NULL_PATH, previous, "");
|
|
764
|
-
}
|
|
765
|
-
function renderMoveDiff(sourcePath, destinationPath) {
|
|
766
|
-
return [
|
|
767
|
-
`diff --aura move ${sourcePath} ${destinationPath}`,
|
|
768
|
-
`rename from ${sourcePath}`,
|
|
769
|
-
`rename to ${destinationPath}`,
|
|
770
|
-
""
|
|
771
|
-
].join("\n");
|
|
772
|
-
}
|
|
773
|
-
function renderArchiveDiff(path, relativePath, before, replacement, replacementMode) {
|
|
774
|
-
const archive = renderSummary("archive", path, `Preserve the original at <backup>/consolidation/${relativePath}.`);
|
|
775
|
-
if (replacement === void 0) return `${archive}${renderRemoveDiff(path, before)}`;
|
|
776
|
-
return replacement.type === "symlink" ? `${archive}${renderSymlinkDiff(path, before, replacement.target)}` : `${archive}${renderWriteDiff(path, before, replacement.content, replacement.mode, replacementMode ?? 420)}`;
|
|
777
|
-
}
|
|
778
|
-
function renderSymlinkDiff(path, before, target) {
|
|
779
|
-
const previous = textOf(before);
|
|
780
|
-
if (previous === void 0) return renderSummary("symlink", path, `Binary or oversized file would become a link.`);
|
|
781
|
-
if (exceedsDiffBudget(previous, target)) return renderSummary("symlink", path, "File is too large to diff; it would become a link.");
|
|
782
|
-
return `${renderPatch("symlink", path, before.kind === "missing" ? NULL_PATH : path, path, previous, `${target}\n`)}link target ${target}\n`;
|
|
783
|
-
}
|
|
784
|
-
/** Describes a conflict in the same shape as a diff, so a renderer can treat previews uniformly. */
|
|
785
|
-
function renderConflict(operation, path, reason) {
|
|
786
|
-
return renderSummary(operation, path, `Blocked: ${reason}.`);
|
|
787
|
-
}
|
|
788
|
-
function renderPatch(operation, displayPath, oldPath, newPath, oldContent, newContent) {
|
|
789
|
-
return `diff --aura ${operation} ${displayPath}\n${createTwoFilesPatch(oldPath, newPath, oldContent, newContent, "before", "after", { context: 3 })}`;
|
|
790
|
-
}
|
|
791
|
-
function renderSummary(operation, path, detail) {
|
|
792
|
-
return [
|
|
793
|
-
`diff --aura ${operation} ${path}`,
|
|
794
|
-
detail,
|
|
795
|
-
""
|
|
796
|
-
].join("\n");
|
|
797
|
-
}
|
|
798
|
-
/**
|
|
799
|
-
* Says what happens to an existing file's permissions.
|
|
800
|
-
*
|
|
801
|
-
* Two cases are worth a line. A mode that changes is a change the diff itself cannot show, and core
|
|
802
|
-
* only does that to files it owns as protocol. A `mode` a plan asked for and will not get is the
|
|
803
|
-
* commoner one: an existing file keeps whatever the user set, and saying so is the difference
|
|
804
|
-
* between a deliberate choice and a silent one.
|
|
805
|
-
*/
|
|
806
|
-
function modeNote(before, requestedMode, mode) {
|
|
807
|
-
if (before.kind !== "file") return "";
|
|
808
|
-
if (before.mode !== mode) return `mode ${formatMode(before.mode)} changes to ${formatMode(mode)}\n`;
|
|
809
|
-
if (requestedMode !== void 0 && requestedMode !== before.mode) return `mode ${formatMode(requestedMode)} requested; existing mode ${formatMode(before.mode)} is preserved\n`;
|
|
810
|
-
return "";
|
|
811
|
-
}
|
|
812
|
-
function formatMode(mode) {
|
|
813
|
-
return `0o${mode.toString(8).padStart(3, "0")}`;
|
|
814
|
-
}
|
|
815
|
-
function exceedsDiffBudget(before, after) {
|
|
816
|
-
return Buffer.byteLength(before, "utf8") + Buffer.byteLength(after, "utf8") > MAX_DIFF_BYTES;
|
|
817
|
-
}
|
|
818
|
-
function textOf(state) {
|
|
819
|
-
switch (state.kind) {
|
|
820
|
-
case "directory":
|
|
821
|
-
case "unsupported": return;
|
|
822
|
-
case "file": return state.content !== void 0 && isUtf8(state.content) ? state.content.toString("utf8") : void 0;
|
|
823
|
-
case "missing": return "";
|
|
824
|
-
case "symlink": return `${state.target}\n`;
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
//#endregion
|
|
828
|
-
//#region ../core/src/fix-plan/write-redaction.ts
|
|
829
|
-
const REDACTORS = /* @__PURE__ */ new WeakMap();
|
|
830
|
-
/**
|
|
831
|
-
* Paths some operation has asked to have redacted, kept beside the identity-keyed registry above.
|
|
832
|
-
*
|
|
833
|
-
* The registry is keyed on the operation object, so anything that copies an operation between
|
|
834
|
-
* planning and preview silently drops its masker — and the symptom of that would be a credential
|
|
835
|
-
* rendered into a diff rather than an error. Remembering the path as well turns the silent case
|
|
836
|
-
* into the conservative one: a write to a path that has ever needed masking and arrives without a
|
|
837
|
-
* masker gets a summary, not a patch.
|
|
838
|
-
*/
|
|
839
|
-
const REDACTED_PATHS = /* @__PURE__ */ new Set();
|
|
840
|
-
/** Associates a semantic content masker with a write without changing the public operation schema. */
|
|
841
|
-
function rememberWriteRedactor(operation, redactor) {
|
|
842
|
-
const existing = REDACTORS.get(operation) ?? [];
|
|
843
|
-
REDACTORS.set(operation, [...existing, redactor]);
|
|
844
|
-
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`;
|
|
845
751
|
}
|
|
846
752
|
/**
|
|
847
|
-
*
|
|
753
|
+
* Fingerprints Markdown Aura records but does not own: an installed snippet, a trusted preset.
|
|
848
754
|
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
* 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.
|
|
853
758
|
*/
|
|
854
|
-
function
|
|
855
|
-
return (content)
|
|
856
|
-
const redaction = transform.redact({
|
|
857
|
-
content,
|
|
858
|
-
sightings
|
|
859
|
-
});
|
|
860
|
-
return redaction === void 0 || redaction.unresolved.length > 0 ? void 0 : redaction.content;
|
|
861
|
-
};
|
|
759
|
+
function hashContent(content) {
|
|
760
|
+
return createHash("sha256").update(canonicalizeContent(content), "utf8").digest("hex");
|
|
862
761
|
}
|
|
863
|
-
/** Renders a write only after every registered semantic masker succeeds on both diff sides. */
|
|
864
|
-
function renderRedactedWriteDiff(operations, path, before, content, requestedMode, mode) {
|
|
865
|
-
const redactors = operations.flatMap((operation) => REDACTORS.get(operation) ?? []);
|
|
866
|
-
if (redactors.length === 0) return REDACTED_PATHS.has(path) ? renderWriteSummary(path, before, requestedMode, mode) : renderWriteDiff(path, before, content, requestedMode, mode);
|
|
867
|
-
const next = redactAll(redactors, content);
|
|
868
|
-
if (next === void 0) return renderWriteSummary(path, before, requestedMode, mode);
|
|
869
|
-
if (before.kind !== "file" || before.content === void 0) return renderWriteDiff(path, before, next, requestedMode, mode);
|
|
870
|
-
const previous = redactAll(redactors, before.content.toString("utf8"));
|
|
871
|
-
if (previous === void 0) return renderWriteSummary(path, before, requestedMode, mode);
|
|
872
|
-
return renderWriteDiff(path, {
|
|
873
|
-
...before,
|
|
874
|
-
content: Buffer.from(previous, "utf8")
|
|
875
|
-
}, next, requestedMode, mode);
|
|
876
|
-
}
|
|
877
|
-
function redactAll(redactors, content) {
|
|
878
|
-
let projected = content;
|
|
879
|
-
for (const redact of redactors) {
|
|
880
|
-
const result = safeRedact(redact, projected);
|
|
881
|
-
if (result === void 0) return;
|
|
882
|
-
projected = result;
|
|
883
|
-
}
|
|
884
|
-
return projected;
|
|
885
|
-
}
|
|
886
|
-
function safeRedact(redact, content) {
|
|
887
|
-
try {
|
|
888
|
-
return redact(content);
|
|
889
|
-
} catch {
|
|
890
|
-
return;
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
//#endregion
|
|
894
|
-
//#region ../core/src/managed-block/protocol.ts
|
|
895
|
-
/** Distribution-independent outer marker opening Aura-managed content. */
|
|
896
|
-
const AURA_MANAGED_BLOCK_BEGIN = "<!-- aura:begin -->";
|
|
897
|
-
/** Distribution-independent outer marker closing Aura-managed content. */
|
|
898
|
-
const AURA_MANAGED_BLOCK_END = "<!-- aura:end -->";
|
|
899
|
-
/** Prefix shared by every Aura snippet opening marker. */
|
|
900
|
-
const AURA_MANAGED_SNIPPET_BEGIN_PREFIX = "<!-- aura:begin id=";
|
|
901
|
-
/** Prefix shared by every Aura snippet closing marker. */
|
|
902
|
-
const AURA_MANAGED_SNIPPET_END_PREFIX = "<!-- aura:end id=";
|
|
903
|
-
/** Human-facing ownership warning rendered once inside the outer block. */
|
|
904
|
-
const AURA_MANAGED_BLOCK_NOTICE = "Managed by Aura. Edit via the Aura CLI; manual edits to this block are overwritten.";
|
|
905
762
|
const MANAGED_SNIPPET_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
906
|
-
const MANAGED_SNIPPET_HASH_PATTERN =
|
|
763
|
+
const MANAGED_SNIPPET_HASH_PATTERN = CONTENT_HASH_PATTERN;
|
|
907
764
|
/** Whether an id is both line-safe and legal inside an HTML comment. */
|
|
908
765
|
function isManagedSnippetId(id) {
|
|
909
766
|
return MANAGED_SNIPPET_ID_PATTERN.test(id) && !id.includes("--");
|
|
910
767
|
}
|
|
911
768
|
/**
|
|
912
|
-
*
|
|
769
|
+
* Computes the protocol hash for a snippet's canonical UTF-8 contents.
|
|
913
770
|
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
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.
|
|
916
774
|
*/
|
|
917
|
-
function canonicalizeManagedSnippet(content) {
|
|
918
|
-
const normalized = content.replace(/\r\n?/g, "\n");
|
|
919
|
-
let end = normalized.length;
|
|
920
|
-
while (end > 0 && normalized[end - 1] === "\n") end -= 1;
|
|
921
|
-
return `${normalized.slice(0, end)}\n`;
|
|
922
|
-
}
|
|
923
|
-
/** Computes the protocol hash for text already in {@link canonicalizeManagedSnippet} form. */
|
|
924
|
-
function hashCanonicalManagedSnippet(canonical) {
|
|
925
|
-
return createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
926
|
-
}
|
|
927
|
-
/** Computes the protocol hash for a snippet's canonical UTF-8 contents. */
|
|
928
775
|
function hashManagedSnippet(content) {
|
|
929
|
-
return
|
|
776
|
+
return hashContent(content);
|
|
930
777
|
}
|
|
931
778
|
//#endregion
|
|
932
779
|
//#region ../core/src/managed-block/parse-state.ts
|
|
@@ -1148,324 +995,23 @@ function collectPlainLine(source, line, protectedByFence, block, snippet, notes)
|
|
|
1148
995
|
}));
|
|
1149
996
|
}
|
|
1150
997
|
//#endregion
|
|
1151
|
-
//#region ../core/src/managed-block/
|
|
1152
|
-
/**
|
|
1153
|
-
* Finds protocol markers the reader would honour, applying the same fence rules as
|
|
1154
|
-
* {@link readManagedBlock} so fenced marker examples stay ordinary text.
|
|
1155
|
-
*/
|
|
1156
|
-
function scanForMarkers(text) {
|
|
1157
|
-
let fence;
|
|
1158
|
-
let markerLine;
|
|
1159
|
-
for (const line of splitSourceLines$1(text)) {
|
|
1160
|
-
const previousFence = fence;
|
|
1161
|
-
fence = advanceMarkdownFence(line.text, fence);
|
|
1162
|
-
if (previousFence !== void 0 || fence !== void 0) continue;
|
|
1163
|
-
if (markerLine === void 0 && parseMarker(line.text) !== void 0) markerLine = line.number;
|
|
1164
|
-
}
|
|
1165
|
-
return {
|
|
1166
|
-
markerLine,
|
|
1167
|
-
unterminatedFence: fence !== void 0
|
|
1168
|
-
};
|
|
1169
|
-
}
|
|
1170
|
-
/**
|
|
1171
|
-
* Rejects content that would escape its own snippet. Written verbatim, a marker inside a snippet
|
|
1172
|
-
* body reopens or closes the protocol, and the resulting file parses as invalid forever — so the
|
|
1173
|
-
* damage has to be caught before the write, not after.
|
|
1174
|
-
*
|
|
1175
|
-
* Every write path shares this guard. A narrower path that skipped it would be a way to write the
|
|
1176
|
-
* exact bytes the wider one refuses, and the file it corrupts is the same file either way.
|
|
1177
|
-
*/
|
|
1178
|
-
function managedSnippetContentProblems(id, canonical) {
|
|
1179
|
-
const scan = scanForMarkers(canonical);
|
|
1180
|
-
if (scan.markerLine !== void 0) return Object.freeze([Object.freeze({
|
|
1181
|
-
code: "invalid-snippet-content",
|
|
1182
|
-
message: `Snippet "${id}" declares an Aura marker on content line ${String(scan.markerLine)}. Wrap marker examples in a Markdown fence.`
|
|
1183
|
-
})]);
|
|
1184
|
-
if (scan.unterminatedFence) return Object.freeze([Object.freeze({
|
|
1185
|
-
code: "invalid-snippet-content",
|
|
1186
|
-
message: `Snippet "${id}" ends inside an unclosed Markdown fence, which would hide every marker after it. Close the fence.`
|
|
1187
|
-
})]);
|
|
1188
|
-
return Object.freeze([]);
|
|
1189
|
-
}
|
|
1190
|
-
/**
|
|
1191
|
-
* Drops every protocol line the reader honours, plus the notice that follows an opening marker,
|
|
1192
|
-
* leaving handwritten text byte-for-byte. Fenced marker examples survive because the reader never
|
|
1193
|
-
* treated them as protocol in the first place.
|
|
1194
|
-
*/
|
|
1195
|
-
function stripManagedMarkers(source) {
|
|
1196
|
-
const kept = [];
|
|
1197
|
-
let fence;
|
|
1198
|
-
let afterBlockBegin = false;
|
|
1199
|
-
for (const line of splitSourceLines$1(source)) {
|
|
1200
|
-
const previousFence = fence;
|
|
1201
|
-
fence = advanceMarkdownFence(line.text, fence);
|
|
1202
|
-
const marker = previousFence !== void 0 || fence !== void 0 ? void 0 : parseMarker(line.text);
|
|
1203
|
-
if (marker !== void 0) {
|
|
1204
|
-
afterBlockBegin = marker.kind === "block-begin";
|
|
1205
|
-
continue;
|
|
1206
|
-
}
|
|
1207
|
-
if (afterBlockBegin && line.text === "Managed by Aura. Edit via the Aura CLI; manual edits to this block are overwritten.") {
|
|
1208
|
-
afterBlockBegin = false;
|
|
1209
|
-
continue;
|
|
1210
|
-
}
|
|
1211
|
-
afterBlockBegin = false;
|
|
1212
|
-
kept.push(source.slice(line.start, line.end));
|
|
1213
|
-
}
|
|
1214
|
-
return kept.join("");
|
|
1215
|
-
}
|
|
1216
|
-
//#endregion
|
|
1217
|
-
//#region ../core/src/managed-block/reconcile-snippet.ts
|
|
1218
|
-
/** Reconciles exactly one snippet while preserving every other source byte. */
|
|
1219
|
-
function reconcileManagedSnippet(source, snippetId, resolution) {
|
|
1220
|
-
const current = readManagedBlock(source);
|
|
1221
|
-
if (current.status === "invalid") return invalidResult$1(source, current.notes, current.problems);
|
|
1222
|
-
if (current.status === "absent") return missingSnippet(source, current.notes, snippetId, "an Aura-managed block");
|
|
1223
|
-
const snippet = current.block.snippets.find((candidate) => candidate.id === snippetId);
|
|
1224
|
-
if (snippet === void 0) return missingSnippet(source, current.notes, snippetId, "the Aura-managed block");
|
|
1225
|
-
const lineEnding = markerLineEnding(source, snippet.startOffset, snippet.contentStartOffset);
|
|
1226
|
-
const canonical = resolution.kind === "restore" ? canonicalizeManagedSnippet(resolution.content) : void 0;
|
|
1227
|
-
if (canonical !== void 0) {
|
|
1228
|
-
const problems = managedSnippetContentProblems(snippet.id, canonical);
|
|
1229
|
-
if (problems.length > 0) return invalidResult$1(source, current.notes, problems);
|
|
1230
|
-
}
|
|
1231
|
-
const content = canonical === void 0 ? snippet.content : withLineEnding(canonical, lineEnding);
|
|
1232
|
-
const hash = canonical === void 0 ? snippet.computedHash : hashCanonicalManagedSnippet(canonical);
|
|
1233
|
-
const opening = `${AURA_MANAGED_SNIPPET_BEGIN_PREFIX}${snippet.id} sha256=${hash} -->${lineEnding}`;
|
|
1234
|
-
const updated = source.slice(0, snippet.startOffset) + opening + content + source.slice(snippet.contentEndOffset);
|
|
1235
|
-
return updated === source ? Object.freeze({
|
|
1236
|
-
content: source,
|
|
1237
|
-
notes: current.notes,
|
|
1238
|
-
status: "unchanged"
|
|
1239
|
-
}) : Object.freeze({
|
|
1240
|
-
content: updated,
|
|
1241
|
-
notes: current.notes,
|
|
1242
|
-
status: "updated"
|
|
1243
|
-
});
|
|
1244
|
-
}
|
|
1245
|
-
/** Creates the opt-in edited-versus-canonical detail used by the merge choice. */
|
|
1246
|
-
function diffManagedSnippet(path, editedContent, canonicalContent) {
|
|
1247
|
-
return createTwoFilesPatch(`${path} (edited)`, `${path} (canonical)`, canonicalizeManagedSnippet(editedContent), canonicalizeManagedSnippet(canonicalContent), "edited", "canonical", { context: 3 });
|
|
1248
|
-
}
|
|
1249
|
-
function missingSnippet(source, notes, snippetId, location) {
|
|
1250
|
-
return invalidResult$1(source, notes, [Object.freeze({
|
|
1251
|
-
code: "missing-snippet",
|
|
1252
|
-
message: `Snippet "${snippetId}" does not exist in ${location}.`
|
|
1253
|
-
})]);
|
|
1254
|
-
}
|
|
1255
|
-
function invalidResult$1(source, notes, problems) {
|
|
1256
|
-
return Object.freeze({
|
|
1257
|
-
content: source,
|
|
1258
|
-
notes,
|
|
1259
|
-
problems,
|
|
1260
|
-
status: "invalid"
|
|
1261
|
-
});
|
|
1262
|
-
}
|
|
1263
|
-
function markerLineEnding(source, startOffset, contentStartOffset) {
|
|
1264
|
-
return source.slice(startOffset, contentStartOffset).endsWith("\r\n") ? "\r\n" : "\n";
|
|
1265
|
-
}
|
|
1266
|
-
function withLineEnding(content, lineEnding) {
|
|
1267
|
-
return lineEnding === "\n" ? content : content.replaceAll("\n", lineEnding);
|
|
1268
|
-
}
|
|
1269
|
-
//#endregion
|
|
1270
|
-
//#region ../core/src/managed-block/reconcile-desired.ts
|
|
1271
|
-
function prepareDesiredSnippets(snippets, options) {
|
|
1272
|
-
const ids = /* @__PURE__ */ new Set();
|
|
1273
|
-
const preserved = new Set(options.preserveSnippetIds ?? []);
|
|
1274
|
-
const prepared = [];
|
|
1275
|
-
const problems = [];
|
|
1276
|
-
for (const snippet of snippets) {
|
|
1277
|
-
if (!isManagedSnippetId(snippet.id)) problems.push(Object.freeze({
|
|
1278
|
-
code: "invalid-snippet-id",
|
|
1279
|
-
message: `Snippet ID "${snippet.id}" is not safe inside an HTML comment marker.`
|
|
1280
|
-
}));
|
|
1281
|
-
if (ids.has(snippet.id)) problems.push(Object.freeze({
|
|
1282
|
-
code: "duplicate-snippet",
|
|
1283
|
-
message: `Desired snippet ID "${snippet.id}" appears more than once.`
|
|
1284
|
-
}));
|
|
1285
|
-
if (preserved.has(snippet.id)) problems.push(Object.freeze({
|
|
1286
|
-
code: "duplicate-snippet",
|
|
1287
|
-
message: `Snippet ID "${snippet.id}" is both desired and preserved; writing both would duplicate it.`
|
|
1288
|
-
}));
|
|
1289
|
-
ids.add(snippet.id);
|
|
1290
|
-
const canonical = canonicalizeManagedSnippet(snippet.content);
|
|
1291
|
-
problems.push(...managedSnippetContentProblems(snippet.id, canonical));
|
|
1292
|
-
prepared.push({
|
|
1293
|
-
canonical,
|
|
1294
|
-
hash: hashCanonicalManagedSnippet(canonical),
|
|
1295
|
-
id: snippet.id,
|
|
1296
|
-
kind: "desired"
|
|
1297
|
-
});
|
|
1298
|
-
}
|
|
1299
|
-
return {
|
|
1300
|
-
prepared,
|
|
1301
|
-
problems: Object.freeze(problems)
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
//#endregion
|
|
1305
|
-
//#region ../core/src/managed-block/reconcile-ledger.ts
|
|
1306
|
-
function renderLedgerSnippets(source, block, desired, options) {
|
|
1307
|
-
if (block === void 0 || options.ownedSnippetIds === void 0) return desired;
|
|
1308
|
-
const controlled = controlledSnippetIds(desired, options);
|
|
1309
|
-
const preserved = new Set(options.preserveSnippetIds ?? []);
|
|
1310
|
-
const remaining = [...desired];
|
|
1311
|
-
const rendered = [];
|
|
1312
|
-
for (const snippet of block.snippets) {
|
|
1313
|
-
if (preserved.has(snippet.id) || !controlled.has(snippet.id)) {
|
|
1314
|
-
rendered.push({
|
|
1315
|
-
id: snippet.id,
|
|
1316
|
-
kind: "preserved",
|
|
1317
|
-
raw: source.slice(snippet.startOffset, snippet.endOffset)
|
|
1318
|
-
});
|
|
1319
|
-
continue;
|
|
1320
|
-
}
|
|
1321
|
-
const replacement = remaining.shift();
|
|
1322
|
-
if (replacement !== void 0) rendered.push(replacement);
|
|
1323
|
-
}
|
|
1324
|
-
rendered.push(...remaining);
|
|
1325
|
-
return rendered;
|
|
1326
|
-
}
|
|
1327
|
-
function controlledSnippetIds(desired, options) {
|
|
1328
|
-
return /* @__PURE__ */ new Set([...options.ownedSnippetIds ?? [], ...desired.map((snippet) => snippet.id)]);
|
|
1329
|
-
}
|
|
1330
|
-
function preservedUnownedNotes(block, desired, options) {
|
|
1331
|
-
if (block === void 0 || options.ownedSnippetIds === void 0) return [];
|
|
1332
|
-
const controlled = controlledSnippetIds(desired, options);
|
|
1333
|
-
return block.snippets.filter((snippet) => !controlled.has(snippet.id)).map((snippet) => Object.freeze({
|
|
1334
|
-
code: "preserved-unowned-snippet",
|
|
1335
|
-
line: snippet.startLine,
|
|
1336
|
-
message: `Snippet "${snippet.id}" is not recorded in Aura's manifest; its section was preserved.`
|
|
1337
|
-
}));
|
|
1338
|
-
}
|
|
1339
|
-
//#endregion
|
|
1340
|
-
//#region ../core/src/managed-block/reconcile.ts
|
|
998
|
+
//#region ../core/src/managed-block/strip.ts
|
|
1341
999
|
/**
|
|
1342
|
-
*
|
|
1000
|
+
* Removes a legacy Aura-managed block from a source string, keeping everything else byte-for-byte.
|
|
1343
1001
|
*
|
|
1344
|
-
*
|
|
1345
|
-
*
|
|
1346
|
-
*
|
|
1347
|
-
*
|
|
1348
|
-
*/
|
|
1349
|
-
function reconcileManagedBlock(source, desiredSnippets, options = {}) {
|
|
1350
|
-
return reconcileParsedManagedBlock(source, readManagedBlock(source), desiredSnippets, options);
|
|
1351
|
-
}
|
|
1352
|
-
/**
|
|
1353
|
-
* {@link reconcileManagedBlock}, for a caller that already parsed `source`.
|
|
1354
|
-
*
|
|
1355
|
-
* `current` must be `readManagedBlock(source)` for the same string; passing a result read from
|
|
1356
|
-
* anything else splices content at offsets that no longer describe the source.
|
|
1357
|
-
*/
|
|
1358
|
-
function reconcileParsedManagedBlock(source, current, desiredSnippets, options = {}) {
|
|
1359
|
-
const desired = prepareDesiredSnippets(desiredSnippets, options);
|
|
1360
|
-
if (desired.problems.length > 0) return invalidResult(source, current.notes, desired.problems);
|
|
1361
|
-
const hiddenMarker = current.notes.find((note) => note.code === "unterminated-fence");
|
|
1362
|
-
if (hiddenMarker !== void 0) return invalidResult(source, current.notes, [Object.freeze({
|
|
1363
|
-
code: "unterminated-fence",
|
|
1364
|
-
line: hiddenMarker.line,
|
|
1365
|
-
message: hiddenMarker.message
|
|
1366
|
-
})]);
|
|
1367
|
-
if (current.status !== "invalid") {
|
|
1368
|
-
const block = current.status === "present" ? current.block : void 0;
|
|
1369
|
-
const rendered = renderLedgerSnippets(source, block, desired.prepared, options);
|
|
1370
|
-
const notes = [
|
|
1371
|
-
...current.notes,
|
|
1372
|
-
...overwrittenNotes(current, desired.prepared, options),
|
|
1373
|
-
...preservedUnownedNotes(block, desired.prepared, options)
|
|
1374
|
-
];
|
|
1375
|
-
return settle(source, buildContent(source, rendered, current), notes);
|
|
1376
|
-
}
|
|
1377
|
-
if (options.onInvalid !== "repair") return invalidResult(source, current.notes, current.problems);
|
|
1378
|
-
if (options.ownedSnippetIds !== void 0) return invalidResult(source, current.notes, current.problems);
|
|
1379
|
-
const stripped = stripManagedMarkers(source);
|
|
1380
|
-
const repaired = readManagedBlock(stripped);
|
|
1381
|
-
if (repaired.status === "invalid") return invalidResult(source, current.notes, current.problems);
|
|
1382
|
-
const notes = [...current.notes, ...repairNotes(current.problems)];
|
|
1383
|
-
return settle(source, buildContent(stripped, desired.prepared, repaired), notes);
|
|
1384
|
-
}
|
|
1385
|
-
function buildContent(source, prepared, current) {
|
|
1386
|
-
if (prepared.length === 0) return current.status === "absent" ? source : source.slice(0, current.block.startOffset) + current.block.unmanagedContent + source.slice(current.block.endOffset);
|
|
1387
|
-
const lineEnding = detectLineEnding$1(source);
|
|
1388
|
-
if (current.status === "absent") return `${source}${source.length === 0 || source.endsWith("\n") ? "" : lineEnding}${renderManagedBlock(prepared, lineEnding, true)}`;
|
|
1389
|
-
const replacement = renderManagedBlock(prepared, lineEnding, source[current.block.endOffset - 1] === "\n" || current.block.unmanagedContent.length > 0);
|
|
1390
|
-
if (current.block.unmanagedContent.length === 0 && replacement === source.slice(current.block.startOffset, current.block.endOffset)) return source;
|
|
1391
|
-
return source.slice(0, current.block.startOffset) + replacement + current.block.unmanagedContent + source.slice(current.block.endOffset);
|
|
1392
|
-
}
|
|
1393
|
-
function renderManagedBlock(snippets, lineEnding, endsWithLineEnding) {
|
|
1394
|
-
const parts = [
|
|
1395
|
-
AURA_MANAGED_BLOCK_BEGIN,
|
|
1396
|
-
lineEnding,
|
|
1397
|
-
AURA_MANAGED_BLOCK_NOTICE,
|
|
1398
|
-
lineEnding
|
|
1399
|
-
];
|
|
1400
|
-
for (const snippet of snippets) {
|
|
1401
|
-
if (snippet.kind === "preserved") {
|
|
1402
|
-
parts.push(snippet.raw);
|
|
1403
|
-
continue;
|
|
1404
|
-
}
|
|
1405
|
-
const content = lineEnding === "\n" ? snippet.canonical : snippet.canonical.replaceAll("\n", lineEnding);
|
|
1406
|
-
parts.push(`${AURA_MANAGED_SNIPPET_BEGIN_PREFIX}${snippet.id} sha256=${snippet.hash} -->`, lineEnding, content, `${AURA_MANAGED_SNIPPET_END_PREFIX}${snippet.id} -->`, lineEnding);
|
|
1407
|
-
}
|
|
1408
|
-
parts.push(AURA_MANAGED_BLOCK_END);
|
|
1409
|
-
if (endsWithLineEnding) parts.push(lineEnding);
|
|
1410
|
-
return parts.join("");
|
|
1411
|
-
}
|
|
1412
|
-
function overwrittenNotes(current, desired, options) {
|
|
1413
|
-
if (current.status === "absent") return [];
|
|
1414
|
-
const controlled = controlledSnippetIds(desired, options);
|
|
1415
|
-
const desiredById = new Map(desired.map((snippet) => [snippet.id, snippet]));
|
|
1416
|
-
const preserved = new Set(options.preserveSnippetIds ?? []);
|
|
1417
|
-
return current.block.snippets.flatMap((snippet) => {
|
|
1418
|
-
if (options.ownedSnippetIds !== void 0 && !controlled.has(snippet.id) || preserved.has(snippet.id) || !handEdited(snippet, options)) return [];
|
|
1419
|
-
const replacement = desiredById.get(snippet.id);
|
|
1420
|
-
if (replacement === void 0) return [Object.freeze({
|
|
1421
|
-
code: "removed-snippet",
|
|
1422
|
-
line: snippet.startLine,
|
|
1423
|
-
message: `Snippet "${snippet.id}" was edited by hand since Aura wrote it; the edit is being removed.`
|
|
1424
|
-
})];
|
|
1425
|
-
if (replacement.hash === snippet.computedHash) return [];
|
|
1426
|
-
return [Object.freeze({
|
|
1427
|
-
code: "overwritten-snippet",
|
|
1428
|
-
line: snippet.startLine,
|
|
1429
|
-
message: `Snippet "${snippet.id}" was edited by hand since Aura wrote it; the edit is being replaced.`
|
|
1430
|
-
})];
|
|
1431
|
-
});
|
|
1432
|
-
}
|
|
1433
|
-
/**
|
|
1434
|
-
* Whether a section differs from what Aura last recorded for it.
|
|
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.
|
|
1435
1006
|
*
|
|
1436
|
-
*
|
|
1437
|
-
*
|
|
1438
|
-
*
|
|
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.
|
|
1439
1010
|
*/
|
|
1440
|
-
function
|
|
1441
|
-
const
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
function repairNotes(problems) {
|
|
1445
|
-
return problems.map((problem) => Object.freeze({
|
|
1446
|
-
code: "repaired-invalid-block",
|
|
1447
|
-
line: problem.line,
|
|
1448
|
-
message: `Rebuilt the managed block to repair: ${problem.message}`
|
|
1449
|
-
}));
|
|
1450
|
-
}
|
|
1451
|
-
function invalidResult(source, notes, problems) {
|
|
1452
|
-
return Object.freeze({
|
|
1453
|
-
content: source,
|
|
1454
|
-
notes: Object.freeze([...notes]),
|
|
1455
|
-
problems,
|
|
1456
|
-
status: "invalid"
|
|
1457
|
-
});
|
|
1458
|
-
}
|
|
1459
|
-
function settle(source, content, notes) {
|
|
1460
|
-
return content === source ? Object.freeze({
|
|
1461
|
-
content: source,
|
|
1462
|
-
notes: Object.freeze([...notes]),
|
|
1463
|
-
status: "unchanged"
|
|
1464
|
-
}) : Object.freeze({
|
|
1465
|
-
content,
|
|
1466
|
-
notes: Object.freeze([...notes]),
|
|
1467
|
-
status: "updated"
|
|
1468
|
-
});
|
|
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);
|
|
1469
1015
|
}
|
|
1470
1016
|
//#endregion
|
|
1471
1017
|
//#region ../core/src/managed-content/revision.ts
|
|
@@ -1596,210 +1142,6 @@ function convergedPlan$1(app, skillId) {
|
|
|
1596
1142
|
};
|
|
1597
1143
|
}
|
|
1598
1144
|
//#endregion
|
|
1599
|
-
//#region ../core/src/workspace/mcp-classify.ts
|
|
1600
|
-
/**
|
|
1601
|
-
* Splits desired servers into the ones Aura may write and the collisions a person has to settle.
|
|
1602
|
-
*
|
|
1603
|
-
* Scope is part of identity here. A server named `docs` in a project `.mcp.json` is not the `docs`
|
|
1604
|
-
* the manifest wants in user-level configuration, and treating them as one either blocks a write
|
|
1605
|
-
* that would not have collided or skips one that never happened.
|
|
1606
|
-
*/
|
|
1607
|
-
function classifyDesired(desired, ledgerNames, state) {
|
|
1608
|
-
const ledger = new Set(ledgerNames);
|
|
1609
|
-
const owned = [];
|
|
1610
|
-
const blockers = [];
|
|
1611
|
-
for (const entry of desired) {
|
|
1612
|
-
const blocker = collisionBlocker(entry, ledger, state);
|
|
1613
|
-
if (blocker === "owned") owned.push(entry);
|
|
1614
|
-
else if (blocker !== void 0) blockers.push(blocker);
|
|
1615
|
-
}
|
|
1616
|
-
return {
|
|
1617
|
-
blockers,
|
|
1618
|
-
owned
|
|
1619
|
-
};
|
|
1620
|
-
}
|
|
1621
|
-
/** `owned` to write it, a blocker to refuse, `undefined` when the config already satisfies it. */
|
|
1622
|
-
function collisionBlocker(entry, ledger, state) {
|
|
1623
|
-
if (ledger.has(entry.name)) return "owned";
|
|
1624
|
-
const sameName = (candidate) => candidate.name === entry.name && candidate.scope === entry.scope;
|
|
1625
|
-
const unusable = state.unusable.find(sameName);
|
|
1626
|
-
if (unusable !== void 0) return {
|
|
1627
|
-
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.`,
|
|
1628
|
-
scope: entry.scope,
|
|
1629
|
-
sourceId: unusable.sourceId
|
|
1630
|
-
};
|
|
1631
|
-
const existing = state.servers.filter(sameName);
|
|
1632
|
-
if (existing.length === 0) return "owned";
|
|
1633
|
-
const normalized = normalizeDesired(entry);
|
|
1634
|
-
if ("message" in normalized) return normalized;
|
|
1635
|
-
return existing.every((server) => isDeepStrictEqual(server.transport, normalized.transport)) ? void 0 : {
|
|
1636
|
-
message: `MCP server ${entry.name} already exists outside Aura's ownership ledger and differs from the manifest.`,
|
|
1637
|
-
scope: entry.scope
|
|
1638
|
-
};
|
|
1639
|
-
}
|
|
1640
|
-
/**
|
|
1641
|
-
* Normalizes one desired transport, reporting a manifest Aura refuses to write as a blocker.
|
|
1642
|
-
*
|
|
1643
|
-
* The manifest is a file a person can edit. One that has acquired a credential literal is not a
|
|
1644
|
-
* crash in a check's `detect`; it is something to say out loud and decline to propagate.
|
|
1645
|
-
*/
|
|
1646
|
-
function normalizeDesired(entry) {
|
|
1647
|
-
try {
|
|
1648
|
-
return { transport: normalizeMcpServerDefinition(entry.transport) };
|
|
1649
|
-
} catch (error) {
|
|
1650
|
-
return {
|
|
1651
|
-
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.`,
|
|
1652
|
-
scope: entry.scope
|
|
1653
|
-
};
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
//#endregion
|
|
1657
|
-
//#region ../core/src/workspace/mcp-convergence.ts
|
|
1658
|
-
/** Captures adapter source bytes behind a function, keeping them out of the model entirely. */
|
|
1659
|
-
function createAppMcpConvergence(adapter, files, state) {
|
|
1660
|
-
const writer = adapter.mcpWrite;
|
|
1661
|
-
if (writer === void 0) return;
|
|
1662
|
-
const targets = [...files.values()].filter((file) => file.spec.kind === "mcp");
|
|
1663
|
-
return (desired, ledgerNames) => {
|
|
1664
|
-
const classified = classifyDesired(desired, ledgerNames, state);
|
|
1665
|
-
if (classified.blockers.length > 0) return blocked(classified.blockers);
|
|
1666
|
-
const scopes = ["global", "project"].map((scope) => planScope(adapter, writer, targets, classified.owned, ledgerNames, scope, state.secretSightings));
|
|
1667
|
-
const blockers = scopes.flatMap((result) => result.blockers);
|
|
1668
|
-
const operations = scopes.flatMap((result) => result.operations);
|
|
1669
|
-
return blockers.length > 0 ? blocked(blockers) : {
|
|
1670
|
-
blockers: [],
|
|
1671
|
-
operations,
|
|
1672
|
-
ownedNames: [...new Set(classified.owned.map((entry) => entry.name))].sort()
|
|
1673
|
-
};
|
|
1674
|
-
};
|
|
1675
|
-
}
|
|
1676
|
-
function planScope(adapter, writer, targets, desired, ledgerNames, scope, sightings) {
|
|
1677
|
-
const scopedDesired = desired.filter((entry) => entry.scope === scope);
|
|
1678
|
-
const scopedTargets = targets.filter((target) => target.spec.scope === scope);
|
|
1679
|
-
if (scopedDesired.length > 0 && scopedTargets.length === 0) return blockedScope(`${adapter.displayName} has no ${scope}-scope MCP configuration target.`, scope);
|
|
1680
|
-
if (scopedTargets.length > 1) return blockedScope(`${adapter.displayName} declares more than one ${scope}-scope MCP write target.`, scope);
|
|
1681
|
-
const target = scopedTargets[0];
|
|
1682
|
-
return target === void 0 ? {
|
|
1683
|
-
blockers: [],
|
|
1684
|
-
operations: []
|
|
1685
|
-
} : writeScopeTarget(adapter, writer, target, scopedDesired, ledgerNames, scope, sightings);
|
|
1686
|
-
}
|
|
1687
|
-
function writeScopeTarget(adapter, writer, target, desired, ledgerNames, scope, sightings) {
|
|
1688
|
-
if (!needsTargetWrite(target, desired, ledgerNames)) return {
|
|
1689
|
-
blockers: [],
|
|
1690
|
-
operations: []
|
|
1691
|
-
};
|
|
1692
|
-
const unwritable = targetRefusal(target);
|
|
1693
|
-
if (unwritable !== void 0) return {
|
|
1694
|
-
blockers: [{
|
|
1695
|
-
...unwritable,
|
|
1696
|
-
scope,
|
|
1697
|
-
sourceId: target.spec.id
|
|
1698
|
-
}],
|
|
1699
|
-
operations: []
|
|
1700
|
-
};
|
|
1701
|
-
const written = runWriter(writer, target, desired, ledgerNames);
|
|
1702
|
-
if ("refusal" in written) return {
|
|
1703
|
-
blockers: [{
|
|
1704
|
-
message: written.refusal,
|
|
1705
|
-
path: target.spec.path,
|
|
1706
|
-
scope,
|
|
1707
|
-
sourceId: target.spec.id
|
|
1708
|
-
}],
|
|
1709
|
-
operations: []
|
|
1710
|
-
};
|
|
1711
|
-
if (Buffer.byteLength(written.content, "utf8") > 4194304) return {
|
|
1712
|
-
blockers: [{
|
|
1713
|
-
message: `${adapter.displayName}'s MCP configuration at ${target.spec.path} is larger than Aura will rewrite in one operation, so Aura left it unchanged.`,
|
|
1714
|
-
path: target.spec.path,
|
|
1715
|
-
scope,
|
|
1716
|
-
sourceId: target.spec.id
|
|
1717
|
-
}],
|
|
1718
|
-
operations: []
|
|
1719
|
-
};
|
|
1720
|
-
const operation = {
|
|
1721
|
-
content: written.content,
|
|
1722
|
-
mode: scope === "global" ? 384 : 420,
|
|
1723
|
-
path: target.spec.path,
|
|
1724
|
-
precondition: targetPrecondition(target),
|
|
1725
|
-
type: "write"
|
|
1726
|
-
};
|
|
1727
|
-
rememberMcpRedactor(adapter, operation, target, sightings);
|
|
1728
|
-
return {
|
|
1729
|
-
blockers: [],
|
|
1730
|
-
operations: [operation]
|
|
1731
|
-
};
|
|
1732
|
-
}
|
|
1733
|
-
/**
|
|
1734
|
-
* Registers preview masking only for a target that actually holds an inline credential.
|
|
1735
|
-
*
|
|
1736
|
-
* Registering unconditionally would cost every convergence preview its diff, because masking has
|
|
1737
|
-
* no previous side to project when the fix is creating the file — which is what most MCP-001 fixes
|
|
1738
|
-
* do, on a file that by definition has no credentials in it yet.
|
|
1739
|
-
*/
|
|
1740
|
-
function rememberMcpRedactor(adapter, operation, target, sightings) {
|
|
1741
|
-
const transform = adapter.mcpSecrets;
|
|
1742
|
-
const scoped = sightings.filter((sighting) => sighting.sourceId === target.spec.id);
|
|
1743
|
-
if (transform === void 0 || scoped.length === 0) return;
|
|
1744
|
-
rememberWriteRedactor(operation, mcpSecretRedactor(transform, scoped));
|
|
1745
|
-
}
|
|
1746
|
-
/** Runs a plugin serializer, treating a thrown error as the refusal it forgot to return. */
|
|
1747
|
-
function runWriter(writer, target, desired, ledgerNames) {
|
|
1748
|
-
try {
|
|
1749
|
-
return writer({
|
|
1750
|
-
desired,
|
|
1751
|
-
...target.content === void 0 ? {} : { existingContent: target.content },
|
|
1752
|
-
ledgerNames
|
|
1753
|
-
});
|
|
1754
|
-
} catch {
|
|
1755
|
-
return { refusal: "The MCP configuration serializer failed, so Aura left the file unchanged." };
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
/**
|
|
1759
|
-
* What the file must still look like when the plan is applied.
|
|
1760
|
-
*
|
|
1761
|
-
* The rendered content is this file's own previous bytes with one section replaced, so applying it
|
|
1762
|
-
* against a file the application has since rewritten would silently revert that work. Declaring
|
|
1763
|
-
* what was read turns the race into a reported conflict.
|
|
1764
|
-
*/
|
|
1765
|
-
function targetPrecondition(target) {
|
|
1766
|
-
return target.content === void 0 ? { kind: "absent" } : {
|
|
1767
|
-
digest: createHash("sha256").update(target.content, "utf8").digest("hex"),
|
|
1768
|
-
kind: "sha256"
|
|
1769
|
-
};
|
|
1770
|
-
}
|
|
1771
|
-
function needsTargetWrite(target, desired, ledgerNames) {
|
|
1772
|
-
return (desired.length > 0 || ledgerNames.length > 0) && (target.exists || desired.length > 0);
|
|
1773
|
-
}
|
|
1774
|
-
/** Why this target cannot be rewritten safely, before a serializer is asked to try. */
|
|
1775
|
-
function targetRefusal(target) {
|
|
1776
|
-
const path = target.spec.path;
|
|
1777
|
-
if (target.problem !== void 0 || target.exists && target.content === void 0) return {
|
|
1778
|
-
message: `MCP configuration at ${path} could not be read safely, so Aura left it unchanged.`,
|
|
1779
|
-
path
|
|
1780
|
-
};
|
|
1781
|
-
return (target.size ?? 0) > 4194304 ? {
|
|
1782
|
-
message: `MCP configuration at ${path} is larger than Aura will rewrite in one operation, so Aura left it unchanged.`,
|
|
1783
|
-
path
|
|
1784
|
-
} : void 0;
|
|
1785
|
-
}
|
|
1786
|
-
function blockedScope(message, scope) {
|
|
1787
|
-
return {
|
|
1788
|
-
blockers: [{
|
|
1789
|
-
message,
|
|
1790
|
-
scope
|
|
1791
|
-
}],
|
|
1792
|
-
operations: []
|
|
1793
|
-
};
|
|
1794
|
-
}
|
|
1795
|
-
function blocked(blockers) {
|
|
1796
|
-
return {
|
|
1797
|
-
blockers,
|
|
1798
|
-
operations: [],
|
|
1799
|
-
ownedNames: []
|
|
1800
|
-
};
|
|
1801
|
-
}
|
|
1802
|
-
//#endregion
|
|
1803
1145
|
//#region ../core/src/workspace/mcp-plan-manifest.ts
|
|
1804
1146
|
/**
|
|
1805
1147
|
* Merges preset-required servers under the manifest's own, keyed by scope and name.
|
|
@@ -1858,12 +1200,16 @@ function withoutServer(manifest, server) {
|
|
|
1858
1200
|
* association lives here instead of on the object: reaching a planner takes this module, which
|
|
1859
1201
|
* plugin code cannot import.
|
|
1860
1202
|
*/
|
|
1861
|
-
const PLANNERS
|
|
1203
|
+
const PLANNERS = /* @__PURE__ */ new WeakMap();
|
|
1862
1204
|
/** Per-scan memo of what convergence planning produced, keyed by the model it was computed from. */
|
|
1863
|
-
const PLANS
|
|
1205
|
+
const PLANS = /* @__PURE__ */ new WeakMap();
|
|
1864
1206
|
/** Associates a planner with the app model core just built for it. */
|
|
1865
1207
|
function rememberMcpConvergence(app, convergence) {
|
|
1866
|
-
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);
|
|
1867
1213
|
}
|
|
1868
1214
|
/**
|
|
1869
1215
|
* Reports only why convergence is impossible, without rendering any file.
|
|
@@ -1877,8 +1223,8 @@ function mcpConvergenceBlockers(model, appId) {
|
|
|
1877
1223
|
}
|
|
1878
1224
|
/** Builds application writes and the ownership-ledger update as one atomic fix plan. */
|
|
1879
1225
|
function planManifestMcpConvergence(model, appId) {
|
|
1880
|
-
const memo = PLANS
|
|
1881
|
-
PLANS
|
|
1226
|
+
const memo = PLANS.get(model) ?? /* @__PURE__ */ new Map();
|
|
1227
|
+
PLANS.set(model, memo);
|
|
1882
1228
|
const cached = memo.get(appId);
|
|
1883
1229
|
if (cached !== void 0) return cached;
|
|
1884
1230
|
const computed = computeConvergence(model, appId);
|
|
@@ -1994,103 +1340,53 @@ function ambiguousRemoval(model, server) {
|
|
|
1994
1340
|
function resolvePlanner(model, appId) {
|
|
1995
1341
|
const app = model.apps.find((candidate) => candidate.adapterId === appId);
|
|
1996
1342
|
if (app === void 0) return { blockers: [{ message: `Application ${appId} was not detected.` }] };
|
|
1997
|
-
const convergence = PLANNERS
|
|
1343
|
+
const convergence = PLANNERS.get(app);
|
|
1998
1344
|
return convergence === void 0 ? { blockers: [{ message: `${app.displayName}'s adapter cannot write MCP configuration.` }] } : {
|
|
1999
1345
|
app,
|
|
2000
1346
|
convergence
|
|
2001
1347
|
};
|
|
2002
1348
|
}
|
|
2003
1349
|
//#endregion
|
|
2004
|
-
//#region ../core/src/workspace/
|
|
2005
|
-
const PLANNERS = /* @__PURE__ */ new WeakMap();
|
|
2006
|
-
const PLANS = /* @__PURE__ */ new WeakMap();
|
|
2007
|
-
/** Captures raw MCP bytes for remediation behind the same private boundary as convergence. */
|
|
2008
|
-
function createAppMcpSecretPlanner(adapter, files, sightings) {
|
|
2009
|
-
const transform = adapter.mcpSecrets;
|
|
2010
|
-
if (transform === void 0) return;
|
|
2011
|
-
return {
|
|
2012
|
-
plan: (sourceId) => {
|
|
2013
|
-
const target = files.get(sourceId);
|
|
2014
|
-
const sourceSightings = sightings.filter((sighting) => sighting.sourceId === sourceId);
|
|
2015
|
-
const supported = sourceSightings.filter(transform.supports);
|
|
2016
|
-
if (target === void 0 || target.spec.kind !== "mcp" || target.content === void 0 || targetRefusal(target) !== void 0 || supported.length === 0) return;
|
|
2017
|
-
let rewritten;
|
|
2018
|
-
try {
|
|
2019
|
-
rewritten = transform.rewrite({
|
|
2020
|
-
content: target.content,
|
|
2021
|
-
sightings: supported
|
|
2022
|
-
});
|
|
2023
|
-
} catch {
|
|
2024
|
-
return;
|
|
2025
|
-
}
|
|
2026
|
-
if ("refusal" in rewritten || rewritten.rewrittenFields.length !== supported.length) return;
|
|
2027
|
-
if (Buffer.byteLength(rewritten.content, "utf8") > 4194304) return;
|
|
2028
|
-
const operation = {
|
|
2029
|
-
content: rewritten.content,
|
|
2030
|
-
mode: target.spec.scope === "global" ? 384 : 420,
|
|
2031
|
-
path: target.spec.path,
|
|
2032
|
-
precondition: targetPrecondition(target),
|
|
2033
|
-
type: "write"
|
|
2034
|
-
};
|
|
2035
|
-
rememberWriteRedactor(operation, mcpSecretRedactor(transform, sourceSightings));
|
|
2036
|
-
return {
|
|
2037
|
-
manualSteps: remediationSteps(target.spec.path, supported),
|
|
2038
|
-
operations: [operation],
|
|
2039
|
-
summary: `Replace inline MCP credentials in ${target.spec.path} with environment references.`
|
|
2040
|
-
};
|
|
2041
|
-
},
|
|
2042
|
-
supports: transform.supports
|
|
2043
|
-
};
|
|
2044
|
-
}
|
|
2045
|
-
/** Associates a private planner with the app model that exposes only safe sightings. */
|
|
2046
|
-
function rememberMcpSecretPlanner(app, planner) {
|
|
2047
|
-
PLANNERS.set(app, planner);
|
|
2048
|
-
}
|
|
2049
|
-
/**
|
|
2050
|
-
* Whether this occurrence has a behavior-preserving adapter rewrite that a plan can actually carry.
|
|
2051
|
-
*
|
|
2052
|
-
* `supports` only reads the locator's shape, so it answers "could this kind of field be rewritten",
|
|
2053
|
-
* not "was this one". Planning is what discovers that the document is spelled in a form the writer
|
|
2054
|
-
* cannot edit, and a finding that advertises a guided fix the planner then declines to produce is
|
|
2055
|
-
* a dead end for the user. The plan is memoized, so asking it here costs nothing.
|
|
2056
|
-
*/
|
|
2057
|
-
function canPlanMcpSecretRemediation(model, sighting) {
|
|
2058
|
-
const app = model.apps.find((candidate) => candidate.adapterId === sighting.appId);
|
|
2059
|
-
if ((app === void 0 ? void 0 : PLANNERS.get(app))?.supports(sighting) !== true) return false;
|
|
2060
|
-
return planMcpSecretRemediation(model, sighting) !== void 0;
|
|
2061
|
-
}
|
|
2062
|
-
/** Builds the one whole-file plan shared by every representable sighting in a source file. */
|
|
2063
|
-
function planMcpSecretRemediation(model, sighting) {
|
|
2064
|
-
const key = `${sighting.appId}:${sighting.sourceId}`;
|
|
2065
|
-
const memo = PLANS.get(model) ?? /* @__PURE__ */ new Map();
|
|
2066
|
-
PLANS.set(model, memo);
|
|
2067
|
-
if (memo.has(key)) return memo.get(key);
|
|
2068
|
-
const app = model.apps.find((candidate) => candidate.adapterId === sighting.appId);
|
|
2069
|
-
const plan = (app === void 0 ? void 0 : PLANNERS.get(app))?.plan(sighting.sourceId);
|
|
2070
|
-
memo.set(key, plan);
|
|
2071
|
-
return plan;
|
|
2072
|
-
}
|
|
1350
|
+
//#region ../core/src/workspace/append-instructions.ts
|
|
2073
1351
|
/**
|
|
2074
|
-
*
|
|
1352
|
+
* Appends Markdown fragments while preserving every existing byte.
|
|
2075
1353
|
*
|
|
2076
|
-
*
|
|
2077
|
-
*
|
|
2078
|
-
*
|
|
2079
|
-
*
|
|
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.
|
|
2080
1358
|
*/
|
|
2081
|
-
function
|
|
2082
|
-
|
|
2083
|
-
const
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
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);
|
|
2090
1387
|
}
|
|
2091
1388
|
//#endregion
|
|
2092
1389
|
//#region ../core/src/workspace/shared-link-plan.ts
|
|
2093
|
-
const SHARED_LINK_SNIPPET_ID = "shared-instructions";
|
|
2094
1390
|
/** Builds the same safe shared-instruction link plan for checks and setup. */
|
|
2095
1391
|
function planSharedInstructionLink(app, model, options = {}) {
|
|
2096
1392
|
const link = options.link ?? app.sharedLink;
|
|
@@ -2099,21 +1395,24 @@ function planSharedInstructionLink(app, model, options = {}) {
|
|
|
2099
1395
|
const refusal = refuseBefore(app, status);
|
|
2100
1396
|
if (refusal !== void 0) return { blocked: refusal };
|
|
2101
1397
|
switch (link.kind) {
|
|
2102
|
-
case "import-line": return planImportLine(app,
|
|
2103
|
-
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);
|
|
2104
1400
|
case "symlink": return planSymlink(app, model, link, status, options);
|
|
2105
1401
|
}
|
|
2106
1402
|
}
|
|
2107
|
-
function planImportLine(app,
|
|
1403
|
+
function planImportLine(app, link, status, sourceContent) {
|
|
2108
1404
|
const source = instructionEntry(app, link.entryPath);
|
|
2109
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.` };
|
|
2110
|
-
const
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
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);
|
|
2117
1416
|
}
|
|
2118
1417
|
/**
|
|
2119
1418
|
* Whether what the scan saw still describes the file when this plan is applied.
|
|
@@ -2132,19 +1431,19 @@ function unreadableInstructionEntry(status, source, sourceContent) {
|
|
|
2132
1431
|
function selectedContent(sourceContent, source) {
|
|
2133
1432
|
return sourceContent ?? source?.content ?? "";
|
|
2134
1433
|
}
|
|
2135
|
-
function planNativeCopy(app,
|
|
1434
|
+
function planNativeCopy(app, link, status, sourceContent) {
|
|
2136
1435
|
const content = sourceContent ?? instructionEntry(app, link.entryPath)?.content;
|
|
2137
1436
|
const refusal = nativeCopyRefusal(status, sourceContent, content, link.content);
|
|
2138
1437
|
if (refusal !== void 0) return { blocked: refusal };
|
|
2139
1438
|
if (status?.exists === true && content === link.content && observedStateHolds(sourceContent)) return { plan: convergedPlan(app) };
|
|
2140
|
-
return { plan: writePlan(app, link, link.content ?? ""
|
|
1439
|
+
return { plan: writePlan(app, link, link.content ?? "") };
|
|
2141
1440
|
}
|
|
2142
1441
|
function nativeCopyRefusal(status, sourceContent, content, desired) {
|
|
2143
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;
|
|
2144
1443
|
}
|
|
2145
1444
|
function planSymlink(app, model, link, status, options) {
|
|
2146
|
-
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." };
|
|
2147
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." };
|
|
2148
1447
|
if (observedStateHolds(options.sourceContent) && pointsAt(status, target)) return { plan: convergedPlan(app) };
|
|
2149
1448
|
return { plan: {
|
|
2150
1449
|
operations: [{
|
|
@@ -2178,9 +1477,16 @@ function refuseBefore(app, status) {
|
|
|
2178
1477
|
if (status?.problem !== void 0) return `Aura could not safely read the instruction entry (${status.problem}) and will not overwrite it.`;
|
|
2179
1478
|
return status?.pathKind === "directory" ? "The instruction entry is a directory and cannot be replaced automatically." : void 0;
|
|
2180
1479
|
}
|
|
2181
|
-
|
|
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) {
|
|
2182
1489
|
return {
|
|
2183
|
-
...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.`] } : {},
|
|
2184
1490
|
operations: [{
|
|
2185
1491
|
content,
|
|
2186
1492
|
mode: 420,
|
|
@@ -2190,6 +1496,19 @@ function writePlan(app, link, content, homeDir) {
|
|
|
2190
1496
|
summary: `Link ${app.displayName} to the shared instruction source.`
|
|
2191
1497
|
};
|
|
2192
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
|
+
}
|
|
2193
1512
|
function instructionEntry(app, path) {
|
|
2194
1513
|
return app.instructionFiles.find((document) => resolve(document.path) === resolve(path));
|
|
2195
1514
|
}
|
|
@@ -2197,4 +1516,4 @@ function entryStatus(app, path) {
|
|
|
2197
1516
|
return app.sourceFiles.find((file) => resolve(file.spec.path) === resolve(path));
|
|
2198
1517
|
}
|
|
2199
1518
|
//#endregion
|
|
2200
|
-
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 };
|