@adaptic/maestro 1.5.0 → 1.5.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/bin/maestro.mjs +92 -1
- package/package.json +1 -1
package/bin/maestro.mjs
CHANGED
|
@@ -741,6 +741,62 @@ 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
|
+
// Directory prefix: `scripts/daemon/` matches scripts/daemon/* (recursive)
|
|
766
|
+
if (pat.endsWith("/")) {
|
|
767
|
+
const prefix = pat.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
768
|
+
return new RegExp("^" + prefix);
|
|
769
|
+
}
|
|
770
|
+
// Glob with * and **.
|
|
771
|
+
// Step order matters: ** must be captured before single * to preserve
|
|
772
|
+
// recursive semantics. * is intentionally NOT in the regex-escape char
|
|
773
|
+
// class so we can rewrite it after escaping the other specials.
|
|
774
|
+
if (pat.includes("*")) {
|
|
775
|
+
const escaped = pat
|
|
776
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
777
|
+
.replace(/\*\*/g, "<DBL>")
|
|
778
|
+
.replace(/\*/g, "[^/]*")
|
|
779
|
+
.replace(/<DBL>/g, ".*");
|
|
780
|
+
return new RegExp("^" + escaped + "$");
|
|
781
|
+
}
|
|
782
|
+
// Exact: also match anything underneath (a path like `scripts/daemon`
|
|
783
|
+
// without trailing slash should still protect the whole directory if
|
|
784
|
+
// it resolves to one — gitignore semantics).
|
|
785
|
+
const escaped = pat.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
786
|
+
return new RegExp("^" + escaped + "(/|$)");
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function matchesIgnore(repoRel, patterns) {
|
|
790
|
+
if (!patterns) return false;
|
|
791
|
+
let ignored = false;
|
|
792
|
+
for (const { pat, negate } of patterns) {
|
|
793
|
+
if (patternToRegex(pat).test(repoRel)) {
|
|
794
|
+
ignored = !negate;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return ignored;
|
|
798
|
+
}
|
|
799
|
+
|
|
744
800
|
function isGitRepo(cwd) {
|
|
745
801
|
try {
|
|
746
802
|
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, stdio: "pipe" });
|
|
@@ -804,6 +860,23 @@ Flags:
|
|
|
804
860
|
--no-incoming Don't write .maestro/incoming/ shadows for preserved files
|
|
805
861
|
--verbose, -v Print classification for every file
|
|
806
862
|
--help, -h Show this help
|
|
863
|
+
|
|
864
|
+
Per-file behaviour:
|
|
865
|
+
added — new upstream file → copy
|
|
866
|
+
updated — existed, byte-equal to your committed copy → safe overwrite
|
|
867
|
+
same — existed and already byte-identical to upstream → skip
|
|
868
|
+
ignored — matches a pattern in .maestroignore → never touched (any state)
|
|
869
|
+
preserved — has uncommitted local edits → keep yours, upstream lands at
|
|
870
|
+
.maestro/incoming/<path> for manual diff
|
|
871
|
+
mergeKept — under agents/ → never overwrite (custom agents preserved)
|
|
872
|
+
forced — overwritten by --force-overwrite; backup at .maestro/backup/<path>
|
|
873
|
+
|
|
874
|
+
.maestroignore format (gitignore-style, top-down, last match wins):
|
|
875
|
+
scripts/slack-send.sh exact file
|
|
876
|
+
scripts/daemon/ directory and everything beneath
|
|
877
|
+
scripts/send-email-*.py single-segment glob
|
|
878
|
+
workflows/** recursive glob
|
|
879
|
+
!workflows/quarterly/ un-ignore (force overwrite)
|
|
807
880
|
`);
|
|
808
881
|
return;
|
|
809
882
|
}
|
|
@@ -823,12 +896,17 @@ Flags:
|
|
|
823
896
|
}
|
|
824
897
|
|
|
825
898
|
const dirty = inGit ? dirtyPathSet(cwd) : null;
|
|
899
|
+
const ignorePatterns = loadMaestroignore(cwd);
|
|
900
|
+
if (ignorePatterns) {
|
|
901
|
+
log(`.maestroignore loaded (${ignorePatterns.length} pattern${ignorePatterns.length === 1 ? "" : "s"})`);
|
|
902
|
+
}
|
|
826
903
|
|
|
827
904
|
const banner = flags.dryRun ? "DRY RUN — " : "";
|
|
828
905
|
log(`${banner}Upgrading framework files from @adaptic/maestro...`);
|
|
829
906
|
|
|
830
|
-
const counts = { added: 0, updated: 0, same: 0, preserved: 0, mergeKept: 0, forced: 0 };
|
|
907
|
+
const counts = { added: 0, updated: 0, same: 0, ignored: 0, preserved: 0, mergeKept: 0, forced: 0 };
|
|
831
908
|
const preservedFiles = [];
|
|
909
|
+
const ignoredFiles = [];
|
|
832
910
|
|
|
833
911
|
for (const { path: relRoot, mode } of UPGRADE_PATHS) {
|
|
834
912
|
const srcRoot = join(MAESTRO_ROOT, relRoot);
|
|
@@ -841,6 +919,18 @@ Flags:
|
|
|
841
919
|
const dstFile = join(dstRoot, relFromRoot);
|
|
842
920
|
const repoRel = relative(cwd, dstFile).split(sep).join("/");
|
|
843
921
|
|
|
922
|
+
const isIgnored = matchesIgnore(repoRel, ignorePatterns);
|
|
923
|
+
|
|
924
|
+
// Case 0: .maestroignore match — never touch, regardless of state.
|
|
925
|
+
// Checked BEFORE existsSync so that protected new-files-from-upstream
|
|
926
|
+
// are never silently introduced into the agent repo either.
|
|
927
|
+
if (isIgnored) {
|
|
928
|
+
counts.ignored++;
|
|
929
|
+
ignoredFiles.push(repoRel);
|
|
930
|
+
if (flags.verbose) console.log(` · ${repoRel} (ignored via .maestroignore)`);
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
|
|
844
934
|
// Case 1: new file (doesn't exist in agent) — always copy.
|
|
845
935
|
if (!existsSync(dstFile)) {
|
|
846
936
|
if (!flags.dryRun) {
|
|
@@ -929,6 +1019,7 @@ Flags:
|
|
|
929
1019
|
if (counts.added) ok(`${counts.added} added (new files)`);
|
|
930
1020
|
if (counts.updated) ok(`${counts.updated} updated (vendored, no local edits)`);
|
|
931
1021
|
if (counts.same) console.log(` = ${counts.same} already up-to-date`);
|
|
1022
|
+
if (counts.ignored) console.log(` · ${counts.ignored} ignored (.maestroignore protected)`);
|
|
932
1023
|
if (counts.mergeKept) console.log(` ~ ${counts.mergeKept} merge-mode kept (agents/ custom files preserved)`);
|
|
933
1024
|
if (counts.preserved) warn(`${counts.preserved} preserved (local edits — kept your version)`);
|
|
934
1025
|
if (counts.forced) warn(`${counts.forced} force-overwritten (backups in .maestro/backup/)`);
|