✨ 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
|
||||
API_PORT=3001
|
||||
WORKER_PORT=3002
|
||||
API_HOST=127.0.0.1
|
||||
|
||||
MONGO_URI=mongodb://127.0.0.1:27017
|
||||
|
||||
MONGO_PORT=27017
|
||||
MONGO_DB=emboflow
|
||||
@ -15,3 +18,6 @@ MINIO_ROOT_USER=emboflow
|
||||
MINIO_ROOT_PASSWORD=emboflow123
|
||||
|
||||
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/
|
||||
.DS_Store
|
||||
dist/
|
||||
|
||||
21
Makefile
21
Makefile
@ -1,6 +1,6 @@
|
||||
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:
|
||||
pnpm install
|
||||
@ -10,6 +10,7 @@ test:
|
||||
python3 -m unittest discover -s tests -p 'test_*.py'
|
||||
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 build
|
||||
pnpm --filter worker test
|
||||
|
||||
dev-api:
|
||||
@ -21,6 +22,24 @@ dev-web:
|
||||
dev-worker:
|
||||
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:
|
||||
python3 scripts/check_doc_code_sync.py . --strict
|
||||
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
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
- `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.
|
||||
- `design/` contains the architecture and product design documents that must stay aligned with implementation.
|
||||
- `docs/` contains workflow guidance and the executable implementation plan.
|
||||
|
||||
@ -5,6 +5,12 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/main.ts",
|
||||
"start": "tsx src/main.ts",
|
||||
"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 { createWorkflowsModule } from "./modules/workflows/workflows.module.ts";
|
||||
import { createWorkspaceModule } from "./modules/workspaces/workspaces.module.ts";
|
||||
import { startApiServer } from "./runtime/server.ts";
|
||||
|
||||
export function createApiApp() {
|
||||
const auth = createAuthModule();
|
||||
@ -31,5 +32,6 @@ export function createApiApp() {
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
await startApiServer();
|
||||
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",
|
||||
"type": "module",
|
||||
"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"
|
||||
},
|
||||
"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() {
|
||||
return {
|
||||
status: "ready" as const,
|
||||
routes: createRouter(),
|
||||
};
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
process.stdout.write(JSON.stringify({ status: createWebApp().status }, null, 2));
|
||||
export function mountWebApp() {
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
- asset registration and probe reporting
|
||||
|
||||
@ -258,6 +258,17 @@ Use consistent object names in the UI:
|
||||
- Run
|
||||
- Task
|
||||
- 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
|
||||
|
||||
Do not rename the same concept differently across pages.
|
||||
|
||||
@ -24,14 +24,15 @@ The database must support:
|
||||
|
||||
## 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
|
||||
- controller and service boundaries
|
||||
- workflow/run/task separation
|
||||
- artifact lookup by producer
|
||||
- asset persistence and probe reports through Mongo-backed collections
|
||||
|
||||
while still targeting the collection model below as the persistent shape.
|
||||
|
||||
|
||||
@ -73,6 +73,10 @@ make test
|
||||
make dev-api
|
||||
make dev-web
|
||||
make dev-worker
|
||||
make serve-api
|
||||
make serve-web
|
||||
make infra-up
|
||||
make infra-down
|
||||
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`: 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`: 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/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: {}
|
||||
|
||||
@ -22,6 +36,15 @@ importers:
|
||||
|
||||
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':
|
||||
resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==}
|
||||
engines: {node: '>=18'}
|
||||
@ -178,11 +201,141 @@ packages:
|
||||
cpu: [x64]
|
||||
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:
|
||||
resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==}
|
||||
engines: {node: '>=18'}
|
||||
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:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@ -191,16 +344,189 @@ packages:
|
||||
get-tsconfig@4.13.7:
|
||||
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:
|
||||
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:
|
||||
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
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:
|
||||
|
||||
'@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':
|
||||
optional: true
|
||||
|
||||
@ -279,6 +605,78 @@ snapshots:
|
||||
'@esbuild/win32-x64@0.27.4':
|
||||
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:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.4
|
||||
@ -308,6 +706,10 @@ snapshots:
|
||||
'@esbuild/win32-ia32': 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:
|
||||
optional: true
|
||||
|
||||
@ -315,11 +717,124 @@ snapshots:
|
||||
dependencies:
|
||||
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: {}
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
esbuild: 0.27.4
|
||||
get-tsconfig: 4.13.7
|
||||
optionalDependencies:
|
||||
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-web:",
|
||||
"dev-worker:",
|
||||
"serve-api:",
|
||||
"serve-web:",
|
||||
"infra-up:",
|
||||
"infra-down:",
|
||||
"guardrails:",
|
||||
):
|
||||
with self.subTest(target=target):
|
||||
@ -30,6 +34,9 @@ class DevCommandDocsTest(unittest.TestCase):
|
||||
"make dev-api",
|
||||
"make dev-web",
|
||||
"make dev-worker",
|
||||
"make infra-up",
|
||||
"make serve-api",
|
||||
"make serve-web",
|
||||
):
|
||||
with self.subTest(phrase=phrase):
|
||||
self.assertIn(phrase, readme)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user