@mastra/factory 0.24.0-alpha.12 → 0.24.0-alpha.14
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/factory.d.ts.map +1 -1
- package/dist/factory.js +33 -4
- package/dist/factory.js.map +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +5 -2
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/routes/fs.d.ts +31 -23
- package/dist/routes/fs.d.ts.map +1 -1
- package/dist/routes/fs.js +214 -61
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/session-access.d.ts +55 -0
- package/dist/routes/session-access.d.ts.map +1 -0
- package/dist/routes/session-access.js +165 -0
- package/dist/routes/session-access.js.map +1 -0
- package/dist/routes/source-control-sessions.d.ts +5 -0
- package/dist/routes/source-control-sessions.d.ts.map +1 -1
- package/dist/routes/source-control-sessions.js +86 -9
- package/dist/routes/source-control-sessions.js.map +1 -1
- package/dist/routes/surface.d.ts +5 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +2 -2
- package/dist/routes/surface.js.map +1 -1
- package/dist/session/resource-migration.d.ts +18 -0
- package/dist/session/resource-migration.d.ts.map +1 -1
- package/dist/session/resource-migration.js +33 -1
- package/dist/session/resource-migration.js.map +1 -1
- package/dist/session/session-threads.d.ts +9 -0
- package/dist/session/session-threads.d.ts.map +1 -1
- package/dist/session/session-threads.js +24 -4
- package/dist/session/session-threads.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +15 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +29 -0
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +5 -1
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +7 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/tools/session-tools.d.ts.map +1 -1
- package/dist/tools/session-tools.js +1 -1
- package/dist/tools/session-tools.js.map +1 -1
- package/dist/workspace.d.ts +5 -2
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +11 -1
- package/dist/workspace.js.map +1 -1
- package/package.json +8 -8
package/dist/routes/fs.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ApiRoute } from '@mastra/core/server';
|
|
2
|
-
import type { FilesystemStorage } from '../storage/domains/filesystem/base.js';
|
|
3
2
|
import type { SourceControlSession } from '../storage/domains/source-control/base.js';
|
|
4
3
|
import type { RouteAuth } from './route.js';
|
|
5
4
|
/**
|
|
@@ -58,23 +57,25 @@ export interface WorkspaceFile {
|
|
|
58
57
|
content?: string;
|
|
59
58
|
truncated?: boolean;
|
|
60
59
|
}
|
|
61
|
-
export interface
|
|
62
|
-
|
|
60
|
+
export interface WorkspaceTreeEntry {
|
|
61
|
+
name: string;
|
|
62
|
+
/** Path from the session's layout root, the form the file route takes. */
|
|
63
|
+
path: string;
|
|
64
|
+
type: 'file' | 'directory';
|
|
65
|
+
size?: number;
|
|
66
|
+
/** Set when the entry is a symlink: where it points, as written in the link. */
|
|
67
|
+
symlinkTarget?: string;
|
|
68
|
+
/** Set when the entry is a symlink: the tree path of what it resolves to. */
|
|
69
|
+
linkedPath?: string;
|
|
70
|
+
}
|
|
71
|
+
/** One directory of a session's checkouts, read live from its sandbox. */
|
|
72
|
+
export interface WorkspaceTreeListing {
|
|
63
73
|
workspacePath: string;
|
|
64
|
-
/** The
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* The environment repositories the paths are prefixed by, in position
|
|
71
|
-
* order. Present only when the session's environment holds two or more
|
|
72
|
-
* repositories; a one-repository session lists unprefixed paths.
|
|
73
|
-
*/
|
|
74
|
-
repositories?: Array<{
|
|
75
|
-
slug: string;
|
|
76
|
-
prefix: string;
|
|
77
|
-
}>;
|
|
74
|
+
/** The listed directory; empty for the top level. */
|
|
75
|
+
path: string;
|
|
76
|
+
/** False when this process holds no sandbox for the session, so nothing was read. */
|
|
77
|
+
available: boolean;
|
|
78
|
+
entries: WorkspaceTreeEntry[];
|
|
78
79
|
}
|
|
79
80
|
export type WorkspaceChangeStatus = 'modified' | 'added' | 'deleted' | 'renamed' | 'copied' | 'untracked' | 'conflicted';
|
|
80
81
|
export interface WorkspaceChange {
|
|
@@ -139,15 +140,22 @@ export interface SessionFsDeps {
|
|
|
139
140
|
sessions: {
|
|
140
141
|
getBySessionId(sessionId: string): Promise<SourceControlSession | null>;
|
|
141
142
|
};
|
|
142
|
-
filesystem: Pick<FilesystemStorage, 'listFiles'>;
|
|
143
143
|
}
|
|
144
|
-
export declare function listSessionFilesystemFiles(filesystem: Pick<FilesystemStorage, 'listFiles'>, session: SourceControlSession, threadId: string): Promise<WorkspaceFilesListing>;
|
|
145
144
|
/** List an approved rendered root inside a Factory session's sandbox workdir. */
|
|
146
145
|
export declare function listSessionRenderedPath(session: SourceControlSession, renderedRoot: string): Promise<WorkspaceRenderedListing>;
|
|
147
|
-
/**
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
146
|
+
/**
|
|
147
|
+
* List one directory of a session's working directory from its sandbox,
|
|
148
|
+
* without git internals or entries a checkout ignores. Passive like every
|
|
149
|
+
* session read: a session with no sandbox in this process comes back
|
|
150
|
+
* `available: false` instead of starting one.
|
|
151
|
+
*/
|
|
152
|
+
export declare function listSessionWorkspaceTree(session: SourceControlSession, path?: string): Promise<WorkspaceTreeListing>;
|
|
153
|
+
/**
|
|
154
|
+
* Read a file inside a session's sandbox: anything under an approved rendered
|
|
155
|
+
* root, or anything the tree would list (not git internals, not ignored by its
|
|
156
|
+
* checkout).
|
|
157
|
+
*/
|
|
158
|
+
export declare function readSessionWorkspaceFile(session: SourceControlSession, path: string): Promise<WorkspaceFile>;
|
|
151
159
|
export declare function parseWorkspaceChanges(output: string): WorkspaceChange[];
|
|
152
160
|
export declare function parseWorkspaceTrackedChanges(output: string): WorkspaceChange[];
|
|
153
161
|
export declare function parseWorkspaceChangeStats(output: string): Map<string, {
|
package/dist/routes/fs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/routes/fs.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/routes/fs.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAQpD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACtF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,8DAA8D;IAC9D,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,4EAA4E;IAC5E,OAAO,EAAE,cAAc,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,wBAAwB;IACvC,2CAA2C;IAC3C,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,sBAAsB,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,aAAa;IAC5B,2CAA2C;IAC3C,aAAa,EAAE,MAAM,CAAC;IACtB,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,aAAa,CAAC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,0EAA0E;AAC1E,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,kBAAkB,EAAE,CAAC;CAC/B;AAED,MAAM,MAAM,qBAAqB,GAC7B,UAAU,GACV,OAAO,GACP,SAAS,GACT,SAAS,GACT,QAAQ,GACR,WAAW,GACX,YAAY,CAAC;AAEjB,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,qBAAqB,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,uFAAuF;IACvF,MAAM,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,4GAA4G;IAC5G,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,YAAY,CAAC,EAAE,0BAA0B,EAAE,CAAC;CAC7C;AAED,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,MAAM,aAAa,GAAG,sBAAsB,CAAC;AAEnD,MAAM,WAAW,eAAe;IAC9B,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAkDD,2EAA2E;AAC3E,wBAAgB,aAAa,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAEnD;AA0ED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAuCnG;AAqCD,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,wBAAwB,CAAC,CAkBnC;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA4CjH;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAOjG;AAUD,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE;QAAE,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAA;KAAE,CAAC;CACvF;AA2GD,iFAAiF;AACjF,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,oBAAoB,EAC7B,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,wBAAwB,CAAC,CAuCnC;AAuID;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,oBAAoB,EAC7B,IAAI,SAAK,GACR,OAAO,CAAC,oBAAoB,CAAC,CAoD/B;AAED;;;;GAIG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,oBAAoB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA2ClH;AAYD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,EAAE,CAoBvE;AACD,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,EAAE,CAiB9E;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM;gBAEV,MAAM;gBAAc,MAAM;aAAW,OAAO;GA+BzF;AA8ED,wBAAsB,2BAA2B,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA4C1G;AAiBD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,oBAAoB,EAC7B,IAAI,EAAE,MAAM,EACZ,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,aAAa,CAAC,CAyExB;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,gBAAgB,CAUrE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,aAAa,CAAA;CAAO,GAAG,QAAQ,EAAE,CA8KpG"}
|
package/dist/routes/fs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { requireExec } from "../sandbox/materialization.js";
|
|
2
|
+
import { SETUP_MARKER_DIR } from "../packages/_internals/workspace/dist/index.js";
|
|
2
3
|
import { peekSessionSandbox, sessionLayout } from "../sandbox/session-sandbox.js";
|
|
3
4
|
import { peekSessionEnvironment } from "../session/environment-state-processor.js";
|
|
4
|
-
import { waitForPendingFilesystemCapture } from "../session/filesystem-capture.js";
|
|
5
5
|
import { registerApiRoute } from "@mastra/core/server";
|
|
6
6
|
import { isAbsolute, join, posix, resolve, sep } from "path";
|
|
7
7
|
import { SandboxFilesystem } from "@mastra/code-sdk/agents/sandbox-filesystem";
|
|
@@ -87,11 +87,10 @@ async function realPathWithinRoot(candidate, root) {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
function assertRelativePath(path, label) {
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
const normalized = resolve("/", trimmed).slice(1);
|
|
90
|
+
if (!path.trim()) throw new Error(`Missing required query param: ${label}`);
|
|
91
|
+
if (isAbsolute(path)) throw new Error(`${label} must be relative`);
|
|
92
|
+
if (path.split(/[\\/]+/).includes("..")) throw new Error(`${label} escapes workspace`);
|
|
93
|
+
const normalized = resolve("/", path).slice(1);
|
|
95
94
|
if (!normalized || normalized === ".." || normalized.startsWith(`..${sep}`)) throw new Error(`${label} escapes workspace`);
|
|
96
95
|
return normalized;
|
|
97
96
|
}
|
|
@@ -275,24 +274,6 @@ async function resolveAuthorizedSession(c, deps, workspacePath) {
|
|
|
275
274
|
}
|
|
276
275
|
return session;
|
|
277
276
|
}
|
|
278
|
-
async function listSessionFilesystemFiles(filesystem, session, threadId) {
|
|
279
|
-
const safeThreadId = threadId.trim();
|
|
280
|
-
if (!safeThreadId) throw new Error("Missing required query param: threadId");
|
|
281
|
-
await waitForPendingFilesystemCapture(session.sessionId);
|
|
282
|
-
const layout = sessionLayout(peekSessionSandbox(session.id));
|
|
283
|
-
return {
|
|
284
|
-
workspacePath: session.sessionId,
|
|
285
|
-
threadId: safeThreadId,
|
|
286
|
-
files: await filesystem.listFiles({
|
|
287
|
-
resourceId: session.sessionId,
|
|
288
|
-
threadId: safeThreadId
|
|
289
|
-
}),
|
|
290
|
-
...layout && layout.repos.length > 1 ? { repositories: layout.repos.map((repo) => ({
|
|
291
|
-
slug: repo.slug,
|
|
292
|
-
prefix: repo.prefix
|
|
293
|
-
})) } : {}
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
277
|
/**
|
|
297
278
|
* Resolve the session's sandbox from the per-process memo and wrap the
|
|
298
279
|
* session layout's root in a `SandboxFilesystem`: the checkout for a
|
|
@@ -398,26 +379,211 @@ async function listSessionRenderedPath(session, renderedRoot) {
|
|
|
398
379
|
entries
|
|
399
380
|
};
|
|
400
381
|
}
|
|
401
|
-
/**
|
|
402
|
-
|
|
382
|
+
/**
|
|
383
|
+
* Directory entries the file tree never shows or serves: git internals and the
|
|
384
|
+
* sandbox's own setup markers.
|
|
385
|
+
*/
|
|
386
|
+
const HIDDEN_TREE_ENTRIES = /* @__PURE__ */ new Set([".git", SETUP_MARKER_DIR]);
|
|
387
|
+
/**
|
|
388
|
+
* Where a tree path lives. Inside a checkout: that checkout and the path within
|
|
389
|
+
* it (a bare repository directory resolves to its root). Elsewhere in the
|
|
390
|
+
* working directory (a multi-repository session's root, `.artifacts`): no
|
|
391
|
+
* checkout, so no ignore rules apply.
|
|
392
|
+
*/
|
|
393
|
+
function treeLocation(layout, path) {
|
|
394
|
+
const safePath = path ? assertRelativePath(path, "path") : "";
|
|
395
|
+
if (safePath.split("/").some((segment) => HIDDEN_TREE_ENTRIES.has(segment))) throw new Error("Path is not available in this session");
|
|
396
|
+
if (layout.repos.length === 1) return {
|
|
397
|
+
repo: layout.repos[0],
|
|
398
|
+
relative: safePath
|
|
399
|
+
};
|
|
400
|
+
const repo = layout.repos.filter((candidate) => `${safePath}/`.startsWith(candidate.prefix)).toSorted((a, b) => b.prefix.length - a.prefix.length)[0];
|
|
401
|
+
return repo ? {
|
|
402
|
+
repo,
|
|
403
|
+
relative: safePath.slice(repo.prefix.length)
|
|
404
|
+
} : {
|
|
405
|
+
repo: void 0,
|
|
406
|
+
relative: safePath
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* The checkout-relative paths git ignores, in one `git check-ignore` call.
|
|
411
|
+
* Ignored files (`.env`, build output, `node_modules`) are neither listed nor
|
|
412
|
+
* served: viewers of a session shouldn't read its local secrets. Fails closed
|
|
413
|
+
* when git can't answer.
|
|
414
|
+
*/
|
|
415
|
+
async function ignoredPaths(sandbox, repoDir, paths) {
|
|
416
|
+
if (paths.length === 0) return /* @__PURE__ */ new Set();
|
|
417
|
+
const result = await sandbox.executeCommand("sh", [
|
|
418
|
+
"-c",
|
|
419
|
+
"cd \"$1\" && shift && printf '%s\\0' \"$@\" | git check-ignore -z --stdin",
|
|
420
|
+
"sh",
|
|
421
|
+
repoDir,
|
|
422
|
+
...paths
|
|
423
|
+
], {
|
|
424
|
+
timeout: 3e4,
|
|
425
|
+
maxRetainedBytes: Infinity
|
|
426
|
+
});
|
|
427
|
+
if (result.exitCode === 1) return /* @__PURE__ */ new Set();
|
|
428
|
+
if (result.exitCode !== 0) throw new Error(`Unable to check ignored files: ${result.stderr.trim() || "git failed"}`);
|
|
429
|
+
return new Set(result.stdout.split("\0").filter(Boolean));
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* The real absolute location of each path (relative to `root`), following
|
|
433
|
+
* every symlink, or `''` when it doesn't exist; plus `root`'s own. Paths go in
|
|
434
|
+
* as positional arguments and come back NUL-separated.
|
|
435
|
+
*/
|
|
436
|
+
async function realPaths(sandbox, root, paths) {
|
|
437
|
+
const result = await sandbox.executeCommand("sh", [
|
|
438
|
+
"-c",
|
|
439
|
+
"cd \"$1\" && shift && printf '%s\\0' \"$(pwd -P)\" && for p; do printf '%s\\0' \"$([ -e \"$p\" ] && readlink -f -- \"$p\")\"; done",
|
|
440
|
+
"sh",
|
|
441
|
+
root,
|
|
442
|
+
...paths
|
|
443
|
+
], {
|
|
444
|
+
timeout: 3e4,
|
|
445
|
+
maxRetainedBytes: Infinity
|
|
446
|
+
});
|
|
447
|
+
if (result.exitCode !== 0) throw new Error(`Unable to resolve symlinks: ${result.stderr.trim() || "failed"}`);
|
|
448
|
+
const [physicalRoot = "", ...resolved] = result.stdout.split("\0");
|
|
449
|
+
return {
|
|
450
|
+
root: physicalRoot,
|
|
451
|
+
paths: resolved.slice(0, paths.length)
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
/** The first `bytes` bytes of a file, base64-encoded in the sandbox so binary content survives. */
|
|
455
|
+
async function readFilePrefix(sandbox, path, bytes) {
|
|
456
|
+
const result = await sandbox.executeCommand("sh", [
|
|
457
|
+
"-c",
|
|
458
|
+
"t=$(mktemp) && trap 'rm -f \"$t\"' EXIT && head -c \"$1\" -- \"$2\" >\"$t\" && base64 <\"$t\"",
|
|
459
|
+
"sh",
|
|
460
|
+
String(bytes),
|
|
461
|
+
path
|
|
462
|
+
], {
|
|
463
|
+
timeout: 3e4,
|
|
464
|
+
maxRetainedBytes: Infinity
|
|
465
|
+
});
|
|
466
|
+
if (result.exitCode !== 0) throw new Error(`Unable to read file: ${result.stderr.trim() || "failed"}`);
|
|
467
|
+
return Buffer.from(result.stdout, "base64");
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Where a real absolute path sits in the tree, or `undefined` when the tree
|
|
471
|
+
* can't show it: missing, outside the working directory, or inside git
|
|
472
|
+
* internals or setup markers.
|
|
473
|
+
*/
|
|
474
|
+
function treePathOf(layout, physicalRoot, real) {
|
|
475
|
+
if (!real) return void 0;
|
|
476
|
+
const relative = posix.relative(physicalRoot, real);
|
|
477
|
+
if (relative === ".." || relative.startsWith("../") || posix.isAbsolute(relative)) return void 0;
|
|
478
|
+
try {
|
|
479
|
+
treeLocation(layout, relative);
|
|
480
|
+
} catch {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
return relative;
|
|
484
|
+
}
|
|
485
|
+
/** Which tree paths their checkout ignores: one `git check-ignore` per checkout involved. */
|
|
486
|
+
async function ignoredTreePaths(sandbox, layout, paths) {
|
|
487
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
488
|
+
for (const path of paths) {
|
|
489
|
+
const { repo, relative } = treeLocation(layout, path);
|
|
490
|
+
if (!repo || !relative) continue;
|
|
491
|
+
const inRepo = byRepo.get(repo) ?? /* @__PURE__ */ new Map();
|
|
492
|
+
inRepo.set(relative, path);
|
|
493
|
+
byRepo.set(repo, inRepo);
|
|
494
|
+
}
|
|
495
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
496
|
+
for (const [repo, inRepo] of byRepo) for (const relative of await ignoredPaths(sandbox, repo.dir, [...inRepo.keys()])) ignored.add(inRepo.get(relative));
|
|
497
|
+
return ignored;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* List one directory of a session's working directory from its sandbox,
|
|
501
|
+
* without git internals or entries a checkout ignores. Passive like every
|
|
502
|
+
* session read: a session with no sandbox in this process comes back
|
|
503
|
+
* `available: false` instead of starting one.
|
|
504
|
+
*/
|
|
505
|
+
async function listSessionWorkspaceTree(session, path = "") {
|
|
506
|
+
const requested = path.trim() ? path : "";
|
|
507
|
+
const handle = await sessionSandbox(session);
|
|
508
|
+
const listing = {
|
|
509
|
+
workspacePath: session.sessionId,
|
|
510
|
+
path: requested
|
|
511
|
+
};
|
|
512
|
+
if (!handle) return {
|
|
513
|
+
...listing,
|
|
514
|
+
available: false,
|
|
515
|
+
entries: []
|
|
516
|
+
};
|
|
517
|
+
const { layout, sandbox, filesystem } = handle;
|
|
518
|
+
const location = treeLocation(layout, requested);
|
|
519
|
+
const dir = (location.repo ? location.repo.prefix + location.relative : location.relative).replace(/\/$/, "");
|
|
520
|
+
const children = (await filesystem.readdir(dir || ".")).filter((entry) => !HIDDEN_TREE_ENTRIES.has(entry.name));
|
|
521
|
+
const childPath = (name) => dir ? `${dir}/${name}` : name;
|
|
522
|
+
const links = children.filter((entry) => entry.isSymlink);
|
|
523
|
+
const resolve = [...dir ? [dir] : [], ...links.map((entry) => childPath(entry.name))];
|
|
524
|
+
const real = resolve.length > 0 ? await realPaths(sandbox, layout.root, resolve) : void 0;
|
|
525
|
+
const linkTargets = real ? real.paths.slice(dir ? 1 : 0) : [];
|
|
526
|
+
if (dir && real.paths[0] !== posix.join(real.root, dir)) throw new Error("Path is not available in this session");
|
|
527
|
+
const targets = new Map(links.map((entry, i) => [entry.name, treePathOf(layout, real.root, linkTargets[i])]));
|
|
528
|
+
const ignored = await ignoredTreePaths(sandbox, layout, [
|
|
529
|
+
...dir ? [dir] : [],
|
|
530
|
+
...children.map((entry) => childPath(entry.name)),
|
|
531
|
+
...[...targets.values()].filter((target) => target !== void 0)
|
|
532
|
+
]);
|
|
533
|
+
if (dir && ignored.has(dir)) throw new Error("Path is not available in this session");
|
|
534
|
+
const entries = children.filter((entry) => {
|
|
535
|
+
if (ignored.has(childPath(entry.name))) return false;
|
|
536
|
+
if (!entry.isSymlink) return true;
|
|
537
|
+
const target = targets.get(entry.name);
|
|
538
|
+
return target !== void 0 && !ignored.has(target);
|
|
539
|
+
}).map((entry) => ({
|
|
540
|
+
name: entry.name,
|
|
541
|
+
path: childPath(entry.name),
|
|
542
|
+
type: entry.type,
|
|
543
|
+
...entry.type === "file" && entry.size !== void 0 && { size: entry.size },
|
|
544
|
+
...entry.isSymlink && {
|
|
545
|
+
symlinkTarget: entry.symlinkTarget ?? targets.get(entry.name),
|
|
546
|
+
linkedPath: targets.get(entry.name)
|
|
547
|
+
}
|
|
548
|
+
})).toSorted((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "directory" ? -1 : 1);
|
|
549
|
+
return {
|
|
550
|
+
...listing,
|
|
551
|
+
available: true,
|
|
552
|
+
entries
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Read a file inside a session's sandbox: anything under an approved rendered
|
|
557
|
+
* root, or anything the tree would list (not git internals, not ignored by its
|
|
558
|
+
* checkout).
|
|
559
|
+
*/
|
|
560
|
+
async function readSessionWorkspaceFile(session, path) {
|
|
403
561
|
const safePath = assertRelativePath(path, "path");
|
|
404
|
-
if (!options.allowUnapprovedPath) assertApprovedRenderedRoot(safePath.split("/")[0] ?? "");
|
|
405
562
|
const handle = await sessionSandbox(session);
|
|
406
563
|
if (!handle) throw new Error("Session workspace is not available");
|
|
564
|
+
const { layout, sandbox } = handle;
|
|
565
|
+
treeLocation(layout, safePath);
|
|
566
|
+
const parent = posix.dirname(safePath);
|
|
567
|
+
const real = await realPaths(sandbox, layout.root, parent === "." ? [safePath] : [safePath, parent]);
|
|
568
|
+
const target = treePathOf(layout, real.root, real.paths[0]);
|
|
569
|
+
const throughLink = parent !== "." && real.paths[1] !== posix.join(real.root, parent);
|
|
570
|
+
if (target === void 0 || throughLink) throw new Error("Path is not available in this session");
|
|
571
|
+
const trusted = APPROVED_RENDERED_ROOTS.has(safePath.split("/")[0] ?? "");
|
|
572
|
+
if ((await ignoredTreePaths(sandbox, layout, [.../* @__PURE__ */ new Set([...trusted ? [] : [safePath], ...target === safePath ? [] : [target]])])).size > 0) throw new Error("Path is not available in this session");
|
|
407
573
|
const { filesystem } = handle;
|
|
408
574
|
const info = await filesystem.stat(safePath);
|
|
409
575
|
if (info.type === "directory") throw new Error("Path is a directory");
|
|
410
|
-
const
|
|
411
|
-
const
|
|
576
|
+
const truncated = info.size > MAX_TEXT_FILE_BYTES;
|
|
577
|
+
const buffer = truncated ? await readFilePrefix(sandbox, real.paths[0], MAX_TEXT_FILE_BYTES) : await filesystem.readFile(safePath);
|
|
412
578
|
const base = {
|
|
413
579
|
workspacePath: session.sessionId,
|
|
414
580
|
path: safePath,
|
|
415
581
|
name: posix.basename(safePath),
|
|
416
|
-
size:
|
|
582
|
+
size: info.size,
|
|
417
583
|
updatedAt: info.modifiedAt.toISOString()
|
|
418
584
|
};
|
|
419
585
|
try {
|
|
420
|
-
const content = TEXT_DECODER.decode(
|
|
586
|
+
const content = TEXT_DECODER.decode(buffer);
|
|
421
587
|
return {
|
|
422
588
|
...base,
|
|
423
589
|
contentType: "text",
|
|
@@ -808,57 +974,55 @@ function buildFsRoutes(options = {}) {
|
|
|
808
974
|
}
|
|
809
975
|
}
|
|
810
976
|
}),
|
|
811
|
-
registerApiRoute("/web/workspace/
|
|
977
|
+
registerApiRoute("/web/workspace/changes", {
|
|
812
978
|
method: "GET",
|
|
813
979
|
requiresAuth: false,
|
|
814
980
|
handler: async (c) => {
|
|
815
981
|
const workspacePath = c.req.query("workspacePath");
|
|
816
|
-
const threadId = c.req.query("threadId")?.trim();
|
|
817
982
|
if (!workspacePath) return c.json({ error: "Missing required query param: workspacePath" }, 400);
|
|
818
|
-
if (!threadId) return c.json({ error: "Missing required query param: threadId" }, 400);
|
|
819
983
|
try {
|
|
820
984
|
const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);
|
|
821
|
-
if (!session || !sessionFs) return c.json(
|
|
822
|
-
return c.json(await
|
|
985
|
+
if (!session || !sessionFs) return c.json(unavailableWorkspaceChanges(workspacePath));
|
|
986
|
+
return c.json(await listSessionWorkspaceChanges(session));
|
|
823
987
|
} catch (error) {
|
|
824
988
|
const message = error instanceof Error ? error.message : String(error);
|
|
825
|
-
|
|
826
|
-
return c.json({ error: message }, status);
|
|
989
|
+
return c.json({ error: message }, message.includes("not available") ? 403 : 500);
|
|
827
990
|
}
|
|
828
991
|
}
|
|
829
992
|
}),
|
|
830
|
-
registerApiRoute("/web/workspace/changes", {
|
|
993
|
+
registerApiRoute("/web/workspace/changes/diff", {
|
|
831
994
|
method: "GET",
|
|
832
995
|
requiresAuth: false,
|
|
833
996
|
handler: async (c) => {
|
|
834
997
|
const workspacePath = c.req.query("workspacePath");
|
|
998
|
+
const path = c.req.query("path");
|
|
999
|
+
const previousPath = c.req.query("previousPath");
|
|
835
1000
|
if (!workspacePath) return c.json({ error: "Missing required query param: workspacePath" }, 400);
|
|
1001
|
+
if (!path) return c.json({ error: "Missing required query param: path" }, 400);
|
|
836
1002
|
try {
|
|
837
1003
|
const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);
|
|
838
|
-
if (!session || !sessionFs) return c.json(
|
|
839
|
-
return c.json(await
|
|
1004
|
+
if (!session || !sessionFs) return c.json({ error: "Session workspace is not available" }, 403);
|
|
1005
|
+
return c.json(await readSessionWorkspaceDiff(session, path, previousPath));
|
|
840
1006
|
} catch (error) {
|
|
841
1007
|
const message = error instanceof Error ? error.message : String(error);
|
|
842
|
-
|
|
1008
|
+
const status = message.includes("relative") || message.includes("escapes") || message.includes("not available") ? 403 : 500;
|
|
1009
|
+
return c.json({ error: message }, status);
|
|
843
1010
|
}
|
|
844
1011
|
}
|
|
845
1012
|
}),
|
|
846
|
-
registerApiRoute("/web/workspace/
|
|
1013
|
+
registerApiRoute("/web/workspace/tree", {
|
|
847
1014
|
method: "GET",
|
|
848
1015
|
requiresAuth: false,
|
|
849
1016
|
handler: async (c) => {
|
|
850
1017
|
const workspacePath = c.req.query("workspacePath");
|
|
851
|
-
const path = c.req.query("path");
|
|
852
|
-
const previousPath = c.req.query("previousPath");
|
|
853
1018
|
if (!workspacePath) return c.json({ error: "Missing required query param: workspacePath" }, 400);
|
|
854
|
-
if (!path) return c.json({ error: "Missing required query param: path" }, 400);
|
|
855
1019
|
try {
|
|
856
1020
|
const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);
|
|
857
1021
|
if (!session || !sessionFs) return c.json({ error: "Session workspace is not available" }, 403);
|
|
858
|
-
return c.json(await
|
|
1022
|
+
return c.json(await listSessionWorkspaceTree(session, c.req.query("path") ?? ""));
|
|
859
1023
|
} catch (error) {
|
|
860
1024
|
const message = error instanceof Error ? error.message : String(error);
|
|
861
|
-
const status = message.includes("relative") || message.includes("escapes") || message.includes("not available") ? 403 : 500;
|
|
1025
|
+
const status = message.includes("relative") || message.includes("escapes") || message.includes("not available") ? 403 : message.includes("not found") ? 404 : 500;
|
|
862
1026
|
return c.json({ error: message }, status);
|
|
863
1027
|
}
|
|
864
1028
|
}
|
|
@@ -869,22 +1033,11 @@ function buildFsRoutes(options = {}) {
|
|
|
869
1033
|
handler: async (c) => {
|
|
870
1034
|
const workspacePath = c.req.query("workspacePath");
|
|
871
1035
|
const path = c.req.query("path");
|
|
872
|
-
const requestedThreadId = c.req.query("threadId");
|
|
873
|
-
const threadId = requestedThreadId?.trim();
|
|
874
1036
|
if (!workspacePath) return c.json({ error: "Missing required query param: workspacePath" }, 400);
|
|
875
1037
|
if (!path) return c.json({ error: "Missing required query param: path" }, 400);
|
|
876
|
-
if (requestedThreadId !== void 0 && !threadId) return c.json({ error: "Missing required query param: threadId" }, 400);
|
|
877
1038
|
try {
|
|
878
1039
|
const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);
|
|
879
|
-
if (session && sessionFs)
|
|
880
|
-
if (threadId) {
|
|
881
|
-
const safePath = assertRelativePath(path, "path");
|
|
882
|
-
if (!(await listSessionFilesystemFiles(sessionFs.filesystem, session, threadId)).files.some((file) => file.path === safePath)) return c.json({ error: "Path is not available for this thread" }, 404);
|
|
883
|
-
return c.json(await readSessionWorkspaceFile(session, safePath, { allowUnapprovedPath: true }));
|
|
884
|
-
}
|
|
885
|
-
return c.json(await readSessionWorkspaceFile(session, path));
|
|
886
|
-
}
|
|
887
|
-
if (threadId) return c.json({ error: "Session workspace is not available" }, 403);
|
|
1040
|
+
if (session && sessionFs) return c.json(await readSessionWorkspaceFile(session, path));
|
|
888
1041
|
return c.json(await readWorkspaceFile(root, workspacePath, path));
|
|
889
1042
|
} catch (error) {
|
|
890
1043
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -912,6 +1065,6 @@ function buildFsRoutes(options = {}) {
|
|
|
912
1065
|
];
|
|
913
1066
|
}
|
|
914
1067
|
//#endregion
|
|
915
|
-
export { buildFsRoutes, listArtifacts, listDirectory,
|
|
1068
|
+
export { buildFsRoutes, listArtifacts, listDirectory, listSessionRenderedPath, listSessionWorkspaceChanges, listSessionWorkspaceTree, listWorkspaceRenderedPath, parseWorkspaceChangeStats, parseWorkspaceChanges, parseWorkspaceTrackedChanges, readSessionWorkspaceDiff, readSessionWorkspaceFile, readWorkspaceFile, resolveCodebase, resolveFsRoot };
|
|
916
1069
|
|
|
917
1070
|
//# sourceMappingURL=fs.js.map
|