@mastra/factory 0.25.0-alpha.1 → 0.25.0-alpha.3
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/routes/config.d.ts +2 -0
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +2 -1
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/fs.d.ts +11 -6
- package/dist/routes/fs.d.ts.map +1 -1
- package/dist/routes/fs.js +161 -133
- package/dist/routes/fs.js.map +1 -1
- package/dist/tools/reports.d.ts.map +1 -1
- package/dist/tools/reports.js +34 -5
- package/dist/tools/reports.js.map +1 -1
- package/package.json +10 -10
package/dist/routes/fs.js
CHANGED
|
@@ -11,16 +11,19 @@ import { detectProject, getResourceIdOverride } from "@mastra/code-sdk/utils/pro
|
|
|
11
11
|
//#region src/routes/fs.ts
|
|
12
12
|
const MAX_TEXT_FILE_BYTES = 524288;
|
|
13
13
|
const MAX_DIFF_BYTES = 524288;
|
|
14
|
-
const
|
|
14
|
+
const MAX_PATCH_SET_BYTES = 8388608;
|
|
15
|
+
/** Diffs the worktree against `$2`, untracked files as added through a throwaway index so the real one stays untouched. */
|
|
16
|
+
const WORKSPACE_DIFF_SCRIPT = `
|
|
15
17
|
set -e
|
|
16
18
|
workdir=$1
|
|
17
19
|
base=$2
|
|
20
|
+
shift 2
|
|
18
21
|
index_file=$(mktemp)
|
|
19
22
|
untracked_file=$(mktemp)
|
|
20
23
|
object_dir=$(mktemp -d)
|
|
21
24
|
trap 'rm -f "$index_file" "$untracked_file"; rm -rf "$object_dir"' EXIT
|
|
22
25
|
rm -f "$index_file"
|
|
23
|
-
git -C "$workdir"
|
|
26
|
+
git -C "$workdir" -c core.quotePath=false diff --no-ext-diff --no-textconv --find-renames "$@" "$base"
|
|
24
27
|
git -C "$workdir" ls-files --others --exclude-standard -z >"$untracked_file"
|
|
25
28
|
git_dir=$(git -C "$workdir" rev-parse --absolute-git-dir)
|
|
26
29
|
export GIT_INDEX_FILE="$index_file"
|
|
@@ -29,24 +32,30 @@ export GIT_ALTERNATE_OBJECT_DIRECTORIES="$git_dir/objects"
|
|
|
29
32
|
git -C "$workdir" read-tree --empty
|
|
30
33
|
git -C "$workdir" update-index --add -z --stdin <"$untracked_file"
|
|
31
34
|
empty_tree=$(git -C "$workdir" hash-object -t tree /dev/null)
|
|
32
|
-
git -C "$workdir" diff --cached --
|
|
35
|
+
git -C "$workdir" -c core.quotePath=false diff --cached --no-ext-diff --no-textconv "$@" "$empty_tree"
|
|
33
36
|
`;
|
|
34
|
-
const
|
|
35
|
-
|
|
37
|
+
const NUMSTAT_FORMAT = ["--numstat", "-z"];
|
|
38
|
+
const PATCH_FORMAT = [
|
|
39
|
+
"--patch",
|
|
40
|
+
"--no-color",
|
|
41
|
+
"--unified=3",
|
|
42
|
+
"--src-prefix=a/",
|
|
43
|
+
"--dst-prefix=b/"
|
|
44
|
+
];
|
|
45
|
+
/** Keeps the first `$1` bytes of the command in the remaining arguments; being cut short is not a failure. */
|
|
46
|
+
const BOUNDED_OUTPUT_SCRIPT = `
|
|
47
|
+
limit=$1
|
|
36
48
|
shift
|
|
37
49
|
status_file=$(mktemp)
|
|
38
|
-
|
|
39
|
-
trap 'rm -f "$status_file" "$stderr_file"' EXIT
|
|
50
|
+
trap 'rm -f "$status_file"' EXIT
|
|
40
51
|
(
|
|
41
|
-
|
|
52
|
+
"$@"
|
|
42
53
|
printf '%s' "$?" >"$status_file"
|
|
43
|
-
) | head -c
|
|
54
|
+
) | head -c "$limit"
|
|
44
55
|
status=$(cat "$status_file")
|
|
45
56
|
case "$status" in
|
|
46
57
|
0|141) exit 0 ;;
|
|
47
|
-
1) [ "$allow_exit_one" = "1" ] && exit 0 ;;
|
|
48
58
|
esac
|
|
49
|
-
cat "$stderr_file" >&2
|
|
50
59
|
exit "\${status:-1}"
|
|
51
60
|
`;
|
|
52
61
|
const TEXT_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
@@ -318,26 +327,6 @@ function repositoryBaseBranches(session, layout, ownRepo) {
|
|
|
318
327
|
}
|
|
319
328
|
return bases;
|
|
320
329
|
}
|
|
321
|
-
/**
|
|
322
|
-
* Split a client path into the layout repository it belongs to and the path
|
|
323
|
-
* inside that checkout. Longest prefix wins; the inside path is validated
|
|
324
|
-
* again so a prefix cannot smuggle a traversal. A path matching no repository
|
|
325
|
-
* (including a bare prefix) is not available: the file routes only serve the
|
|
326
|
-
* checkouts, never the workspace root itself.
|
|
327
|
-
*/
|
|
328
|
-
function resolveLayoutPath(layout, path, label) {
|
|
329
|
-
const safePath = assertRelativePath(path, label);
|
|
330
|
-
if (layout.repos.length === 1) return {
|
|
331
|
-
repo: layout.repos[0],
|
|
332
|
-
relative: safePath
|
|
333
|
-
};
|
|
334
|
-
const repo = layout.repos.filter((candidate) => safePath.startsWith(candidate.prefix) && safePath.length > candidate.prefix.length).toSorted((a, b) => b.prefix.length - a.prefix.length)[0];
|
|
335
|
-
if (!repo) throw new Error(`${label} is not available in this session's repositories`);
|
|
336
|
-
return {
|
|
337
|
-
repo,
|
|
338
|
-
relative: assertRelativePath(safePath.slice(repo.prefix.length), label)
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
330
|
/** List an approved rendered root inside a Factory session's sandbox workdir. */
|
|
342
331
|
async function listSessionRenderedPath(session, renderedRoot) {
|
|
343
332
|
const safeRoot = assertApprovedRenderedRoot(renderedRoot);
|
|
@@ -696,7 +685,11 @@ function unavailableWorkspaceChanges(workspacePath) {
|
|
|
696
685
|
changes: []
|
|
697
686
|
};
|
|
698
687
|
}
|
|
699
|
-
|
|
688
|
+
function isWorkspaceChangesScope(value) {
|
|
689
|
+
return value === "branch" || value === "uncommitted";
|
|
690
|
+
}
|
|
691
|
+
async function resolveSessionComparisonBase(sandbox, workdir, baseBranch, scope) {
|
|
692
|
+
if (scope === "uncommitted") return "HEAD";
|
|
700
693
|
const result = await sandbox.executeCommand("git", [
|
|
701
694
|
"-C",
|
|
702
695
|
workdir,
|
|
@@ -708,8 +701,7 @@ async function resolveSessionComparisonBase(sandbox, workdir, baseBranch) {
|
|
|
708
701
|
return result.exitCode === 0 && /^[0-9a-f]{40,64}$/i.test(sha) ? sha : "HEAD";
|
|
709
702
|
}
|
|
710
703
|
/** The changes of one checkout against its comparison base; `available: false` when git cannot inspect it. */
|
|
711
|
-
async function listRepositoryChanges(sandbox, workdir,
|
|
712
|
-
const comparisonBase = await resolveSessionComparisonBase(sandbox, workdir, baseBranch);
|
|
704
|
+
async function listRepositoryChanges(sandbox, workdir, comparisonBase) {
|
|
713
705
|
const [statusResult, trackedResult, statsResult] = await Promise.all([
|
|
714
706
|
sandbox.executeCommand("git", [
|
|
715
707
|
"-C",
|
|
@@ -736,10 +728,11 @@ async function listRepositoryChanges(sandbox, workdir, baseBranch) {
|
|
|
736
728
|
}),
|
|
737
729
|
sandbox.executeCommand("sh", [
|
|
738
730
|
"-c",
|
|
739
|
-
|
|
731
|
+
WORKSPACE_DIFF_SCRIPT,
|
|
740
732
|
"mastracode-numstat",
|
|
741
733
|
workdir,
|
|
742
|
-
comparisonBase
|
|
734
|
+
comparisonBase,
|
|
735
|
+
...NUMSTAT_FORMAT
|
|
743
736
|
], {
|
|
744
737
|
timeout: 3e4,
|
|
745
738
|
maxRetainedBytes: Infinity
|
|
@@ -784,27 +777,20 @@ function prefixChange(change, prefix) {
|
|
|
784
777
|
...change.previousPath ? { previousPath: `${prefix}${change.previousPath}` } : {}
|
|
785
778
|
};
|
|
786
779
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
if (layout.repos.length === 1) {
|
|
792
|
-
const repo = layout.repos[0];
|
|
793
|
-
const result = await listRepositoryChanges(handle.sandbox, repo.dir, bases.get(repo.slug) ?? session.baseBranch);
|
|
794
|
-
if (!result.available) return unavailableWorkspaceChanges(session.sessionId);
|
|
780
|
+
function assembleWorkspaceChanges(workspacePath, inspected) {
|
|
781
|
+
if (inspected.length === 1) {
|
|
782
|
+
const { changes } = inspected[0];
|
|
783
|
+
if (!changes.available) return unavailableWorkspaceChanges(workspacePath);
|
|
795
784
|
return {
|
|
796
|
-
workspacePath
|
|
797
|
-
...
|
|
785
|
+
workspacePath,
|
|
786
|
+
...changes
|
|
798
787
|
};
|
|
799
788
|
}
|
|
800
|
-
const groups =
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
...result,
|
|
806
|
-
changes: result.changes.map((change) => prefixChange(change, repo.prefix))
|
|
807
|
-
};
|
|
789
|
+
const groups = inspected.map(({ repo, changes }) => ({
|
|
790
|
+
slug: repo.slug,
|
|
791
|
+
prefix: repo.prefix,
|
|
792
|
+
...changes,
|
|
793
|
+
changes: changes.changes.map((change) => prefixChange(change, repo.prefix))
|
|
808
794
|
}));
|
|
809
795
|
const available = groups.filter((group) => group.available);
|
|
810
796
|
const changes = available.flatMap((group) => group.changes).toSorted((a, b) => a.path.localeCompare(b.path));
|
|
@@ -814,90 +800,134 @@ async function listSessionWorkspaceChanges(session) {
|
|
|
814
800
|
deletions: counted.reduce((sum, group) => sum + (group.deletions ?? 0), 0)
|
|
815
801
|
} : {};
|
|
816
802
|
return {
|
|
817
|
-
workspacePath
|
|
803
|
+
workspacePath,
|
|
818
804
|
available: available.length > 0,
|
|
819
805
|
changes,
|
|
820
806
|
...totals,
|
|
821
807
|
repositories: groups
|
|
822
808
|
};
|
|
823
809
|
}
|
|
824
|
-
async function
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
810
|
+
async function listSessionWorkspaceChanges(session) {
|
|
811
|
+
const handle = await sessionSandbox(session);
|
|
812
|
+
if (!handle) return unavailableWorkspaceChanges(session.sessionId);
|
|
813
|
+
const { sandbox, layout, baseBranches } = handle;
|
|
814
|
+
const inspected = await Promise.all(layout.repos.map(async (repo) => {
|
|
815
|
+
const comparisonBase = await resolveSessionComparisonBase(sandbox, repo.dir, baseBranches.get(repo.slug) ?? session.baseBranch, "branch");
|
|
816
|
+
return {
|
|
817
|
+
repo,
|
|
818
|
+
changes: await listRepositoryChanges(sandbox, repo.dir, comparisonBase)
|
|
819
|
+
};
|
|
820
|
+
}));
|
|
821
|
+
return assembleWorkspaceChanges(session.sessionId, inspected);
|
|
832
822
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
823
|
+
const GIT_C_ESCAPES = {
|
|
824
|
+
"\"": "\\\"",
|
|
825
|
+
"\\": "\\\\",
|
|
826
|
+
"\x07": "\\a",
|
|
827
|
+
"\b": "\\b",
|
|
828
|
+
" ": "\\t",
|
|
829
|
+
"\n": "\\n",
|
|
830
|
+
"\v": "\\v",
|
|
831
|
+
"\f": "\\f",
|
|
832
|
+
"\r": "\\r"
|
|
833
|
+
};
|
|
834
|
+
/** A path as git writes it in a diff header: `core.quotePath=false` quotes only for quotes, backslashes and control characters. */
|
|
835
|
+
function gitHeaderPath(path) {
|
|
836
|
+
let escaped = "";
|
|
837
|
+
let quoted = false;
|
|
838
|
+
for (const char of path) {
|
|
839
|
+
const code = char.charCodeAt(0);
|
|
840
|
+
const escape = GIT_C_ESCAPES[char] ?? (code < 32 || code === 127 ? `\\${code.toString(8).padStart(3, "0")}` : "");
|
|
841
|
+
quoted ||= escape !== "";
|
|
842
|
+
escaped += escape || char;
|
|
843
|
+
}
|
|
844
|
+
return quoted ? `"${escaped}"` : path;
|
|
838
845
|
}
|
|
839
|
-
|
|
840
|
-
const
|
|
841
|
-
|
|
846
|
+
function keyWorkspacePatches(patch, changes) {
|
|
847
|
+
const sections = patch.split(/^(?=diff --git )/m);
|
|
848
|
+
const budgetSpent = Buffer.byteLength(patch) > MAX_PATCH_SET_BYTES;
|
|
849
|
+
if (budgetSpent) sections.pop();
|
|
850
|
+
const sectionsByHeader = /* @__PURE__ */ new Map();
|
|
851
|
+
for (const section of sections) {
|
|
852
|
+
const headerEnd = section.indexOf("\n");
|
|
853
|
+
if (headerEnd >= 0) sectionsByHeader.set(section.slice(0, headerEnd), section);
|
|
854
|
+
}
|
|
855
|
+
const keyed = {
|
|
856
|
+
patches: /* @__PURE__ */ new Map(),
|
|
857
|
+
omitted: /* @__PURE__ */ new Map()
|
|
858
|
+
};
|
|
859
|
+
for (const change of changes) {
|
|
860
|
+
const header = `diff --git ${gitHeaderPath(`a/${change.previousPath ?? change.path}`)} ${gitHeaderPath(`b/${change.path}`)}`;
|
|
861
|
+
const section = sectionsByHeader.get(header);
|
|
862
|
+
if (section === void 0) {
|
|
863
|
+
if (budgetSpent) keyed.omitted.set(change.path, "total-too-large");
|
|
864
|
+
} else if (Buffer.byteLength(section) > MAX_DIFF_BYTES) keyed.omitted.set(change.path, "file-too-large");
|
|
865
|
+
else keyed.patches.set(change.path, section);
|
|
866
|
+
}
|
|
867
|
+
return keyed;
|
|
868
|
+
}
|
|
869
|
+
function prefixedEntries(prefix, entries) {
|
|
870
|
+
return Array.from(entries, ([path, value]) => [`${prefix}${path}`, value]);
|
|
871
|
+
}
|
|
872
|
+
function unavailableWorkspacePatches(workspacePath, scope) {
|
|
873
|
+
return {
|
|
874
|
+
...unavailableWorkspaceChanges(workspacePath),
|
|
875
|
+
scope,
|
|
876
|
+
patches: {},
|
|
877
|
+
omittedPatches: {}
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
async function listSessionWorkspacePatches(session, scope) {
|
|
842
881
|
const handle = await sessionSandbox(session);
|
|
843
|
-
if (!handle)
|
|
844
|
-
const
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
comparisonBase,
|
|
860
|
-
"--",
|
|
861
|
-
...pathspecs
|
|
862
|
-
]);
|
|
863
|
-
if (result.exitCode !== 0) throw new Error(result.stderr || "Unable to read workspace diff");
|
|
864
|
-
if (!result.stdout) {
|
|
865
|
-
const untracked = await handle.sandbox.executeCommand("git", [
|
|
866
|
-
"--literal-pathspecs",
|
|
867
|
-
"-C",
|
|
868
|
-
workdir,
|
|
869
|
-
"ls-files",
|
|
870
|
-
"--others",
|
|
871
|
-
"--exclude-standard",
|
|
872
|
-
"--",
|
|
873
|
-
target.relative
|
|
882
|
+
if (!handle) return unavailableWorkspacePatches(session.sessionId, scope);
|
|
883
|
+
const { sandbox, layout, baseBranches } = handle;
|
|
884
|
+
const inspected = await Promise.all(layout.repos.map(async (repo) => {
|
|
885
|
+
const comparisonBase = await resolveSessionComparisonBase(sandbox, repo.dir, baseBranches.get(repo.slug) ?? session.baseBranch, scope);
|
|
886
|
+
const [changes, patch] = await Promise.all([listRepositoryChanges(sandbox, repo.dir, comparisonBase), sandbox.executeCommand("sh", [
|
|
887
|
+
"-c",
|
|
888
|
+
BOUNDED_OUTPUT_SCRIPT,
|
|
889
|
+
"mastracode-bounded",
|
|
890
|
+
String(8388609),
|
|
891
|
+
"sh",
|
|
892
|
+
"-c",
|
|
893
|
+
WORKSPACE_DIFF_SCRIPT,
|
|
894
|
+
"mastracode-patch",
|
|
895
|
+
repo.dir,
|
|
896
|
+
comparisonBase,
|
|
897
|
+
...PATCH_FORMAT
|
|
874
898
|
], {
|
|
875
899
|
timeout: 3e4,
|
|
876
900
|
maxRetainedBytes: Infinity
|
|
877
|
-
});
|
|
878
|
-
if (
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
target.relative
|
|
890
|
-
], true);
|
|
891
|
-
if (result.exitCode !== 0 && result.exitCode !== 1) throw new Error(result.stderr || "Unable to read workspace diff");
|
|
901
|
+
})]);
|
|
902
|
+
if (patch.exitCode !== 0 && changes.changes.length > 0) {
|
|
903
|
+
const diffError = patch.stderr.trim().split("\n")[0] || `git diff exited with code ${patch.exitCode}`;
|
|
904
|
+
return {
|
|
905
|
+
repo,
|
|
906
|
+
changes,
|
|
907
|
+
keyed: {
|
|
908
|
+
patches: /* @__PURE__ */ new Map(),
|
|
909
|
+
omitted: /* @__PURE__ */ new Map()
|
|
910
|
+
},
|
|
911
|
+
diffError
|
|
912
|
+
};
|
|
892
913
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
914
|
+
return {
|
|
915
|
+
repo,
|
|
916
|
+
changes,
|
|
917
|
+
keyed: keyWorkspacePatches(patch.stdout, changes.changes),
|
|
918
|
+
diffError: void 0
|
|
919
|
+
};
|
|
920
|
+
}));
|
|
921
|
+
const diffErrors = inspected.flatMap(({ repo, diffError }) => {
|
|
922
|
+
if (!diffError) return [];
|
|
923
|
+
return [layout.repos.length > 1 ? `${repo.slug}: ${diffError}` : diffError];
|
|
924
|
+
});
|
|
896
925
|
return {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
926
|
+
...assembleWorkspaceChanges(session.sessionId, inspected),
|
|
927
|
+
scope,
|
|
928
|
+
patches: Object.fromEntries(inspected.flatMap(({ repo, keyed }) => prefixedEntries(repo.prefix, keyed.patches))),
|
|
929
|
+
omittedPatches: Object.fromEntries(inspected.flatMap(({ repo, keyed }) => prefixedEntries(repo.prefix, keyed.omitted))),
|
|
930
|
+
...diffErrors.length > 0 ? { diffError: diffErrors.join("\n") } : {}
|
|
901
931
|
};
|
|
902
932
|
}
|
|
903
933
|
/**
|
|
@@ -990,23 +1020,21 @@ function buildFsRoutes(options = {}) {
|
|
|
990
1020
|
}
|
|
991
1021
|
}
|
|
992
1022
|
}),
|
|
993
|
-
registerApiRoute("/web/workspace/changes/
|
|
1023
|
+
registerApiRoute("/web/workspace/changes/patches", {
|
|
994
1024
|
method: "GET",
|
|
995
1025
|
requiresAuth: false,
|
|
996
1026
|
handler: async (c) => {
|
|
997
1027
|
const workspacePath = c.req.query("workspacePath");
|
|
998
|
-
const
|
|
999
|
-
const previousPath = c.req.query("previousPath");
|
|
1028
|
+
const scope = c.req.query("scope") ?? "branch";
|
|
1000
1029
|
if (!workspacePath) return c.json({ error: "Missing required query param: workspacePath" }, 400);
|
|
1001
|
-
if (!
|
|
1030
|
+
if (!isWorkspaceChangesScope(scope)) return c.json({ error: "scope must be branch or uncommitted" }, 400);
|
|
1002
1031
|
try {
|
|
1003
1032
|
const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);
|
|
1004
|
-
if (!session || !sessionFs) return c.json(
|
|
1005
|
-
return c.json(await
|
|
1033
|
+
if (!session || !sessionFs) return c.json(unavailableWorkspacePatches(workspacePath, scope));
|
|
1034
|
+
return c.json(await listSessionWorkspacePatches(session, scope));
|
|
1006
1035
|
} catch (error) {
|
|
1007
1036
|
const message = error instanceof Error ? error.message : String(error);
|
|
1008
|
-
|
|
1009
|
-
return c.json({ error: message }, status);
|
|
1037
|
+
return c.json({ error: message }, message.includes("not available") ? 403 : 500);
|
|
1010
1038
|
}
|
|
1011
1039
|
}
|
|
1012
1040
|
}),
|
|
@@ -1065,6 +1093,6 @@ function buildFsRoutes(options = {}) {
|
|
|
1065
1093
|
];
|
|
1066
1094
|
}
|
|
1067
1095
|
//#endregion
|
|
1068
|
-
export { buildFsRoutes, listArtifacts, listDirectory, listSessionRenderedPath, listSessionWorkspaceChanges, listSessionWorkspaceTree, listWorkspaceRenderedPath, parseWorkspaceChangeStats, parseWorkspaceChanges, parseWorkspaceTrackedChanges,
|
|
1096
|
+
export { buildFsRoutes, listArtifacts, listDirectory, listSessionRenderedPath, listSessionWorkspaceChanges, listSessionWorkspacePatches, listSessionWorkspaceTree, listWorkspaceRenderedPath, parseWorkspaceChangeStats, parseWorkspaceChanges, parseWorkspaceTrackedChanges, readSessionWorkspaceFile, readWorkspaceFile, resolveCodebase, resolveFsRoot };
|
|
1069
1097
|
|
|
1070
1098
|
//# sourceMappingURL=fs.js.map
|