@galaxy-tool-util/gxwf-web 0.0.0
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/LICENSE +21 -0
- package/dist/app.d.ts +27 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +37 -0
- package/dist/app.js.map +1 -0
- package/dist/bin/gxwf-web.d.ts +3 -0
- package/dist/bin/gxwf-web.d.ts.map +1 -0
- package/dist/bin/gxwf-web.js +93 -0
- package/dist/bin/gxwf-web.js.map +1 -0
- package/dist/contents.d.ts +29 -0
- package/dist/contents.d.ts.map +1 -0
- package/dist/contents.js +408 -0
- package/dist/contents.js.map +1 -0
- package/dist/generated/api-types.d.ts +1512 -0
- package/dist/generated/api-types.d.ts.map +1 -0
- package/dist/generated/api-types.js +6 -0
- package/dist/generated/api-types.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/models.d.ts +26 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +3 -0
- package/dist/models.js.map +1 -0
- package/dist/router.d.ts +20 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +398 -0
- package/dist/router.js.map +1 -0
- package/dist/workflows.d.ts +126 -0
- package/dist/workflows.d.ts.map +1 -0
- package/dist/workflows.js +462 -0
- package/dist/workflows.js.map +1 -0
- package/openapi.json +2575 -0
- package/package.json +54 -0
- package/public/assets/atkinson-hyperlegible-latin-400-italic-D-qjh7ci.woff2 +0 -0
- package/public/assets/atkinson-hyperlegible-latin-400-italic-OoEIrRJc.woff +0 -0
- package/public/assets/atkinson-hyperlegible-latin-400-normal-BbWidj28.woff +0 -0
- package/public/assets/atkinson-hyperlegible-latin-400-normal-BrHNak5F.woff2 +0 -0
- package/public/assets/atkinson-hyperlegible-latin-700-normal-BK6Glc0m.woff +0 -0
- package/public/assets/atkinson-hyperlegible-latin-700-normal-GZI4o3u0.woff2 +0 -0
- package/public/assets/atkinson-hyperlegible-latin-ext-400-italic-3fJ3SmOv.woff2 +0 -0
- package/public/assets/atkinson-hyperlegible-latin-ext-400-italic-B-Yabllp.woff +0 -0
- package/public/assets/atkinson-hyperlegible-latin-ext-400-normal-Bbz-b3yf.woff +0 -0
- package/public/assets/atkinson-hyperlegible-latin-ext-400-normal-DRk46D-x.woff2 +0 -0
- package/public/assets/atkinson-hyperlegible-latin-ext-700-normal-BoVPHkS0.woff2 +0 -0
- package/public/assets/atkinson-hyperlegible-latin-ext-700-normal-CKkU2Dpt.woff +0 -0
- package/public/assets/index-DgcQmTAM.css +1 -0
- package/public/assets/index-DsYOTNI9.js +3595 -0
- package/public/assets/primeicons-C6QP2o4f.woff2 +0 -0
- package/public/assets/primeicons-DMOk5skT.eot +0 -0
- package/public/assets/primeicons-Dr5RGzOO.svg +345 -0
- package/public/assets/primeicons-MpK4pl85.ttf +0 -0
- package/public/assets/primeicons-WjwUDZjB.woff +0 -0
- package/public/index.html +13 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 John Chilton
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP server factory for gxwf-web.
|
|
3
|
+
*
|
|
4
|
+
* Creates a Node.js http.Server wired to the contents/workflow request handler.
|
|
5
|
+
* Returns a `ready` promise that resolves once the ToolCache is loaded and
|
|
6
|
+
* initial workflow discovery completes — callers may await it before serving
|
|
7
|
+
* traffic but the server is safe to bind before that.
|
|
8
|
+
*/
|
|
9
|
+
import { type Server } from "node:http";
|
|
10
|
+
import { type ToolSource } from "@galaxy-tool-util/core";
|
|
11
|
+
import { type AppState } from "./router.js";
|
|
12
|
+
export type { AppState };
|
|
13
|
+
export interface CreateAppOptions {
|
|
14
|
+
cacheDir?: string;
|
|
15
|
+
/** Tool sources to fetch from when a tool is not in cache. */
|
|
16
|
+
sources?: ToolSource[];
|
|
17
|
+
/** Absolute path to a built gxwf-ui dist directory. When set, the server
|
|
18
|
+
* serves the frontend at the root and falls back to index.html for SPA routing. */
|
|
19
|
+
uiDir?: string;
|
|
20
|
+
}
|
|
21
|
+
/** Create a configured gxwf-web HTTP server for the given workflow directory. */
|
|
22
|
+
export declare function createApp(directory: string, opts?: CreateAppOptions): {
|
|
23
|
+
server: Server;
|
|
24
|
+
state: AppState;
|
|
25
|
+
ready: Promise<void>;
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=app.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAAmB,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAC1E,OAAO,EAAwB,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGlE,YAAY,EAAE,QAAQ,EAAE,CAAC;AAEzB,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IACvB;wFACoF;IACpF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,iFAAiF;AACjF,wBAAgB,SAAS,CACvB,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,gBAAqB,GAC1B;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CA0B3D"}
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP server factory for gxwf-web.
|
|
3
|
+
*
|
|
4
|
+
* Creates a Node.js http.Server wired to the contents/workflow request handler.
|
|
5
|
+
* Returns a `ready` promise that resolves once the ToolCache is loaded and
|
|
6
|
+
* initial workflow discovery completes — callers may await it before serving
|
|
7
|
+
* traffic but the server is safe to bind before that.
|
|
8
|
+
*/
|
|
9
|
+
import { createServer } from "node:http";
|
|
10
|
+
import { ToolInfoService } from "@galaxy-tool-util/core";
|
|
11
|
+
import { createRequestHandler } from "./router.js";
|
|
12
|
+
import { discoverWorkflows } from "./workflows.js";
|
|
13
|
+
/** Create a configured gxwf-web HTTP server for the given workflow directory. */
|
|
14
|
+
export function createApp(directory, opts = {}) {
|
|
15
|
+
const service = new ToolInfoService({
|
|
16
|
+
cacheDir: opts.cacheDir,
|
|
17
|
+
sources: opts.sources,
|
|
18
|
+
});
|
|
19
|
+
const cache = service.cache;
|
|
20
|
+
const state = {
|
|
21
|
+
directory,
|
|
22
|
+
cache,
|
|
23
|
+
workflows: { directory, workflows: [] },
|
|
24
|
+
cacheDir: opts.cacheDir,
|
|
25
|
+
uiDir: opts.uiDir,
|
|
26
|
+
};
|
|
27
|
+
const handler = createRequestHandler(state);
|
|
28
|
+
const server = createServer((req, res) => {
|
|
29
|
+
void handler(req, res);
|
|
30
|
+
});
|
|
31
|
+
const ready = (async () => {
|
|
32
|
+
await cache.index.load();
|
|
33
|
+
state.workflows = discoverWorkflows(directory);
|
|
34
|
+
})();
|
|
35
|
+
return { server, state, ready };
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=app.js.map
|
package/dist/app.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,YAAY,EAAe,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,eAAe,EAAmB,MAAM,wBAAwB,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAiB,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAanD,iFAAiF;AACjF,MAAM,UAAU,SAAS,CACvB,SAAiB,EACjB,OAAyB,EAAE;IAE3B,MAAM,OAAO,GAAG,IAAI,eAAe,CAAC;QAClC,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,OAAO,EAAE,IAAI,CAAC,OAAO;KACtB,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;IAE5B,MAAM,KAAK,GAAa;QACtB,SAAS;QACT,KAAK;QACL,SAAS,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,EAAE;QACvC,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,KAAK,EAAE,IAAI,CAAC,KAAK;KAClB,CAAC;IAEF,MAAM,OAAO,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QACvC,KAAK,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,CAAC,KAAK,IAAI,EAAE;QACxB,MAAM,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QACzB,KAAK,CAAC,SAAS,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IACjD,CAAC,CAAC,EAAE,CAAC;IAEL,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAClC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gxwf-web.d.ts","sourceRoot":"","sources":["../../src/bin/gxwf-web.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { loadWorkflowToolConfig, toolInfoOptionsFromConfig, } from "@galaxy-tool-util/core";
|
|
4
|
+
import { createApp } from "../app.js";
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
let directory = null;
|
|
7
|
+
let host = "127.0.0.1";
|
|
8
|
+
let port = 8000;
|
|
9
|
+
let cacheDir;
|
|
10
|
+
let configPath;
|
|
11
|
+
let outputSchema = false;
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
if (args[i] === "--host" && args[i + 1]) {
|
|
14
|
+
host = args[++i];
|
|
15
|
+
}
|
|
16
|
+
else if (args[i] === "--port" && args[i + 1]) {
|
|
17
|
+
port = parseInt(args[++i], 10);
|
|
18
|
+
}
|
|
19
|
+
else if (args[i] === "--cache-dir" && args[i + 1]) {
|
|
20
|
+
cacheDir = args[++i];
|
|
21
|
+
}
|
|
22
|
+
else if (args[i] === "--config" && args[i + 1]) {
|
|
23
|
+
configPath = args[++i];
|
|
24
|
+
}
|
|
25
|
+
else if (args[i] === "--output-schema") {
|
|
26
|
+
outputSchema = true;
|
|
27
|
+
}
|
|
28
|
+
else if (!args[i].startsWith("--")) {
|
|
29
|
+
directory = args[i];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (outputSchema) {
|
|
33
|
+
// openapi.json lives at <package-root>/openapi.json — two levels above dist/bin/
|
|
34
|
+
const specPath = new URL("../../openapi.json", import.meta.url);
|
|
35
|
+
// Write through the stream and exit in the drain callback so large specs
|
|
36
|
+
// (which exceed typical OS pipe buffers) aren't truncated by process.exit.
|
|
37
|
+
process.stdout.write(readFileSync(specPath), () => process.exit(0));
|
|
38
|
+
}
|
|
39
|
+
else if (!directory) {
|
|
40
|
+
console.error("Usage: gxwf-web <directory> [--host 127.0.0.1] [--port 8000] [--cache-dir <path>] [--config <path>] [--output-schema]");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
else if (!existsSync(directory)) {
|
|
44
|
+
console.error(`Directory does not exist: ${directory}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
main().catch((err) => {
|
|
49
|
+
console.error("Failed to start server:", err);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async function main() {
|
|
54
|
+
let sources;
|
|
55
|
+
if (configPath) {
|
|
56
|
+
if (!existsSync(configPath)) {
|
|
57
|
+
console.error(`Config file not found: ${configPath}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
const config = await loadWorkflowToolConfig(configPath);
|
|
61
|
+
const toolOpts = toolInfoOptionsFromConfig(config);
|
|
62
|
+
sources = toolOpts.sources;
|
|
63
|
+
// --cache-dir CLI flag takes precedence over config file
|
|
64
|
+
cacheDir ??= toolOpts.cacheDir;
|
|
65
|
+
}
|
|
66
|
+
// Resolve UI dist: GXWF_UI_DIST env var overrides the bundled copy.
|
|
67
|
+
// Bundled copy is copied from gxwf-ui during build → public/;
|
|
68
|
+
// two levels up from dist/bin/ lands at the package root.
|
|
69
|
+
const uiDirFromEnv = process.env.GXWF_UI_DIST;
|
|
70
|
+
if (uiDirFromEnv && !existsSync(uiDirFromEnv)) {
|
|
71
|
+
console.error(`GXWF_UI_DIST points to non-existent path: ${uiDirFromEnv}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
const uiDirCandidate = new URL("../../public", import.meta.url).pathname;
|
|
75
|
+
const uiDir = uiDirFromEnv ?? (existsSync(uiDirCandidate) ? uiDirCandidate : undefined);
|
|
76
|
+
const { server, ready } = createApp(directory, { cacheDir, sources, uiDir });
|
|
77
|
+
server.listen(port, host, () => {
|
|
78
|
+
console.log(`gxwf-web listening on ${host}:${port}`);
|
|
79
|
+
console.log(`Serving workflows from: ${directory}`);
|
|
80
|
+
if (uiDir) {
|
|
81
|
+
console.log(`UI: http://${host}:${port}/`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log("UI: not available (run pnpm build to bundle the frontend)");
|
|
85
|
+
}
|
|
86
|
+
if (configPath)
|
|
87
|
+
console.log(`Config: ${configPath}`);
|
|
88
|
+
});
|
|
89
|
+
void ready.then(() => {
|
|
90
|
+
console.log("Tool cache loaded, workflows discovered.");
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=gxwf-web.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gxwf-web.js","sourceRoot":"","sources":["../../src/bin/gxwf-web.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File CRUD operations mirroring the Jupyter Contents API shape.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions — takes the target directory as an argument.
|
|
5
|
+
* Port of Python's gxwf_web/contents.py.
|
|
6
|
+
*/
|
|
7
|
+
import type { CheckpointModel, ContentsModel } from "./models.js";
|
|
8
|
+
export declare const CHECKPOINTS_DIR = ".checkpoints";
|
|
9
|
+
export declare class HttpError extends Error {
|
|
10
|
+
readonly status: number;
|
|
11
|
+
constructor(status: number, message: string);
|
|
12
|
+
}
|
|
13
|
+
export declare function isIgnored(name: string): boolean;
|
|
14
|
+
export declare function isWorkflowFile(relPath: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Resolve relPath under directory, rejecting traversal and symlink escapes.
|
|
17
|
+
* Returns the absolute path.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveSafePath(directory: string, relPath: string): string;
|
|
20
|
+
export declare function readContents(directory: string, relPath: string, includeContent?: boolean, formatOverride?: string | null): ContentsModel;
|
|
21
|
+
export declare function writeContents(directory: string, relPath: string, model: ContentsModel, expectedMtime?: Date): ContentsModel;
|
|
22
|
+
export declare function deleteContents(directory: string, relPath: string): void;
|
|
23
|
+
export declare function createUntitled(directory: string, parentRel: string, type: "file" | "directory", ext?: string | null): ContentsModel;
|
|
24
|
+
export declare function renameContents(directory: string, relPath: string, newRelPath: string): ContentsModel;
|
|
25
|
+
export declare function createCheckpoint(directory: string, relPath: string): CheckpointModel;
|
|
26
|
+
export declare function listCheckpoints(directory: string, relPath: string): CheckpointModel[];
|
|
27
|
+
export declare function restoreCheckpoint(directory: string, relPath: string, checkpointId: string): void;
|
|
28
|
+
export declare function deleteCheckpoint(directory: string, relPath: string, checkpointId: string): void;
|
|
29
|
+
//# sourceMappingURL=contents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contents.d.ts","sourceRoot":"","sources":["../src/contents.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAIlE,eAAO,MAAM,eAAe,iBAAiB,CAAC;AAuB9C,qBAAa,SAAU,SAAQ,KAAK;aAEhB,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM,EAC9B,OAAO,EAAE,MAAM;CAKlB;AAQD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAG/C;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAiC1E;AAmGD,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,cAAc,UAAO,EACrB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,GAC7B,aAAa,CAyDf;AAED,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,aAAa,EACpB,aAAa,CAAC,EAAE,IAAI,GACnB,aAAa,CAkCf;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAkBvE;AAED,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GAAG,WAAW,EAC1B,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,GAClB,aAAa,CAiCf;AAED,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,aAAa,CAsBf;AAaD,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,eAAe,CAapF;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,eAAe,EAAE,CAiBrF;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAchG;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAmB/F"}
|
package/dist/contents.js
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File CRUD operations mirroring the Jupyter Contents API shape.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions — takes the target directory as an argument.
|
|
5
|
+
* Port of Python's gxwf_web/contents.py.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
// ── Constants ────────────────────────────────────────────────────────
|
|
10
|
+
export const CHECKPOINTS_DIR = ".checkpoints";
|
|
11
|
+
const DEFAULT_CHECKPOINT_ID = "checkpoint";
|
|
12
|
+
const UNTITLED_FILE_STEM = "untitled";
|
|
13
|
+
const UNTITLED_DIRECTORY_STEM = "Untitled Folder";
|
|
14
|
+
/** Conflict detection tolerance in milliseconds (matches Python's 1s). */
|
|
15
|
+
const MTIME_TOLERANCE_MS = 1000;
|
|
16
|
+
const IGNORE_NAMES = new Set([
|
|
17
|
+
".git",
|
|
18
|
+
"__pycache__",
|
|
19
|
+
".venv",
|
|
20
|
+
"node_modules",
|
|
21
|
+
".ruff_cache",
|
|
22
|
+
".pytest_cache",
|
|
23
|
+
".mypy_cache",
|
|
24
|
+
".tox",
|
|
25
|
+
CHECKPOINTS_DIR,
|
|
26
|
+
]);
|
|
27
|
+
const IGNORE_SUFFIXES = [".pyc", ".pyo"];
|
|
28
|
+
const WORKFLOW_SUFFIXES = [".ga", ".gxwf.yml", ".gxwf.yaml"];
|
|
29
|
+
// ── Error type ───────────────────────────────────────────────────────
|
|
30
|
+
export class HttpError extends Error {
|
|
31
|
+
status;
|
|
32
|
+
constructor(status, message) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.status = status;
|
|
35
|
+
this.name = "HttpError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function fail(status, message) {
|
|
39
|
+
throw new HttpError(status, message);
|
|
40
|
+
}
|
|
41
|
+
// ── Path helpers ─────────────────────────────────────────────────────
|
|
42
|
+
export function isIgnored(name) {
|
|
43
|
+
if (IGNORE_NAMES.has(name))
|
|
44
|
+
return true;
|
|
45
|
+
return IGNORE_SUFFIXES.some((s) => name.endsWith(s));
|
|
46
|
+
}
|
|
47
|
+
export function isWorkflowFile(relPath) {
|
|
48
|
+
return WORKFLOW_SUFFIXES.some((s) => relPath.endsWith(s));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve relPath under directory, rejecting traversal and symlink escapes.
|
|
52
|
+
* Returns the absolute path.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveSafePath(directory, relPath) {
|
|
55
|
+
directory = path.resolve(directory);
|
|
56
|
+
if (relPath === "" || relPath === "/") {
|
|
57
|
+
return directory;
|
|
58
|
+
}
|
|
59
|
+
if (path.isAbsolute(relPath)) {
|
|
60
|
+
fail(400, "Path must be relative");
|
|
61
|
+
}
|
|
62
|
+
const joined = path.resolve(path.join(directory, relPath));
|
|
63
|
+
if (joined !== directory && !joined.startsWith(directory + path.sep)) {
|
|
64
|
+
fail(403, "Path escapes configured directory");
|
|
65
|
+
}
|
|
66
|
+
// Symlink escape check — only when the target exists (else realpath == joined).
|
|
67
|
+
if (fs.existsSync(joined)) {
|
|
68
|
+
const realJoined = fs.realpathSync(joined);
|
|
69
|
+
const realDir = fs.realpathSync(directory);
|
|
70
|
+
if (realJoined !== realDir && !realJoined.startsWith(realDir + path.sep)) {
|
|
71
|
+
fail(403, "Path escapes configured directory via symlink");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Ignored component check.
|
|
75
|
+
for (const part of relPath.replace(/\\/g, "/").split("/")) {
|
|
76
|
+
if (part && isIgnored(part)) {
|
|
77
|
+
fail(403, `Path contains ignored component: ${part}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return joined;
|
|
81
|
+
}
|
|
82
|
+
// ── Mime type lookup ─────────────────────────────────────────────────
|
|
83
|
+
function guessMimetype(absPath) {
|
|
84
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
85
|
+
const map = {
|
|
86
|
+
".txt": "text/plain",
|
|
87
|
+
".html": "text/html",
|
|
88
|
+
".htm": "text/html",
|
|
89
|
+
".css": "text/css",
|
|
90
|
+
".js": "application/javascript",
|
|
91
|
+
".mjs": "application/javascript",
|
|
92
|
+
".ts": "text/plain",
|
|
93
|
+
".json": "application/json",
|
|
94
|
+
".xml": "application/xml",
|
|
95
|
+
".md": "text/markdown",
|
|
96
|
+
".ga": "application/json",
|
|
97
|
+
".yml": "text/yaml",
|
|
98
|
+
".yaml": "text/yaml",
|
|
99
|
+
".csv": "text/csv",
|
|
100
|
+
".tsv": "text/tab-separated-values",
|
|
101
|
+
".png": "image/png",
|
|
102
|
+
".jpg": "image/jpeg",
|
|
103
|
+
".jpeg": "image/jpeg",
|
|
104
|
+
".gif": "image/gif",
|
|
105
|
+
".svg": "image/svg+xml",
|
|
106
|
+
".pdf": "application/pdf",
|
|
107
|
+
".zip": "application/zip",
|
|
108
|
+
};
|
|
109
|
+
return map[ext] ?? null;
|
|
110
|
+
}
|
|
111
|
+
// ── Stat helpers ─────────────────────────────────────────────────────
|
|
112
|
+
function statTimes(absPath) {
|
|
113
|
+
const st = fs.statSync(absPath);
|
|
114
|
+
return {
|
|
115
|
+
created: new Date(st.ctimeMs).toISOString(),
|
|
116
|
+
lastModified: new Date(st.mtimeMs).toISOString(),
|
|
117
|
+
size: st.size,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function isWritable(absPath) {
|
|
121
|
+
try {
|
|
122
|
+
fs.accessSync(absPath, fs.constants.W_OK);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// ── File body read ───────────────────────────────────────────────────
|
|
130
|
+
function readFileBody(absPath, formatOverride) {
|
|
131
|
+
const raw = fs.readFileSync(absPath);
|
|
132
|
+
const mimetype = guessMimetype(absPath);
|
|
133
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
134
|
+
if (formatOverride === "text") {
|
|
135
|
+
try {
|
|
136
|
+
const text = decoder.decode(raw);
|
|
137
|
+
return { format: "text", content: text, mimetype: mimetype ?? "text/plain" };
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
fail(400, "File is not valid utf-8");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (formatOverride === "base64") {
|
|
144
|
+
return {
|
|
145
|
+
format: "base64",
|
|
146
|
+
content: raw.toString("base64"),
|
|
147
|
+
mimetype: mimetype ?? "application/octet-stream",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (formatOverride != null) {
|
|
151
|
+
fail(400, `Unsupported format: ${formatOverride}`);
|
|
152
|
+
}
|
|
153
|
+
// Auto-detect: try UTF-8 first, fall back to base64.
|
|
154
|
+
try {
|
|
155
|
+
const text = decoder.decode(raw);
|
|
156
|
+
return { format: "text", content: text, mimetype: mimetype ?? "text/plain" };
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return {
|
|
160
|
+
format: "base64",
|
|
161
|
+
content: raw.toString("base64"),
|
|
162
|
+
mimetype: mimetype ?? "application/octet-stream",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// ── Public operations ────────────────────────────────────────────────
|
|
167
|
+
export function readContents(directory, relPath, includeContent = true, formatOverride) {
|
|
168
|
+
const absPath = resolveSafePath(directory, relPath);
|
|
169
|
+
if (!fs.existsSync(absPath)) {
|
|
170
|
+
fail(404, `Not found: ${relPath}`);
|
|
171
|
+
}
|
|
172
|
+
const name = path.basename(absPath) || path.basename(path.resolve(directory));
|
|
173
|
+
const writable = isWritable(absPath);
|
|
174
|
+
const { created, lastModified, size } = statTimes(absPath);
|
|
175
|
+
if (fs.statSync(absPath).isDirectory()) {
|
|
176
|
+
let children = null;
|
|
177
|
+
if (includeContent) {
|
|
178
|
+
children = [];
|
|
179
|
+
for (const entry of fs.readdirSync(absPath).sort()) {
|
|
180
|
+
if (isIgnored(entry))
|
|
181
|
+
continue;
|
|
182
|
+
const childRel = relPath ? `${relPath}/${entry}` : entry;
|
|
183
|
+
children.push(readContents(directory, childRel, false));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
name,
|
|
188
|
+
path: relPath,
|
|
189
|
+
type: "directory",
|
|
190
|
+
writable,
|
|
191
|
+
created,
|
|
192
|
+
last_modified: lastModified,
|
|
193
|
+
size: null,
|
|
194
|
+
mimetype: null,
|
|
195
|
+
format: null,
|
|
196
|
+
content: children,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
let format = null;
|
|
200
|
+
let content = null;
|
|
201
|
+
let mimetype = guessMimetype(absPath);
|
|
202
|
+
if (includeContent) {
|
|
203
|
+
const body = readFileBody(absPath, formatOverride);
|
|
204
|
+
format = body.format;
|
|
205
|
+
content = body.content;
|
|
206
|
+
mimetype = body.mimetype;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
name,
|
|
210
|
+
path: relPath,
|
|
211
|
+
type: "file",
|
|
212
|
+
writable,
|
|
213
|
+
created,
|
|
214
|
+
last_modified: lastModified,
|
|
215
|
+
size,
|
|
216
|
+
mimetype,
|
|
217
|
+
format,
|
|
218
|
+
content,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
export function writeContents(directory, relPath, model, expectedMtime) {
|
|
222
|
+
const absPath = resolveSafePath(directory, relPath);
|
|
223
|
+
const parent = path.dirname(absPath);
|
|
224
|
+
if (parent && !fs.existsSync(parent)) {
|
|
225
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
226
|
+
}
|
|
227
|
+
// Conflict detection.
|
|
228
|
+
if (expectedMtime !== undefined && fs.existsSync(absPath) && fs.statSync(absPath).isFile()) {
|
|
229
|
+
const diskMtime = fs.statSync(absPath).mtimeMs;
|
|
230
|
+
if (diskMtime - expectedMtime.getTime() > MTIME_TOLERANCE_MS) {
|
|
231
|
+
fail(409, `File modified on disk since ${expectedMtime.toISOString()} (disk mtime ${new Date(diskMtime).toISOString()})`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (model.type === "directory") {
|
|
235
|
+
fs.mkdirSync(absPath, { recursive: true });
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
const fmt = model.format ?? "text";
|
|
239
|
+
if (fmt === "text") {
|
|
240
|
+
const raw = typeof model.content === "string" ? model.content : "";
|
|
241
|
+
fs.writeFileSync(absPath, raw, "utf-8");
|
|
242
|
+
}
|
|
243
|
+
else if (fmt === "base64") {
|
|
244
|
+
const raw = typeof model.content === "string" ? model.content : "";
|
|
245
|
+
fs.writeFileSync(absPath, Buffer.from(raw, "base64"));
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
fail(400, `Unsupported format: ${fmt}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return readContents(directory, relPath, false);
|
|
252
|
+
}
|
|
253
|
+
export function deleteContents(directory, relPath) {
|
|
254
|
+
const absPath = resolveSafePath(directory, relPath);
|
|
255
|
+
if (!fs.existsSync(absPath)) {
|
|
256
|
+
fail(404, `Not found: ${relPath}`);
|
|
257
|
+
}
|
|
258
|
+
if (absPath === path.resolve(directory)) {
|
|
259
|
+
fail(403, "Cannot delete configured root directory");
|
|
260
|
+
}
|
|
261
|
+
if (fs.statSync(absPath).isDirectory()) {
|
|
262
|
+
fs.rmSync(absPath, { recursive: true });
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
fs.unlinkSync(absPath);
|
|
266
|
+
}
|
|
267
|
+
// Cascade: remove any checkpoints for this path.
|
|
268
|
+
const cpDir = checkpointDirFor(directory, relPath);
|
|
269
|
+
if (fs.existsSync(cpDir) && fs.statSync(cpDir).isDirectory()) {
|
|
270
|
+
fs.rmSync(cpDir, { recursive: true });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
export function createUntitled(directory, parentRel, type, ext) {
|
|
274
|
+
const parentAbs = resolveSafePath(directory, parentRel);
|
|
275
|
+
if (!fs.existsSync(parentAbs) || !fs.statSync(parentAbs).isDirectory()) {
|
|
276
|
+
fail(404, `Parent directory not found: ${parentRel}`);
|
|
277
|
+
}
|
|
278
|
+
if (type === "file") {
|
|
279
|
+
const stem = UNTITLED_FILE_STEM;
|
|
280
|
+
let suffix = ext ?? "";
|
|
281
|
+
if (suffix && !suffix.startsWith("."))
|
|
282
|
+
suffix = "." + suffix;
|
|
283
|
+
for (let i = 0;; i++) {
|
|
284
|
+
const name = i === 0 ? `${stem}${suffix}` : `${stem}${i}${suffix}`;
|
|
285
|
+
const candidateRel = parentRel ? `${parentRel}/${name}` : name;
|
|
286
|
+
const candidateAbs = resolveSafePath(directory, candidateRel);
|
|
287
|
+
if (!fs.existsSync(candidateAbs)) {
|
|
288
|
+
fs.writeFileSync(candidateAbs, "", "utf-8");
|
|
289
|
+
return readContents(directory, candidateRel, false);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else if (type === "directory") {
|
|
294
|
+
const stem = UNTITLED_DIRECTORY_STEM;
|
|
295
|
+
for (let i = 0;; i++) {
|
|
296
|
+
const name = i === 0 ? stem : `${stem} ${i}`;
|
|
297
|
+
const candidateRel = parentRel ? `${parentRel}/${name}` : name;
|
|
298
|
+
const candidateAbs = resolveSafePath(directory, candidateRel);
|
|
299
|
+
if (!fs.existsSync(candidateAbs)) {
|
|
300
|
+
fs.mkdirSync(candidateAbs);
|
|
301
|
+
return readContents(directory, candidateRel, false);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
fail(400, `Unsupported type: ${type}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
export function renameContents(directory, relPath, newRelPath) {
|
|
310
|
+
const src = resolveSafePath(directory, relPath);
|
|
311
|
+
const dst = resolveSafePath(directory, newRelPath);
|
|
312
|
+
if (!fs.existsSync(src)) {
|
|
313
|
+
fail(404, `Not found: ${relPath}`);
|
|
314
|
+
}
|
|
315
|
+
if (fs.existsSync(dst)) {
|
|
316
|
+
fail(409, `Destination exists: ${newRelPath}`);
|
|
317
|
+
}
|
|
318
|
+
const parent = path.dirname(dst);
|
|
319
|
+
if (parent && !fs.existsSync(parent)) {
|
|
320
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
321
|
+
}
|
|
322
|
+
fs.renameSync(src, dst);
|
|
323
|
+
// Cascade: move any checkpoints.
|
|
324
|
+
const srcCp = checkpointDirFor(directory, relPath);
|
|
325
|
+
if (fs.existsSync(srcCp) && fs.statSync(srcCp).isDirectory()) {
|
|
326
|
+
const dstCp = checkpointDirFor(directory, newRelPath);
|
|
327
|
+
fs.mkdirSync(path.dirname(dstCp), { recursive: true });
|
|
328
|
+
fs.renameSync(srcCp, dstCp);
|
|
329
|
+
}
|
|
330
|
+
return readContents(directory, newRelPath, false);
|
|
331
|
+
}
|
|
332
|
+
// ── Checkpoint operations ─────────────────────────────────────────────
|
|
333
|
+
function checkpointDirFor(directory, relPath) {
|
|
334
|
+
return path.join(path.resolve(directory), CHECKPOINTS_DIR, relPath);
|
|
335
|
+
}
|
|
336
|
+
function checkpointModel(cpFile, cpId) {
|
|
337
|
+
const mtime = fs.statSync(cpFile).mtimeMs;
|
|
338
|
+
return { id: cpId, last_modified: new Date(mtime).toISOString() };
|
|
339
|
+
}
|
|
340
|
+
export function createCheckpoint(directory, relPath) {
|
|
341
|
+
const absPath = resolveSafePath(directory, relPath);
|
|
342
|
+
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) {
|
|
343
|
+
fail(404, `File not found: ${relPath}`);
|
|
344
|
+
}
|
|
345
|
+
const cpDir = checkpointDirFor(directory, relPath);
|
|
346
|
+
fs.mkdirSync(cpDir, { recursive: true });
|
|
347
|
+
const cpFile = path.join(cpDir, DEFAULT_CHECKPOINT_ID);
|
|
348
|
+
fs.copyFileSync(absPath, cpFile);
|
|
349
|
+
// Preserve mtime via utimes (mirrors Python's shutil.copy2).
|
|
350
|
+
const st = fs.statSync(absPath);
|
|
351
|
+
fs.utimesSync(cpFile, st.atime, st.mtime);
|
|
352
|
+
return checkpointModel(cpFile, DEFAULT_CHECKPOINT_ID);
|
|
353
|
+
}
|
|
354
|
+
export function listCheckpoints(directory, relPath) {
|
|
355
|
+
const absPath = resolveSafePath(directory, relPath);
|
|
356
|
+
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) {
|
|
357
|
+
fail(404, `File not found: ${relPath}`);
|
|
358
|
+
}
|
|
359
|
+
const cpDir = checkpointDirFor(directory, relPath);
|
|
360
|
+
if (!fs.existsSync(cpDir) || !fs.statSync(cpDir).isDirectory()) {
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
return fs
|
|
364
|
+
.readdirSync(cpDir)
|
|
365
|
+
.sort()
|
|
366
|
+
.filter((entry) => {
|
|
367
|
+
const f = path.join(cpDir, entry);
|
|
368
|
+
return fs.statSync(f).isFile();
|
|
369
|
+
})
|
|
370
|
+
.map((entry) => checkpointModel(path.join(cpDir, entry), entry));
|
|
371
|
+
}
|
|
372
|
+
export function restoreCheckpoint(directory, relPath, checkpointId) {
|
|
373
|
+
const absPath = resolveSafePath(directory, relPath);
|
|
374
|
+
if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) {
|
|
375
|
+
fail(404, `File not found: ${relPath}`);
|
|
376
|
+
}
|
|
377
|
+
const cpDir = checkpointDirFor(directory, relPath);
|
|
378
|
+
const cpFile = path.join(cpDir, checkpointId);
|
|
379
|
+
if (!fs.existsSync(cpFile) || !fs.statSync(cpFile).isFile()) {
|
|
380
|
+
fail(404, `Checkpoint not found: ${checkpointId}`);
|
|
381
|
+
}
|
|
382
|
+
fs.copyFileSync(cpFile, absPath);
|
|
383
|
+
// Preserve mtime (mirrors Python's shutil.copy2).
|
|
384
|
+
const cpSt = fs.statSync(cpFile);
|
|
385
|
+
fs.utimesSync(absPath, cpSt.atime, cpSt.mtime);
|
|
386
|
+
}
|
|
387
|
+
export function deleteCheckpoint(directory, relPath, checkpointId) {
|
|
388
|
+
resolveSafePath(directory, relPath); // validation only
|
|
389
|
+
const cpDir = checkpointDirFor(directory, relPath);
|
|
390
|
+
const cpFile = path.join(cpDir, checkpointId);
|
|
391
|
+
if (!fs.existsSync(cpFile) || !fs.statSync(cpFile).isFile()) {
|
|
392
|
+
fail(404, `Checkpoint not found: ${checkpointId}`);
|
|
393
|
+
}
|
|
394
|
+
fs.unlinkSync(cpFile);
|
|
395
|
+
// Clean up empty checkpoint dir tree (mirrors Python's os.removedirs — walk up).
|
|
396
|
+
let dir = cpDir;
|
|
397
|
+
const root = path.resolve(directory);
|
|
398
|
+
while (dir !== root) {
|
|
399
|
+
try {
|
|
400
|
+
fs.rmdirSync(dir); // throws if non-empty
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
dir = path.dirname(dir);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
//# sourceMappingURL=contents.js.map
|