@adaptic/maestro 1.5.0 → 1.5.2
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/bin/maestro.mjs +94 -1
- package/package.json +1 -1
package/bin/maestro.mjs
CHANGED
|
@@ -741,6 +741,64 @@ function walkFiles(root) {
|
|
|
741
741
|
return out;
|
|
742
742
|
}
|
|
743
743
|
|
|
744
|
+
// .maestroignore — gitignore-style allowlist of paths the upgrade must NOT
|
|
745
|
+
// touch. Supports comments (#), directory prefixes (trailing /), exact paths,
|
|
746
|
+
// simple globs (* matches a single path segment, ** matches recursively), and
|
|
747
|
+
// negation (! prefix to un-ignore). Match order is top-to-bottom; last match
|
|
748
|
+
// wins, like .gitignore.
|
|
749
|
+
function loadMaestroignore(cwd) {
|
|
750
|
+
const file = join(cwd, ".maestroignore");
|
|
751
|
+
if (!existsSync(file)) return null;
|
|
752
|
+
const patterns = [];
|
|
753
|
+
for (const raw of readFileSync(file, "utf-8").split(/\r?\n/)) {
|
|
754
|
+
const stripped = raw.replace(/^\s+|\s+$/g, "");
|
|
755
|
+
if (!stripped || stripped.startsWith("#")) continue;
|
|
756
|
+
const negate = stripped.startsWith("!");
|
|
757
|
+
const pat = negate ? stripped.slice(1).trim() : stripped;
|
|
758
|
+
if (!pat) continue;
|
|
759
|
+
patterns.push({ pat, negate });
|
|
760
|
+
}
|
|
761
|
+
return patterns.length ? patterns : null;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function patternToRegex(pat) {
|
|
765
|
+
// Helper: convert a glob-aware pattern to its regex source.
|
|
766
|
+
// ** must be substituted before single * so we don't double-rewrite it.
|
|
767
|
+
// * is intentionally outside the regex-escape char class so the
|
|
768
|
+
// glob substitution can find unescaped `*` characters afterward.
|
|
769
|
+
const globToRe = (p) =>
|
|
770
|
+
p.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
771
|
+
.replace(/\*\*/g, "<DBL>")
|
|
772
|
+
.replace(/\*/g, "[^/]*")
|
|
773
|
+
.replace(/<DBL>/g, ".*");
|
|
774
|
+
|
|
775
|
+
// Directory prefix: `scripts/daemon/` or `agents/sophie-*/` match
|
|
776
|
+
// anything under that path (recursive).
|
|
777
|
+
if (pat.endsWith("/")) {
|
|
778
|
+
return new RegExp("^" + globToRe(pat));
|
|
779
|
+
}
|
|
780
|
+
// Single-line glob: must match in full (no trailing slash).
|
|
781
|
+
if (pat.includes("*")) {
|
|
782
|
+
return new RegExp("^" + globToRe(pat) + "$");
|
|
783
|
+
}
|
|
784
|
+
// Exact: also matches anything underneath (gitignore semantics — a
|
|
785
|
+
// pattern without trailing slash still protects a directory if it
|
|
786
|
+
// resolves to one).
|
|
787
|
+
const escaped = pat.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
788
|
+
return new RegExp("^" + escaped + "(/|$)");
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function matchesIgnore(repoRel, patterns) {
|
|
792
|
+
if (!patterns) return false;
|
|
793
|
+
let ignored = false;
|
|
794
|
+
for (const { pat, negate } of patterns) {
|
|
795
|
+
if (patternToRegex(pat).test(repoRel)) {
|
|
796
|
+
ignored = !negate;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return ignored;
|
|
800
|
+
}
|
|
801
|
+
|
|
744
802
|
function isGitRepo(cwd) {
|
|
745
803
|
try {
|
|
746
804
|
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, stdio: "pipe" });
|
|
@@ -804,6 +862,23 @@ Flags:
|
|
|
804
862
|
--no-incoming Don't write .maestro/incoming/ shadows for preserved files
|
|
805
863
|
--verbose, -v Print classification for every file
|
|
806
864
|
--help, -h Show this help
|
|
865
|
+
|
|
866
|
+
Per-file behaviour:
|
|
867
|
+
added — new upstream file → copy
|
|
868
|
+
updated — existed, byte-equal to your committed copy → safe overwrite
|
|
869
|
+
same — existed and already byte-identical to upstream → skip
|
|
870
|
+
ignored — matches a pattern in .maestroignore → never touched (any state)
|
|
871
|
+
preserved — has uncommitted local edits → keep yours, upstream lands at
|
|
872
|
+
.maestro/incoming/<path> for manual diff
|
|
873
|
+
mergeKept — under agents/ → never overwrite (custom agents preserved)
|
|
874
|
+
forced — overwritten by --force-overwrite; backup at .maestro/backup/<path>
|
|
875
|
+
|
|
876
|
+
.maestroignore format (gitignore-style, top-down, last match wins):
|
|
877
|
+
scripts/slack-send.sh exact file
|
|
878
|
+
scripts/daemon/ directory and everything beneath
|
|
879
|
+
scripts/send-email-*.py single-segment glob
|
|
880
|
+
workflows/** recursive glob
|
|
881
|
+
!workflows/quarterly/ un-ignore (force overwrite)
|
|
807
882
|
`);
|
|
808
883
|
return;
|
|
809
884
|
}
|
|
@@ -823,12 +898,17 @@ Flags:
|
|
|
823
898
|
}
|
|
824
899
|
|
|
825
900
|
const dirty = inGit ? dirtyPathSet(cwd) : null;
|
|
901
|
+
const ignorePatterns = loadMaestroignore(cwd);
|
|
902
|
+
if (ignorePatterns) {
|
|
903
|
+
log(`.maestroignore loaded (${ignorePatterns.length} pattern${ignorePatterns.length === 1 ? "" : "s"})`);
|
|
904
|
+
}
|
|
826
905
|
|
|
827
906
|
const banner = flags.dryRun ? "DRY RUN — " : "";
|
|
828
907
|
log(`${banner}Upgrading framework files from @adaptic/maestro...`);
|
|
829
908
|
|
|
830
|
-
const counts = { added: 0, updated: 0, same: 0, preserved: 0, mergeKept: 0, forced: 0 };
|
|
909
|
+
const counts = { added: 0, updated: 0, same: 0, ignored: 0, preserved: 0, mergeKept: 0, forced: 0 };
|
|
831
910
|
const preservedFiles = [];
|
|
911
|
+
const ignoredFiles = [];
|
|
832
912
|
|
|
833
913
|
for (const { path: relRoot, mode } of UPGRADE_PATHS) {
|
|
834
914
|
const srcRoot = join(MAESTRO_ROOT, relRoot);
|
|
@@ -841,6 +921,18 @@ Flags:
|
|
|
841
921
|
const dstFile = join(dstRoot, relFromRoot);
|
|
842
922
|
const repoRel = relative(cwd, dstFile).split(sep).join("/");
|
|
843
923
|
|
|
924
|
+
const isIgnored = matchesIgnore(repoRel, ignorePatterns);
|
|
925
|
+
|
|
926
|
+
// Case 0: .maestroignore match — never touch, regardless of state.
|
|
927
|
+
// Checked BEFORE existsSync so that protected new-files-from-upstream
|
|
928
|
+
// are never silently introduced into the agent repo either.
|
|
929
|
+
if (isIgnored) {
|
|
930
|
+
counts.ignored++;
|
|
931
|
+
ignoredFiles.push(repoRel);
|
|
932
|
+
if (flags.verbose) console.log(` · ${repoRel} (ignored via .maestroignore)`);
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
|
|
844
936
|
// Case 1: new file (doesn't exist in agent) — always copy.
|
|
845
937
|
if (!existsSync(dstFile)) {
|
|
846
938
|
if (!flags.dryRun) {
|
|
@@ -929,6 +1021,7 @@ Flags:
|
|
|
929
1021
|
if (counts.added) ok(`${counts.added} added (new files)`);
|
|
930
1022
|
if (counts.updated) ok(`${counts.updated} updated (vendored, no local edits)`);
|
|
931
1023
|
if (counts.same) console.log(` = ${counts.same} already up-to-date`);
|
|
1024
|
+
if (counts.ignored) console.log(` · ${counts.ignored} ignored (.maestroignore protected)`);
|
|
932
1025
|
if (counts.mergeKept) console.log(` ~ ${counts.mergeKept} merge-mode kept (agents/ custom files preserved)`);
|
|
933
1026
|
if (counts.preserved) warn(`${counts.preserved} preserved (local edits — kept your version)`);
|
|
934
1027
|
if (counts.forced) warn(`${counts.forced} force-overwritten (backups in .maestro/backup/)`);
|