mirror of
https://github.com/different-ai/openwork
synced 2026-04-26 01:25:10 +02:00
feat(share): add local docker publisher flow
This commit is contained in:
@@ -4,7 +4,10 @@ export type PublishBundleResult = {
|
|||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_OPENWORK_PUBLISHER_BASE_URL = "https://share.openwork.software";
|
const ENV_OPENWORK_PUBLISHER_BASE_URL = String(import.meta.env.VITE_OPENWORK_PUBLISHER_BASE_URL ?? "").trim();
|
||||||
|
|
||||||
|
export const DEFAULT_OPENWORK_PUBLISHER_BASE_URL =
|
||||||
|
ENV_OPENWORK_PUBLISHER_BASE_URL || "https://share.openwork.software";
|
||||||
|
|
||||||
function normalizeBaseUrl(input: string): string {
|
function normalizeBaseUrl(input: string): string {
|
||||||
const trimmed = String(input ?? "").trim();
|
const trimmed = String(input ?? "").trim();
|
||||||
|
|||||||
@@ -33,10 +33,13 @@ Optional env vars (via `.env` or `export`):
|
|||||||
- `OPENWORK_WORKSPACE` — host path to mount as workspace
|
- `OPENWORK_WORKSPACE` — host path to mount as workspace
|
||||||
- `OPENWORK_PORT` — host port to map to container :8787
|
- `OPENWORK_PORT` — host port to map to container :8787
|
||||||
- `WEB_PORT` — host port to map to container :5173
|
- `WEB_PORT` — host port to map to container :5173
|
||||||
|
- `SHARE_PORT` — host port to map to the local share service :3000
|
||||||
- `OPENWORK_DOCKER_DEV_MOUNT_HOST_OPENCODE=1` — import host OpenCode config/auth into the isolated dev state
|
- `OPENWORK_DOCKER_DEV_MOUNT_HOST_OPENCODE=1` — import host OpenCode config/auth into the isolated dev state
|
||||||
- `OPENWORK_OPENCODE_CONFIG_DIR` — override the host OpenCode config source used for that optional import
|
- `OPENWORK_OPENCODE_CONFIG_DIR` — override the host OpenCode config source used for that optional import
|
||||||
- `OPENWORK_OPENCODE_DATA_DIR` — override the host OpenCode data source used for that optional import
|
- `OPENWORK_OPENCODE_DATA_DIR` — override the host OpenCode data source used for that optional import
|
||||||
|
|
||||||
|
The dev stack also starts the local share service automatically and points the OpenWork app at it, so share-link flows publish to a local service instead of `https://share.openwork.software`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Den local stack (Docker)
|
## Den local stack (Docker)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ set -euo pipefail
|
|||||||
# Outputs:
|
# Outputs:
|
||||||
# - Web UI URL
|
# - Web UI URL
|
||||||
# - OpenWork server URL
|
# - OpenWork server URL
|
||||||
|
# - Share service URL
|
||||||
# - Token file path
|
# - Token file path
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
@@ -140,10 +141,15 @@ WEB_PORT="$(pick_port)"
|
|||||||
if [ "$WEB_PORT" = "$OPENWORK_PORT" ]; then
|
if [ "$WEB_PORT" = "$OPENWORK_PORT" ]; then
|
||||||
WEB_PORT="$(pick_port)"
|
WEB_PORT="$(pick_port)"
|
||||||
fi
|
fi
|
||||||
|
SHARE_PORT="${SHARE_PORT:-$(pick_port)}"
|
||||||
|
if [ "$SHARE_PORT" = "$OPENWORK_PORT" ] || [ "$SHARE_PORT" = "$WEB_PORT" ]; then
|
||||||
|
SHARE_PORT="$(pick_port)"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Starting Docker Compose project: $PROJECT" >&2
|
echo "Starting Docker Compose project: $PROJECT" >&2
|
||||||
echo "- OPENWORK_PORT=$OPENWORK_PORT" >&2
|
echo "- OPENWORK_PORT=$OPENWORK_PORT" >&2
|
||||||
echo "- WEB_PORT=$WEB_PORT" >&2
|
echo "- WEB_PORT=$WEB_PORT" >&2
|
||||||
|
echo "- SHARE_PORT=$SHARE_PORT" >&2
|
||||||
echo "- OPENWORK_DEV_MODE=1" >&2
|
echo "- OPENWORK_DEV_MODE=1" >&2
|
||||||
if [ "$MOUNT_HOST_OPENCODE" = "1" ]; then
|
if [ "$MOUNT_HOST_OPENCODE" = "1" ]; then
|
||||||
echo "- Host OpenCode import: enabled" >&2
|
echo "- Host OpenCode import: enabled" >&2
|
||||||
@@ -154,7 +160,7 @@ fi
|
|||||||
start_stack() {
|
start_stack() {
|
||||||
local config_dir="$1"
|
local config_dir="$1"
|
||||||
local data_dir="$2"
|
local data_dir="$2"
|
||||||
OPENWORK_DEV_ID="$DEV_ID" OPENWORK_PORT="$OPENWORK_PORT" WEB_PORT="$WEB_PORT" \
|
OPENWORK_DEV_ID="$DEV_ID" OPENWORK_PORT="$OPENWORK_PORT" WEB_PORT="$WEB_PORT" SHARE_PORT="$SHARE_PORT" \
|
||||||
OPENWORK_DEV_MODE="1" \
|
OPENWORK_DEV_MODE="1" \
|
||||||
OPENWORK_HOST_OPENCODE_CONFIG_DIR="$config_dir" \
|
OPENWORK_HOST_OPENCODE_CONFIG_DIR="$config_dir" \
|
||||||
OPENWORK_HOST_OPENCODE_DATA_DIR="$data_dir" \
|
OPENWORK_HOST_OPENCODE_DATA_DIR="$data_dir" \
|
||||||
@@ -184,6 +190,7 @@ fi
|
|||||||
echo "" >&2
|
echo "" >&2
|
||||||
echo "OpenWork web UI: http://localhost:$WEB_PORT" >&2
|
echo "OpenWork web UI: http://localhost:$WEB_PORT" >&2
|
||||||
echo "OpenWork server: http://localhost:$OPENWORK_PORT" >&2
|
echo "OpenWork server: http://localhost:$OPENWORK_PORT" >&2
|
||||||
|
echo "Share service: http://localhost:$SHARE_PORT" >&2
|
||||||
echo "Token file: $ROOT_DIR/tmp/.dev-env-$DEV_ID" >&2
|
echo "Token file: $ROOT_DIR/tmp/.dev-env-$DEV_ID" >&2
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
echo "To stop this stack:" >&2
|
echo "To stop this stack:" >&2
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
# OPENWORK_WORKSPACE — host path to mount as workspace (default: ./workspace)
|
# OPENWORK_WORKSPACE — host path to mount as workspace (default: ./workspace)
|
||||||
# OPENWORK_PORT — host port to map to container :8787 (default: 8787)
|
# OPENWORK_PORT — host port to map to container :8787 (default: 8787)
|
||||||
# WEB_PORT — host port to map to container :5173 (default: 5173)
|
# WEB_PORT — host port to map to container :5173 (default: 5173)
|
||||||
|
# SHARE_PORT — host port to map to the share service :3000 (default: 3006)
|
||||||
# OPENWORK_DEV_ID — unique ID for this stack (default: default)
|
# OPENWORK_DEV_ID — unique ID for this stack (default: default)
|
||||||
# OPENWORK_DEV_MODE — enables isolated OpenCode dev state (set by dev-up.sh)
|
# OPENWORK_DEV_MODE — enables isolated OpenCode dev state (set by dev-up.sh)
|
||||||
# OPENWORK_DOCKER_DEV_MOUNT_HOST_OPENCODE=1 — import host OpenCode config/auth into the isolated dev state
|
# OPENWORK_DOCKER_DEV_MOUNT_HOST_OPENCODE=1 — import host OpenCode config/auth into the isolated dev state
|
||||||
@@ -151,6 +152,8 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
orchestrator:
|
orchestrator:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
share:
|
||||||
|
condition: service_healthy
|
||||||
entrypoint: ["/bin/sh", "-c"]
|
entrypoint: ["/bin/sh", "-c"]
|
||||||
command:
|
command:
|
||||||
- |
|
- |
|
||||||
@@ -184,6 +187,7 @@ services:
|
|||||||
|
|
||||||
export VITE_OPENWORK_URL="http://localhost:${OPENWORK_PORT:-8787}"
|
export VITE_OPENWORK_URL="http://localhost:${OPENWORK_PORT:-8787}"
|
||||||
export VITE_OPENWORK_PORT="${OPENWORK_PORT:-8787}"
|
export VITE_OPENWORK_PORT="${OPENWORK_PORT:-8787}"
|
||||||
|
export VITE_OPENWORK_PUBLISHER_BASE_URL="http://localhost:${SHARE_PORT:-3006}"
|
||||||
export VITE_ALLOWED_HOSTS="all"
|
export VITE_ALLOWED_HOSTS="all"
|
||||||
export HOST="0.0.0.0"
|
export HOST="0.0.0.0"
|
||||||
export PORT="5173"
|
export PORT="5173"
|
||||||
@@ -197,6 +201,49 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
OPENWORK_DEV_ID: ${OPENWORK_DEV_ID:-default}
|
OPENWORK_DEV_ID: ${OPENWORK_DEV_ID:-default}
|
||||||
|
|
||||||
|
share:
|
||||||
|
<<: *shared
|
||||||
|
entrypoint: ["/bin/sh", "-c"]
|
||||||
|
command:
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
apt-get update -qq && apt-get install -y -qq --no-install-recommends \
|
||||||
|
curl ca-certificates >/dev/null 2>&1
|
||||||
|
|
||||||
|
corepack enable && corepack prepare pnpm@10.27.0 --activate
|
||||||
|
|
||||||
|
echo "[share] Installing dependencies..."
|
||||||
|
pnpm install --no-frozen-lockfile --network-concurrency 1 --child-concurrency 1
|
||||||
|
|
||||||
|
mkdir -p /app/tmp/share-service-blobs
|
||||||
|
|
||||||
|
echo "[share] Building Next app..."
|
||||||
|
pnpm --dir services/openwork-share build
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================"
|
||||||
|
echo " OpenWork share service"
|
||||||
|
echo " URL: http://localhost:${SHARE_PORT:-3006}"
|
||||||
|
echo "============================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
exec pnpm --dir services/openwork-share exec next start --hostname 0.0.0.0 --port 3000
|
||||||
|
ports:
|
||||||
|
- "${SHARE_PORT:-3006}:3000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -sf http://localhost:3000/api/health || exit 1"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 30
|
||||||
|
start_period: 180s
|
||||||
|
environment:
|
||||||
|
CI: "true"
|
||||||
|
OPENWORK_DEV_MODE: ${OPENWORK_DEV_MODE:-1}
|
||||||
|
LOCAL_BLOB_DIR: /app/tmp/share-service-blobs
|
||||||
|
PUBLIC_BASE_URL: http://localhost:${SHARE_PORT:-3006}
|
||||||
|
PUBLIC_OPENWORK_APP_URL: http://localhost:${WEB_PORT:-5173}
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pnpm-store:
|
pnpm-store:
|
||||||
name: openwork-dev-pnpm-store
|
name: openwork-dev-pnpm-store
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ The packager rejects files that appear to contain secrets in shareable config.
|
|||||||
- Default: `https://app.openwork.software`
|
- Default: `https://app.openwork.software`
|
||||||
- Target app URL for the Open in app action on bundle pages.
|
- Target app URL for the Open in app action on bundle pages.
|
||||||
|
|
||||||
|
- `LOCAL_BLOB_DIR`
|
||||||
|
- Optional local filesystem storage root for bundle JSON.
|
||||||
|
- When `BLOB_READ_WRITE_TOKEN` is unset in local/dev mode, the service falls back to local file storage automatically.
|
||||||
|
|
||||||
## Local development
|
## Local development
|
||||||
|
|
||||||
For local testing you can use:
|
For local testing you can use:
|
||||||
@@ -83,6 +87,8 @@ pnpm --dir services/openwork-share dev
|
|||||||
|
|
||||||
Open `http://localhost:3000`.
|
Open `http://localhost:3000`.
|
||||||
|
|
||||||
|
Without a `BLOB_READ_WRITE_TOKEN`, local development now stores bundles on disk in a local dev blob directory so publishing works out of the box.
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
Recommended project settings:
|
Recommended project settings:
|
||||||
|
|||||||
@@ -1,11 +1,79 @@
|
|||||||
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
import { head, put } from "@vercel/blob";
|
import { head, put } from "@vercel/blob";
|
||||||
import { ulid } from "ulid";
|
import { ulid } from "ulid";
|
||||||
|
|
||||||
import type { FetchBundleResult, StoreBundleResult } from "./types.ts";
|
import type { FetchBundleResult, StoreBundleResult } from "./types.ts";
|
||||||
|
|
||||||
export async function storeBundleJson(rawJson: string): Promise<StoreBundleResult> {
|
function resolveLocalBlobDir(): string | null {
|
||||||
|
const explicitDir = String(process.env.LOCAL_BLOB_DIR ?? "").trim();
|
||||||
|
if (explicitDir) {
|
||||||
|
return explicitDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(process.env.BLOB_READ_WRITE_TOKEN ?? "").trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDevLike =
|
||||||
|
String(process.env.OPENWORK_DEV_MODE ?? "") === "1" ||
|
||||||
|
String(process.env.NODE_ENV ?? "").trim() !== "production";
|
||||||
|
|
||||||
|
if (!isDevLike) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.resolve(process.cwd(), ".openwork-share-blobs");
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveBundlePathname(id: string): string {
|
||||||
|
return `bundles/${id}.json`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function storeBundleJsonLocally(rawJson: string): Promise<StoreBundleResult> {
|
||||||
const id = ulid();
|
const id = ulid();
|
||||||
const pathname = `bundles/${id}.json`;
|
const pathname = resolveBundlePathname(id);
|
||||||
|
const localBlobDir = resolveLocalBlobDir();
|
||||||
|
|
||||||
|
if (!localBlobDir) {
|
||||||
|
throw new Error("Local blob storage is not configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPath = path.join(localBlobDir, pathname);
|
||||||
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
||||||
|
await writeFile(targetPath, Buffer.from(String(rawJson), "utf8"));
|
||||||
|
|
||||||
|
return { id, pathname };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchBundleJsonLocally(id: string): Promise<FetchBundleResult> {
|
||||||
|
const pathname = resolveBundlePathname(id);
|
||||||
|
const localBlobDir = resolveLocalBlobDir();
|
||||||
|
|
||||||
|
if (!localBlobDir) {
|
||||||
|
throw new Error("Local blob storage is not configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPath = path.join(localBlobDir, pathname);
|
||||||
|
const rawBuffer = await readFile(targetPath);
|
||||||
|
return {
|
||||||
|
blob: {
|
||||||
|
url: `file://${targetPath}`,
|
||||||
|
contentType: "application/json",
|
||||||
|
},
|
||||||
|
rawBuffer,
|
||||||
|
rawJson: rawBuffer.toString("utf8"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function storeBundleJson(rawJson: string): Promise<StoreBundleResult> {
|
||||||
|
if (resolveLocalBlobDir()) {
|
||||||
|
return storeBundleJsonLocally(rawJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = ulid();
|
||||||
|
const pathname = resolveBundlePathname(id);
|
||||||
const buffer = Buffer.from(String(rawJson), "utf8");
|
const buffer = Buffer.from(String(rawJson), "utf8");
|
||||||
|
|
||||||
await put(pathname, buffer, {
|
await put(pathname, buffer, {
|
||||||
@@ -18,7 +86,11 @@ export async function storeBundleJson(rawJson: string): Promise<StoreBundleResul
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchBundleJsonById(id: string): Promise<FetchBundleResult> {
|
export async function fetchBundleJsonById(id: string): Promise<FetchBundleResult> {
|
||||||
const pathname = `bundles/${id}.json`;
|
if (resolveLocalBlobDir()) {
|
||||||
|
return fetchBundleJsonLocally(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathname = resolveBundlePathname(id);
|
||||||
const blob = await head(pathname);
|
const blob = await head(pathname);
|
||||||
const response = await fetch(blob.url, { method: "GET" });
|
const response = await fetch(blob.url, { method: "GET" });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user