✨ feat: add mongo runtime and react local stack
This commit is contained in:
parent
f81eda80bb
commit
38ddecbe20
@ -3,6 +3,9 @@ NODE_ENV=development
|
|||||||
WEB_PORT=3000
|
WEB_PORT=3000
|
||||||
API_PORT=3001
|
API_PORT=3001
|
||||||
WORKER_PORT=3002
|
WORKER_PORT=3002
|
||||||
|
API_HOST=127.0.0.1
|
||||||
|
|
||||||
|
MONGO_URI=mongodb://127.0.0.1:27017
|
||||||
|
|
||||||
MONGO_PORT=27017
|
MONGO_PORT=27017
|
||||||
MONGO_DB=emboflow
|
MONGO_DB=emboflow
|
||||||
@ -15,3 +18,6 @@ MINIO_ROOT_USER=emboflow
|
|||||||
MINIO_ROOT_PASSWORD=emboflow123
|
MINIO_ROOT_PASSWORD=emboflow123
|
||||||
|
|
||||||
STORAGE_PROVIDER=minio
|
STORAGE_PROVIDER=minio
|
||||||
|
CORS_ORIGIN=http://127.0.0.1:3000
|
||||||
|
VITE_API_BASE_URL=http://127.0.0.1:3001
|
||||||
|
LOCAL_SAMPLE_DATA_PATH=/Users/longtaowu/workspace/emboldata/data
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
dist/
|
||||||
|
|||||||
21
Makefile
21
Makefile
@ -1,6 +1,6 @@
|
|||||||
SHELL := /bin/bash
|
SHELL := /bin/bash
|
||||||
|
|
||||||
.PHONY: bootstrap test dev-api dev-web dev-worker guardrails
|
.PHONY: bootstrap test dev-api dev-web dev-worker serve-api serve-web infra-up infra-down guardrails
|
||||||
|
|
||||||
bootstrap:
|
bootstrap:
|
||||||
pnpm install
|
pnpm install
|
||||||
@ -10,6 +10,7 @@ test:
|
|||||||
python3 -m unittest discover -s tests -p 'test_*.py'
|
python3 -m unittest discover -s tests -p 'test_*.py'
|
||||||
pnpm --filter api test
|
pnpm --filter api test
|
||||||
pnpm --filter web test src/features/assets/assets-page.test.tsx src/features/workflows/workflow-editor-page.test.tsx src/features/explore/explore-page.test.tsx
|
pnpm --filter web test src/features/assets/assets-page.test.tsx src/features/workflows/workflow-editor-page.test.tsx src/features/explore/explore-page.test.tsx
|
||||||
|
pnpm --filter web build
|
||||||
pnpm --filter worker test
|
pnpm --filter worker test
|
||||||
|
|
||||||
dev-api:
|
dev-api:
|
||||||
@ -21,6 +22,24 @@ dev-web:
|
|||||||
dev-worker:
|
dev-worker:
|
||||||
pnpm --filter worker dev
|
pnpm --filter worker dev
|
||||||
|
|
||||||
|
serve-api:
|
||||||
|
MONGO_URI="$${MONGO_URI:-mongodb://127.0.0.1:27017}" \
|
||||||
|
MONGO_DB="$${MONGO_DB:-emboflow}" \
|
||||||
|
API_HOST="$${API_HOST:-127.0.0.1}" \
|
||||||
|
API_PORT="$${API_PORT:-3001}" \
|
||||||
|
CORS_ORIGIN="$${CORS_ORIGIN:-http://127.0.0.1:3000}" \
|
||||||
|
pnpm --filter api start
|
||||||
|
|
||||||
|
serve-web:
|
||||||
|
VITE_API_BASE_URL="$${VITE_API_BASE_URL:-http://127.0.0.1:3001}" \
|
||||||
|
pnpm --filter web start -- --host 127.0.0.1 --port 3000
|
||||||
|
|
||||||
|
infra-up:
|
||||||
|
docker compose up -d mongo minio
|
||||||
|
|
||||||
|
infra-down:
|
||||||
|
docker compose down
|
||||||
|
|
||||||
guardrails:
|
guardrails:
|
||||||
python3 scripts/check_doc_code_sync.py . --strict
|
python3 scripts/check_doc_code_sync.py . --strict
|
||||||
python3 scripts/check_commit_message.py --rev-range HEAD
|
python3 scripts/check_commit_message.py --rev-range HEAD
|
||||||
|
|||||||
32
README.md
32
README.md
@ -34,10 +34,40 @@ make dev-web
|
|||||||
make dev-worker
|
make dev-worker
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Local Deployment
|
||||||
|
|
||||||
|
Start MongoDB and MinIO:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make infra-up
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the API and web app in separate terminals:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make serve-api
|
||||||
|
make serve-web
|
||||||
|
```
|
||||||
|
|
||||||
|
The default local stack uses:
|
||||||
|
|
||||||
|
- API: `http://127.0.0.1:3001`
|
||||||
|
- Web: `http://127.0.0.1:3000`
|
||||||
|
|
||||||
|
### Local Data Validation
|
||||||
|
|
||||||
|
The local validation path currently used for embodied data testing is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/Users/longtaowu/workspace/emboldata/data
|
||||||
|
```
|
||||||
|
|
||||||
|
You can register that directory from the Assets page or via `POST /api/assets/register`.
|
||||||
|
|
||||||
## Repository Structure
|
## Repository Structure
|
||||||
|
|
||||||
- `apps/api` contains the control-plane modules for workspaces, assets, workflows, runs, and artifacts.
|
- `apps/api` contains the control-plane modules for workspaces, assets, workflows, runs, and artifacts.
|
||||||
- `apps/web` contains the initial shell, asset workspace, workflow editor surface, run detail view, and explore renderers.
|
- `apps/web` contains the React shell, asset workspace, workflow editor surface, run detail view, and explore renderers.
|
||||||
- `apps/worker` contains the local scheduler, task runner, and executor contracts.
|
- `apps/worker` contains the local scheduler, task runner, and executor contracts.
|
||||||
- `design/` contains the architecture and product design documents that must stay aligned with implementation.
|
- `design/` contains the architecture and product design documents that must stay aligned with implementation.
|
||||||
- `docs/` contains workflow guidance and the executable implementation plan.
|
- `docs/` contains workflow guidance and the executable implementation plan.
|
||||||
|
|||||||
@ -5,6 +5,12 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx watch src/main.ts",
|
"dev": "tsx watch src/main.ts",
|
||||||
|
"start": "tsx src/main.ts",
|
||||||
"test": "node --test --experimental-strip-types"
|
"test": "node --test --experimental-strip-types"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.6",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"mongodb": "^7.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { createRunsModule } from "./modules/runs/runs.module.ts";
|
|||||||
import { createStorageModule } from "./modules/storage/storage.module.ts";
|
import { createStorageModule } from "./modules/storage/storage.module.ts";
|
||||||
import { createWorkflowsModule } from "./modules/workflows/workflows.module.ts";
|
import { createWorkflowsModule } from "./modules/workflows/workflows.module.ts";
|
||||||
import { createWorkspaceModule } from "./modules/workspaces/workspaces.module.ts";
|
import { createWorkspaceModule } from "./modules/workspaces/workspaces.module.ts";
|
||||||
|
import { startApiServer } from "./runtime/server.ts";
|
||||||
|
|
||||||
export function createApiApp() {
|
export function createApiApp() {
|
||||||
const auth = createAuthModule();
|
const auth = createAuthModule();
|
||||||
@ -31,5 +32,6 @@ export function createApiApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
await startApiServer();
|
||||||
process.stdout.write(JSON.stringify({ status: createApiApp().status }, null, 2));
|
process.stdout.write(JSON.stringify({ status: createApiApp().status }, null, 2));
|
||||||
}
|
}
|
||||||
|
|||||||
224
apps/api/src/runtime/local-source-probe.ts
Normal file
224
apps/api/src/runtime/local-source-probe.ts
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { readdir, stat } from "node:fs/promises";
|
||||||
|
|
||||||
|
import type { AssetType } from "../../../../packages/contracts/src/domain.ts";
|
||||||
|
|
||||||
|
export type LocalAssetShape = {
|
||||||
|
type: AssetType;
|
||||||
|
sourceType: "local_path";
|
||||||
|
displayName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LocalProbeSummary = {
|
||||||
|
pathExists: boolean;
|
||||||
|
kind: "file" | "directory" | "missing";
|
||||||
|
detectedFormats: string[];
|
||||||
|
warnings: string[];
|
||||||
|
recommendedNextNodes: string[];
|
||||||
|
topLevelPaths: string[];
|
||||||
|
fileCount: number;
|
||||||
|
structureSummary: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ARCHIVE_EXTENSIONS = new Set([".zip", ".tar", ".tgz", ".gz", ".zst"]);
|
||||||
|
const ROSBAG_EXTENSIONS = new Set([".bag", ".mcap"]);
|
||||||
|
const HDF5_EXTENSIONS = new Set([".h5", ".hdf5"]);
|
||||||
|
|
||||||
|
export function detectAssetShapeFromLocalPath(sourcePath: string): LocalAssetShape {
|
||||||
|
const displayName = path.basename(sourcePath);
|
||||||
|
const extension = path.extname(displayName).toLowerCase();
|
||||||
|
|
||||||
|
if (ARCHIVE_EXTENSIONS.has(extension)) {
|
||||||
|
return { type: "archive", sourceType: "local_path", displayName };
|
||||||
|
}
|
||||||
|
if (ROSBAG_EXTENSIONS.has(extension)) {
|
||||||
|
return { type: "rosbag", sourceType: "local_path", displayName };
|
||||||
|
}
|
||||||
|
if (HDF5_EXTENSIONS.has(extension)) {
|
||||||
|
return { type: "hdf5_dataset", sourceType: "local_path", displayName };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: "folder", sourceType: "local_path", displayName };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function probeLocalSourcePath(input: {
|
||||||
|
sourcePath: string;
|
||||||
|
assetType: AssetType;
|
||||||
|
displayName: string;
|
||||||
|
}): Promise<LocalProbeSummary> {
|
||||||
|
try {
|
||||||
|
const info = await stat(input.sourcePath);
|
||||||
|
if (info.isDirectory()) {
|
||||||
|
const entries = await readdir(input.sourcePath, { withFileTypes: true });
|
||||||
|
const topLevelPaths = entries.slice(0, 200).map((entry) => entry.name).sort();
|
||||||
|
const detectedFormats = detectFormatsFromDirectory({
|
||||||
|
displayName: input.displayName,
|
||||||
|
topLevelPaths,
|
||||||
|
entries,
|
||||||
|
assetType: input.assetType,
|
||||||
|
});
|
||||||
|
const warnings = buildWarnings(topLevelPaths, detectedFormats);
|
||||||
|
return {
|
||||||
|
pathExists: true,
|
||||||
|
kind: "directory",
|
||||||
|
detectedFormats,
|
||||||
|
warnings,
|
||||||
|
recommendedNextNodes: recommendNextNodes(detectedFormats),
|
||||||
|
topLevelPaths,
|
||||||
|
fileCount: entries.length,
|
||||||
|
structureSummary: {
|
||||||
|
entryCount: entries.length,
|
||||||
|
hasArchives: topLevelPaths.some((item) =>
|
||||||
|
ARCHIVE_EXTENSIONS.has(path.extname(item).toLowerCase()),
|
||||||
|
),
|
||||||
|
hasRosbag: topLevelPaths.some((item) =>
|
||||||
|
ROSBAG_EXTENSIONS.has(path.extname(item).toLowerCase()),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const detectedFormats = detectFormatsFromFile(input.displayName, input.assetType);
|
||||||
|
return {
|
||||||
|
pathExists: true,
|
||||||
|
kind: "file",
|
||||||
|
detectedFormats,
|
||||||
|
warnings: [],
|
||||||
|
recommendedNextNodes: recommendNextNodes(detectedFormats),
|
||||||
|
topLevelPaths: [path.basename(input.sourcePath)],
|
||||||
|
fileCount: 1,
|
||||||
|
structureSummary: {
|
||||||
|
extension: path.extname(input.displayName).toLowerCase(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
pathExists: false,
|
||||||
|
kind: "missing",
|
||||||
|
detectedFormats: [input.assetType],
|
||||||
|
warnings: [
|
||||||
|
error instanceof Error ? error.message : "local path is not accessible",
|
||||||
|
],
|
||||||
|
recommendedNextNodes: ["inspect_asset"],
|
||||||
|
topLevelPaths: [],
|
||||||
|
fileCount: 0,
|
||||||
|
structureSummary: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectFormatsFromFile(
|
||||||
|
displayName: string,
|
||||||
|
assetType: AssetType,
|
||||||
|
): string[] {
|
||||||
|
const extension = path.extname(displayName).toLowerCase();
|
||||||
|
if (ARCHIVE_EXTENSIONS.has(extension)) {
|
||||||
|
return ["compressed_archive"];
|
||||||
|
}
|
||||||
|
if (ROSBAG_EXTENSIONS.has(extension)) {
|
||||||
|
return ["rosbag"];
|
||||||
|
}
|
||||||
|
if (HDF5_EXTENSIONS.has(extension)) {
|
||||||
|
return ["hdf5_dataset"];
|
||||||
|
}
|
||||||
|
return [assetType];
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectFormatsFromDirectory(input: {
|
||||||
|
displayName: string;
|
||||||
|
topLevelPaths: string[];
|
||||||
|
entries: { name: string; isDirectory(): boolean }[];
|
||||||
|
assetType: AssetType;
|
||||||
|
}): string[] {
|
||||||
|
const detected = new Set<string>([input.assetType]);
|
||||||
|
const topLevel = new Set(input.topLevelPaths);
|
||||||
|
|
||||||
|
if (
|
||||||
|
topLevel.has("meta.json") ||
|
||||||
|
topLevel.has("intrinsics.json") ||
|
||||||
|
topLevel.has("video_meta.json") ||
|
||||||
|
input.displayName.startsWith("BJ_")
|
||||||
|
) {
|
||||||
|
detected.add("delivery_package");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
topLevel.has("meta") ||
|
||||||
|
topLevel.has("episodes") ||
|
||||||
|
input.topLevelPaths.some((item) => item.endsWith(".parquet"))
|
||||||
|
) {
|
||||||
|
detected.add("lerobot_dataset");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
topLevel.has("data") ||
|
||||||
|
topLevel.has("task.json") ||
|
||||||
|
input.topLevelPaths.some((item) => item.endsWith(".tfrecord"))
|
||||||
|
) {
|
||||||
|
detected.add("rlds_dataset");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
input.topLevelPaths.some((item) =>
|
||||||
|
ARCHIVE_EXTENSIONS.has(path.extname(item).toLowerCase()),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
detected.add("archive_collection");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
input.topLevelPaths.some((item) =>
|
||||||
|
ROSBAG_EXTENSIONS.has(path.extname(item).toLowerCase()),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
detected.add("rosbag");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
input.topLevelPaths.some((item) =>
|
||||||
|
HDF5_EXTENSIONS.has(path.extname(item).toLowerCase()),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
detected.add("hdf5_dataset");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.entries.some((entry) => entry.isDirectory() && entry.name.startsWith("DJI_"))) {
|
||||||
|
detected.add("video_collection");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(detected);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWarnings(topLevelPaths: string[], detectedFormats: string[]): string[] {
|
||||||
|
const warnings: string[] = [];
|
||||||
|
if (detectedFormats.includes("delivery_package")) {
|
||||||
|
for (const required of ["meta.json", "intrinsics.json", "video_meta.json"]) {
|
||||||
|
if (!topLevelPaths.includes(required)) {
|
||||||
|
warnings.push(`${required} missing`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
function recommendNextNodes(detectedFormats: string[]): string[] {
|
||||||
|
if (detectedFormats.includes("compressed_archive")) {
|
||||||
|
return ["extract_archive", "validate_structure"];
|
||||||
|
}
|
||||||
|
if (detectedFormats.includes("delivery_package")) {
|
||||||
|
return ["validate_structure", "validate_metadata"];
|
||||||
|
}
|
||||||
|
if (detectedFormats.includes("rosbag")) {
|
||||||
|
return ["rosbag_reader", "canonical_mapper"];
|
||||||
|
}
|
||||||
|
if (detectedFormats.includes("hdf5_dataset")) {
|
||||||
|
return ["hdf5_reader", "canonical_mapper"];
|
||||||
|
}
|
||||||
|
if (detectedFormats.includes("lerobot_dataset")) {
|
||||||
|
return ["lerobot_reader", "canonical_mapper"];
|
||||||
|
}
|
||||||
|
if (detectedFormats.includes("rlds_dataset")) {
|
||||||
|
return ["rlds_reader", "canonical_mapper"];
|
||||||
|
}
|
||||||
|
return ["inspect_asset"];
|
||||||
|
}
|
||||||
521
apps/api/src/runtime/mongo-store.ts
Normal file
521
apps/api/src/runtime/mongo-store.ts
Normal file
@ -0,0 +1,521 @@
|
|||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
|
import type { Db, Document, WithId } from "mongodb";
|
||||||
|
|
||||||
|
import type { AssetType } from "../../../../packages/contracts/src/domain.ts";
|
||||||
|
import { DELIVERY_NODE_DEFINITIONS } from "../modules/plugins/builtin/delivery-nodes.ts";
|
||||||
|
import { probeLocalSourcePath } from "./local-source-probe.ts";
|
||||||
|
|
||||||
|
type Timestamped = {
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WorkspaceDocument = Timestamped & {
|
||||||
|
_id: string;
|
||||||
|
type: "personal" | "team";
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
ownerId: string;
|
||||||
|
status: "active";
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProjectDocument = Timestamped & {
|
||||||
|
_id: string;
|
||||||
|
workspaceId: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
description: string;
|
||||||
|
status: "active";
|
||||||
|
createdBy: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AssetDocument = Timestamped & {
|
||||||
|
_id: string;
|
||||||
|
workspaceId: string;
|
||||||
|
projectId: string;
|
||||||
|
type: AssetType;
|
||||||
|
sourceType: string;
|
||||||
|
displayName: string;
|
||||||
|
sourcePath?: string;
|
||||||
|
status: "registered" | "probed";
|
||||||
|
storageRef: Record<string, unknown>;
|
||||||
|
topLevelPaths: string[];
|
||||||
|
detectedFormats: string[];
|
||||||
|
fileCount: number;
|
||||||
|
summary: Record<string, unknown>;
|
||||||
|
createdBy: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AssetProbeReportDocument = {
|
||||||
|
_id: string;
|
||||||
|
assetId: string;
|
||||||
|
reportVersion: number;
|
||||||
|
detectedFormatCandidates: string[];
|
||||||
|
structureSummary: Record<string, unknown>;
|
||||||
|
warnings: string[];
|
||||||
|
recommendedNextNodes: string[];
|
||||||
|
rawReport: Record<string, unknown>;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WorkflowDefinitionDocument = Timestamped & {
|
||||||
|
_id: string;
|
||||||
|
workspaceId: string;
|
||||||
|
projectId: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
status: "draft";
|
||||||
|
latestVersionNumber: number;
|
||||||
|
publishedVersionNumber: number | null;
|
||||||
|
createdBy: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WorkflowDefinitionVersionDocument = Timestamped & {
|
||||||
|
_id: string;
|
||||||
|
workflowDefinitionId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
projectId: string;
|
||||||
|
versionNumber: number;
|
||||||
|
visualGraph: Record<string, unknown>;
|
||||||
|
logicGraph: {
|
||||||
|
nodes: Array<{ id: string; type: string }>;
|
||||||
|
edges: Array<{ from: string; to: string }>;
|
||||||
|
};
|
||||||
|
runtimeGraph: Record<string, unknown>;
|
||||||
|
pluginRefs: string[];
|
||||||
|
createdBy: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WorkflowRunDocument = Timestamped & {
|
||||||
|
_id: string;
|
||||||
|
workflowDefinitionId: string;
|
||||||
|
workflowVersionId: string;
|
||||||
|
status: "queued";
|
||||||
|
triggeredBy: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RunTaskDocument = Timestamped & {
|
||||||
|
_id: string;
|
||||||
|
workflowRunId: string;
|
||||||
|
workflowVersionId: string;
|
||||||
|
nodeId: string;
|
||||||
|
nodeType: string;
|
||||||
|
status: "queued" | "pending";
|
||||||
|
attempt: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ArtifactDocument = Timestamped & {
|
||||||
|
_id: string;
|
||||||
|
type: "json" | "directory" | "video";
|
||||||
|
title: string;
|
||||||
|
producerType: string;
|
||||||
|
producerId: string;
|
||||||
|
payload: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function nowIso(): string {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function slugify(input: string): string {
|
||||||
|
return input
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/^-+|-+$/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDoc<T extends Document>(document: WithId<T>): T {
|
||||||
|
return document as unknown as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MongoAppStore {
|
||||||
|
constructor(private readonly db: Db) {}
|
||||||
|
|
||||||
|
async bootstrapDevContext(input: {
|
||||||
|
userId?: string;
|
||||||
|
workspaceName?: string;
|
||||||
|
projectName?: string;
|
||||||
|
}) {
|
||||||
|
const userId = input.userId ?? "local-user";
|
||||||
|
const workspaceCollection = this.db.collection<WorkspaceDocument>("workspaces");
|
||||||
|
const workspaceUpsert = await workspaceCollection.findOneAndUpdate(
|
||||||
|
{
|
||||||
|
ownerId: userId,
|
||||||
|
type: "personal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$setOnInsert: {
|
||||||
|
_id: `workspace-${randomUUID()}`,
|
||||||
|
type: "personal",
|
||||||
|
name: input.workspaceName ?? "Local Personal Workspace",
|
||||||
|
slug: `${slugify(userId)}-personal`,
|
||||||
|
ownerId: userId,
|
||||||
|
status: "active",
|
||||||
|
createdAt: nowIso(),
|
||||||
|
updatedAt: nowIso(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
upsert: true,
|
||||||
|
returnDocument: "after",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const workspace = workspaceUpsert as WorkspaceDocument | null;
|
||||||
|
if (!workspace) {
|
||||||
|
throw new Error("failed to bootstrap workspace");
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectSlug = slugify(input.projectName ?? "Local Demo Project");
|
||||||
|
const projectCollection = this.db.collection<ProjectDocument>("projects");
|
||||||
|
const projectUpsert = await projectCollection.findOneAndUpdate(
|
||||||
|
{
|
||||||
|
workspaceId: workspace._id,
|
||||||
|
slug: projectSlug,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$setOnInsert: {
|
||||||
|
_id: `project-${randomUUID()}`,
|
||||||
|
workspaceId: workspace._id,
|
||||||
|
name: input.projectName ?? "Local Demo Project",
|
||||||
|
slug: projectSlug,
|
||||||
|
description: "Local development project",
|
||||||
|
status: "active",
|
||||||
|
createdBy: userId,
|
||||||
|
createdAt: nowIso(),
|
||||||
|
updatedAt: nowIso(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
upsert: true,
|
||||||
|
returnDocument: "after",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const project = projectUpsert as ProjectDocument | null;
|
||||||
|
if (!project) {
|
||||||
|
throw new Error("failed to bootstrap project");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId,
|
||||||
|
workspace: mapDoc(workspace as WithId<WorkspaceDocument>),
|
||||||
|
project: mapDoc(project as WithId<ProjectDocument>),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async listWorkspaces(ownerId: string) {
|
||||||
|
return this.db
|
||||||
|
.collection<WorkspaceDocument>("workspaces")
|
||||||
|
.find({ ownerId })
|
||||||
|
.sort({ createdAt: 1 })
|
||||||
|
.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
async createProject(input: {
|
||||||
|
workspaceId: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
createdBy: string;
|
||||||
|
}) {
|
||||||
|
const project: ProjectDocument = {
|
||||||
|
_id: `project-${randomUUID()}`,
|
||||||
|
workspaceId: input.workspaceId,
|
||||||
|
name: input.name,
|
||||||
|
slug: slugify(input.name),
|
||||||
|
description: input.description ?? "",
|
||||||
|
status: "active",
|
||||||
|
createdBy: input.createdBy,
|
||||||
|
createdAt: nowIso(),
|
||||||
|
updatedAt: nowIso(),
|
||||||
|
};
|
||||||
|
await this.db.collection("projects").insertOne(project);
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listProjects(workspaceId: string) {
|
||||||
|
return this.db
|
||||||
|
.collection<ProjectDocument>("projects")
|
||||||
|
.find({ workspaceId })
|
||||||
|
.sort({ createdAt: 1 })
|
||||||
|
.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerAsset(input: {
|
||||||
|
workspaceId: string;
|
||||||
|
projectId: string;
|
||||||
|
type: AssetType;
|
||||||
|
sourceType: string;
|
||||||
|
displayName: string;
|
||||||
|
sourcePath?: string;
|
||||||
|
createdBy: string;
|
||||||
|
}) {
|
||||||
|
const asset: AssetDocument = {
|
||||||
|
_id: `asset-${randomUUID()}`,
|
||||||
|
workspaceId: input.workspaceId,
|
||||||
|
projectId: input.projectId,
|
||||||
|
type: input.type,
|
||||||
|
sourceType: input.sourceType,
|
||||||
|
displayName: input.displayName,
|
||||||
|
sourcePath: input.sourcePath,
|
||||||
|
status: "registered",
|
||||||
|
storageRef: input.sourcePath
|
||||||
|
? { provider: "local_path", path: input.sourcePath }
|
||||||
|
: { provider: "upload_placeholder" },
|
||||||
|
topLevelPaths: [],
|
||||||
|
detectedFormats: [],
|
||||||
|
fileCount: 0,
|
||||||
|
summary: {},
|
||||||
|
createdBy: input.createdBy,
|
||||||
|
createdAt: nowIso(),
|
||||||
|
updatedAt: nowIso(),
|
||||||
|
};
|
||||||
|
await this.db.collection("assets").insertOne(asset);
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listAssets(projectId: string) {
|
||||||
|
return this.db
|
||||||
|
.collection<AssetDocument>("assets")
|
||||||
|
.find({ projectId })
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAsset(assetId: string) {
|
||||||
|
return this.db.collection<AssetDocument>("assets").findOne({ _id: assetId });
|
||||||
|
}
|
||||||
|
|
||||||
|
async probeAsset(assetId: string) {
|
||||||
|
const asset = await this.getAsset(assetId);
|
||||||
|
if (!asset) {
|
||||||
|
throw new Error(`asset not found: ${assetId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = await probeLocalSourcePath({
|
||||||
|
sourcePath: asset.sourcePath ?? asset.displayName,
|
||||||
|
assetType: asset.type,
|
||||||
|
displayName: asset.displayName,
|
||||||
|
});
|
||||||
|
|
||||||
|
const report: AssetProbeReportDocument = {
|
||||||
|
_id: `probe-${randomUUID()}`,
|
||||||
|
assetId: asset._id,
|
||||||
|
reportVersion: 1,
|
||||||
|
detectedFormatCandidates: summary.detectedFormats,
|
||||||
|
structureSummary: summary.structureSummary,
|
||||||
|
warnings: summary.warnings,
|
||||||
|
recommendedNextNodes: summary.recommendedNextNodes,
|
||||||
|
rawReport: {
|
||||||
|
kind: summary.kind,
|
||||||
|
pathExists: summary.pathExists,
|
||||||
|
topLevelPaths: summary.topLevelPaths,
|
||||||
|
fileCount: summary.fileCount,
|
||||||
|
},
|
||||||
|
createdAt: nowIso(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.db.collection("asset_probe_reports").insertOne(report);
|
||||||
|
await this.db.collection("assets").updateOne(
|
||||||
|
{ _id: asset._id },
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
status: "probed",
|
||||||
|
topLevelPaths: summary.topLevelPaths,
|
||||||
|
detectedFormats: summary.detectedFormats,
|
||||||
|
fileCount: summary.fileCount,
|
||||||
|
summary: summary.structureSummary,
|
||||||
|
updatedAt: nowIso(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLatestProbeReport(assetId: string) {
|
||||||
|
return this.db
|
||||||
|
.collection<AssetProbeReportDocument>("asset_probe_reports")
|
||||||
|
.find({ assetId })
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.limit(1)
|
||||||
|
.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
async createWorkflowDefinition(input: {
|
||||||
|
workspaceId: string;
|
||||||
|
projectId: string;
|
||||||
|
name: string;
|
||||||
|
createdBy: string;
|
||||||
|
}) {
|
||||||
|
const definition: WorkflowDefinitionDocument = {
|
||||||
|
_id: `workflow-${randomUUID()}`,
|
||||||
|
workspaceId: input.workspaceId,
|
||||||
|
projectId: input.projectId,
|
||||||
|
name: input.name,
|
||||||
|
slug: slugify(input.name),
|
||||||
|
status: "draft",
|
||||||
|
latestVersionNumber: 0,
|
||||||
|
publishedVersionNumber: null,
|
||||||
|
createdBy: input.createdBy,
|
||||||
|
createdAt: nowIso(),
|
||||||
|
updatedAt: nowIso(),
|
||||||
|
};
|
||||||
|
await this.db.collection("workflow_definitions").insertOne(definition);
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listWorkflowDefinitions(projectId: string) {
|
||||||
|
return this.db
|
||||||
|
.collection<WorkflowDefinitionDocument>("workflow_definitions")
|
||||||
|
.find({ projectId })
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWorkflowDefinition(workflowDefinitionId: string) {
|
||||||
|
return this.db
|
||||||
|
.collection<WorkflowDefinitionDocument>("workflow_definitions")
|
||||||
|
.findOne({ _id: workflowDefinitionId });
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveWorkflowVersion(input: {
|
||||||
|
workflowDefinitionId: string;
|
||||||
|
visualGraph: Record<string, unknown>;
|
||||||
|
logicGraph: WorkflowDefinitionVersionDocument["logicGraph"];
|
||||||
|
runtimeGraph: Record<string, unknown>;
|
||||||
|
pluginRefs: string[];
|
||||||
|
createdBy: string;
|
||||||
|
}) {
|
||||||
|
const definition = await this.getWorkflowDefinition(input.workflowDefinitionId);
|
||||||
|
if (!definition) {
|
||||||
|
throw new Error(`workflow definition not found: ${input.workflowDefinitionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionNumber = definition.latestVersionNumber + 1;
|
||||||
|
const version: WorkflowDefinitionVersionDocument = {
|
||||||
|
_id: `${definition._id}-v${versionNumber}`,
|
||||||
|
workflowDefinitionId: definition._id,
|
||||||
|
workspaceId: definition.workspaceId,
|
||||||
|
projectId: definition.projectId,
|
||||||
|
versionNumber,
|
||||||
|
visualGraph: input.visualGraph,
|
||||||
|
logicGraph: input.logicGraph,
|
||||||
|
runtimeGraph: input.runtimeGraph,
|
||||||
|
pluginRefs: input.pluginRefs,
|
||||||
|
createdBy: input.createdBy,
|
||||||
|
createdAt: nowIso(),
|
||||||
|
updatedAt: nowIso(),
|
||||||
|
};
|
||||||
|
await this.db.collection("workflow_definition_versions").insertOne(version);
|
||||||
|
await this.db.collection("workflow_definitions").updateOne(
|
||||||
|
{ _id: definition._id },
|
||||||
|
{ $set: { latestVersionNumber: versionNumber, updatedAt: nowIso() } },
|
||||||
|
);
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listWorkflowVersions(workflowDefinitionId: string) {
|
||||||
|
return this.db
|
||||||
|
.collection<WorkflowDefinitionVersionDocument>("workflow_definition_versions")
|
||||||
|
.find({ workflowDefinitionId })
|
||||||
|
.sort({ versionNumber: -1 })
|
||||||
|
.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWorkflowVersion(workflowVersionId: string) {
|
||||||
|
return this.db
|
||||||
|
.collection<WorkflowDefinitionVersionDocument>("workflow_definition_versions")
|
||||||
|
.findOne({ _id: workflowVersionId });
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRun(input: {
|
||||||
|
workflowDefinitionId: string;
|
||||||
|
workflowVersionId: string;
|
||||||
|
triggeredBy: string;
|
||||||
|
}) {
|
||||||
|
const version = await this.getWorkflowVersion(input.workflowVersionId);
|
||||||
|
if (!version) {
|
||||||
|
throw new Error(`workflow version not found: ${input.workflowVersionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const run: WorkflowRunDocument = {
|
||||||
|
_id: `run-${randomUUID()}`,
|
||||||
|
workflowDefinitionId: input.workflowDefinitionId,
|
||||||
|
workflowVersionId: input.workflowVersionId,
|
||||||
|
status: "queued",
|
||||||
|
triggeredBy: input.triggeredBy,
|
||||||
|
createdAt: nowIso(),
|
||||||
|
updatedAt: nowIso(),
|
||||||
|
};
|
||||||
|
await this.db.collection("workflow_runs").insertOne(run);
|
||||||
|
|
||||||
|
const targetNodes = new Set(version.logicGraph.edges.map((edge) => edge.to));
|
||||||
|
const tasks = version.logicGraph.nodes.map<RunTaskDocument>((node) => ({
|
||||||
|
_id: `task-${randomUUID()}`,
|
||||||
|
workflowRunId: run._id,
|
||||||
|
workflowVersionId: version._id,
|
||||||
|
nodeId: node.id,
|
||||||
|
nodeType: node.type,
|
||||||
|
status: targetNodes.has(node.id) ? "pending" : "queued",
|
||||||
|
attempt: 1,
|
||||||
|
createdAt: nowIso(),
|
||||||
|
updatedAt: nowIso(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (tasks.length > 0) {
|
||||||
|
await this.db.collection("run_tasks").insertMany(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
return run;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRun(runId: string) {
|
||||||
|
return this.db.collection<WorkflowRunDocument>("workflow_runs").findOne({ _id: runId });
|
||||||
|
}
|
||||||
|
|
||||||
|
async listRunTasks(runId: string) {
|
||||||
|
return this.db
|
||||||
|
.collection<RunTaskDocument>("run_tasks")
|
||||||
|
.find({ workflowRunId: runId })
|
||||||
|
.sort({ createdAt: 1 })
|
||||||
|
.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
async createArtifact(input: {
|
||||||
|
type: "json" | "directory" | "video";
|
||||||
|
title: string;
|
||||||
|
producerType: string;
|
||||||
|
producerId: string;
|
||||||
|
payload: Record<string, unknown>;
|
||||||
|
}) {
|
||||||
|
const artifact: ArtifactDocument = {
|
||||||
|
_id: `artifact-${randomUUID()}`,
|
||||||
|
type: input.type,
|
||||||
|
title: input.title,
|
||||||
|
producerType: input.producerType,
|
||||||
|
producerId: input.producerId,
|
||||||
|
payload: input.payload,
|
||||||
|
createdAt: nowIso(),
|
||||||
|
updatedAt: nowIso(),
|
||||||
|
};
|
||||||
|
await this.db.collection("artifacts").insertOne(artifact);
|
||||||
|
return artifact;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getArtifact(artifactId: string) {
|
||||||
|
return this.db.collection<ArtifactDocument>("artifacts").findOne({ _id: artifactId });
|
||||||
|
}
|
||||||
|
|
||||||
|
async listArtifactsByProducer(producerType: string, producerId: string) {
|
||||||
|
return this.db
|
||||||
|
.collection<ArtifactDocument>("artifacts")
|
||||||
|
.find({ producerType, producerId })
|
||||||
|
.sort({ createdAt: 1 })
|
||||||
|
.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
listNodeDefinitions() {
|
||||||
|
return DELIVERY_NODE_DEFINITIONS;
|
||||||
|
}
|
||||||
|
}
|
||||||
343
apps/api/src/runtime/server.ts
Normal file
343
apps/api/src/runtime/server.ts
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
import process from "node:process";
|
||||||
|
|
||||||
|
import cors from "cors";
|
||||||
|
import express from "express";
|
||||||
|
import { MongoClient } from "mongodb";
|
||||||
|
|
||||||
|
import { createMongoConnectionUri } from "../common/mongo/mongo.module.ts";
|
||||||
|
import { MongoAppStore } from "./mongo-store.ts";
|
||||||
|
import { detectAssetShapeFromLocalPath } from "./local-source-probe.ts";
|
||||||
|
|
||||||
|
export type ApiRuntimeConfig = {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
mongoUri: string;
|
||||||
|
database: string;
|
||||||
|
corsOrigin: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolveApiRuntimeConfig(
|
||||||
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
|
): ApiRuntimeConfig {
|
||||||
|
const mongoUri =
|
||||||
|
env.MONGO_URI ??
|
||||||
|
createMongoConnectionUri({
|
||||||
|
username: env.MONGO_ROOT_USERNAME ?? "emboflow",
|
||||||
|
password: env.MONGO_ROOT_PASSWORD ?? "emboflow",
|
||||||
|
host: env.MONGO_HOST ?? "127.0.0.1",
|
||||||
|
port: Number(env.MONGO_PORT ?? 27017),
|
||||||
|
database: env.MONGO_DB ?? "emboflow",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
host: env.API_HOST ?? "127.0.0.1",
|
||||||
|
port: Number(env.API_PORT ?? 3001),
|
||||||
|
mongoUri,
|
||||||
|
database: env.MONGO_DB ?? "emboflow",
|
||||||
|
corsOrigin: env.CORS_ORIGIN ?? "http://127.0.0.1:3000",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createApiRuntime(config = resolveApiRuntimeConfig()) {
|
||||||
|
const client = new MongoClient(config.mongoUri);
|
||||||
|
await client.connect();
|
||||||
|
const db = client.db(config.database);
|
||||||
|
const store = new MongoAppStore(db);
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(cors({ origin: config.corsOrigin, credentials: true }));
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
app.get("/api/health", async (_request, response) => {
|
||||||
|
await db.command({ ping: 1 });
|
||||||
|
response.json({ status: "ok" });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/dev/bootstrap", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
const result = await store.bootstrapDevContext(request.body ?? {});
|
||||||
|
response.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/workspaces", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
const ownerId = String(request.query.ownerId ?? "local-user");
|
||||||
|
response.json(await store.listWorkspaces(ownerId));
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/projects", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
response.json(
|
||||||
|
await store.createProject({
|
||||||
|
workspaceId: request.body.workspaceId,
|
||||||
|
name: request.body.name,
|
||||||
|
description: request.body.description,
|
||||||
|
createdBy: request.body.createdBy ?? "local-user",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/projects", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
response.json(await store.listProjects(String(request.query.workspaceId)));
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/assets/register", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
const sourcePath = request.body.sourcePath as string | undefined;
|
||||||
|
const inferred = sourcePath
|
||||||
|
? detectAssetShapeFromLocalPath(sourcePath)
|
||||||
|
: undefined;
|
||||||
|
response.json(
|
||||||
|
await store.registerAsset({
|
||||||
|
workspaceId: request.body.workspaceId,
|
||||||
|
projectId: request.body.projectId,
|
||||||
|
type: request.body.type ?? inferred?.type ?? "folder",
|
||||||
|
sourceType: request.body.sourceType ?? inferred?.sourceType ?? "upload",
|
||||||
|
displayName: request.body.displayName ?? inferred?.displayName ?? "asset",
|
||||||
|
sourcePath,
|
||||||
|
createdBy: request.body.createdBy ?? "local-user",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/assets", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
response.json(await store.listAssets(String(request.query.projectId)));
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/assets/:assetId", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
const asset = await store.getAsset(request.params.assetId);
|
||||||
|
if (!asset) {
|
||||||
|
response.status(404).json({ message: "asset not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response.json(asset);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/assets/:assetId/probe", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
response.json(await store.probeAsset(request.params.assetId));
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/assets/:assetId/probe-report", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
const report = await store.getLatestProbeReport(request.params.assetId);
|
||||||
|
if (!report) {
|
||||||
|
response.status(404).json({ message: "probe report not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response.json(report);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/node-definitions", (_request, response) => {
|
||||||
|
response.json(store.listNodeDefinitions());
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/workflows", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
response.json(
|
||||||
|
await store.createWorkflowDefinition({
|
||||||
|
workspaceId: request.body.workspaceId,
|
||||||
|
projectId: request.body.projectId,
|
||||||
|
name: request.body.name,
|
||||||
|
createdBy: request.body.createdBy ?? "local-user",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/workflows", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
response.json(await store.listWorkflowDefinitions(String(request.query.projectId)));
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/workflows/:workflowDefinitionId", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
const definition = await store.getWorkflowDefinition(request.params.workflowDefinitionId);
|
||||||
|
if (!definition) {
|
||||||
|
response.status(404).json({ message: "workflow definition not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response.json(definition);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/workflows/:workflowDefinitionId/versions", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
response.json(
|
||||||
|
await store.saveWorkflowVersion({
|
||||||
|
workflowDefinitionId: request.params.workflowDefinitionId,
|
||||||
|
visualGraph: request.body.visualGraph ?? {},
|
||||||
|
logicGraph: request.body.logicGraph,
|
||||||
|
runtimeGraph: request.body.runtimeGraph ?? {},
|
||||||
|
pluginRefs: request.body.pluginRefs ?? [],
|
||||||
|
createdBy: request.body.createdBy ?? "local-user",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/workflows/:workflowDefinitionId/versions", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
response.json(await store.listWorkflowVersions(request.params.workflowDefinitionId));
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/runs", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
response.json(
|
||||||
|
await store.createRun({
|
||||||
|
workflowDefinitionId: request.body.workflowDefinitionId,
|
||||||
|
workflowVersionId: request.body.workflowVersionId,
|
||||||
|
triggeredBy: request.body.triggeredBy ?? "local-user",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/runs/:runId", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
const run = await store.getRun(request.params.runId);
|
||||||
|
if (!run) {
|
||||||
|
response.status(404).json({ message: "run not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response.json(run);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/runs/:runId/tasks", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
response.json(await store.listRunTasks(request.params.runId));
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/artifacts", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
response.json(
|
||||||
|
await store.createArtifact({
|
||||||
|
type: request.body.type,
|
||||||
|
title: request.body.title,
|
||||||
|
producerType: request.body.producerType,
|
||||||
|
producerId: request.body.producerId,
|
||||||
|
payload: request.body.payload ?? {},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/artifacts", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
response.json(
|
||||||
|
await store.listArtifactsByProducer(
|
||||||
|
String(request.query.producerType),
|
||||||
|
String(request.query.producerId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/artifacts/:artifactId", async (request, response, next) => {
|
||||||
|
try {
|
||||||
|
const artifact = await store.getArtifact(request.params.artifactId);
|
||||||
|
if (!artifact) {
|
||||||
|
response.status(404).json({ message: "artifact not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response.json(artifact);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
(
|
||||||
|
error: unknown,
|
||||||
|
_request: express.Request,
|
||||||
|
response: express.Response,
|
||||||
|
_next: express.NextFunction,
|
||||||
|
) => {
|
||||||
|
const message = error instanceof Error ? error.message : "unknown error";
|
||||||
|
response.status(400).json({ message });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return { app, client, db, store, config };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startApiServer(config = resolveApiRuntimeConfig()) {
|
||||||
|
const runtime = await createApiRuntime(config);
|
||||||
|
|
||||||
|
return new Promise<{
|
||||||
|
close: () => Promise<void>;
|
||||||
|
port: number;
|
||||||
|
}>((resolve) => {
|
||||||
|
const server = runtime.app.listen(config.port, config.host, () => {
|
||||||
|
resolve({
|
||||||
|
port: config.port,
|
||||||
|
close: async () => {
|
||||||
|
await new Promise<void>((done, reject) => {
|
||||||
|
server.close((error) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await runtime.client.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
38
apps/api/test/local-source-probe.spec.ts
Normal file
38
apps/api/test/local-source-probe.spec.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
|
||||||
|
|
||||||
|
import {
|
||||||
|
detectAssetShapeFromLocalPath,
|
||||||
|
probeLocalSourcePath,
|
||||||
|
} from "../src/runtime/local-source-probe.ts";
|
||||||
|
|
||||||
|
test("detect archive asset shape from local path", () => {
|
||||||
|
const result = detectAssetShapeFromLocalPath("/tmp/sample-data.zip");
|
||||||
|
|
||||||
|
assert.equal(result.type, "archive");
|
||||||
|
assert.equal(result.sourceType, "local_path");
|
||||||
|
assert.equal(result.displayName, "sample-data.zip");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("probe local source path summarizes top-level entries and delivery recommendations", async () => {
|
||||||
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "emboflow-probe-"));
|
||||||
|
const deliveryRoot = path.join(tempRoot, "BJ_001_0001_OsmoNano_2026-03-19_14-51-43");
|
||||||
|
|
||||||
|
await mkdir(path.join(deliveryRoot, "DJI_001"), { recursive: true });
|
||||||
|
await writeFile(path.join(deliveryRoot, "meta.json"), "{}");
|
||||||
|
await writeFile(path.join(deliveryRoot, "video_meta.json"), "{}");
|
||||||
|
|
||||||
|
const summary = await probeLocalSourcePath({
|
||||||
|
sourcePath: deliveryRoot,
|
||||||
|
assetType: "folder",
|
||||||
|
displayName: path.basename(deliveryRoot),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(summary.pathExists, true);
|
||||||
|
assert.match(summary.detectedFormats.join(","), /delivery_package/);
|
||||||
|
assert.ok(summary.topLevelPaths.includes("DJI_001"));
|
||||||
|
assert.ok(summary.recommendedNextNodes.includes("validate_structure"));
|
||||||
|
});
|
||||||
12
apps/web/index.html
Normal file
12
apps/web/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>EmboFlow</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -4,7 +4,18 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx watch src/main.tsx",
|
"dev": "vite --host 0.0.0.0 --port 3000",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview --host 0.0.0.0 --port 3000",
|
||||||
|
"start": "vite --host 0.0.0.0 --port 3000",
|
||||||
"test": "tsx --test"
|
"test": "tsx --test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.2.4",
|
||||||
|
"react-dom": "^19.2.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
|
"vite": "^8.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,33 @@
|
|||||||
import { createRouter } from "./app/router.tsx";
|
import React from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
import { App } from "./runtime/app.tsx";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
export function createWebApp() {
|
export function createWebApp() {
|
||||||
return {
|
return {
|
||||||
status: "ready" as const,
|
status: "ready" as const,
|
||||||
routes: createRouter(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
export function mountWebApp() {
|
||||||
process.stdout.write(JSON.stringify({ status: createWebApp().status }, null, 2));
|
const target = document.getElementById("root");
|
||||||
|
if (!target) {
|
||||||
|
throw new Error("root element not found");
|
||||||
|
}
|
||||||
|
const apiBaseUrl =
|
||||||
|
typeof import.meta !== "undefined" && import.meta.env?.VITE_API_BASE_URL
|
||||||
|
? String(import.meta.env.VITE_API_BASE_URL)
|
||||||
|
: "http://127.0.0.1:3001";
|
||||||
|
createRoot(target).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App apiBaseUrl={apiBaseUrl} />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
|
mountWebApp();
|
||||||
|
} else if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
process.stdout.write(JSON.stringify(createWebApp(), null, 2));
|
||||||
}
|
}
|
||||||
|
|||||||
144
apps/web/src/runtime/api-client.ts
Normal file
144
apps/web/src/runtime/api-client.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
export type BootstrapContext = {
|
||||||
|
userId: string;
|
||||||
|
workspace: { _id: string; name: string };
|
||||||
|
project: { _id: string; name: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
async function readJson<T>(response: Response): Promise<T> {
|
||||||
|
if (!response.ok) {
|
||||||
|
const payload = (await response.json().catch(() => null)) as { message?: string } | null;
|
||||||
|
throw new Error(payload?.message ?? `Request failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
return (await response.json()) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiClient {
|
||||||
|
constructor(private readonly baseUrl: string) {}
|
||||||
|
|
||||||
|
async bootstrapDev(projectName?: string): Promise<BootstrapContext> {
|
||||||
|
const response = await fetch(`${this.baseUrl}/api/dev/bootstrap`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ projectName }),
|
||||||
|
});
|
||||||
|
return readJson<BootstrapContext>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listAssets(projectId: string) {
|
||||||
|
return readJson<any[]>(
|
||||||
|
await fetch(`${this.baseUrl}/api/assets?projectId=${encodeURIComponent(projectId)}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerLocalAsset(input: {
|
||||||
|
workspaceId: string;
|
||||||
|
projectId: string;
|
||||||
|
sourcePath: string;
|
||||||
|
displayName?: string;
|
||||||
|
}) {
|
||||||
|
return readJson<any>(
|
||||||
|
await fetch(`${this.baseUrl}/api/assets/register`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAsset(assetId: string) {
|
||||||
|
return readJson<any>(await fetch(`${this.baseUrl}/api/assets/${assetId}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
async probeAsset(assetId: string) {
|
||||||
|
return readJson<any>(
|
||||||
|
await fetch(`${this.baseUrl}/api/assets/${assetId}/probe`, { method: "POST" }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProbeReport(assetId: string) {
|
||||||
|
return readJson<any>(
|
||||||
|
await fetch(`${this.baseUrl}/api/assets/${assetId}/probe-report`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listWorkflows(projectId: string) {
|
||||||
|
return readJson<any[]>(
|
||||||
|
await fetch(`${this.baseUrl}/api/workflows?projectId=${encodeURIComponent(projectId)}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createWorkflow(input: {
|
||||||
|
workspaceId: string;
|
||||||
|
projectId: string;
|
||||||
|
name: string;
|
||||||
|
}) {
|
||||||
|
return readJson<any>(
|
||||||
|
await fetch(`${this.baseUrl}/api/workflows`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listWorkflowVersions(workflowDefinitionId: string) {
|
||||||
|
return readJson<any[]>(
|
||||||
|
await fetch(`${this.baseUrl}/api/workflows/${workflowDefinitionId}/versions`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveWorkflowVersion(workflowDefinitionId: string, payload: Record<string, unknown>) {
|
||||||
|
return readJson<any>(
|
||||||
|
await fetch(`${this.baseUrl}/api/workflows/${workflowDefinitionId}/versions`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listNodeDefinitions() {
|
||||||
|
return readJson<any[]>(await fetch(`${this.baseUrl}/api/node-definitions`));
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRun(input: {
|
||||||
|
workflowDefinitionId: string;
|
||||||
|
workflowVersionId: string;
|
||||||
|
}) {
|
||||||
|
return readJson<any>(
|
||||||
|
await fetch(`${this.baseUrl}/api/runs`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRun(runId: string) {
|
||||||
|
return readJson<any>(await fetch(`${this.baseUrl}/api/runs/${runId}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
async listRunTasks(runId: string) {
|
||||||
|
return readJson<any[]>(await fetch(`${this.baseUrl}/api/runs/${runId}/tasks`));
|
||||||
|
}
|
||||||
|
|
||||||
|
async createArtifact(input: {
|
||||||
|
type: "json" | "directory" | "video";
|
||||||
|
title: string;
|
||||||
|
producerType: string;
|
||||||
|
producerId: string;
|
||||||
|
payload: Record<string, unknown>;
|
||||||
|
}) {
|
||||||
|
return readJson<any>(
|
||||||
|
await fetch(`${this.baseUrl}/api/artifacts`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getArtifact(artifactId: string) {
|
||||||
|
return readJson<any>(await fetch(`${this.baseUrl}/api/artifacts/${artifactId}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
659
apps/web/src/runtime/app.tsx
Normal file
659
apps/web/src/runtime/app.tsx
Normal file
@ -0,0 +1,659 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { ApiClient } from "./api-client.ts";
|
||||||
|
|
||||||
|
type NavItem = "Assets" | "Workflows" | "Runs" | "Explore" | "Labels" | "Admin";
|
||||||
|
|
||||||
|
type BootstrapContext = {
|
||||||
|
userId: string;
|
||||||
|
workspace: { _id: string; name: string };
|
||||||
|
project: { _id: string; name: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
type AppProps = {
|
||||||
|
apiBaseUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function usePathname() {
|
||||||
|
const [pathname, setPathname] = useState(
|
||||||
|
typeof window === "undefined" ? "/assets" : window.location.pathname || "/assets",
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handle = () => setPathname(window.location.pathname || "/assets");
|
||||||
|
window.addEventListener("popstate", handle);
|
||||||
|
return () => window.removeEventListener("popstate", handle);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return pathname === "/" ? "/assets" : pathname;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppShell(props: {
|
||||||
|
workspaceName: string;
|
||||||
|
projectName: string;
|
||||||
|
active: NavItem;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const navItems: Array<{ label: NavItem; href: string }> = [
|
||||||
|
{ label: "Assets", href: "/assets" },
|
||||||
|
{ label: "Workflows", href: "/workflows" },
|
||||||
|
{ label: "Runs", href: "/runs" },
|
||||||
|
{ label: "Explore", href: "/explore" },
|
||||||
|
{ label: "Labels", href: "/labels" },
|
||||||
|
{ label: "Admin", href: "/admin" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-shell">
|
||||||
|
<header className="app-header">
|
||||||
|
<div className="app-header__group">
|
||||||
|
<div className="app-header__pill">
|
||||||
|
<span className="app-header__label">Workspace</span>
|
||||||
|
<strong>{props.workspaceName}</strong>
|
||||||
|
</div>
|
||||||
|
<div className="app-header__pill">
|
||||||
|
<span className="app-header__label">Project</span>
|
||||||
|
<strong>{props.projectName}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="app-header__group">
|
||||||
|
<span className="app-header__label">Runs</span>
|
||||||
|
<span className="app-header__label">Local Dev</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<aside className="app-sidebar">
|
||||||
|
<ul>
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<li key={item.label}>
|
||||||
|
<a href={item.href} data-active={String(item.label === props.active)}>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
<main className="app-main">{props.children}</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AssetsPage(props: {
|
||||||
|
api: ApiClient;
|
||||||
|
bootstrap: BootstrapContext;
|
||||||
|
}) {
|
||||||
|
const [sourcePath, setSourcePath] = useState("");
|
||||||
|
const [assets, setAssets] = useState<any[]>([]);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const loadAssets = async () => {
|
||||||
|
try {
|
||||||
|
setAssets(await props.api.listAssets(props.bootstrap.project._id));
|
||||||
|
} catch (loadError) {
|
||||||
|
setError(loadError instanceof Error ? loadError.message : "Failed to load assets");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadAssets();
|
||||||
|
}, [props.bootstrap.project._id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-stack">
|
||||||
|
<section className="panel">
|
||||||
|
<h1>Assets</h1>
|
||||||
|
<p>Register local folders, archives, or dataset files, then probe them into managed asset metadata.</p>
|
||||||
|
<div className="field-grid">
|
||||||
|
<label>
|
||||||
|
Local Path
|
||||||
|
<input
|
||||||
|
value={sourcePath}
|
||||||
|
onChange={(event) => setSourcePath(event.target.value)}
|
||||||
|
placeholder="/Users/longtaowu/workspace/emboldata/data"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="button-row" style={{ marginTop: 12 }}>
|
||||||
|
<button
|
||||||
|
className="button-primary"
|
||||||
|
onClick={async () => {
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const asset = await props.api.registerLocalAsset({
|
||||||
|
workspaceId: props.bootstrap.workspace._id,
|
||||||
|
projectId: props.bootstrap.project._id,
|
||||||
|
sourcePath,
|
||||||
|
});
|
||||||
|
await props.api.probeAsset(asset._id);
|
||||||
|
await loadAssets();
|
||||||
|
} catch (registerError) {
|
||||||
|
setError(
|
||||||
|
registerError instanceof Error
|
||||||
|
? registerError.message
|
||||||
|
: "Failed to register local asset",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Register Local Path
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{error ? <p>{error}</p> : null}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="panel">
|
||||||
|
<div className="list-grid">
|
||||||
|
{assets.length === 0 ? (
|
||||||
|
<p className="empty-state">No assets have been registered yet.</p>
|
||||||
|
) : (
|
||||||
|
assets.map((asset) => (
|
||||||
|
<article key={asset._id} className="asset-card">
|
||||||
|
<div className="toolbar">
|
||||||
|
<a href={`/assets/${asset._id}`}>
|
||||||
|
<strong>{asset.displayName}</strong>
|
||||||
|
</a>
|
||||||
|
<span className="status-pill" data-status={asset.status}>
|
||||||
|
{asset.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p>Type: {asset.type}</p>
|
||||||
|
<p>Source: {asset.sourceType}</p>
|
||||||
|
<p>Detected: {(asset.detectedFormats ?? []).join(", ") || "pending"}</p>
|
||||||
|
<p>Top-level entries: {(asset.topLevelPaths ?? []).slice(0, 6).join(", ") || "n/a"}</p>
|
||||||
|
</article>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AssetDetailPage(props: {
|
||||||
|
api: ApiClient;
|
||||||
|
assetId: string;
|
||||||
|
}) {
|
||||||
|
const [asset, setAsset] = useState<any | null>(null);
|
||||||
|
const [probeReport, setProbeReport] = useState<any | null>(null);
|
||||||
|
const [artifactId, setArtifactId] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const [assetData, reportData] = await Promise.all([
|
||||||
|
props.api.getAsset(props.assetId),
|
||||||
|
props.api.getProbeReport(props.assetId).catch(() => null),
|
||||||
|
]);
|
||||||
|
setAsset(assetData);
|
||||||
|
setProbeReport(reportData);
|
||||||
|
} catch (loadError) {
|
||||||
|
setError(loadError instanceof Error ? loadError.message : "Failed to load asset detail");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [props.assetId]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <section className="panel">{error}</section>;
|
||||||
|
}
|
||||||
|
if (!asset) {
|
||||||
|
return <section className="panel">Loading asset detail...</section>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-stack">
|
||||||
|
<section className="two-column">
|
||||||
|
<div className="panel">
|
||||||
|
<h1>{asset.displayName}</h1>
|
||||||
|
<p>Asset ID: {asset._id}</p>
|
||||||
|
<p>Type: {asset.type}</p>
|
||||||
|
<p>Status: {asset.status}</p>
|
||||||
|
<p>Source path: {asset.sourcePath ?? "n/a"}</p>
|
||||||
|
<div className="button-row" style={{ marginTop: 12 }}>
|
||||||
|
<button
|
||||||
|
className="button-primary"
|
||||||
|
onClick={async () => {
|
||||||
|
const report = await props.api.probeAsset(asset._id);
|
||||||
|
setProbeReport(report);
|
||||||
|
const reloaded = await props.api.getAsset(asset._id);
|
||||||
|
setAsset(reloaded);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Probe Again
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="button-secondary"
|
||||||
|
onClick={async () => {
|
||||||
|
if (!probeReport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const artifact = await props.api.createArtifact({
|
||||||
|
type: "json",
|
||||||
|
title: `Probe Report: ${asset.displayName}`,
|
||||||
|
producerType: "asset",
|
||||||
|
producerId: asset._id,
|
||||||
|
payload: probeReport.rawReport ?? {},
|
||||||
|
});
|
||||||
|
setArtifactId(artifact._id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create Explore Artifact
|
||||||
|
</button>
|
||||||
|
{artifactId ? <a href={`/explore/${artifactId}`}>Open Explore View</a> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside className="panel">
|
||||||
|
<h2>Probe Summary</h2>
|
||||||
|
{probeReport ? (
|
||||||
|
<>
|
||||||
|
<p>Detected: {(probeReport.detectedFormatCandidates ?? []).join(", ")}</p>
|
||||||
|
<p>Warnings: {(probeReport.warnings ?? []).join(", ") || "none"}</p>
|
||||||
|
<p>
|
||||||
|
Recommended nodes: {(probeReport.recommendedNextNodes ?? []).join(", ") || "inspect_asset"}
|
||||||
|
</p>
|
||||||
|
<pre className="mono-block">
|
||||||
|
{JSON.stringify(probeReport.structureSummary ?? {}, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="empty-state">No probe report yet.</p>
|
||||||
|
)}
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SAMPLE_WORKFLOW_VERSION = {
|
||||||
|
visualGraph: {
|
||||||
|
viewport: { x: 0, y: 0, zoom: 1 },
|
||||||
|
},
|
||||||
|
logicGraph: {
|
||||||
|
nodes: [
|
||||||
|
{ id: "source-asset", type: "source" },
|
||||||
|
{ id: "rename-folder", type: "transform" },
|
||||||
|
{ id: "validate-structure", type: "inspect" },
|
||||||
|
{ id: "export-delivery-package", type: "export" },
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{ from: "source-asset", to: "rename-folder" },
|
||||||
|
{ from: "rename-folder", to: "validate-structure" },
|
||||||
|
{ from: "validate-structure", to: "export-delivery-package" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
runtimeGraph: {
|
||||||
|
selectedPreset: "delivery-normalization",
|
||||||
|
},
|
||||||
|
pluginRefs: ["builtin:delivery-nodes"],
|
||||||
|
};
|
||||||
|
|
||||||
|
function WorkflowsPage(props: {
|
||||||
|
api: ApiClient;
|
||||||
|
bootstrap: BootstrapContext;
|
||||||
|
}) {
|
||||||
|
const [workflows, setWorkflows] = useState<any[]>([]);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
try {
|
||||||
|
setWorkflows(await props.api.listWorkflows(props.bootstrap.project._id));
|
||||||
|
} catch (loadError) {
|
||||||
|
setError(loadError instanceof Error ? loadError.message : "Failed to load workflows");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void load();
|
||||||
|
}, [props.bootstrap.project._id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-stack">
|
||||||
|
<section className="panel">
|
||||||
|
<div className="toolbar">
|
||||||
|
<h1 style={{ margin: 0 }}>Workflows</h1>
|
||||||
|
<button
|
||||||
|
className="button-primary"
|
||||||
|
onClick={async () => {
|
||||||
|
await props.api.createWorkflow({
|
||||||
|
workspaceId: props.bootstrap.workspace._id,
|
||||||
|
projectId: props.bootstrap.project._id,
|
||||||
|
name: `Delivery Normalize ${workflows.length + 1}`,
|
||||||
|
});
|
||||||
|
await load();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create Workflow
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{error ? <p>{error}</p> : null}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="panel">
|
||||||
|
<div className="list-grid">
|
||||||
|
{workflows.length === 0 ? (
|
||||||
|
<p className="empty-state">No workflows yet.</p>
|
||||||
|
) : (
|
||||||
|
workflows.map((workflow) => (
|
||||||
|
<article key={workflow._id} className="asset-card">
|
||||||
|
<a href={`/workflows/${workflow._id}`}>
|
||||||
|
<strong>{workflow.name}</strong>
|
||||||
|
</a>
|
||||||
|
<p>Status: {workflow.status}</p>
|
||||||
|
<p>Latest version: {workflow.latestVersionNumber}</p>
|
||||||
|
</article>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function WorkflowEditorPage(props: {
|
||||||
|
api: ApiClient;
|
||||||
|
workflowId: string;
|
||||||
|
}) {
|
||||||
|
const [nodes, setNodes] = useState<any[]>([]);
|
||||||
|
const [versions, setVersions] = useState<any[]>([]);
|
||||||
|
const [selectedNodeId, setSelectedNodeId] = useState("rename-folder");
|
||||||
|
const [lastRunId, setLastRunId] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const [nodeDefs, workflowVersions] = await Promise.all([
|
||||||
|
props.api.listNodeDefinitions(),
|
||||||
|
props.api.listWorkflowVersions(props.workflowId),
|
||||||
|
]);
|
||||||
|
setNodes(nodeDefs);
|
||||||
|
setVersions(workflowVersions);
|
||||||
|
} catch (loadError) {
|
||||||
|
setError(loadError instanceof Error ? loadError.message : "Failed to load workflow");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [props.workflowId]);
|
||||||
|
|
||||||
|
const selectedNode = useMemo(
|
||||||
|
() => nodes.find((node) => node.id === selectedNodeId) ?? null,
|
||||||
|
[nodes, selectedNodeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-stack">
|
||||||
|
<section className="panel">
|
||||||
|
<div className="toolbar">
|
||||||
|
<h1 style={{ margin: 0 }}>Workflow Editor</h1>
|
||||||
|
<button
|
||||||
|
className="button-primary"
|
||||||
|
onClick={async () => {
|
||||||
|
const version = await props.api.saveWorkflowVersion(
|
||||||
|
props.workflowId,
|
||||||
|
SAMPLE_WORKFLOW_VERSION,
|
||||||
|
);
|
||||||
|
setVersions([version, ...versions]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save Workflow Version
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="button-secondary"
|
||||||
|
onClick={async () => {
|
||||||
|
const latestVersion = versions[0] ??
|
||||||
|
(await props.api.saveWorkflowVersion(props.workflowId, SAMPLE_WORKFLOW_VERSION));
|
||||||
|
const run = await props.api.createRun({
|
||||||
|
workflowDefinitionId: props.workflowId,
|
||||||
|
workflowVersionId: latestVersion._id,
|
||||||
|
});
|
||||||
|
setLastRunId(run._id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Trigger Workflow Run
|
||||||
|
</button>
|
||||||
|
{lastRunId ? <a href={`/runs/${lastRunId}`}>Open Latest Run</a> : null}
|
||||||
|
</div>
|
||||||
|
{error ? <p>{error}</p> : null}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="editor-layout">
|
||||||
|
<aside className="panel">
|
||||||
|
<h2>Node Library</h2>
|
||||||
|
<div className="list-grid">
|
||||||
|
{nodes.map((node) => (
|
||||||
|
<button
|
||||||
|
key={node.id}
|
||||||
|
className="button-secondary"
|
||||||
|
onClick={() => setSelectedNodeId(node.id)}
|
||||||
|
>
|
||||||
|
{node.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section className="panel">
|
||||||
|
<h2>Canvas</h2>
|
||||||
|
<div className="list-grid">
|
||||||
|
{SAMPLE_WORKFLOW_VERSION.logicGraph.nodes.map((node) => (
|
||||||
|
<div key={node.id} className="node-card">
|
||||||
|
<strong>{node.id}</strong>
|
||||||
|
<p>Type: {node.type}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p style={{ marginTop: 12 }}>
|
||||||
|
Latest saved versions: {versions.length > 0 ? versions.map((item) => item.versionNumber).join(", ") : "none"}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside className="panel">
|
||||||
|
<h2>Node Configuration</h2>
|
||||||
|
{selectedNode ? (
|
||||||
|
<>
|
||||||
|
<p><strong>{selectedNode.name}</strong></p>
|
||||||
|
<p>{selectedNode.description}</p>
|
||||||
|
<p>Category: {selectedNode.category}</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="empty-state">Select a node.</p>
|
||||||
|
)}
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RunsIndexPage() {
|
||||||
|
return (
|
||||||
|
<section className="panel">
|
||||||
|
<h1>Runs</h1>
|
||||||
|
<p className="empty-state">Open a specific run from the workflow editor.</p>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RunDetailPage(props: {
|
||||||
|
api: ApiClient;
|
||||||
|
runId: string;
|
||||||
|
}) {
|
||||||
|
const [run, setRun] = useState<any | null>(null);
|
||||||
|
const [tasks, setTasks] = useState<any[]>([]);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const [runData, taskData] = await Promise.all([
|
||||||
|
props.api.getRun(props.runId),
|
||||||
|
props.api.listRunTasks(props.runId),
|
||||||
|
]);
|
||||||
|
setRun(runData);
|
||||||
|
setTasks(taskData);
|
||||||
|
} catch (loadError) {
|
||||||
|
setError(loadError instanceof Error ? loadError.message : "Failed to load run detail");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [props.runId]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <section className="panel">{error}</section>;
|
||||||
|
}
|
||||||
|
if (!run) {
|
||||||
|
return <section className="panel">Loading run...</section>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-stack">
|
||||||
|
<section className="panel">
|
||||||
|
<h1>Run Detail</h1>
|
||||||
|
<p>Run ID: {run._id}</p>
|
||||||
|
<p>Status: {run.status}</p>
|
||||||
|
</section>
|
||||||
|
<section className="two-column">
|
||||||
|
<div className="panel">
|
||||||
|
<h2>Run Graph</h2>
|
||||||
|
<div className="list-grid">
|
||||||
|
{tasks.map((task) => (
|
||||||
|
<article key={task._id} className="task-card">
|
||||||
|
<div className="toolbar">
|
||||||
|
<strong>{task.nodeId}</strong>
|
||||||
|
<span className="status-pill" data-status={task.status}>
|
||||||
|
{task.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p>Node type: {task.nodeType}</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<aside className="panel">
|
||||||
|
<h2>Selected Task</h2>
|
||||||
|
{tasks[0] ? (
|
||||||
|
<>
|
||||||
|
<p>Node: {tasks[0].nodeId}</p>
|
||||||
|
<p>Status: {tasks[0].status}</p>
|
||||||
|
<pre className="mono-block">{JSON.stringify(tasks[0], null, 2)}</pre>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="empty-state">No tasks created.</p>
|
||||||
|
)}
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ExplorePage(props: {
|
||||||
|
api: ApiClient;
|
||||||
|
artifactId: string | null;
|
||||||
|
}) {
|
||||||
|
const [artifact, setArtifact] = useState<any | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!props.artifactId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
setArtifact(await props.api.getArtifact(props.artifactId!));
|
||||||
|
} catch (loadError) {
|
||||||
|
setError(loadError instanceof Error ? loadError.message : "Failed to load artifact");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [props.artifactId]);
|
||||||
|
|
||||||
|
if (!props.artifactId) {
|
||||||
|
return (
|
||||||
|
<section className="panel">
|
||||||
|
<h1>Explore</h1>
|
||||||
|
<p className="empty-state">Create an artifact from asset detail to inspect it here.</p>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <section className="panel">{error}</section>;
|
||||||
|
}
|
||||||
|
if (!artifact) {
|
||||||
|
return <section className="panel">Loading artifact...</section>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-stack">
|
||||||
|
<section className="panel">
|
||||||
|
<h1>{artifact.title}</h1>
|
||||||
|
<p>Artifact ID: {artifact._id}</p>
|
||||||
|
<p>Type: {artifact.type}</p>
|
||||||
|
<pre className="mono-block">{JSON.stringify(artifact.payload, null, 2)}</pre>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function App(props: AppProps) {
|
||||||
|
const api = useMemo(() => new ApiClient(props.apiBaseUrl), [props.apiBaseUrl]);
|
||||||
|
const pathname = usePathname();
|
||||||
|
const [bootstrap, setBootstrap] = useState<BootstrapContext | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
setBootstrap(await api.bootstrapDev());
|
||||||
|
} catch (bootstrapError) {
|
||||||
|
setError(
|
||||||
|
bootstrapError instanceof Error ? bootstrapError.message : "Failed to bootstrap local context",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <section className="panel">{error}</section>;
|
||||||
|
}
|
||||||
|
if (!bootstrap) {
|
||||||
|
return <section className="panel">Bootstrapping local workspace...</section>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetMatch = pathname.match(/^\/assets\/([^/]+)$/);
|
||||||
|
const workflowMatch = pathname.match(/^\/workflows\/([^/]+)$/);
|
||||||
|
const runMatch = pathname.match(/^\/runs\/([^/]+)$/);
|
||||||
|
const exploreMatch = pathname.match(/^\/explore\/([^/]+)$/);
|
||||||
|
|
||||||
|
let active: NavItem = "Assets";
|
||||||
|
let content: React.ReactNode = <AssetsPage api={api} bootstrap={bootstrap} />;
|
||||||
|
|
||||||
|
if (pathname === "/workflows") {
|
||||||
|
active = "Workflows";
|
||||||
|
content = <WorkflowsPage api={api} bootstrap={bootstrap} />;
|
||||||
|
} else if (workflowMatch) {
|
||||||
|
active = "Workflows";
|
||||||
|
content = <WorkflowEditorPage api={api} workflowId={workflowMatch[1]} />;
|
||||||
|
} else if (pathname === "/runs") {
|
||||||
|
active = "Runs";
|
||||||
|
content = <RunsIndexPage />;
|
||||||
|
} else if (runMatch) {
|
||||||
|
active = "Runs";
|
||||||
|
content = <RunDetailPage api={api} runId={runMatch[1]} />;
|
||||||
|
} else if (exploreMatch) {
|
||||||
|
active = "Explore";
|
||||||
|
content = <ExplorePage api={api} artifactId={exploreMatch[1]} />;
|
||||||
|
} else if (pathname === "/explore") {
|
||||||
|
active = "Explore";
|
||||||
|
content = <ExplorePage api={api} artifactId={null} />;
|
||||||
|
} else if (assetMatch) {
|
||||||
|
active = "Assets";
|
||||||
|
content = <AssetDetailPage api={api} assetId={assetMatch[1]} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell
|
||||||
|
workspaceName={bootstrap.workspace.name}
|
||||||
|
projectName={bootstrap.project.name}
|
||||||
|
active={active}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
231
apps/web/src/styles.css
Normal file
231
apps/web/src/styles.css
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
:root {
|
||||||
|
color: #111827;
|
||||||
|
background: #f5f6f7;
|
||||||
|
font-family:
|
||||||
|
"SF Pro Text",
|
||||||
|
"Helvetica Neue",
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 240px 1fr;
|
||||||
|
grid-template-rows: 72px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 0 20px;
|
||||||
|
border-bottom: 1px solid #d1d5db;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header__group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header__pill {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header__label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar {
|
||||||
|
border-right: 1px solid #d1d5db;
|
||||||
|
background: linear-gradient(180deg, #fafaf9 0%, #f0fdf4 100%);
|
||||||
|
padding: 20px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar a {
|
||||||
|
display: block;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar a[data-active="true"] {
|
||||||
|
background: #111827;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-main {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-column {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 2fr) minmax(320px, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 280px minmax(0, 1fr) 320px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-card,
|
||||||
|
.task-card,
|
||||||
|
.node-card {
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #e5e7eb;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill[data-status="success"] {
|
||||||
|
background: #dcfce7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill[data-status="running"] {
|
||||||
|
background: #dbeafe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill[data-status="queued"],
|
||||||
|
.status-pill[data-status="pending"] {
|
||||||
|
background: #fef3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-grid label {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-grid input,
|
||||||
|
.field-grid textarea {
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-primary,
|
||||||
|
.button-secondary {
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-primary {
|
||||||
|
background: #111827;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-secondary {
|
||||||
|
background: #e5e7eb;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mono-block {
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
font-family: "SF Mono", Menlo, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1080px) {
|
||||||
|
.app-shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 72px auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-layout,
|
||||||
|
.two-column {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/web/vite.config.ts
Normal file
10
apps/web/vite.config.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: "0.0.0.0",
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -198,3 +198,13 @@ V1 runtime policy:
|
|||||||
## Deployment Direction
|
## Deployment Direction
|
||||||
|
|
||||||
V1 deployment target is a single public server using containerized application services. The architecture must still preserve future migration to multi-node environments.
|
V1 deployment target is a single public server using containerized application services. The architecture must still preserve future migration to multi-node environments.
|
||||||
|
|
||||||
|
## Current Runtime Implementation Notes
|
||||||
|
|
||||||
|
The current repository runtime now includes:
|
||||||
|
|
||||||
|
- a real HTTP API process backed by MongoDB
|
||||||
|
- a React and Vite web application that reads those APIs
|
||||||
|
- a local-path asset registration flow for development and dataset inspection
|
||||||
|
|
||||||
|
The repository still keeps some in-memory module tests for contract stability, but the executable local stack now runs through Mongo-backed runtime services.
|
||||||
|
|||||||
@ -261,7 +261,9 @@ Each task must emit:
|
|||||||
|
|
||||||
## Current V1 Implementation Notes
|
## Current V1 Implementation Notes
|
||||||
|
|
||||||
The current foundation implementation keeps the control plane in memory while stabilizing contracts for:
|
The current codebase keeps the low-level contract tests in memory while the executable local runtime persists workflow state to MongoDB.
|
||||||
|
|
||||||
|
The persisted local runtime now covers:
|
||||||
|
|
||||||
- workspace and project bootstrap
|
- workspace and project bootstrap
|
||||||
- asset registration and probe reporting
|
- asset registration and probe reporting
|
||||||
|
|||||||
@ -258,6 +258,17 @@ Use consistent object names in the UI:
|
|||||||
- Run
|
- Run
|
||||||
- Task
|
- Task
|
||||||
- Artifact
|
- Artifact
|
||||||
|
|
||||||
|
## Current Runtime Implementation Notes
|
||||||
|
|
||||||
|
The current local runtime now exposes these surfaces as a real React application:
|
||||||
|
|
||||||
|
- Assets list and asset detail
|
||||||
|
- Workflows list and workflow editor
|
||||||
|
- Run detail
|
||||||
|
- Explore artifact detail
|
||||||
|
|
||||||
|
The current implementation uses direct API-driven page loads and lightweight route handling instead of a deeper client-side state framework.
|
||||||
- Plugin
|
- Plugin
|
||||||
|
|
||||||
Do not rename the same concept differently across pages.
|
Do not rename the same concept differently across pages.
|
||||||
|
|||||||
@ -24,14 +24,15 @@ The database must support:
|
|||||||
|
|
||||||
## Current V1 Implementation Notes
|
## Current V1 Implementation Notes
|
||||||
|
|
||||||
The first code pass stabilizes these collection boundaries with in-memory services before full MongoDB persistence is wired through every module.
|
The first code pass stabilized these collection boundaries with in-memory services. The executable local runtime now persists the core objects below into MongoDB.
|
||||||
|
|
||||||
This means the implementation currently validates:
|
This means the implementation now validates:
|
||||||
|
|
||||||
- document shapes
|
- document shapes
|
||||||
- controller and service boundaries
|
- controller and service boundaries
|
||||||
- workflow/run/task separation
|
- workflow/run/task separation
|
||||||
- artifact lookup by producer
|
- artifact lookup by producer
|
||||||
|
- asset persistence and probe reports through Mongo-backed collections
|
||||||
|
|
||||||
while still targeting the collection model below as the persistent shape.
|
while still targeting the collection model below as the persistent shape.
|
||||||
|
|
||||||
|
|||||||
@ -73,6 +73,10 @@ make test
|
|||||||
make dev-api
|
make dev-api
|
||||||
make dev-web
|
make dev-web
|
||||||
make dev-worker
|
make dev-worker
|
||||||
|
make serve-api
|
||||||
|
make serve-web
|
||||||
|
make infra-up
|
||||||
|
make infra-down
|
||||||
make guardrails
|
make guardrails
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
- `2026-03-26`: Tasks 3 through 6 are implemented against in-memory V1 control-plane services so the API and worker contracts can stabilize before persistence and framework wiring are deepened.
|
- `2026-03-26`: Tasks 3 through 6 are implemented against in-memory V1 control-plane services so the API and worker contracts can stabilize before persistence and framework wiring are deepened.
|
||||||
- `2026-03-26`: Package-level verification continues to use the Node 22 built-in test runner with direct file targets such as `pnpm --filter api test test/projects.e2e-spec.ts` and `pnpm --filter worker test test/task-runner.spec.ts`.
|
- `2026-03-26`: Package-level verification continues to use the Node 22 built-in test runner with direct file targets such as `pnpm --filter api test test/projects.e2e-spec.ts` and `pnpm --filter worker test test/task-runner.spec.ts`.
|
||||||
- `2026-03-26`: Tasks 7 through 10 add the first web shell, workflow editor surfaces, artifact explore renderers, developer entry commands, and CI/pre-push test execution through `make test`.
|
- `2026-03-26`: Tasks 7 through 10 add the first web shell, workflow editor surfaces, artifact explore renderers, developer entry commands, and CI/pre-push test execution through `make test`.
|
||||||
|
- `2026-03-26`: The next runtime pass adds a Mongo-backed HTTP API, a real React and Vite web runtime, and local data validation against `/Users/longtaowu/workspace/emboldata/data`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
517
pnpm-lock.yaml
generated
517
pnpm-lock.yaml
generated
@ -14,7 +14,21 @@ importers:
|
|||||||
|
|
||||||
apps/api: {}
|
apps/api: {}
|
||||||
|
|
||||||
apps/web: {}
|
apps/web:
|
||||||
|
dependencies:
|
||||||
|
react:
|
||||||
|
specifier: ^19.2.4
|
||||||
|
version: 19.2.4
|
||||||
|
react-dom:
|
||||||
|
specifier: ^19.2.4
|
||||||
|
version: 19.2.4(react@19.2.4)
|
||||||
|
devDependencies:
|
||||||
|
'@vitejs/plugin-react':
|
||||||
|
specifier: ^6.0.1
|
||||||
|
version: 6.0.1(vite@8.0.3(esbuild@0.27.4)(tsx@4.21.0))
|
||||||
|
vite:
|
||||||
|
specifier: ^8.0.3
|
||||||
|
version: 8.0.3(esbuild@0.27.4)(tsx@4.21.0)
|
||||||
|
|
||||||
apps/worker: {}
|
apps/worker: {}
|
||||||
|
|
||||||
@ -22,6 +36,15 @@ importers:
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
'@emnapi/core@1.9.1':
|
||||||
|
resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==}
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.9.1':
|
||||||
|
resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==}
|
||||||
|
|
||||||
|
'@emnapi/wasi-threads@1.2.0':
|
||||||
|
resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.27.4':
|
'@esbuild/aix-ppc64@0.27.4':
|
||||||
resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==}
|
resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -178,11 +201,141 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@napi-rs/wasm-runtime@1.1.1':
|
||||||
|
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
|
||||||
|
|
||||||
|
'@oxc-project/types@0.122.0':
|
||||||
|
resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==}
|
||||||
|
|
||||||
|
'@rolldown/binding-android-arm64@1.0.0-rc.12':
|
||||||
|
resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@rolldown/binding-darwin-arm64@1.0.0-rc.12':
|
||||||
|
resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@rolldown/binding-darwin-x64@1.0.0-rc.12':
|
||||||
|
resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@rolldown/binding-freebsd-x64@1.0.0-rc.12':
|
||||||
|
resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12':
|
||||||
|
resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12':
|
||||||
|
resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.12':
|
||||||
|
resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12':
|
||||||
|
resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12':
|
||||||
|
resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.12':
|
||||||
|
resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-x64-musl@1.0.0-rc.12':
|
||||||
|
resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rolldown/binding-openharmony-arm64@1.0.0-rc.12':
|
||||||
|
resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openharmony]
|
||||||
|
|
||||||
|
'@rolldown/binding-wasm32-wasi@1.0.0-rc.12':
|
||||||
|
resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
cpu: [wasm32]
|
||||||
|
|
||||||
|
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12':
|
||||||
|
resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.12':
|
||||||
|
resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@rolldown/pluginutils@1.0.0-rc.12':
|
||||||
|
resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==}
|
||||||
|
|
||||||
|
'@rolldown/pluginutils@1.0.0-rc.7':
|
||||||
|
resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==}
|
||||||
|
|
||||||
|
'@tybys/wasm-util@0.10.1':
|
||||||
|
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||||
|
|
||||||
|
'@vitejs/plugin-react@6.0.1':
|
||||||
|
resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@rolldown/plugin-babel': ^0.1.7 || ^0.2.0
|
||||||
|
babel-plugin-react-compiler: ^1.0.0
|
||||||
|
vite: ^8.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@rolldown/plugin-babel':
|
||||||
|
optional: true
|
||||||
|
babel-plugin-react-compiler:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
detect-libc@2.1.2:
|
||||||
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
esbuild@0.27.4:
|
esbuild@0.27.4:
|
||||||
resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==}
|
resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
fdir@6.5.0:
|
||||||
|
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
picomatch: ^3 || ^4
|
||||||
|
peerDependenciesMeta:
|
||||||
|
picomatch:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
@ -191,16 +344,189 @@ packages:
|
|||||||
get-tsconfig@4.13.7:
|
get-tsconfig@4.13.7:
|
||||||
resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==}
|
resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==}
|
||||||
|
|
||||||
|
lightningcss-android-arm64@1.32.0:
|
||||||
|
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
lightningcss-darwin-arm64@1.32.0:
|
||||||
|
resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
lightningcss-darwin-x64@1.32.0:
|
||||||
|
resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
lightningcss-freebsd-x64@1.32.0:
|
||||||
|
resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
lightningcss-linux-arm-gnueabihf@1.32.0:
|
||||||
|
resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-gnu@1.32.0:
|
||||||
|
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-musl@1.32.0:
|
||||||
|
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-x64-gnu@1.32.0:
|
||||||
|
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-x64-musl@1.32.0:
|
||||||
|
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-win32-arm64-msvc@1.32.0:
|
||||||
|
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
lightningcss-win32-x64-msvc@1.32.0:
|
||||||
|
resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
lightningcss@1.32.0:
|
||||||
|
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
|
nanoid@3.3.11:
|
||||||
|
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||||
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
picocolors@1.1.1:
|
||||||
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
|
picomatch@4.0.4:
|
||||||
|
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
postcss@8.5.8:
|
||||||
|
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
|
||||||
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
react-dom@19.2.4:
|
||||||
|
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^19.2.4
|
||||||
|
|
||||||
|
react@19.2.4:
|
||||||
|
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
resolve-pkg-maps@1.0.0:
|
resolve-pkg-maps@1.0.0:
|
||||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||||
|
|
||||||
|
rolldown@1.0.0-rc.12:
|
||||||
|
resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
scheduler@0.27.0:
|
||||||
|
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||||
|
|
||||||
|
source-map-js@1.2.1:
|
||||||
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
tinyglobby@0.2.15:
|
||||||
|
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
tslib@2.8.1:
|
||||||
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
tsx@4.21.0:
|
tsx@4.21.0:
|
||||||
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
|
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
vite@8.0.3:
|
||||||
|
resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@types/node': ^20.19.0 || >=22.12.0
|
||||||
|
'@vitejs/devtools': ^0.1.0
|
||||||
|
esbuild: ^0.27.0
|
||||||
|
jiti: '>=1.21.0'
|
||||||
|
less: ^4.0.0
|
||||||
|
sass: ^1.70.0
|
||||||
|
sass-embedded: ^1.70.0
|
||||||
|
stylus: '>=0.54.8'
|
||||||
|
sugarss: ^5.0.0
|
||||||
|
terser: ^5.16.0
|
||||||
|
tsx: ^4.8.1
|
||||||
|
yaml: ^2.4.2
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
'@vitejs/devtools':
|
||||||
|
optional: true
|
||||||
|
esbuild:
|
||||||
|
optional: true
|
||||||
|
jiti:
|
||||||
|
optional: true
|
||||||
|
less:
|
||||||
|
optional: true
|
||||||
|
sass:
|
||||||
|
optional: true
|
||||||
|
sass-embedded:
|
||||||
|
optional: true
|
||||||
|
stylus:
|
||||||
|
optional: true
|
||||||
|
sugarss:
|
||||||
|
optional: true
|
||||||
|
terser:
|
||||||
|
optional: true
|
||||||
|
tsx:
|
||||||
|
optional: true
|
||||||
|
yaml:
|
||||||
|
optional: true
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
|
'@emnapi/core@1.9.1':
|
||||||
|
dependencies:
|
||||||
|
'@emnapi/wasi-threads': 1.2.0
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.9.1':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@emnapi/wasi-threads@1.2.0':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.27.4':
|
'@esbuild/aix-ppc64@0.27.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -279,6 +605,78 @@ snapshots:
|
|||||||
'@esbuild/win32-x64@0.27.4':
|
'@esbuild/win32-x64@0.27.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/wasm-runtime@1.1.1':
|
||||||
|
dependencies:
|
||||||
|
'@emnapi/core': 1.9.1
|
||||||
|
'@emnapi/runtime': 1.9.1
|
||||||
|
'@tybys/wasm-util': 0.10.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxc-project/types@0.122.0': {}
|
||||||
|
|
||||||
|
'@rolldown/binding-android-arm64@1.0.0-rc.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-darwin-arm64@1.0.0-rc.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-darwin-x64@1.0.0-rc.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-freebsd-x64@1.0.0-rc.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-x64-musl@1.0.0-rc.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-openharmony-arm64@1.0.0-rc.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-wasm32-wasi@1.0.0-rc.12':
|
||||||
|
dependencies:
|
||||||
|
'@napi-rs/wasm-runtime': 1.1.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/pluginutils@1.0.0-rc.12': {}
|
||||||
|
|
||||||
|
'@rolldown/pluginutils@1.0.0-rc.7': {}
|
||||||
|
|
||||||
|
'@tybys/wasm-util@0.10.1':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@vitejs/plugin-react@6.0.1(vite@8.0.3(esbuild@0.27.4)(tsx@4.21.0))':
|
||||||
|
dependencies:
|
||||||
|
'@rolldown/pluginutils': 1.0.0-rc.7
|
||||||
|
vite: 8.0.3(esbuild@0.27.4)(tsx@4.21.0)
|
||||||
|
|
||||||
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
esbuild@0.27.4:
|
esbuild@0.27.4:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@esbuild/aix-ppc64': 0.27.4
|
'@esbuild/aix-ppc64': 0.27.4
|
||||||
@ -308,6 +706,10 @@ snapshots:
|
|||||||
'@esbuild/win32-ia32': 0.27.4
|
'@esbuild/win32-ia32': 0.27.4
|
||||||
'@esbuild/win32-x64': 0.27.4
|
'@esbuild/win32-x64': 0.27.4
|
||||||
|
|
||||||
|
fdir@6.5.0(picomatch@4.0.4):
|
||||||
|
optionalDependencies:
|
||||||
|
picomatch: 4.0.4
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -315,11 +717,124 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
resolve-pkg-maps: 1.0.0
|
resolve-pkg-maps: 1.0.0
|
||||||
|
|
||||||
|
lightningcss-android-arm64@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-darwin-arm64@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-darwin-x64@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-freebsd-x64@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-arm-gnueabihf@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-gnu@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-musl@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-x64-gnu@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-x64-musl@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-win32-arm64-msvc@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-win32-x64-msvc@1.32.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss@1.32.0:
|
||||||
|
dependencies:
|
||||||
|
detect-libc: 2.1.2
|
||||||
|
optionalDependencies:
|
||||||
|
lightningcss-android-arm64: 1.32.0
|
||||||
|
lightningcss-darwin-arm64: 1.32.0
|
||||||
|
lightningcss-darwin-x64: 1.32.0
|
||||||
|
lightningcss-freebsd-x64: 1.32.0
|
||||||
|
lightningcss-linux-arm-gnueabihf: 1.32.0
|
||||||
|
lightningcss-linux-arm64-gnu: 1.32.0
|
||||||
|
lightningcss-linux-arm64-musl: 1.32.0
|
||||||
|
lightningcss-linux-x64-gnu: 1.32.0
|
||||||
|
lightningcss-linux-x64-musl: 1.32.0
|
||||||
|
lightningcss-win32-arm64-msvc: 1.32.0
|
||||||
|
lightningcss-win32-x64-msvc: 1.32.0
|
||||||
|
|
||||||
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
|
picomatch@4.0.4: {}
|
||||||
|
|
||||||
|
postcss@8.5.8:
|
||||||
|
dependencies:
|
||||||
|
nanoid: 3.3.11
|
||||||
|
picocolors: 1.1.1
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
react-dom@19.2.4(react@19.2.4):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.4
|
||||||
|
scheduler: 0.27.0
|
||||||
|
|
||||||
|
react@19.2.4: {}
|
||||||
|
|
||||||
resolve-pkg-maps@1.0.0: {}
|
resolve-pkg-maps@1.0.0: {}
|
||||||
|
|
||||||
|
rolldown@1.0.0-rc.12:
|
||||||
|
dependencies:
|
||||||
|
'@oxc-project/types': 0.122.0
|
||||||
|
'@rolldown/pluginutils': 1.0.0-rc.12
|
||||||
|
optionalDependencies:
|
||||||
|
'@rolldown/binding-android-arm64': 1.0.0-rc.12
|
||||||
|
'@rolldown/binding-darwin-arm64': 1.0.0-rc.12
|
||||||
|
'@rolldown/binding-darwin-x64': 1.0.0-rc.12
|
||||||
|
'@rolldown/binding-freebsd-x64': 1.0.0-rc.12
|
||||||
|
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12
|
||||||
|
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12
|
||||||
|
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12
|
||||||
|
'@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12
|
||||||
|
'@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12
|
||||||
|
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12
|
||||||
|
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.12
|
||||||
|
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.12
|
||||||
|
'@rolldown/binding-wasm32-wasi': 1.0.0-rc.12
|
||||||
|
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12
|
||||||
|
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12
|
||||||
|
|
||||||
|
scheduler@0.27.0: {}
|
||||||
|
|
||||||
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
tinyglobby@0.2.15:
|
||||||
|
dependencies:
|
||||||
|
fdir: 6.5.0(picomatch@4.0.4)
|
||||||
|
picomatch: 4.0.4
|
||||||
|
|
||||||
|
tslib@2.8.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
tsx@4.21.0:
|
tsx@4.21.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.4
|
esbuild: 0.27.4
|
||||||
get-tsconfig: 4.13.7
|
get-tsconfig: 4.13.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
vite@8.0.3(esbuild@0.27.4)(tsx@4.21.0):
|
||||||
|
dependencies:
|
||||||
|
lightningcss: 1.32.0
|
||||||
|
picomatch: 4.0.4
|
||||||
|
postcss: 8.5.8
|
||||||
|
rolldown: 1.0.0-rc.12
|
||||||
|
tinyglobby: 0.2.15
|
||||||
|
optionalDependencies:
|
||||||
|
esbuild: 0.27.4
|
||||||
|
fsevents: 2.3.3
|
||||||
|
tsx: 4.21.0
|
||||||
|
|||||||
@ -15,6 +15,10 @@ class DevCommandDocsTest(unittest.TestCase):
|
|||||||
"dev-api:",
|
"dev-api:",
|
||||||
"dev-web:",
|
"dev-web:",
|
||||||
"dev-worker:",
|
"dev-worker:",
|
||||||
|
"serve-api:",
|
||||||
|
"serve-web:",
|
||||||
|
"infra-up:",
|
||||||
|
"infra-down:",
|
||||||
"guardrails:",
|
"guardrails:",
|
||||||
):
|
):
|
||||||
with self.subTest(target=target):
|
with self.subTest(target=target):
|
||||||
@ -30,6 +34,9 @@ class DevCommandDocsTest(unittest.TestCase):
|
|||||||
"make dev-api",
|
"make dev-api",
|
||||||
"make dev-web",
|
"make dev-web",
|
||||||
"make dev-worker",
|
"make dev-worker",
|
||||||
|
"make infra-up",
|
||||||
|
"make serve-api",
|
||||||
|
"make serve-web",
|
||||||
):
|
):
|
||||||
with self.subTest(phrase=phrase):
|
with self.subTest(phrase=phrase):
|
||||||
self.assertIn(phrase, readme)
|
self.assertIn(phrase, readme)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user