feat(planning): ship holiday-aware planning and assistant upgrades
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { URL } from "node:url";
|
||||
import { loadWorkspaceEnv, resolveWorkspaceEnvPath } from "./load-env.mjs";
|
||||
|
||||
const envPath = loadWorkspaceEnv();
|
||||
const expectedDatabase = process.argv[2] ?? "capakraken";
|
||||
const rawUrl = process.env.DATABASE_URL;
|
||||
|
||||
if (!rawUrl) {
|
||||
console.error(`DATABASE_URL is not configured. Expected it from ${envPath ?? resolveWorkspaceEnvPath()}.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(rawUrl);
|
||||
} catch (error) {
|
||||
console.error(`DATABASE_URL is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const databaseName = parsed.pathname.replace(/^\/+/, "");
|
||||
const target = `${parsed.protocol}//${decodeURIComponent(parsed.username)}@${parsed.hostname}${parsed.port ? `:${parsed.port}` : ""}/${databaseName}`;
|
||||
|
||||
if (!databaseName) {
|
||||
console.error(`DATABASE_URL does not contain a database name. Target=${target}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (databaseName !== expectedDatabase) {
|
||||
console.error(`Unexpected database target '${databaseName}'. Expected '${expectedDatabase}'. Target=${target}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (databaseName === "planarchy") {
|
||||
console.error(`Refusing to continue with deprecated database '${databaseName}'. Target=${target}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`DB target OK: ${target}`);
|
||||
@@ -2,17 +2,16 @@
|
||||
# Remove SUPERUSER from the application database user
|
||||
# Run after initial setup: bash scripts/harden-postgres.sh
|
||||
|
||||
CONTAINER="planarchy-postgres-1" # Note: container name may still use old naming
|
||||
DB_USER="capakraken"
|
||||
DB_NAME="capakraken"
|
||||
|
||||
echo "Hardening PostgreSQL for $DB_USER..."
|
||||
|
||||
# Remove SUPERUSER privilege
|
||||
docker exec $CONTAINER psql -U postgres -c "ALTER USER $DB_USER NOSUPERUSER;"
|
||||
docker compose exec -T postgres psql -U postgres -c "ALTER USER $DB_USER NOSUPERUSER;"
|
||||
|
||||
# Grant only needed permissions
|
||||
docker exec $CONTAINER psql -U postgres -d $DB_NAME -c "
|
||||
docker compose exec -T postgres psql -U postgres -d $DB_NAME -c "
|
||||
GRANT CONNECT ON DATABASE $DB_NAME TO $DB_USER;
|
||||
GRANT USAGE ON SCHEMA public TO $DB_USER;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO $DB_USER;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
function resolveWorkspaceRoot() {
|
||||
return resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
}
|
||||
|
||||
export function resolveWorkspaceEnvPath() {
|
||||
return resolve(resolveWorkspaceRoot(), ".env");
|
||||
}
|
||||
|
||||
export function loadWorkspaceEnv() {
|
||||
const envPath = resolveWorkspaceEnvPath();
|
||||
if (!existsSync(envPath)) {
|
||||
return envPath;
|
||||
}
|
||||
|
||||
const contents = readFileSync(envPath, "utf8");
|
||||
for (const rawLine of contents.split(/\r?\n/u)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const separatorIndex = line.indexOf("=");
|
||||
if (separatorIndex <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = line.slice(0, separatorIndex).trim();
|
||||
const rawValue = line.slice(separatorIndex + 1).trim();
|
||||
const quoted =
|
||||
(rawValue.startsWith("\"") && rawValue.endsWith("\""))
|
||||
|| (rawValue.startsWith("'") && rawValue.endsWith("'"));
|
||||
const value = quoted ? rawValue.slice(1, -1) : rawValue;
|
||||
|
||||
if (process.env[key] == null) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return envPath;
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "Restarting Planarchy..."
|
||||
echo "Restarting CapaKraken..."
|
||||
echo ""
|
||||
|
||||
# Stop
|
||||
|
||||
+9
-18
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "Starting Planarchy..."
|
||||
echo "Starting CapaKraken..."
|
||||
|
||||
# 1. Start Docker services
|
||||
echo " Starting PostgreSQL + Redis..."
|
||||
@@ -12,40 +12,31 @@ sleep 2
|
||||
# 2. Wait for PostgreSQL to be healthy
|
||||
echo " Waiting for PostgreSQL..."
|
||||
for i in {1..30}; do
|
||||
if docker exec capakraken-postgres-1 pg_isready -U capakraken -q 2>/dev/null; then
|
||||
if docker compose exec -T postgres pg_isready -U capakraken -d capakraken -q 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# 3. Regenerate Prisma client
|
||||
echo " Generating Prisma client..."
|
||||
pnpm --filter @capakraken/db exec prisma generate --no-hints 2>/dev/null
|
||||
# 3. Start the web app in Docker for a stable lifecycle
|
||||
echo " Starting app container on port 3100..."
|
||||
docker compose --profile full up -d app
|
||||
|
||||
# 4. Clear stale Next.js cache
|
||||
rm -rf apps/web/.next
|
||||
|
||||
# 5. Start Next.js dev server
|
||||
echo " Starting Next.js on port 3100..."
|
||||
nohup pnpm --filter @capakraken/web dev > /tmp/capakraken-dev.log 2>&1 &
|
||||
echo $! > /tmp/capakraken-dev.pid
|
||||
|
||||
# 6. Wait for server to be ready
|
||||
# 4. Wait for server to be ready
|
||||
echo " Waiting for server..."
|
||||
for i in {1..30}; do
|
||||
if curl -sf http://localhost:3100/api/health > /dev/null 2>&1; then
|
||||
echo ""
|
||||
echo "Planarchy is running!"
|
||||
echo "CapaKraken is running!"
|
||||
curl -s http://localhost:3100/api/ready | python3 -m json.tool 2>/dev/null || curl -s http://localhost:3100/api/ready
|
||||
echo ""
|
||||
echo " URL: http://localhost:3100"
|
||||
echo " Logs: tail -f /tmp/capakraken-dev.log"
|
||||
echo " PID: $(cat /tmp/capakraken-dev.pid)"
|
||||
echo " Logs: docker logs -f capakraken-app-1"
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "ERROR: Server failed to start within 30 seconds"
|
||||
echo "Check logs: tail -50 /tmp/capakraken-dev.log"
|
||||
echo "Check logs: docker logs --tail 100 capakraken-app-1"
|
||||
exit 1
|
||||
|
||||
+5
-5
@@ -2,9 +2,9 @@
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "Stopping Planarchy..."
|
||||
echo "Stopping CapaKraken..."
|
||||
|
||||
# 1. Stop Next.js dev server
|
||||
# 1. Stop any legacy local dev server
|
||||
if [ -f /tmp/capakraken-dev.pid ]; then
|
||||
PID=$(cat /tmp/capakraken-dev.pid)
|
||||
if kill -0 "$PID" 2>/dev/null; then
|
||||
@@ -20,8 +20,8 @@ fi
|
||||
fuser -k 3100/tcp 2>/dev/null || true
|
||||
|
||||
# 2. Stop Docker services (keep data volumes)
|
||||
echo " Stopping PostgreSQL + Redis..."
|
||||
docker compose stop postgres redis 2>/dev/null || true
|
||||
echo " Stopping app, PostgreSQL and Redis..."
|
||||
docker compose --profile full stop app postgres redis 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "Planarchy stopped."
|
||||
echo "CapaKraken stopped."
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { loadWorkspaceEnv } from "./load-env.mjs";
|
||||
|
||||
loadWorkspaceEnv();
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error("Usage: node scripts/with-env.mjs <command> [args...]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const result = spawnSync(args[0], args.slice(1), {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error(result.error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit(result.status ?? 1);
|
||||
|
||||
Reference in New Issue
Block a user