295 lines
8.0 KiB
Bash
Executable File
295 lines
8.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
SOURCE_REPO_ROOT="$(git -C "$SCRIPT_DIR/../.." rev-parse --show-toplevel)"
|
|
DEFAULT_TARGET="../cad-viewer"
|
|
TARGET_ARG="$DEFAULT_TARGET"
|
|
TARGET_SET=0
|
|
DRY_RUN=0
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage:
|
|
scripts/viewer/sync-cad-viewer-repo.sh [--dry-run] [target-relative-dir]
|
|
|
|
Copies the CAD Viewer source repo layout into a separate standalone cad-viewer
|
|
git checkout. The target path is resolved relative to this text-to-cad repo.
|
|
|
|
Default target:
|
|
../cad-viewer
|
|
|
|
What gets copied:
|
|
viewer/* -> target root
|
|
packages/cadjs/* -> target/packages/cadjs
|
|
packages/cadpy/* -> target/packages/cadpy
|
|
|
|
The target checkout must already exist and must be the root of a separate git
|
|
repo. Everything in the target root is deleted before copying, except .git.
|
|
|
|
Options:
|
|
--dry-run Print what would be overwritten without changing files.
|
|
-h, --help Show this help.
|
|
EOF
|
|
}
|
|
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--dry-run)
|
|
DRY_RUN=1
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
--)
|
|
shift
|
|
if [ "$#" -gt 0 ]; then
|
|
if [ "$TARGET_SET" -eq 1 ]; then
|
|
echo "Target path was provided more than once." >&2
|
|
usage >&2
|
|
exit 2
|
|
fi
|
|
TARGET_ARG="$1"
|
|
TARGET_SET=1
|
|
shift
|
|
fi
|
|
break
|
|
;;
|
|
-*)
|
|
echo "Unknown option: $1" >&2
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
*)
|
|
if [ "$TARGET_SET" -eq 1 ]; then
|
|
echo "Target path was provided more than once." >&2
|
|
usage >&2
|
|
exit 2
|
|
fi
|
|
TARGET_ARG="$1"
|
|
TARGET_SET=1
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [ "$#" -gt 0 ]; then
|
|
echo "Unexpected extra arguments: $*" >&2
|
|
usage >&2
|
|
exit 2
|
|
fi
|
|
|
|
case "$TARGET_ARG" in
|
|
/*)
|
|
echo "Target must be a relative path; got: $TARGET_ARG" >&2
|
|
exit 2
|
|
;;
|
|
"")
|
|
echo "Target path must not be empty." >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
require_command() {
|
|
if ! command -v "$1" >/dev/null 2>&1; then
|
|
echo "$1 is required." >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
require_path() {
|
|
local path_to_check="$1"
|
|
local label="$2"
|
|
if [ ! -e "$path_to_check" ]; then
|
|
echo "Missing $label: $path_to_check" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
require_command git
|
|
require_command node
|
|
require_command rsync
|
|
|
|
VIEWER_DIR="$SOURCE_REPO_ROOT/viewer"
|
|
CADJS_DIR="$SOURCE_REPO_ROOT/packages/cadjs"
|
|
CADPY_DIR="$SOURCE_REPO_ROOT/packages/cadpy"
|
|
TARGET_INPUT="$SOURCE_REPO_ROOT/$TARGET_ARG"
|
|
|
|
require_path "$VIEWER_DIR/package.json" "viewer package"
|
|
require_path "$CADJS_DIR/package.json" "cadjs package"
|
|
require_path "$CADPY_DIR/pyproject.toml" "cadpy package"
|
|
|
|
if [ ! -d "$TARGET_INPUT" ]; then
|
|
echo "Target directory does not exist: $TARGET_INPUT" >&2
|
|
echo "Create or clone the standalone cad-viewer repo first." >&2
|
|
exit 1
|
|
fi
|
|
|
|
TARGET_DIR="$(cd "$TARGET_INPUT" && pwd -P)"
|
|
SOURCE_REPO_ROOT="$(cd "$SOURCE_REPO_ROOT" && pwd -P)"
|
|
TARGET_GIT_ROOT="$(git -C "$TARGET_DIR" rev-parse --show-toplevel 2>/dev/null || true)"
|
|
|
|
if [ -z "$TARGET_GIT_ROOT" ]; then
|
|
echo "Target is not inside a git repository: $TARGET_DIR" >&2
|
|
exit 1
|
|
fi
|
|
|
|
TARGET_GIT_ROOT="$(cd "$TARGET_GIT_ROOT" && pwd -P)"
|
|
|
|
if [ "$TARGET_GIT_ROOT" != "$TARGET_DIR" ]; then
|
|
echo "Target must be the root of its own git repo." >&2
|
|
echo "Target: $TARGET_DIR" >&2
|
|
echo "Git root: $TARGET_GIT_ROOT" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$TARGET_DIR" = "$SOURCE_REPO_ROOT" ]; then
|
|
echo "Refusing to overwrite the source repo." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$TARGET_DIR" = "$VIEWER_DIR" ] || [ "$TARGET_DIR" = "$CADJS_DIR" ] || [ "$TARGET_DIR" = "$CADPY_DIR" ]; then
|
|
echo "Refusing to overwrite a source directory: $TARGET_DIR" >&2
|
|
exit 1
|
|
fi
|
|
|
|
RSYNC_EXCLUDES=(
|
|
--exclude node_modules
|
|
--exclude dist
|
|
--exclude dist-verify
|
|
--exclude .vite
|
|
--exclude .next
|
|
--exclude .next-build
|
|
--exclude .next-dev
|
|
--exclude .next-verify
|
|
--exclude .vercel
|
|
--exclude coverage
|
|
--exclude tmp
|
|
--exclude .pytest_cache
|
|
--exclude __pycache__
|
|
--exclude '*.pyc'
|
|
--exclude '*.pyo'
|
|
--exclude .DS_Store
|
|
--include '.env.example'
|
|
--include '.env.*.example'
|
|
--exclude '.env'
|
|
--exclude '.env.*'
|
|
)
|
|
|
|
echo "Source: $SOURCE_REPO_ROOT"
|
|
echo "Target: $TARGET_DIR"
|
|
echo ""
|
|
echo "This will delete everything in the target root except .git, then copy viewer/, packages/cadjs, and packages/cadpy."
|
|
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
echo ""
|
|
echo "Dry run only. Target entries that would be removed:"
|
|
find "$TARGET_DIR" -mindepth 1 -maxdepth 1 ! -name .git -print | sort
|
|
echo ""
|
|
echo "Dry run only. Copy plan:"
|
|
echo " $VIEWER_DIR/ -> $TARGET_DIR/"
|
|
echo " $CADJS_DIR/ -> $TARGET_DIR/packages/cadjs/"
|
|
echo " $CADPY_DIR/ -> $TARGET_DIR/packages/cadpy/"
|
|
exit 0
|
|
fi
|
|
|
|
find "$TARGET_DIR" -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} +
|
|
|
|
rsync -a "${RSYNC_EXCLUDES[@]}" "$VIEWER_DIR/" "$TARGET_DIR/"
|
|
mkdir -p "$TARGET_DIR/packages"
|
|
rsync -a "${RSYNC_EXCLUDES[@]}" "$CADJS_DIR/" "$TARGET_DIR/packages/cadjs/"
|
|
rsync -a "${RSYNC_EXCLUDES[@]}" "$CADPY_DIR/" "$TARGET_DIR/packages/cadpy/"
|
|
|
|
node --input-type=module - "$TARGET_DIR" <<'NODE'
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const targetRoot = process.argv[2];
|
|
|
|
function readJson(relativePath) {
|
|
return JSON.parse(fs.readFileSync(path.join(targetRoot, relativePath), "utf8"));
|
|
}
|
|
|
|
function writeJson(relativePath, value) {
|
|
fs.writeFileSync(path.join(targetRoot, relativePath), `${JSON.stringify(value, null, 2)}\n`);
|
|
}
|
|
|
|
function replaceText(relativePath, replacements) {
|
|
const filePath = path.join(targetRoot, relativePath);
|
|
if (!fs.existsSync(filePath)) {
|
|
return;
|
|
}
|
|
let text = fs.readFileSync(filePath, "utf8");
|
|
for (const [from, to] of replacements) {
|
|
text = text.split(from).join(to);
|
|
}
|
|
fs.writeFileSync(filePath, text);
|
|
}
|
|
|
|
const packageJson = readJson("package.json");
|
|
packageJson.dependencies = {
|
|
...packageJson.dependencies,
|
|
cadjs: "file:./packages/cadjs",
|
|
};
|
|
if (packageJson.scripts) {
|
|
delete packageJson.scripts["runtime:bundle"];
|
|
delete packageJson.scripts["runtime:check"];
|
|
}
|
|
writeJson("package.json", packageJson);
|
|
|
|
const lockPath = path.join(targetRoot, "package-lock.json");
|
|
if (fs.existsSync(lockPath)) {
|
|
const lock = readJson("package-lock.json");
|
|
const rootPackage = lock.packages?.[""];
|
|
if (rootPackage?.dependencies) {
|
|
rootPackage.dependencies.cadjs = "file:./packages/cadjs";
|
|
}
|
|
if (lock.packages?.["../packages/cadjs"]) {
|
|
lock.packages["packages/cadjs"] = lock.packages["../packages/cadjs"];
|
|
delete lock.packages["../packages/cadjs"];
|
|
}
|
|
if (lock.packages?.["node_modules/cadjs"]) {
|
|
lock.packages["node_modules/cadjs"].resolved = "packages/cadjs";
|
|
}
|
|
writeJson("package-lock.json", lock);
|
|
}
|
|
|
|
const pathReplacements = [
|
|
["file:../packages/cadjs", "file:./packages/cadjs"],
|
|
["../packages/cadjs", "packages/cadjs"],
|
|
["../packages/cadpy", "packages/cadpy"],
|
|
["../.venv/bin/python -m pip install -e packages/cadpy", "python -m pip install -e packages/cadpy"],
|
|
["npm --prefix viewer run upload:blob", "npm run upload:blob"],
|
|
["npm run build\nnpm run runtime:check", "npm run build"],
|
|
[
|
|
"\nWhen changing Viewer source that feeds the cad-viewer skill runtime, refresh the\ngenerated runtime from the repository root:\n\n```bash\nscripts/bundle/bundle-skill.sh cad-viewer\nscripts/bundle/bundle-skill.sh cad-viewer --check\n```\n",
|
|
"\n",
|
|
],
|
|
[
|
|
"The generated cad-viewer skill runtime\nbundles `cadpy` under `packages/cadpy` and does not need the repository\nroot.",
|
|
"The standalone checkout keeps `cadpy` under `packages/cadpy` so local\nregeneration can discover it without the source workbench.",
|
|
],
|
|
];
|
|
replaceText("README.md", pathReplacements);
|
|
replaceText("docs/backend.md", pathReplacements);
|
|
|
|
const gitignorePath = path.join(targetRoot, ".gitignore");
|
|
if (fs.existsSync(gitignorePath)) {
|
|
const gitignore = fs.readFileSync(gitignorePath, "utf8");
|
|
if (!/^\.venv$/mu.test(gitignore)) {
|
|
fs.writeFileSync(gitignorePath, `${gitignore.replace(/\s*$/u, "\n")}.venv\n`);
|
|
}
|
|
}
|
|
NODE
|
|
|
|
echo ""
|
|
echo "Synced standalone CAD Viewer checkout:"
|
|
echo " $TARGET_DIR"
|
|
echo ""
|
|
echo "Next steps:"
|
|
echo " cd \"$TARGET_DIR\""
|
|
echo " npm install"
|
|
echo " npm run test"
|
|
echo " npm run build"
|