@tryaura/aura-cli 0.2.1 → 0.3.1
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-BEuoEUo_.js} +58 -580
- package/dist/{run.boundary-CF5kZTH3.js → run.boundary-NVWIwGgg.js} +7294 -4828
- package/dist/{shared-link-plan-D8fxdFYv.js → shared-link-plan-AcSkw9bO.js} +170 -850
- 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.
|
|
@@ -403,7 +445,7 @@ function trustedPath(value, jsonPath) {
|
|
|
403
445
|
const APP_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/u;
|
|
404
446
|
const MCP_CATALOG_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*\/[a-zA-Z0-9][a-zA-Z0-9._-]*$/u;
|
|
405
447
|
const SKILL_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
406
|
-
const SKILL_SOURCE_PATTERN = /^(?:directory|driver|plugin):[^\s:]+$/u;
|
|
448
|
+
const SKILL_SOURCE_PATTERN = /^(?:directory|driver|plugin|repo):[^\s:]+$/u;
|
|
407
449
|
const OVERRIDE_KEY_PATTERN = /^[a-z][a-zA-Z0-9]{0,63}$/u;
|
|
408
450
|
/** Room for several future override kinds without leaving the forward-compat window unbounded. */
|
|
409
451
|
const MAX_OVERRIDE_KEYS = 32;
|
|
@@ -495,9 +537,10 @@ function skills(value) {
|
|
|
495
537
|
}
|
|
496
538
|
function requiredSkillSource(value, path) {
|
|
497
539
|
const source = requiredString(value, "source", path);
|
|
498
|
-
if (!SKILL_SOURCE_PATTERN.test(source)) throw invalid(`${path}.source`, "must be a plugin:, directory:, or
|
|
540
|
+
if (!SKILL_SOURCE_PATTERN.test(source)) throw invalid(`${path}.source`, "must be a plugin:, directory:, driver:, or repo: source ID");
|
|
499
541
|
if (source.startsWith("plugin:")) return `plugin:${source.slice(7)}`;
|
|
500
542
|
if (source.startsWith("directory:")) return `directory:${source.slice(10)}`;
|
|
543
|
+
if (source.startsWith("repo:")) return `repo:${source.slice(5)}`;
|
|
501
544
|
return `driver:${source.slice(7)}`;
|
|
502
545
|
}
|
|
503
546
|
var UnsupportedAuraManifestVersionError = class extends Error {
|
|
@@ -521,22 +564,6 @@ function apps(value) {
|
|
|
521
564
|
}
|
|
522
565
|
return Object.freeze(result);
|
|
523
566
|
}
|
|
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
567
|
function ownership(value) {
|
|
541
568
|
const source = requiredObject(value, "$.ownership");
|
|
542
569
|
const result = {};
|
|
@@ -705,228 +732,49 @@ function createAuraManifestWriteOperation(state, manifest) {
|
|
|
705
732
|
});
|
|
706
733
|
}
|
|
707
734
|
//#endregion
|
|
708
|
-
//#region ../core/src/
|
|
735
|
+
//#region ../core/src/content-hash.ts
|
|
736
|
+
/** Shape of every hash this module produces, for validators that must recognize one. */
|
|
737
|
+
const CONTENT_HASH_PATTERN = /^[0-9a-f]{64}$/u;
|
|
709
738
|
/**
|
|
710
|
-
*
|
|
739
|
+
* Normalizes Markdown to LF and exactly one trailing newline.
|
|
711
740
|
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
741
|
+
* Line endings and trailing blank lines are what a checkout, an editor, or an append seam change
|
|
742
|
+
* without changing what the text says, so a hash that counted them would report drift on every
|
|
743
|
+
* machine that rewrote them — and a fragment appended in this form is the same text the hash
|
|
744
|
+
* covers. Trailing newlines are trimmed by scanning rather than with `/\n+$/`, whose backtracking
|
|
745
|
+
* is quadratic when a long newline run does not reach the end of the string.
|
|
715
746
|
*/
|
|
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);
|
|
747
|
+
function canonicalizeContent(content) {
|
|
748
|
+
const normalized = content.replace(/\r\n?/gu, "\n");
|
|
749
|
+
let end = normalized.length;
|
|
750
|
+
while (end > 0 && normalized[end - 1] === "\n") end -= 1;
|
|
751
|
+
return `${normalized.slice(0, end)}\n`;
|
|
845
752
|
}
|
|
846
753
|
/**
|
|
847
|
-
*
|
|
754
|
+
* Fingerprints Markdown Aura records but does not own: an installed snippet, a trusted preset.
|
|
848
755
|
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
* and still holds the credential.
|
|
756
|
+
* One function for both because the two records answer the same question — "is this still the text
|
|
757
|
+
* the user accepted?" — and two spellings of the canonical form would answer it differently on the
|
|
758
|
+
* first file whose line endings a checkout rewrote.
|
|
853
759
|
*/
|
|
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
|
-
};
|
|
760
|
+
function hashContent(content) {
|
|
761
|
+
return createHash("sha256").update(canonicalizeContent(content), "utf8").digest("hex");
|
|
862
762
|
}
|
|
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
763
|
const MANAGED_SNIPPET_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
906
|
-
const MANAGED_SNIPPET_HASH_PATTERN =
|
|
764
|
+
const MANAGED_SNIPPET_HASH_PATTERN = CONTENT_HASH_PATTERN;
|
|
907
765
|
/** Whether an id is both line-safe and legal inside an HTML comment. */
|
|
908
766
|
function isManagedSnippetId(id) {
|
|
909
767
|
return MANAGED_SNIPPET_ID_PATTERN.test(id) && !id.includes("--");
|
|
910
768
|
}
|
|
911
769
|
/**
|
|
912
|
-
*
|
|
770
|
+
* Computes the protocol hash for a snippet's canonical UTF-8 contents.
|
|
913
771
|
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
772
|
+
* The legacy block's hash is {@link hashContent} under another name: a marked section read back
|
|
773
|
+
* from an older file has to compare equal to the same text hashed anywhere else in Aura, or a
|
|
774
|
+
* migration would report drift on content nobody touched.
|
|
916
775
|
*/
|
|
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
776
|
function hashManagedSnippet(content) {
|
|
929
|
-
return
|
|
777
|
+
return hashContent(content);
|
|
930
778
|
}
|
|
931
779
|
//#endregion
|
|
932
780
|
//#region ../core/src/managed-block/parse-state.ts
|
|
@@ -1148,324 +996,23 @@ function collectPlainLine(source, line, protectedByFence, block, snippet, notes)
|
|
|
1148
996
|
}));
|
|
1149
997
|
}
|
|
1150
998
|
//#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
|
|
999
|
+
//#region ../core/src/managed-block/strip.ts
|
|
1341
1000
|
/**
|
|
1342
|
-
*
|
|
1001
|
+
* Removes a legacy Aura-managed block from a source string, keeping everything else byte-for-byte.
|
|
1343
1002
|
*
|
|
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.
|
|
1003
|
+
* For a consumer deciding what a file's *user-authored* content is — instruction consolidation
|
|
1004
|
+
* above all — the managed block is Aura's own artifact: merging it elsewhere plants links and
|
|
1005
|
+
* legacy ledger sections in files that must not carry them. Unmanaged lines found inside the block
|
|
1006
|
+
* are user text and are kept.
|
|
1435
1007
|
*
|
|
1436
|
-
*
|
|
1437
|
-
*
|
|
1438
|
-
*
|
|
1008
|
+
* A source with no block, and one whose block does not parse, are returned unchanged: a malformed
|
|
1009
|
+
* block cannot be attributed to Aura with confidence, and dropping bytes on that guess would lose
|
|
1010
|
+
* user content.
|
|
1439
1011
|
*/
|
|
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
|
-
});
|
|
1012
|
+
function stripLegacyManagedBlock(source) {
|
|
1013
|
+
const parsed = readManagedBlock(source);
|
|
1014
|
+
if (parsed.status !== "present") return source;
|
|
1015
|
+
return source.slice(0, parsed.block.startOffset) + parsed.block.unmanagedContent + source.slice(parsed.block.endOffset);
|
|
1469
1016
|
}
|
|
1470
1017
|
//#endregion
|
|
1471
1018
|
//#region ../core/src/managed-content/revision.ts
|
|
@@ -1596,210 +1143,6 @@ function convergedPlan$1(app, skillId) {
|
|
|
1596
1143
|
};
|
|
1597
1144
|
}
|
|
1598
1145
|
//#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
1146
|
//#region ../core/src/workspace/mcp-plan-manifest.ts
|
|
1804
1147
|
/**
|
|
1805
1148
|
* Merges preset-required servers under the manifest's own, keyed by scope and name.
|
|
@@ -1858,12 +1201,16 @@ function withoutServer(manifest, server) {
|
|
|
1858
1201
|
* association lives here instead of on the object: reaching a planner takes this module, which
|
|
1859
1202
|
* plugin code cannot import.
|
|
1860
1203
|
*/
|
|
1861
|
-
const PLANNERS
|
|
1204
|
+
const PLANNERS = /* @__PURE__ */ new WeakMap();
|
|
1862
1205
|
/** Per-scan memo of what convergence planning produced, keyed by the model it was computed from. */
|
|
1863
|
-
const PLANS
|
|
1206
|
+
const PLANS = /* @__PURE__ */ new WeakMap();
|
|
1864
1207
|
/** Associates a planner with the app model core just built for it. */
|
|
1865
1208
|
function rememberMcpConvergence(app, convergence) {
|
|
1866
|
-
PLANNERS
|
|
1209
|
+
PLANNERS.set(app, convergence);
|
|
1210
|
+
}
|
|
1211
|
+
/** Forgets one scan's memoized plans, after a refresh replaced the bytes they were built from. */
|
|
1212
|
+
function forgetMcpPlans(model) {
|
|
1213
|
+
PLANS.delete(model);
|
|
1867
1214
|
}
|
|
1868
1215
|
/**
|
|
1869
1216
|
* Reports only why convergence is impossible, without rendering any file.
|
|
@@ -1877,8 +1224,8 @@ function mcpConvergenceBlockers(model, appId) {
|
|
|
1877
1224
|
}
|
|
1878
1225
|
/** Builds application writes and the ownership-ledger update as one atomic fix plan. */
|
|
1879
1226
|
function planManifestMcpConvergence(model, appId) {
|
|
1880
|
-
const memo = PLANS
|
|
1881
|
-
PLANS
|
|
1227
|
+
const memo = PLANS.get(model) ?? /* @__PURE__ */ new Map();
|
|
1228
|
+
PLANS.set(model, memo);
|
|
1882
1229
|
const cached = memo.get(appId);
|
|
1883
1230
|
if (cached !== void 0) return cached;
|
|
1884
1231
|
const computed = computeConvergence(model, appId);
|
|
@@ -1994,103 +1341,53 @@ function ambiguousRemoval(model, server) {
|
|
|
1994
1341
|
function resolvePlanner(model, appId) {
|
|
1995
1342
|
const app = model.apps.find((candidate) => candidate.adapterId === appId);
|
|
1996
1343
|
if (app === void 0) return { blockers: [{ message: `Application ${appId} was not detected.` }] };
|
|
1997
|
-
const convergence = PLANNERS
|
|
1344
|
+
const convergence = PLANNERS.get(app);
|
|
1998
1345
|
return convergence === void 0 ? { blockers: [{ message: `${app.displayName}'s adapter cannot write MCP configuration.` }] } : {
|
|
1999
1346
|
app,
|
|
2000
1347
|
convergence
|
|
2001
1348
|
};
|
|
2002
1349
|
}
|
|
2003
1350
|
//#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
|
-
}
|
|
1351
|
+
//#region ../core/src/workspace/append-instructions.ts
|
|
2073
1352
|
/**
|
|
2074
|
-
*
|
|
1353
|
+
* Appends Markdown fragments while preserving every existing byte.
|
|
2075
1354
|
*
|
|
2076
|
-
*
|
|
2077
|
-
*
|
|
2078
|
-
*
|
|
2079
|
-
*
|
|
1355
|
+
* Only appended fragments are canonicalized, and by the same normalization the content hash
|
|
1356
|
+
* covers — that is what lets a recorded fingerprint answer for text already in the file. They use
|
|
1357
|
+
* the target's line ending and end in one line ending, with one blank-line seam between
|
|
1358
|
+
* consecutive fragments.
|
|
2080
1359
|
*/
|
|
2081
|
-
function
|
|
2082
|
-
|
|
2083
|
-
const
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
1360
|
+
function appendInstructionFragments(source, fragments) {
|
|
1361
|
+
if (fragments.length === 0) return source;
|
|
1362
|
+
const lineEnding = source.includes("\r\n") ? "\r\n" : "\n";
|
|
1363
|
+
let result = source;
|
|
1364
|
+
for (const fragment of fragments) {
|
|
1365
|
+
const canonical = canonicalizeContent(fragment).replaceAll("\n", lineEnding);
|
|
1366
|
+
if (result.length > 0) result += blankLineSeparator(result, lineEnding);
|
|
1367
|
+
result += canonical;
|
|
1368
|
+
}
|
|
1369
|
+
return result;
|
|
1370
|
+
}
|
|
1371
|
+
function blankLineSeparator(source, lineEnding) {
|
|
1372
|
+
let trailingLineEndings = 0;
|
|
1373
|
+
let end = source.length;
|
|
1374
|
+
while (end > 0 && trailingLineEndings < 2) {
|
|
1375
|
+
if (source.endsWith("\r\n", end)) {
|
|
1376
|
+
end -= 2;
|
|
1377
|
+
trailingLineEndings += 1;
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
if (source[end - 1] === "\n" || source[end - 1] === "\r") {
|
|
1381
|
+
end -= 1;
|
|
1382
|
+
trailingLineEndings += 1;
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
break;
|
|
1386
|
+
}
|
|
1387
|
+
return lineEnding.repeat(2 - trailingLineEndings);
|
|
2090
1388
|
}
|
|
2091
1389
|
//#endregion
|
|
2092
1390
|
//#region ../core/src/workspace/shared-link-plan.ts
|
|
2093
|
-
const SHARED_LINK_SNIPPET_ID = "shared-instructions";
|
|
2094
1391
|
/** Builds the same safe shared-instruction link plan for checks and setup. */
|
|
2095
1392
|
function planSharedInstructionLink(app, model, options = {}) {
|
|
2096
1393
|
const link = options.link ?? app.sharedLink;
|
|
@@ -2099,21 +1396,24 @@ function planSharedInstructionLink(app, model, options = {}) {
|
|
|
2099
1396
|
const refusal = refuseBefore(app, status);
|
|
2100
1397
|
if (refusal !== void 0) return { blocked: refusal };
|
|
2101
1398
|
switch (link.kind) {
|
|
2102
|
-
case "import-line": return planImportLine(app,
|
|
2103
|
-
case "native-copy": return planNativeCopy(app,
|
|
1399
|
+
case "import-line": return planImportLine(app, link, status, options.sourceContent);
|
|
1400
|
+
case "native-copy": return planNativeCopy(app, link, status, options.sourceContent);
|
|
2104
1401
|
case "symlink": return planSymlink(app, model, link, status, options);
|
|
2105
1402
|
}
|
|
2106
1403
|
}
|
|
2107
|
-
function planImportLine(app,
|
|
1404
|
+
function planImportLine(app, link, status, sourceContent) {
|
|
2108
1405
|
const source = instructionEntry(app, link.entryPath);
|
|
2109
1406
|
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
|
-
|
|
1407
|
+
const current = selectedContent(sourceContent, source);
|
|
1408
|
+
const desired = link.content ?? "";
|
|
1409
|
+
if (observedStateHolds(sourceContent) && containsImport(current, desired)) return { plan: convergedPlan(app) };
|
|
1410
|
+
return { plan: writePlan(app, link, appendInstructionFragments(current, [desired])) };
|
|
1411
|
+
}
|
|
1412
|
+
/** Legacy marked imports and new plain imports both contain the exact adapter-rendered line. */
|
|
1413
|
+
function containsImport(source, desired) {
|
|
1414
|
+
const normalized = desired.replace(/\r\n?/gu, "\n");
|
|
1415
|
+
const normalizedDesired = normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
|
|
1416
|
+
return source.replace(/\r\n?/gu, "\n").split("\n").some((line) => line === normalizedDesired);
|
|
2117
1417
|
}
|
|
2118
1418
|
/**
|
|
2119
1419
|
* Whether what the scan saw still describes the file when this plan is applied.
|
|
@@ -2132,19 +1432,19 @@ function unreadableInstructionEntry(status, source, sourceContent) {
|
|
|
2132
1432
|
function selectedContent(sourceContent, source) {
|
|
2133
1433
|
return sourceContent ?? source?.content ?? "";
|
|
2134
1434
|
}
|
|
2135
|
-
function planNativeCopy(app,
|
|
1435
|
+
function planNativeCopy(app, link, status, sourceContent) {
|
|
2136
1436
|
const content = sourceContent ?? instructionEntry(app, link.entryPath)?.content;
|
|
2137
1437
|
const refusal = nativeCopyRefusal(status, sourceContent, content, link.content);
|
|
2138
1438
|
if (refusal !== void 0) return { blocked: refusal };
|
|
2139
1439
|
if (status?.exists === true && content === link.content && observedStateHolds(sourceContent)) return { plan: convergedPlan(app) };
|
|
2140
|
-
return { plan: writePlan(app, link, link.content ?? ""
|
|
1440
|
+
return { plan: writePlan(app, link, link.content ?? "") };
|
|
2141
1441
|
}
|
|
2142
1442
|
function nativeCopyRefusal(status, sourceContent, content, desired) {
|
|
2143
1443
|
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
1444
|
}
|
|
2145
1445
|
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
1446
|
const target = options.symlinkTarget ?? model.sharedInstructions.path;
|
|
1447
|
+
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
1448
|
if (observedStateHolds(options.sourceContent) && pointsAt(status, target)) return { plan: convergedPlan(app) };
|
|
2149
1449
|
return { plan: {
|
|
2150
1450
|
operations: [{
|
|
@@ -2178,9 +1478,16 @@ function refuseBefore(app, status) {
|
|
|
2178
1478
|
if (status?.problem !== void 0) return `Aura could not safely read the instruction entry (${status.problem}) and will not overwrite it.`;
|
|
2179
1479
|
return status?.pathKind === "directory" ? "The instruction entry is a directory and cannot be replaced automatically." : void 0;
|
|
2180
1480
|
}
|
|
2181
|
-
|
|
1481
|
+
/**
|
|
1482
|
+
* Writes one entry, with nothing to warn the user about afterwards.
|
|
1483
|
+
*
|
|
1484
|
+
* There used to be a manual step here telling the user to keep a project entry out of version
|
|
1485
|
+
* control, because a project entry could name the shared source by absolute path. It cannot any
|
|
1486
|
+
* more: a project entry names it relatively, and a home entry through `~`. Both are committable or
|
|
1487
|
+
* personal by construction rather than by advice the user has to remember to follow.
|
|
1488
|
+
*/
|
|
1489
|
+
function writePlan(app, link, content) {
|
|
2182
1490
|
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
1491
|
operations: [{
|
|
2185
1492
|
content,
|
|
2186
1493
|
mode: 420,
|
|
@@ -2190,6 +1497,19 @@ function writePlan(app, link, content, homeDir) {
|
|
|
2190
1497
|
summary: `Link ${app.displayName} to the shared instruction source.`
|
|
2191
1498
|
};
|
|
2192
1499
|
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Whether a real file at this entry holds nothing but a legacy block or plain link Aura wrote.
|
|
1502
|
+
*
|
|
1503
|
+
* The refusal above guards the user's own guidance, and an entry Aura wired as an import line holds
|
|
1504
|
+
* none. Without this, an app switching from an import line to a link strands every machine already
|
|
1505
|
+
* wired the old way: consolidation has no source to offer for a file that is Aura's.
|
|
1506
|
+
*/
|
|
1507
|
+
function holdsOnlySharedReference(app, path, target) {
|
|
1508
|
+
const document = instructionEntry(app, path);
|
|
1509
|
+
if (document === void 0) return false;
|
|
1510
|
+
if (stripLegacyManagedBlock(document.content).trim().length === 0) return true;
|
|
1511
|
+
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));
|
|
1512
|
+
}
|
|
2193
1513
|
function instructionEntry(app, path) {
|
|
2194
1514
|
return app.instructionFiles.find((document) => resolve(document.path) === resolve(path));
|
|
2195
1515
|
}
|
|
@@ -2197,4 +1517,4 @@ function entryStatus(app, path) {
|
|
|
2197
1517
|
return app.sourceFiles.find((file) => resolve(file.spec.path) === resolve(path));
|
|
2198
1518
|
}
|
|
2199
1519
|
//#endregion
|
|
2200
|
-
export {
|
|
1520
|
+
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 };
|