#!/usr/bin/env bash # prep-consumer.sh — Pack @bytelyst/* packages as tarballs and rewrite # a consumer's package.json file: refs so builds are fully self-contained. # # This script lives in learning_ai_common_plat and can be called by any # consumer repo that uses file: refs to @bytelyst/* packages. # # Usage: # ../learning_ai_common_plat/scripts/prep-consumer.sh # ../learning_ai_common_plat/scripts/prep-consumer.sh --restore # # Prerequisites: # cd learning_ai_common_plat && pnpm build # # Example: # # From learning_voice_ai_agent: # ../learning_ai_common_plat/scripts/prep-consumer.sh user-dashboard-web # # # From learning_ai_clock: # ../learning_ai_common_plat/scripts/prep-consumer.sh web # # # Restore original package.json: # ../learning_ai_common_plat/scripts/prep-consumer.sh user-dashboard-web --restore set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" COMMON_PLAT="$(cd "$SCRIPT_DIR/.." && pwd)" if [ $# -lt 1 ]; then echo "Usage: $0 [--restore]" echo " target-dir: path to consumer directory containing package.json" exit 1 fi TARGET_DIR="$1" # Resolve relative paths if [[ ! "$TARGET_DIR" = /* ]]; then TARGET_DIR="$(pwd)/$TARGET_DIR" fi if [ ! -f "$TARGET_DIR/package.json" ]; then echo "❌ No package.json found at $TARGET_DIR" exit 1 fi # ── Restore mode ────────────────────────────────────────────── if [[ "${2:-}" == "--restore" ]]; then BACKUP="$TARGET_DIR/package.json.bak" if [ -f "$BACKUP" ]; then mv "$BACKUP" "$TARGET_DIR/package.json" rm -rf "$TARGET_DIR/.docker-deps" echo "✅ Restored $TARGET_DIR/package.json" else echo "⚠️ No backup found at $BACKUP" fi exit 0 fi # ── Validate packages are built ────────────────────────────── if [ ! -d "$COMMON_PLAT/packages" ]; then echo "❌ Cannot find packages/ in $COMMON_PLAT" exit 1 fi # ── Pack + rewrite ──────────────────────────────────────────── DEPS_DIR="$TARGET_DIR/.docker-deps" PACK_SRC_DIR="$DEPS_DIR/.pack-src" PLAN_FILE="$DEPS_DIR/.pack-plan.json" rm -rf "$DEPS_DIR" mkdir -p "$DEPS_DIR" # Back up original package.json cp "$TARGET_DIR/package.json" "$TARGET_DIR/package.json.bak" DIRNAME="$(basename "$TARGET_DIR")" echo "📦 Prepping $DIRNAME..." # Find all @bytelyst/* file: refs in package.json PKGS=$(grep -oE '"@bytelyst/[^"]+": *"file:[^"]+"' "$TARGET_DIR/package.json" | grep -oE '@bytelyst/[^"]+' || true) if [ -z "$PKGS" ]; then echo " ℹ️ No @bytelyst/* file: refs found — nothing to do" rm -f "$TARGET_DIR/package.json.bak" rm -rf "$DEPS_DIR" exit 0 fi node - "$TARGET_DIR/package.json" "$COMMON_PLAT" "$PLAN_FILE" <<'NODE' const fs = require('fs'); const path = require('path'); const consumerPath = process.argv[2]; const commonPlat = process.argv[3]; const planPath = process.argv[4]; const packageRoot = path.join(commonPlat, 'packages'); const consumer = JSON.parse(fs.readFileSync(consumerPath, 'utf8')); const directEntries = []; for (const depType of ['dependencies', 'devDependencies']) { for (const [name, version] of Object.entries(consumer[depType] ?? {})) { if (name.startsWith('@bytelyst/') && typeof version === 'string' && version.startsWith('file:')) { directEntries.push({ name, depType }); } } } const queue = [...new Set(directEntries.map((entry) => entry.name))]; const closure = new Set(queue); const versions = {}; while (queue.length > 0) { const scopedName = queue.shift(); const shortName = scopedName.replace('@bytelyst/', ''); const pkgPath = path.join(packageRoot, shortName, 'package.json'); if (!fs.existsSync(pkgPath)) { throw new Error(`Package ${scopedName} not found at ${pkgPath}`); } const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); versions[scopedName] = pkg.version; for (const depField of ['dependencies', 'optionalDependencies', 'peerDependencies']) { for (const depName of Object.keys(pkg[depField] ?? {})) { if (!depName.startsWith('@bytelyst/')) { continue; } if (!closure.has(depName)) { closure.add(depName); queue.push(depName); } } } } fs.writeFileSync( planPath, JSON.stringify( { directEntries, packages: [...closure], versions, }, null, 2, ) + '\n', ); NODE mkdir -p "$PACK_SRC_DIR" TARGET_BASENAME="$(basename "$TARGET_DIR")" COUNT=0 while IFS= read -r scoped_name; do pkg_short="${scoped_name#@bytelyst/}" PKG_DIR="$COMMON_PLAT/packages/$pkg_short" TMP_PKG_DIR="$PACK_SRC_DIR/$pkg_short" if [ ! -d "$PKG_DIR" ]; then echo " ⚠️ Package $pkg_short not found in $COMMON_PLAT/packages/" continue fi if [ ! -d "$PKG_DIR/dist" ]; then echo " ⚠️ $pkg_short has no dist/ — run 'pnpm build' in common plat first" continue fi rm -rf "$TMP_PKG_DIR" cp -R "$PKG_DIR" "$TMP_PKG_DIR" rm -rf "$TMP_PKG_DIR/node_modules" node - "$TMP_PKG_DIR/package.json" "$PLAN_FILE" "$TARGET_BASENAME" <<'NODE' const fs = require('fs'); const packageJsonPath = process.argv[2]; const planPath = process.argv[3]; const targetBasename = process.argv[4]; const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); const plan = JSON.parse(fs.readFileSync(planPath, 'utf8')); const tarballRefFor = (scopedName) => `file:/app/${targetBasename}/.docker-deps/${scopedName.replace('@bytelyst/', 'bytelyst-')}-${plan.versions[scopedName]}.tgz`; for (const depField of ['dependencies', 'optionalDependencies', 'peerDependencies']) { for (const [depName, depVersion] of Object.entries(pkg[depField] ?? {})) { if (!depName.startsWith('@bytelyst/')) { continue; } if (plan.versions[depName]) { pkg[depField][depName] = tarballRefFor(depName); } } } fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n'); NODE TARBALL=$(cd "$TMP_PKG_DIR" && npm pack --pack-destination "$DEPS_DIR" 2>/dev/null | tail -1) echo " 📦 $pkg_short → $TARBALL" COUNT=$((COUNT + 1)) done < <(node -e "const fs=require('fs'); const plan=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); for (const name of plan.packages) console.log(name);" "$PLAN_FILE") node - "$TARGET_DIR/package.json" "$PLAN_FILE" <<'NODE' const fs = require('fs'); const consumerPath = process.argv[2]; const planPath = process.argv[3]; const consumer = JSON.parse(fs.readFileSync(consumerPath, 'utf8')); const plan = JSON.parse(fs.readFileSync(planPath, 'utf8')); const tarballRefFor = (scopedName) => `file:.docker-deps/${scopedName.replace('@bytelyst/', 'bytelyst-')}-${plan.versions[scopedName]}.tgz`; for (const depType of ['dependencies', 'devDependencies']) { for (const [name, version] of Object.entries(consumer[depType] ?? {})) { if (name.startsWith('@bytelyst/') && typeof version === 'string' && version.startsWith('file:') && plan.versions[name]) { consumer[depType][name] = tarballRefFor(name); } } } consumer.dependencies ??= {}; for (const scopedName of plan.packages) { const alreadyDeclared = (consumer.dependencies && consumer.dependencies[scopedName]) || (consumer.devDependencies && consumer.devDependencies[scopedName]); if (!alreadyDeclared) { consumer.dependencies[scopedName] = tarballRefFor(scopedName); } } fs.writeFileSync(consumerPath, JSON.stringify(consumer, null, 2) + '\n'); NODE rm -rf "$PACK_SRC_DIR" "$PLAN_FILE" echo " ✅ $DIRNAME ready ($COUNT tarballs in .docker-deps/)" echo "" echo " Restore with: $0 $1 --restore"