spaps 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI_TOOLS.json +10 -11
- package/README.md +267 -110
- package/assets/local-runtime/Dockerfile +28 -0
- package/assets/local-runtime/alembic/env.py +101 -0
- package/assets/local-runtime/alembic/path_bootstrap.py +71 -0
- package/assets/local-runtime/alembic/versions/000000000001_baseline_consolidated_schema.py +1076 -0
- package/assets/local-runtime/alembic/versions/000000000002_fix_column_types_to_match_prod.py +83 -0
- package/assets/local-runtime/alembic/versions/000000000003_fix_email_template_key_uniqueness.py +49 -0
- package/assets/local-runtime/alembic/versions/000000000004_add_hold_duration_minutes_to_dayrate_config.py +30 -0
- package/assets/local-runtime/alembic/versions/000000000005_resource_scoped_entitlements.py +77 -0
- package/assets/local-runtime/alembic/versions/000000000006_cfo_rbac_add_is_admin.py +37 -0
- package/assets/local-runtime/alembic/versions/000000000007_agent_approvals.py +158 -0
- package/assets/local-runtime/alembic/versions/000000000008_add_company_id_to_cfo_connections.py +35 -0
- package/assets/local-runtime/alembic/versions/000000000009_tx_signing.py +62 -0
- package/assets/local-runtime/alembic/versions/000000000010_affiliate_referrals.py +235 -0
- package/assets/local-runtime/alembic/versions/000000000011_checkin_call_booking.py +137 -0
- package/assets/local-runtime/alembic/versions/000000000012_subscription_application_scoping.py +55 -0
- package/assets/local-runtime/alembic/versions/000000000013_refresh_token_anomaly_context.py +61 -0
- package/assets/local-runtime/alembic/versions/000000000014_buildooor_dayrate_hire_schedule.py +39 -0
- package/assets/local-runtime/alembic/versions/000000000015_support_telemetry_platform.py +112 -0
- package/assets/local-runtime/alembic/versions/000000000016_issue_reporting_platform.py +54 -0
- package/assets/local-runtime/alembic/versions/000000000017_issue_reporting_platform_import_tracking.py +44 -0
- package/assets/local-runtime/alembic/versions/000000000018_authorization_policy_engine.py +76 -0
- package/assets/local-runtime/alembic.ini +47 -0
- package/assets/local-runtime/docker-compose.yml +61 -0
- package/assets/local-runtime/manifest.json +8 -0
- package/assets/local-runtime/scripts/container-entrypoint.sh +13 -0
- package/assets/local-runtime/scripts/fetch-prod-db.sh +112 -0
- package/assets/local-runtime/scripts/run-migrations.sh +96 -0
- package/package.json +5 -4
- package/src/ai-helper.js +176 -234
- package/src/ai-tool-spec.js +52 -20
- package/src/auth/api-key.js +119 -0
- package/src/auth/client-id.js +136 -0
- package/src/auth/client.js +169 -0
- package/src/auth/credentials.js +110 -0
- package/src/auth/device-flow.js +159 -0
- package/src/auth/env.js +57 -0
- package/src/auth/handlers.js +462 -0
- package/src/auth/http.js +74 -0
- package/src/cli-dispatcher.js +155 -24
- package/src/docs-system.js +7 -7
- package/src/error-handler.js +42 -0
- package/src/fixture-kernel.js +1143 -0
- package/src/handlers.js +252 -15
- package/src/help-system.js +3 -1
- package/src/local-runtime.js +258 -0
- package/src/local-server.js +597 -199
- package/src/project-scaffolder.js +441 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""issue reporting platform
|
|
2
|
+
|
|
3
|
+
Revision ID: 000000000016
|
|
4
|
+
Revises: 000000000015
|
|
5
|
+
Create Date: 2026-03-29
|
|
6
|
+
|
|
7
|
+
Creates the issue_reports table for shared end-user issue reporting linked to
|
|
8
|
+
the support_telemetry_platform support-case substrate.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from alembic import op
|
|
12
|
+
|
|
13
|
+
revision = "000000000016"
|
|
14
|
+
down_revision = "000000000015"
|
|
15
|
+
branch_labels = None
|
|
16
|
+
depends_on = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def upgrade() -> None:
|
|
20
|
+
op.execute("""
|
|
21
|
+
CREATE TABLE IF NOT EXISTS issue_reports (
|
|
22
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
23
|
+
application_id UUID NOT NULL REFERENCES applications(id) ON DELETE CASCADE,
|
|
24
|
+
reporter_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
25
|
+
reporter_role_hint VARCHAR(64),
|
|
26
|
+
component_key VARCHAR(255) NOT NULL,
|
|
27
|
+
component_label VARCHAR(255) NOT NULL,
|
|
28
|
+
page_url VARCHAR(500) NOT NULL,
|
|
29
|
+
surface_ref VARCHAR(255),
|
|
30
|
+
target_metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
31
|
+
note TEXT NOT NULL,
|
|
32
|
+
parent_issue_report_id UUID REFERENCES issue_reports(id) ON DELETE SET NULL,
|
|
33
|
+
support_case_id UUID NOT NULL REFERENCES support_cases(id) ON DELETE RESTRICT,
|
|
34
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
35
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
36
|
+
);
|
|
37
|
+
""")
|
|
38
|
+
|
|
39
|
+
op.execute("""
|
|
40
|
+
CREATE INDEX IF NOT EXISTS ix_issue_reports_app_reporter_created
|
|
41
|
+
ON issue_reports (application_id, reporter_user_id, created_at);
|
|
42
|
+
""")
|
|
43
|
+
op.execute("""
|
|
44
|
+
CREATE INDEX IF NOT EXISTS ix_issue_reports_support_case
|
|
45
|
+
ON issue_reports (support_case_id);
|
|
46
|
+
""")
|
|
47
|
+
op.execute("""
|
|
48
|
+
CREATE INDEX IF NOT EXISTS ix_issue_reports_parent
|
|
49
|
+
ON issue_reports (parent_issue_report_id);
|
|
50
|
+
""")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def downgrade() -> None:
|
|
54
|
+
op.execute("DROP TABLE IF EXISTS issue_reports CASCADE;")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""issue reporting platform import tracking
|
|
2
|
+
|
|
3
|
+
Revision ID: 000000000017
|
|
4
|
+
Revises: 000000000016
|
|
5
|
+
Create Date: 2026-03-29
|
|
6
|
+
|
|
7
|
+
Adds replay-safe legacy source tracking to issue_reports so first-adopter
|
|
8
|
+
cutovers can import HTMA issue logs without duplicating issue reports.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from alembic import op
|
|
12
|
+
|
|
13
|
+
revision = "000000000017"
|
|
14
|
+
down_revision = "000000000016"
|
|
15
|
+
branch_labels = None
|
|
16
|
+
depends_on = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def upgrade() -> None:
|
|
20
|
+
op.execute("""
|
|
21
|
+
ALTER TABLE issue_reports
|
|
22
|
+
ADD COLUMN IF NOT EXISTS source_app VARCHAR(64);
|
|
23
|
+
""")
|
|
24
|
+
op.execute("""
|
|
25
|
+
ALTER TABLE issue_reports
|
|
26
|
+
ADD COLUMN IF NOT EXISTS source_record_id VARCHAR(255);
|
|
27
|
+
""")
|
|
28
|
+
op.execute("""
|
|
29
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_issue_reports_source_ref
|
|
30
|
+
ON issue_reports (application_id, source_app, source_record_id)
|
|
31
|
+
WHERE source_app IS NOT NULL AND source_record_id IS NOT NULL;
|
|
32
|
+
""")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def downgrade() -> None:
|
|
36
|
+
op.execute("DROP INDEX IF EXISTS uq_issue_reports_source_ref;")
|
|
37
|
+
op.execute("""
|
|
38
|
+
ALTER TABLE issue_reports
|
|
39
|
+
DROP COLUMN IF EXISTS source_record_id;
|
|
40
|
+
""")
|
|
41
|
+
op.execute("""
|
|
42
|
+
ALTER TABLE issue_reports
|
|
43
|
+
DROP COLUMN IF EXISTS source_app;
|
|
44
|
+
""")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""authorization policy engine
|
|
2
|
+
|
|
3
|
+
Revision ID: 000000000018
|
|
4
|
+
Revises: 000000000017
|
|
5
|
+
Create Date: 2026-03-31
|
|
6
|
+
|
|
7
|
+
Creates the policies and policy_evaluation_logs tables for the composable
|
|
8
|
+
authorization policy engine.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from alembic import op
|
|
12
|
+
|
|
13
|
+
revision = "000000000018"
|
|
14
|
+
down_revision = "000000000017"
|
|
15
|
+
branch_labels = None
|
|
16
|
+
depends_on = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def upgrade() -> None:
|
|
20
|
+
op.execute("""
|
|
21
|
+
CREATE TABLE IF NOT EXISTS policies (
|
|
22
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
23
|
+
application_id UUID NOT NULL REFERENCES applications(id) ON DELETE CASCADE,
|
|
24
|
+
name VARCHAR(255) NOT NULL,
|
|
25
|
+
description TEXT,
|
|
26
|
+
effect VARCHAR(10) NOT NULL DEFAULT 'allow',
|
|
27
|
+
conditions JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
28
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
29
|
+
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
30
|
+
metadata JSONB DEFAULT '{}'::jsonb,
|
|
31
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
32
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
33
|
+
);
|
|
34
|
+
""")
|
|
35
|
+
|
|
36
|
+
# Unique constraint: one policy name per application
|
|
37
|
+
op.execute("""
|
|
38
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_policies_app_name
|
|
39
|
+
ON policies (application_id, name);
|
|
40
|
+
""")
|
|
41
|
+
|
|
42
|
+
op.execute("""
|
|
43
|
+
CREATE INDEX IF NOT EXISTS ix_policies_app_active
|
|
44
|
+
ON policies (application_id, is_active)
|
|
45
|
+
WHERE is_active = true;
|
|
46
|
+
""")
|
|
47
|
+
|
|
48
|
+
op.execute("""
|
|
49
|
+
CREATE TABLE IF NOT EXISTS policy_evaluation_logs (
|
|
50
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
51
|
+
policy_id UUID REFERENCES policies(id) ON DELETE SET NULL,
|
|
52
|
+
application_id UUID NOT NULL,
|
|
53
|
+
user_id UUID,
|
|
54
|
+
policy_name VARCHAR(255) NOT NULL,
|
|
55
|
+
decision VARCHAR(10) NOT NULL,
|
|
56
|
+
reasons JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
57
|
+
evaluation_ms INTEGER NOT NULL DEFAULT 0,
|
|
58
|
+
context_snapshot JSONB,
|
|
59
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
60
|
+
);
|
|
61
|
+
""")
|
|
62
|
+
|
|
63
|
+
op.execute("""
|
|
64
|
+
CREATE INDEX IF NOT EXISTS ix_policy_eval_logs_app_created
|
|
65
|
+
ON policy_evaluation_logs (application_id, created_at DESC);
|
|
66
|
+
""")
|
|
67
|
+
|
|
68
|
+
op.execute("""
|
|
69
|
+
CREATE INDEX IF NOT EXISTS ix_policy_eval_logs_policy_name
|
|
70
|
+
ON policy_evaluation_logs (application_id, policy_name);
|
|
71
|
+
""")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def downgrade() -> None:
|
|
75
|
+
op.execute("DROP TABLE IF EXISTS policy_evaluation_logs;")
|
|
76
|
+
op.execute("DROP TABLE IF EXISTS policies;")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# SPAPS Alembic configuration
|
|
2
|
+
# Migrations live at repo root: sweet-potato/alembic/
|
|
3
|
+
# Models are imported from: spaps_server_quickstart.domains
|
|
4
|
+
|
|
5
|
+
[alembic]
|
|
6
|
+
script_location = %(here)s/alembic
|
|
7
|
+
prepend_sys_path = packages/python-server-quickstart/src
|
|
8
|
+
path_separator = os
|
|
9
|
+
|
|
10
|
+
# DATABASE_URL is set via env.py from settings
|
|
11
|
+
sqlalchemy.url =
|
|
12
|
+
|
|
13
|
+
[post_write_hooks]
|
|
14
|
+
|
|
15
|
+
[loggers]
|
|
16
|
+
keys = root,sqlalchemy,alembic
|
|
17
|
+
|
|
18
|
+
[handlers]
|
|
19
|
+
keys = console
|
|
20
|
+
|
|
21
|
+
[formatters]
|
|
22
|
+
keys = generic
|
|
23
|
+
|
|
24
|
+
[logger_root]
|
|
25
|
+
level = WARN
|
|
26
|
+
handlers = console
|
|
27
|
+
qualname =
|
|
28
|
+
|
|
29
|
+
[logger_sqlalchemy]
|
|
30
|
+
level = WARN
|
|
31
|
+
handlers =
|
|
32
|
+
qualname = sqlalchemy.engine
|
|
33
|
+
|
|
34
|
+
[logger_alembic]
|
|
35
|
+
level = INFO
|
|
36
|
+
handlers =
|
|
37
|
+
qualname = alembic
|
|
38
|
+
|
|
39
|
+
[handler_console]
|
|
40
|
+
class = StreamHandler
|
|
41
|
+
args = (sys.stderr,)
|
|
42
|
+
level = NOTSET
|
|
43
|
+
formatter = generic
|
|
44
|
+
|
|
45
|
+
[formatter_generic]
|
|
46
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
47
|
+
datefmt = %H:%M:%S
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
services:
|
|
2
|
+
spaps-dev-api:
|
|
3
|
+
build:
|
|
4
|
+
context: .
|
|
5
|
+
dockerfile: Dockerfile
|
|
6
|
+
args:
|
|
7
|
+
SPAPS_SERVER_QUICKSTART_VERSION: ${SPAPS_SERVER_QUICKSTART_VERSION:-0.5.0}
|
|
8
|
+
restart: unless-stopped
|
|
9
|
+
ports:
|
|
10
|
+
- "${SPAPS_LOCAL_PORT:-3301}:8000"
|
|
11
|
+
environment:
|
|
12
|
+
ENV: dev
|
|
13
|
+
DATABASE_URL: postgresql+asyncpg://postgres:postgres@spaps-dev-db:5432/spaps
|
|
14
|
+
REDIS_URL: redis://spaps-dev-redis:6379/0
|
|
15
|
+
SPAPS_LOCAL_MODE: "${SPAPS_LOCAL_MODE:-true}"
|
|
16
|
+
JWT_SECRET: "${JWT_SECRET:-spaps_local_dev_jwt_secret}"
|
|
17
|
+
REFRESH_TOKEN_SECRET: "${REFRESH_TOKEN_SECRET:-spaps_local_dev_refresh_secret}"
|
|
18
|
+
SELF_SERVICE_PASSWORD: "${SELF_SERVICE_PASSWORD:-spaps_local_self_service_password}"
|
|
19
|
+
ALEMBIC_RUN_ON_START_ROLE: primary
|
|
20
|
+
LOG_LEVEL: DEBUG
|
|
21
|
+
LOG_FORMAT: console
|
|
22
|
+
AUTH_BASE_URL: "${SPAPS_AUTH_BASE_URL:-http://localhost:5173}"
|
|
23
|
+
CORS_ALLOW_ORIGINS: "${CORS_ALLOW_ORIGINS:-*}"
|
|
24
|
+
LEGACY_API_KEY_AUTH_ENABLED: "${LEGACY_API_KEY_AUTH_ENABLED:-true}"
|
|
25
|
+
depends_on:
|
|
26
|
+
spaps-dev-db:
|
|
27
|
+
condition: service_healthy
|
|
28
|
+
spaps-dev-redis:
|
|
29
|
+
condition: service_started
|
|
30
|
+
healthcheck:
|
|
31
|
+
test: ["CMD", "python", "-c", "import httpx; r = httpx.get('http://localhost:8000/health'); r.raise_for_status()"]
|
|
32
|
+
interval: 10s
|
|
33
|
+
timeout: 5s
|
|
34
|
+
retries: 5
|
|
35
|
+
start_period: 15s
|
|
36
|
+
|
|
37
|
+
spaps-dev-db:
|
|
38
|
+
image: ankane/pgvector:v0.5.1
|
|
39
|
+
restart: unless-stopped
|
|
40
|
+
environment:
|
|
41
|
+
POSTGRES_USER: postgres
|
|
42
|
+
POSTGRES_PASSWORD: postgres
|
|
43
|
+
POSTGRES_DB: spaps
|
|
44
|
+
volumes:
|
|
45
|
+
- spaps-dev-pgdata:/var/lib/postgresql/data
|
|
46
|
+
healthcheck:
|
|
47
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
48
|
+
interval: 10s
|
|
49
|
+
timeout: 5s
|
|
50
|
+
retries: 5
|
|
51
|
+
|
|
52
|
+
spaps-dev-redis:
|
|
53
|
+
image: redis:7-alpine
|
|
54
|
+
restart: unless-stopped
|
|
55
|
+
command: redis-server --appendonly yes
|
|
56
|
+
volumes:
|
|
57
|
+
- spaps-dev-redisdata:/data
|
|
58
|
+
|
|
59
|
+
volumes:
|
|
60
|
+
spaps-dev-pgdata:
|
|
61
|
+
spaps-dev-redisdata:
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT="${APP_ROOT:-/app}"
|
|
5
|
+
MIGRATION_RUNNER="${ROOT}/scripts/run-migrations.sh"
|
|
6
|
+
|
|
7
|
+
if [[ -x "${MIGRATION_RUNNER}" ]]; then
|
|
8
|
+
"${MIGRATION_RUNNER}"
|
|
9
|
+
else
|
|
10
|
+
echo "⚠️ ${MIGRATION_RUNNER} not executable; skipping migrations." >&2
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
exec "$@"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Fetch SPAPS production database dump via SSH (read-only on production).
|
|
5
|
+
# Output: ${SPAPS_CACHE_ROOT:-~/.cache/spaps}/db/prod.sql.gz
|
|
6
|
+
#
|
|
7
|
+
# Environment variables:
|
|
8
|
+
# SPAPS_PROD_DB_FRESH=1 Force re-fetch even if a cached dump exists
|
|
9
|
+
# SPAPS_CACHE_ROOT Override cache directory
|
|
10
|
+
# SPAPS_PROD_HOST Primary SSH host
|
|
11
|
+
# SPAPS_PROD_HOST_FALLBACK Fallback SSH host
|
|
12
|
+
# SPAPS_SSH_BIN SSH binary path
|
|
13
|
+
|
|
14
|
+
PRIMARY_PROD_HOST="${SPAPS_PROD_HOST:-aiops@sweet-potato-prod}"
|
|
15
|
+
FALLBACK_PROD_HOST="${SPAPS_PROD_HOST_FALLBACK:-root@104.131.188.214}"
|
|
16
|
+
PROD_HOST=""
|
|
17
|
+
SSH_BIN="${SPAPS_SSH_BIN:-ssh}"
|
|
18
|
+
CACHE_DIR="${SPAPS_CACHE_ROOT:-${HOME}/.cache/spaps}/db"
|
|
19
|
+
PROD_DUMP="${CACHE_DIR}/prod.sql.gz"
|
|
20
|
+
FRESH="${SPAPS_PROD_DB_FRESH:-0}"
|
|
21
|
+
|
|
22
|
+
info() {
|
|
23
|
+
printf '\n%s %s\n' "🔧" "$1"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
err() {
|
|
27
|
+
printf '\n%s %s\n' "❌" "$1" >&2
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
resolve_prod_host() {
|
|
31
|
+
local candidate
|
|
32
|
+
local candidates=("${PRIMARY_PROD_HOST}")
|
|
33
|
+
if [[ -n "${FALLBACK_PROD_HOST}" && "${FALLBACK_PROD_HOST}" != "${PRIMARY_PROD_HOST}" ]]; then
|
|
34
|
+
candidates+=("${FALLBACK_PROD_HOST}")
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
for candidate in "${candidates[@]}"; do
|
|
38
|
+
info "Testing SSH connection to ${candidate}..."
|
|
39
|
+
if "${SSH_BIN}" -o ConnectTimeout=5 -o BatchMode=yes "${candidate}" "echo ok" >/dev/null 2>&1; then
|
|
40
|
+
PROD_HOST="${candidate}"
|
|
41
|
+
return 0
|
|
42
|
+
fi
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
return 1
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if [[ -f "${PROD_DUMP}" ]]; then
|
|
49
|
+
case "${FRESH}" in
|
|
50
|
+
1|true|TRUE|yes|YES)
|
|
51
|
+
info "SPAPS_PROD_DB_FRESH=1 set, fetching fresh dump from production..."
|
|
52
|
+
;;
|
|
53
|
+
*)
|
|
54
|
+
DUMP_AGE=$(( $(date +%s) - $(stat -f %m "${PROD_DUMP}" 2>/dev/null || stat -c %Y "${PROD_DUMP}" 2>/dev/null) ))
|
|
55
|
+
DUMP_AGE_HOURS=$(( DUMP_AGE / 3600 ))
|
|
56
|
+
info "Using cached prod dump (${DUMP_AGE_HOURS}h old): ${PROD_DUMP}"
|
|
57
|
+
info "Run with SPAPS_PROD_DB_FRESH=1 to fetch fresh data."
|
|
58
|
+
exit 0
|
|
59
|
+
;;
|
|
60
|
+
esac
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
mkdir -p "${CACHE_DIR}"
|
|
64
|
+
|
|
65
|
+
if ! command -v "${SSH_BIN}" >/dev/null 2>&1; then
|
|
66
|
+
err "ssh command not found: ${SSH_BIN}"
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
if ! resolve_prod_host; then
|
|
71
|
+
err "Cannot connect to any configured production host."
|
|
72
|
+
err "Tried: ${PRIMARY_PROD_HOST} ${FALLBACK_PROD_HOST}"
|
|
73
|
+
err "Check your SSH key is loaded: ssh-add -l"
|
|
74
|
+
err "Try manually: ssh ${PRIMARY_PROD_HOST}"
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
info "Using production SSH host: ${PROD_HOST}"
|
|
78
|
+
|
|
79
|
+
info "Fetching SPAPS production database dump via SSH (read-only)..."
|
|
80
|
+
info "This may take a minute..."
|
|
81
|
+
|
|
82
|
+
set +e
|
|
83
|
+
"${SSH_BIN}" "${PROD_HOST}" "docker exec spaps-python-db pg_dump -U spaps spaps" 2>"${PROD_DUMP}.err" | gzip > "${PROD_DUMP}.tmp"
|
|
84
|
+
pipe_status=("${PIPESTATUS[@]}")
|
|
85
|
+
set -e
|
|
86
|
+
ssh_status=${pipe_status[0]:-1}
|
|
87
|
+
gzip_status=${pipe_status[1]:-1}
|
|
88
|
+
|
|
89
|
+
if [[ $ssh_status -ne 0 || $gzip_status -ne 0 ]]; then
|
|
90
|
+
err "Failed to dump production database (ssh exit ${ssh_status}, gzip exit ${gzip_status})"
|
|
91
|
+
if [[ $gzip_status -ne 0 ]]; then
|
|
92
|
+
err "Local compression/write failed (check disk space and file permissions)."
|
|
93
|
+
fi
|
|
94
|
+
if [[ -s "${PROD_DUMP}.err" ]]; then
|
|
95
|
+
err "Error output:"
|
|
96
|
+
cat "${PROD_DUMP}.err" >&2
|
|
97
|
+
fi
|
|
98
|
+
rm -f "${PROD_DUMP}.tmp" "${PROD_DUMP}.err"
|
|
99
|
+
exit 1
|
|
100
|
+
fi
|
|
101
|
+
rm -f "${PROD_DUMP}.err"
|
|
102
|
+
|
|
103
|
+
if [[ ! -s "${PROD_DUMP}.tmp" ]]; then
|
|
104
|
+
err "Dump file is empty"
|
|
105
|
+
rm -f "${PROD_DUMP}.tmp"
|
|
106
|
+
exit 1
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
mv "${PROD_DUMP}.tmp" "${PROD_DUMP}"
|
|
110
|
+
|
|
111
|
+
DUMP_SIZE=$(du -h "${PROD_DUMP}" | cut -f1)
|
|
112
|
+
info "Production dump saved: ${PROD_DUMP} (${DUMP_SIZE})"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT="${APP_ROOT:-/app}"
|
|
5
|
+
PYTHON_BIN="${PYTHON_BIN:-python}"
|
|
6
|
+
|
|
7
|
+
if [[ ! -d "${ROOT}" ]]; then
|
|
8
|
+
echo "⚠️ Application root ${ROOT} not found; skipping migrations." >&2
|
|
9
|
+
exit 0
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
cd "${ROOT}"
|
|
13
|
+
|
|
14
|
+
if [[ ! -f "${ROOT}/alembic.ini" ]]; then
|
|
15
|
+
echo "⚠️ alembic.ini not present in ${ROOT}; skipping migrations." >&2
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
if [[ "${ALEMBIC_SKIP_ON_START:-0}" == "1" ]]; then
|
|
20
|
+
echo "ℹ️ ALEMBIC_SKIP_ON_START=1; skipping migrations."
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
ROLE="${ALEMBIC_RUN_ON_START_ROLE:-primary}"
|
|
25
|
+
|
|
26
|
+
if [[ "${ROLE}" == "primary" ]]; then
|
|
27
|
+
echo "🏃 Applying database migrations..."
|
|
28
|
+
else
|
|
29
|
+
echo "ℹ️ ALEMBIC_RUN_ON_START_ROLE=${ROLE}; verifying schema without applying upgrades."
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
normalize_revisions() {
|
|
33
|
+
matches="$(printf '%s\n' "$1" | grep -Eo '[0-9a-f]{12,}' | sort -u | paste -sd ',' -)"
|
|
34
|
+
if [[ -n "${matches}" ]]; then
|
|
35
|
+
printf '%s' "${matches}"
|
|
36
|
+
fi
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
restore_database_url() {
|
|
40
|
+
if [[ "${ORIGINAL_DATABASE_URL}" != "__unset__" ]]; then
|
|
41
|
+
export DATABASE_URL="${ORIGINAL_DATABASE_URL}"
|
|
42
|
+
else
|
|
43
|
+
unset DATABASE_URL
|
|
44
|
+
fi
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
assert_single_head() {
|
|
48
|
+
local heads_raw heads
|
|
49
|
+
heads_raw="$("${PYTHON_BIN}" -m alembic heads || true)"
|
|
50
|
+
heads="$(normalize_revisions "${heads_raw:-}")"
|
|
51
|
+
if [[ -n "${heads}" && "${heads}" == *","* ]]; then
|
|
52
|
+
echo "❌ Multiple Alembic heads detected (${heads})." >&2
|
|
53
|
+
echo " HINT: Align migration down_revision values or create a merge revision to restore a single head." >&2
|
|
54
|
+
restore_database_url
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Get sync (non-async) database URL from SPAPS settings
|
|
60
|
+
SYNC_URL="$("${PYTHON_BIN}" - <<'PY'
|
|
61
|
+
from spaps_server_quickstart.spaps_settings import get_spaps_settings
|
|
62
|
+
print(get_spaps_settings().sync_database_url, end="")
|
|
63
|
+
PY
|
|
64
|
+
)"
|
|
65
|
+
|
|
66
|
+
ORIGINAL_DATABASE_URL="${DATABASE_URL-__unset__}"
|
|
67
|
+
if [[ -n "${SYNC_URL}" ]]; then
|
|
68
|
+
export DATABASE_URL="${SYNC_URL}"
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
assert_single_head
|
|
72
|
+
|
|
73
|
+
if [[ "${ROLE}" == "primary" ]]; then
|
|
74
|
+
"${PYTHON_BIN}" -m alembic upgrade head
|
|
75
|
+
else
|
|
76
|
+
echo "ℹ️ Skipping alembic upgrade (role=${ROLE})."
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
current_raw="$("${PYTHON_BIN}" -m alembic current || true)"
|
|
80
|
+
heads_raw="$("${PYTHON_BIN}" -m alembic heads || true)"
|
|
81
|
+
|
|
82
|
+
current="$(normalize_revisions "${current_raw:-}")"
|
|
83
|
+
heads="$(normalize_revisions "${heads_raw:-}")"
|
|
84
|
+
|
|
85
|
+
if [[ -n "${heads}" && "${current}" != "${heads}" ]]; then
|
|
86
|
+
echo "❌ Alembic drift detected (current=${current:-none} expected=${heads:-none})." >&2
|
|
87
|
+
if [[ "${ROLE}" != "primary" ]]; then
|
|
88
|
+
echo " HINT: Ensure the primary migration runner has applied upgrades." >&2
|
|
89
|
+
fi
|
|
90
|
+
restore_database_url
|
|
91
|
+
exit 1
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
restore_database_url
|
|
95
|
+
|
|
96
|
+
echo "✅ Alembic migrations current (${current:-none})."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spaps",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Sweet Potato Authentication & Payment Service CLI - Docker Compose orchestrator for local Python/FastAPI SPAPS server with built-in admin middleware",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
"./middleware": "./src/middleware/admin.js"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
|
-
"test": "
|
|
16
|
+
"test": "node --test"
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
19
19
|
"authentication",
|
|
20
20
|
"payments",
|
|
21
21
|
"stripe",
|
|
22
|
-
"
|
|
22
|
+
"fastapi",
|
|
23
23
|
"local-development",
|
|
24
24
|
"cli",
|
|
25
25
|
"spaps",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"ethereum"
|
|
30
30
|
],
|
|
31
31
|
"author": "buildooor",
|
|
32
|
-
"license": "
|
|
32
|
+
"license": "MIT",
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
35
35
|
"url": "https://github.com/build000r"
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"files": [
|
|
53
53
|
"bin",
|
|
54
54
|
"src",
|
|
55
|
+
"assets",
|
|
55
56
|
"AI_TOOLS.json",
|
|
56
57
|
"client.js",
|
|
57
58
|
"README.md"
|