spine-framework 0.1.60 → 0.2.0
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/.framework/cli/commands/create-app.ts +1 -1
- package/.framework/cli/commands/dev.ts +215 -33
- package/.framework/cli/index.ts +37 -63
- package/.framework/functions/_shared/app-manifest.ts +8 -15
- package/.framework/functions/_shared/db.ts +11 -15
- package/.framework/functions/_shared/index.ts +0 -9
- package/.framework/functions/_shared/principal.ts +4 -62
- package/.framework/functions/_shared/resolve-ids.ts +6 -6
- package/.framework/functions/_shared/webhook-registry.ts +1 -1
- package/.framework/functions/admin-funnel.ts +104 -0
- package/.framework/functions/admin-install-funnel.ts +225 -0
- package/.framework/functions/admin-signals.ts +241 -0
- package/.framework/migrations/001_schema.sql +406 -0
- package/.framework/migrations/002_rls_and_functions.sql +710 -0
- package/.framework/migrations/003_system_data.sql +520 -0
- package/.framework/migrations/004_enhancements.sql +43 -0
- package/.framework/public/_redirects +3 -0
- package/.framework/public/spine-logo.jpg +0 -0
- package/.framework/public/spine-logo.svg +3 -0
- package/{scripts → .framework/scripts}/assemble-frontend.sh +1 -1
- package/{scripts → .framework/scripts}/assemble-functions.sh +1 -1
- package/{scripts → .framework/scripts}/assemble.sh +2 -8
- package/{scripts → .framework/scripts}/check-core-integrity.sh +0 -0
- package/.framework/seeds/link_types/account_cli_instances.json +9 -0
- package/.framework/seeds/types/funnel_signal_use.json +98 -0
- package/.framework/seeds/types/spine_instance.json +109 -0
- package/.framework/src/App.tsx +6 -4
- package/.framework/src/components/ui/accordion.tsx +1 -1
- package/.framework/src/components/ui/alert-dialog.tsx +1 -1
- package/.framework/src/components/ui/aspect-ratio.tsx +1 -1
- package/.framework/src/components/ui/avatar.tsx +1 -1
- package/.framework/src/components/ui/badge.tsx +2 -2
- package/.framework/src/components/ui/breadcrumb.tsx +2 -2
- package/.framework/src/components/ui/button-group.tsx +2 -2
- package/.framework/src/components/ui/button.tsx +2 -2
- package/.framework/src/components/ui/chart.tsx +6 -1
- package/.framework/src/components/ui/checkbox.tsx +1 -1
- package/.framework/src/components/ui/collapsible.tsx +1 -1
- package/.framework/src/components/ui/context-menu.tsx +1 -1
- package/.framework/src/components/ui/dialog.tsx +1 -1
- package/.framework/src/components/ui/direction.tsx +17 -1
- package/.framework/src/components/ui/dropdown-menu.tsx +1 -1
- package/.framework/src/components/ui/hover-card.tsx +1 -1
- package/.framework/src/components/ui/item.tsx +2 -2
- package/.framework/src/components/ui/label.tsx +1 -1
- package/.framework/src/components/ui/menubar.tsx +1 -1
- package/.framework/src/components/ui/navigation-menu.tsx +1 -1
- package/.framework/src/components/ui/popover.tsx +1 -1
- package/.framework/src/components/ui/progress.tsx +1 -1
- package/.framework/src/components/ui/radio-group.tsx +1 -1
- package/.framework/src/components/ui/scroll-area.tsx +1 -1
- package/.framework/src/components/ui/select.tsx +1 -1
- package/.framework/src/components/ui/separator.tsx +1 -1
- package/.framework/src/components/ui/sheet.tsx +1 -1
- package/.framework/src/components/ui/sidebar.tsx +6 -6
- package/.framework/src/components/ui/slider.tsx +1 -1
- package/.framework/src/components/ui/switch.tsx +1 -1
- package/.framework/src/components/ui/tabs.tsx +1 -1
- package/.framework/src/components/ui/toggle-group.tsx +1 -1
- package/.framework/src/components/ui/toggle.tsx +1 -1
- package/.framework/src/components/ui/tooltip.tsx +1 -1
- package/.framework/src/pages/auth/LoginPage.tsx +10 -9
- package/LICENSE.md +193 -0
- package/README.md +254 -0
- package/custom/.devin/skills/install-app/SKILL.md +39 -0
- package/custom/.devin/workflows/app-install.yaml +83 -0
- package/custom/README.md +79 -0
- package/docs/APPS.md +232 -0
- package/docs/ARCHITECTURE.md +246 -0
- package/docs/INSTALL.md +155 -0
- package/docs/README.md +170 -0
- package/docs/design-schema-spec.md +1837 -0
- package/docs/dev-guides/01-create-an-app.md +183 -0
- package/docs/dev-guides/02-create-a-function.md +178 -0
- package/docs/dev-guides/03-create-a-component.md +178 -0
- package/docs/dev-guides/04-webhook-handlers.md +129 -0
- package/docs/dev-guides/05-testing.md +168 -0
- package/docs/dev-guides/README.md +40 -0
- package/docs/npm-packages-plan.md +288 -0
- package/docs/spine-core--api-reference.md +822 -0
- package/package.json +78 -144
- package/.framework/cli/bin.cjs +0 -14
- package/.framework/cli/commands/init.ts +0 -186
- package/.framework/cli/commands/install-app.ts +0 -560
- package/.framework/cli/commands/migrate.ts +0 -139
- package/.framework/cli/commands/status.ts +0 -197
- package/.framework/cli/commands/uninstall-app.ts +0 -166
- package/.framework/cli/welcome.cjs +0 -45
- package/.framework/docs/STABILITY.md +0 -52
- package/.framework/docs/examples/custom-app/README.md +0 -77
- package/.framework/docs/examples/custom-function/README.md +0 -27
- package/.framework/docs/examples/custom-function/handler.ts +0 -48
- package/.framework/docs/examples/custom-webhook/README.md +0 -68
- package/.framework/migrations/000_foundation.sql +0 -1265
- package/.framework/migrations/001_seed.sql +0 -163
- package/.framework/migrations/002_seed_constraints.sql +0 -19
- package/.framework/migrations/003_auth_user_trigger.sql +0 -67
- package/.framework/src/pages/auth/RegisterPage.tsx +0 -163
- package/STRUCTURE.md +0 -150
- package/bin/spine-framework.cjs +0 -62
- package/bin/welcome.cjs +0 -45
- package/bin/ws-shim.cjs +0 -8
- package/bin/ws-shim.ts +0 -10
- package/config/components.json +0 -25
- package/config/deno.lock +0 -108
- package/config/package-lock.json +0 -17183
- package/config/postcss.config.cjs +0 -10
- package/config/tailwind.config.cjs +0 -78
- package/config/tsconfig.build.json +0 -32
- package/config/tsconfig.cli.json +0 -18
- package/config/tsconfig.json +0 -41
- package/config/tsconfig.node.json +0 -17
- package/config/tsconfig.node.tsbuildinfo +0 -1
- package/config/tsconfig.tsbuildinfo +0 -1
- package/config/typedoc.json +0 -16
- package/config/vite.config.d.ts +0 -2
- package/config/vite.config.ts +0 -72
- package/dist/cli/commands/agents.d.ts +0 -39
- package/dist/cli/commands/agents.d.ts.map +0 -1
- package/dist/cli/commands/auth.d.ts +0 -36
- package/dist/cli/commands/auth.d.ts.map +0 -1
- package/dist/cli/commands/create-app.d.ts +0 -23
- package/dist/cli/commands/create-app.d.ts.map +0 -1
- package/dist/cli/commands/dev.d.ts +0 -24
- package/dist/cli/commands/dev.d.ts.map +0 -1
- package/dist/cli/commands/doctor.d.ts +0 -42
- package/dist/cli/commands/doctor.d.ts.map +0 -1
- package/dist/cli/commands/generate.d.ts +0 -36
- package/dist/cli/commands/generate.d.ts.map +0 -1
- package/dist/cli/commands/init.d.ts +0 -20
- package/dist/cli/commands/init.d.ts.map +0 -1
- package/dist/cli/commands/install-app.d.ts +0 -30
- package/dist/cli/commands/install-app.d.ts.map +0 -1
- package/dist/cli/commands/items.d.ts +0 -45
- package/dist/cli/commands/items.d.ts.map +0 -1
- package/dist/cli/commands/migrate.d.ts +0 -21
- package/dist/cli/commands/migrate.d.ts.map +0 -1
- package/dist/cli/commands/migrations.d.ts +0 -41
- package/dist/cli/commands/migrations.d.ts.map +0 -1
- package/dist/cli/commands/pipelines.d.ts +0 -40
- package/dist/cli/commands/pipelines.d.ts.map +0 -1
- package/dist/cli/commands/status.d.ts +0 -23
- package/dist/cli/commands/status.d.ts.map +0 -1
- package/dist/cli/commands/system.d.ts +0 -29
- package/dist/cli/commands/system.d.ts.map +0 -1
- package/dist/cli/commands/test.d.ts +0 -46
- package/dist/cli/commands/test.d.ts.map +0 -1
- package/dist/cli/commands/uninstall-app.d.ts +0 -23
- package/dist/cli/commands/uninstall-app.d.ts.map +0 -1
- package/dist/cli/context.d.ts +0 -88
- package/dist/cli/context.d.ts.map +0 -1
- package/dist/cli/env-loader.d.ts +0 -14
- package/dist/cli/env-loader.d.ts.map +0 -1
- package/dist/cli/index.d.ts +0 -41
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/functions/_shared/agent-runner.d.ts +0 -156
- package/dist/functions/_shared/agent-runner.d.ts.map +0 -1
- package/dist/functions/_shared/app-manifest.d.ts +0 -68
- package/dist/functions/_shared/app-manifest.d.ts.map +0 -1
- package/dist/functions/_shared/audit.d.ts +0 -91
- package/dist/functions/_shared/audit.d.ts.map +0 -1
- package/dist/functions/_shared/db.d.ts +0 -125
- package/dist/functions/_shared/db.d.ts.map +0 -1
- package/dist/functions/_shared/index.d.ts +0 -299
- package/dist/functions/_shared/index.d.ts.map +0 -1
- package/dist/functions/_shared/middleware.d.ts +0 -315
- package/dist/functions/_shared/middleware.d.ts.map +0 -1
- package/dist/functions/_shared/permissions.d.ts +0 -626
- package/dist/functions/_shared/permissions.d.ts.map +0 -1
- package/dist/functions/_shared/pipeline-runner.d.ts +0 -124
- package/dist/functions/_shared/pipeline-runner.d.ts.map +0 -1
- package/dist/functions/_shared/principal.d.ts +0 -284
- package/dist/functions/_shared/principal.d.ts.map +0 -1
- package/dist/functions/_shared/resolve-ids.d.ts +0 -10
- package/dist/functions/_shared/resolve-ids.d.ts.map +0 -1
- package/dist/functions/_shared/schema-utils.d.ts +0 -181
- package/dist/functions/_shared/schema-utils.d.ts.map +0 -1
- package/dist/functions/_shared/testing.d.ts +0 -172
- package/dist/functions/_shared/testing.d.ts.map +0 -1
- package/dist/functions/_shared/trigger-engine.d.ts +0 -140
- package/dist/functions/_shared/trigger-engine.d.ts.map +0 -1
- package/dist/functions/_shared/webhook-registration.d.ts +0 -81
- package/dist/functions/_shared/webhook-registration.d.ts.map +0 -1
- package/dist/functions/_shared/webhook-registry.d.ts +0 -57
- package/dist/functions/_shared/webhook-registry.d.ts.map +0 -1
- package/dist/functions/account-nodes.d.ts +0 -48
- package/dist/functions/account-nodes.d.ts.map +0 -1
- package/dist/functions/admin-data.d.ts +0 -178
- package/dist/functions/admin-data.d.ts.map +0 -1
- package/dist/functions/ai-agents.d.ts +0 -125
- package/dist/functions/ai-agents.d.ts.map +0 -1
- package/dist/functions/api-keys.d.ts +0 -140
- package/dist/functions/api-keys.d.ts.map +0 -1
- package/dist/functions/apps.d.ts +0 -163
- package/dist/functions/apps.d.ts.map +0 -1
- package/dist/functions/auth.d.ts +0 -74
- package/dist/functions/auth.d.ts.map +0 -1
- package/dist/functions/debug-auth.d.ts +0 -33
- package/dist/functions/debug-auth.d.ts.map +0 -1
- package/dist/functions/embeddings.d.ts +0 -205
- package/dist/functions/embeddings.d.ts.map +0 -1
- package/dist/functions/integration-routes.d.ts +0 -45
- package/dist/functions/integration-routes.d.ts.map +0 -1
- package/dist/functions/integrations.d.ts +0 -124
- package/dist/functions/integrations.d.ts.map +0 -1
- package/dist/functions/item-progress.d.ts +0 -41
- package/dist/functions/item-progress.d.ts.map +0 -1
- package/dist/functions/logs.d.ts +0 -162
- package/dist/functions/logs.d.ts.map +0 -1
- package/dist/functions/observability.d.ts +0 -123
- package/dist/functions/observability.d.ts.map +0 -1
- package/dist/functions/pipeline-executions.d.ts +0 -190
- package/dist/functions/pipeline-executions.d.ts.map +0 -1
- package/dist/functions/pipelines.d.ts +0 -171
- package/dist/functions/pipelines.d.ts.map +0 -1
- package/dist/functions/prompt-configs.d.ts +0 -125
- package/dist/functions/prompt-configs.d.ts.map +0 -1
- package/dist/functions/roles.d.ts +0 -118
- package/dist/functions/roles.d.ts.map +0 -1
- package/dist/functions/system-cron.d.ts +0 -65
- package/dist/functions/system-cron.d.ts.map +0 -1
- package/dist/functions/system.d.ts +0 -29
- package/dist/functions/system.d.ts.map +0 -1
- package/dist/functions/tests.d.ts +0 -28
- package/dist/functions/tests.d.ts.map +0 -1
- package/dist/functions/timers.d.ts +0 -139
- package/dist/functions/timers.d.ts.map +0 -1
- package/dist/functions/triggers.d.ts +0 -203
- package/dist/functions/triggers.d.ts.map +0 -1
- package/dist/functions/types.d.ts +0 -151
- package/dist/functions/types.d.ts.map +0 -1
- package/dist/src/types/types.d.ts +0 -364
- package/dist/src/types/types.d.ts.map +0 -1
- package/index.html +0 -13
- package/netlify.toml +0 -36
- package/package-project.json +0 -64
- package/scripts/ingest-chunks.cjs +0 -202
- package/scripts/kb-chunk-parser.cjs +0 -312
- package/scripts/kb-chunk-parser.ts +0 -330
- package/scripts/load-test-app-install.ts +0 -484
- package/scripts/netlify-dev-wrapper.sh +0 -22
- package/vitest.config.ts +0 -45
- /package/{scripts → .framework/scripts}/app-install-cli.ts +0 -0
- /package/{scripts → .framework/scripts}/boundary-check.sh +0 -0
- /package/{scripts → .framework/scripts}/build-manifest.sh +0 -0
- /package/{scripts → .framework/scripts}/verify-integrity.sh +0 -0
|
@@ -1,560 +0,0 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
/**
|
|
3
|
-
* @module cli/commands/install-app
|
|
4
|
-
* @audience installer
|
|
5
|
-
* @layer cli
|
|
6
|
-
* @stability stable
|
|
7
|
-
*
|
|
8
|
-
* `spine-framework install-app <slug>` — Install an app into the current Spine instance.
|
|
9
|
-
*
|
|
10
|
-
* Reads seed/*.json files from the app package directory and upserts their
|
|
11
|
-
* contents into the database. Also registers the app in `app_installations`.
|
|
12
|
-
*
|
|
13
|
-
* **Seed file conventions:**
|
|
14
|
-
* - `types.json` → upsert into `types` table
|
|
15
|
-
* - `link-types.json` → upsert into `link_types` table
|
|
16
|
-
* - `triggers.json` → upsert into `triggers` table
|
|
17
|
-
* - `accounts.json` → upsert into `accounts` table
|
|
18
|
-
* - `pipelines.json` → upsert into `pipelines` table
|
|
19
|
-
*
|
|
20
|
-
* All upserts keyed on `(app_id, slug)` — idempotent and safe to re-run.
|
|
21
|
-
*
|
|
22
|
-
* **Usage:**
|
|
23
|
-
* ```bash
|
|
24
|
-
* spine-framework install-app cortex
|
|
25
|
-
* spine-framework install-app customer-portal
|
|
26
|
-
* spine-framework install-app cortex --account <account-id>
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import type { Command } from 'commander'
|
|
31
|
-
import { existsSync, readFileSync, readdirSync, cpSync, mkdirSync } from 'fs'
|
|
32
|
-
import { resolve, dirname } from 'path'
|
|
33
|
-
import { fileURLToPath } from 'url'
|
|
34
|
-
import { execSync } from 'child_process'
|
|
35
|
-
import { adminDb } from '../../functions/_shared/index.ts'
|
|
36
|
-
|
|
37
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
38
|
-
const __dirname = dirname(__filename)
|
|
39
|
-
const PROJECT_ROOT = process.cwd()
|
|
40
|
-
|
|
41
|
-
interface InstallOptions {
|
|
42
|
-
account?: string
|
|
43
|
-
force: boolean
|
|
44
|
-
dryRun: boolean
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface SeedResult {
|
|
48
|
-
table: string
|
|
49
|
-
inserted: number
|
|
50
|
-
skipped: number
|
|
51
|
-
errors: string[]
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function resolveAppId(slug: string): Promise<string | null> {
|
|
55
|
-
const { data } = await adminDb
|
|
56
|
-
.from('apps')
|
|
57
|
-
.select('id')
|
|
58
|
-
.eq('slug', slug)
|
|
59
|
-
.single()
|
|
60
|
-
|
|
61
|
-
return data?.id || null
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function resolveSystemAccountId(): Promise<string | null> {
|
|
65
|
-
const { data } = await adminDb
|
|
66
|
-
.from('accounts')
|
|
67
|
-
.select('id')
|
|
68
|
-
.eq('slug', 'spine-system')
|
|
69
|
-
.maybeSingle()
|
|
70
|
-
return data?.id || null
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function ensureAppRecord(slug: string, name: string): Promise<string> {
|
|
74
|
-
let appId = await resolveAppId(slug)
|
|
75
|
-
|
|
76
|
-
if (!appId) {
|
|
77
|
-
const ownerAccountId = await resolveSystemAccountId()
|
|
78
|
-
const { error: insertErr, data } = await adminDb
|
|
79
|
-
.from('apps')
|
|
80
|
-
.insert({
|
|
81
|
-
slug,
|
|
82
|
-
name,
|
|
83
|
-
route_prefix: `/${slug}`,
|
|
84
|
-
renderer: 'custom',
|
|
85
|
-
is_active: true,
|
|
86
|
-
is_system: false,
|
|
87
|
-
owner_account_id: ownerAccountId,
|
|
88
|
-
})
|
|
89
|
-
.select('id')
|
|
90
|
-
.single()
|
|
91
|
-
|
|
92
|
-
if (insertErr) {
|
|
93
|
-
throw new Error(`Failed to create app record: ${insertErr.message}`)
|
|
94
|
-
}
|
|
95
|
-
appId = data.id
|
|
96
|
-
console.log(` ✓ Created app record for '${slug}'`)
|
|
97
|
-
} else {
|
|
98
|
-
console.log(` ⏭️ App '${slug}' already in database`)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return appId!
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function upsertTypes(appId: string, records: any[], dryRun: boolean): Promise<SeedResult> {
|
|
105
|
-
const result: SeedResult = { table: 'types', inserted: 0, skipped: 0, errors: [] }
|
|
106
|
-
|
|
107
|
-
for (const record of records) {
|
|
108
|
-
if (dryRun) {
|
|
109
|
-
console.log(` [dry-run] Would upsert type: ${record.slug}`)
|
|
110
|
-
result.inserted++
|
|
111
|
-
continue
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const { error } = await adminDb
|
|
115
|
-
.from('types')
|
|
116
|
-
.upsert({
|
|
117
|
-
...record,
|
|
118
|
-
app_id: appId,
|
|
119
|
-
}, {
|
|
120
|
-
onConflict: 'app_id,kind,slug',
|
|
121
|
-
ignoreDuplicates: false
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
if (error) {
|
|
125
|
-
result.errors.push(`${record.slug}: ${error.message}`)
|
|
126
|
-
} else {
|
|
127
|
-
result.inserted++
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return result
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function upsertLinkTypes(appId: string, records: any[], dryRun: boolean): Promise<SeedResult> {
|
|
135
|
-
const result: SeedResult = { table: 'link_types', inserted: 0, skipped: 0, errors: [] }
|
|
136
|
-
|
|
137
|
-
for (const record of records) {
|
|
138
|
-
if (dryRun) {
|
|
139
|
-
console.log(` [dry-run] Would upsert link type: ${record.slug}`)
|
|
140
|
-
result.inserted++
|
|
141
|
-
continue
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const { error } = await adminDb
|
|
145
|
-
.from('link_types')
|
|
146
|
-
.upsert({
|
|
147
|
-
...record,
|
|
148
|
-
app_id: appId,
|
|
149
|
-
}, {
|
|
150
|
-
onConflict: 'app_id,slug',
|
|
151
|
-
ignoreDuplicates: false
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
if (error) {
|
|
155
|
-
result.errors.push(`${record.slug}: ${error.message}`)
|
|
156
|
-
} else {
|
|
157
|
-
result.inserted++
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return result
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async function upsertTriggers(appId: string, records: any[], dryRun: boolean): Promise<SeedResult> {
|
|
165
|
-
const result: SeedResult = { table: 'triggers', inserted: 0, skipped: 0, errors: [] }
|
|
166
|
-
|
|
167
|
-
for (const record of records) {
|
|
168
|
-
if (dryRun) {
|
|
169
|
-
console.log(` [dry-run] Would upsert trigger: ${record.name}`)
|
|
170
|
-
result.inserted++
|
|
171
|
-
continue
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Clone config to avoid mutating the original
|
|
175
|
-
const config = record.config ? JSON.parse(JSON.stringify(record.config)) : {}
|
|
176
|
-
|
|
177
|
-
// Resolve type_slug references in filters to type_id
|
|
178
|
-
if (config.filters && Array.isArray(config.filters)) {
|
|
179
|
-
for (const filter of config.filters) {
|
|
180
|
-
if (filter.type_slug && !filter.value) {
|
|
181
|
-
const { data: typeData, error: typeError } = await adminDb
|
|
182
|
-
.from('types')
|
|
183
|
-
.select('id')
|
|
184
|
-
.eq('slug', filter.type_slug)
|
|
185
|
-
.eq('app_id', appId)
|
|
186
|
-
.single()
|
|
187
|
-
|
|
188
|
-
if (typeError || !typeData) {
|
|
189
|
-
result.errors.push(`${record.name}: type_slug '${filter.type_slug}' not found`)
|
|
190
|
-
continue
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
filter.value = typeData.id
|
|
194
|
-
delete filter.type_slug
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const { error } = await adminDb
|
|
200
|
-
.from('triggers')
|
|
201
|
-
.upsert({
|
|
202
|
-
...record,
|
|
203
|
-
app_id: appId,
|
|
204
|
-
config,
|
|
205
|
-
}, {
|
|
206
|
-
onConflict: 'app_id,name',
|
|
207
|
-
ignoreDuplicates: false
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
if (error) {
|
|
211
|
-
result.errors.push(`${record.name}: ${error.message}`)
|
|
212
|
-
} else {
|
|
213
|
-
result.inserted++
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return result
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
async function upsertAccounts(appId: string, records: any[], dryRun: boolean): Promise<SeedResult> {
|
|
221
|
-
const result: SeedResult = { table: 'accounts', inserted: 0, skipped: 0, errors: [] }
|
|
222
|
-
|
|
223
|
-
for (const record of records) {
|
|
224
|
-
if (dryRun) {
|
|
225
|
-
console.log(` [dry-run] Would upsert account: ${record.slug}`)
|
|
226
|
-
result.inserted++
|
|
227
|
-
continue
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Resolve type_slug → type_id if present
|
|
231
|
-
const row = { ...record, app_id: appId }
|
|
232
|
-
if (row.type_slug && !row.type_id) {
|
|
233
|
-
const { data: typeData } = await adminDb
|
|
234
|
-
.from('types')
|
|
235
|
-
.select('id')
|
|
236
|
-
.eq('kind', 'account')
|
|
237
|
-
.eq('slug', row.type_slug)
|
|
238
|
-
.limit(1)
|
|
239
|
-
.single()
|
|
240
|
-
if (typeData) {
|
|
241
|
-
row.type_id = typeData.id
|
|
242
|
-
} else {
|
|
243
|
-
result.errors.push(`${record.slug}: type_slug '${row.type_slug}' not found`)
|
|
244
|
-
continue
|
|
245
|
-
}
|
|
246
|
-
delete row.type_slug
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const { error } = await adminDb
|
|
250
|
-
.from('accounts')
|
|
251
|
-
.upsert(row, {
|
|
252
|
-
onConflict: 'slug',
|
|
253
|
-
ignoreDuplicates: false
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
if (error) {
|
|
257
|
-
result.errors.push(`${record.slug}: ${error.message}`)
|
|
258
|
-
} else {
|
|
259
|
-
result.inserted++
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return result
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
async function upsertPipelines(appId: string, records: any[], dryRun: boolean): Promise<SeedResult> {
|
|
267
|
-
const result: SeedResult = { table: 'pipelines', inserted: 0, skipped: 0, errors: [] }
|
|
268
|
-
|
|
269
|
-
for (const record of records) {
|
|
270
|
-
if (dryRun) {
|
|
271
|
-
console.log(` [dry-run] Would upsert pipeline: ${record.slug}`)
|
|
272
|
-
result.inserted++
|
|
273
|
-
continue
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const { error } = await adminDb
|
|
277
|
-
.from('pipelines')
|
|
278
|
-
.upsert({
|
|
279
|
-
...record,
|
|
280
|
-
app_id: appId,
|
|
281
|
-
}, {
|
|
282
|
-
onConflict: 'app_id,name',
|
|
283
|
-
ignoreDuplicates: false
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
if (error) {
|
|
287
|
-
result.errors.push(`${record.slug}: ${error.message}`)
|
|
288
|
-
} else {
|
|
289
|
-
result.inserted++
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return result
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
async function upsertRoles(appId: string, records: any[], dryRun: boolean): Promise<SeedResult> {
|
|
297
|
-
const result: SeedResult = { table: 'roles', inserted: 0, skipped: 0, errors: [] }
|
|
298
|
-
|
|
299
|
-
for (const record of records) {
|
|
300
|
-
if (dryRun) {
|
|
301
|
-
console.log(` [dry-run] Would upsert role: ${record.slug}`)
|
|
302
|
-
result.inserted++
|
|
303
|
-
continue
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const { error } = await adminDb
|
|
307
|
-
.from('roles')
|
|
308
|
-
.upsert({
|
|
309
|
-
...record,
|
|
310
|
-
app_id: appId,
|
|
311
|
-
}, {
|
|
312
|
-
onConflict: 'app_id,slug',
|
|
313
|
-
ignoreDuplicates: false
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
if (error) {
|
|
317
|
-
result.errors.push(`${record.slug}: ${error.message}`)
|
|
318
|
-
} else {
|
|
319
|
-
result.inserted++
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return result
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const SEED_HANDLERS: Record<string, (appId: string, records: any[], dryRun: boolean) => Promise<SeedResult>> = {
|
|
327
|
-
'types.json': upsertTypes,
|
|
328
|
-
'link-types.json': upsertLinkTypes,
|
|
329
|
-
'triggers.json': upsertTriggers,
|
|
330
|
-
'accounts.json': upsertAccounts,
|
|
331
|
-
'pipelines.json': upsertPipelines,
|
|
332
|
-
'roles.json': upsertRoles,
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
async function recordInstallation(appSlug: string, accountId: string | null, dryRun: boolean): Promise<void> {
|
|
336
|
-
if (dryRun) {
|
|
337
|
-
console.log(` [dry-run] Would record installation for '${appSlug}'`)
|
|
338
|
-
return
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const { error } = await adminDb
|
|
342
|
-
.from('app_installations')
|
|
343
|
-
.upsert({
|
|
344
|
-
app_slug: appSlug,
|
|
345
|
-
account_id: accountId,
|
|
346
|
-
is_enabled: true,
|
|
347
|
-
updated_at: new Date().toISOString(),
|
|
348
|
-
}, {
|
|
349
|
-
onConflict: 'app_slug,account_id',
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
if (error) {
|
|
353
|
-
console.log(` ⚠️ Failed to record installation: ${error.message}`)
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function isNpmPackage(slug: string): boolean {
|
|
358
|
-
return slug.includes('/') || slug.startsWith('spine-framework-') || slug.startsWith('@')
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function resolveSlugFromPackage(pkg: string): string {
|
|
362
|
-
const manifest = resolve(PROJECT_ROOT, `node_modules/${pkg}/spine-manifest.json`)
|
|
363
|
-
if (existsSync(manifest)) {
|
|
364
|
-
try {
|
|
365
|
-
return JSON.parse(readFileSync(manifest, 'utf8')).slug
|
|
366
|
-
} catch { /* fall through */ }
|
|
367
|
-
}
|
|
368
|
-
const pkgJson = resolve(PROJECT_ROOT, `node_modules/${pkg}/package.json`)
|
|
369
|
-
if (existsSync(pkgJson)) {
|
|
370
|
-
try {
|
|
371
|
-
const meta = JSON.parse(readFileSync(pkgJson, 'utf8'))
|
|
372
|
-
if (meta.spine?.slug) return meta.spine.slug
|
|
373
|
-
} catch { /* fall through */ }
|
|
374
|
-
}
|
|
375
|
-
return pkg.replace(/^spine-framework-/, '').replace(/^@[^/]+\//, '')
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
async function checkPrerequisites(_slug: string): Promise<void> {
|
|
379
|
-
// Apps are independently installable — no hardcoded prereqs.
|
|
380
|
-
// Future: read prereqs from the app's manifest.json if needed.
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function copyAppFiles(pkg: string, slug: string): void {
|
|
384
|
-
const pkgDir = resolve(PROJECT_ROOT, `node_modules/${pkg}`)
|
|
385
|
-
const targetAppDir = resolve(PROJECT_ROOT, `custom/apps/${slug}`)
|
|
386
|
-
|
|
387
|
-
mkdirSync(targetAppDir, { recursive: true })
|
|
388
|
-
|
|
389
|
-
// Always copy core app files
|
|
390
|
-
const appFiles = ['index.tsx', 'manifest.json', 'package.json']
|
|
391
|
-
for (const file of appFiles) {
|
|
392
|
-
const src = resolve(pkgDir, file)
|
|
393
|
-
if (existsSync(src)) {
|
|
394
|
-
cpSync(src, resolve(targetAppDir, file), { force: true })
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Read directories from manifest (self-contained apps)
|
|
399
|
-
const manifestPath = resolve(pkgDir, 'manifest.json')
|
|
400
|
-
let appDirs = ['pages', 'components', 'hooks', 'config', 'seed', 'api', 'functions', 'migrations', 'tests']
|
|
401
|
-
|
|
402
|
-
if (existsSync(manifestPath)) {
|
|
403
|
-
try {
|
|
404
|
-
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'))
|
|
405
|
-
if (manifest.directories && Array.isArray(manifest.directories)) {
|
|
406
|
-
appDirs = manifest.directories
|
|
407
|
-
}
|
|
408
|
-
} catch (e) {
|
|
409
|
-
console.warn(` ⚠️ Could not read manifest.json, using default directories`)
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Copy app directories (all self-contained within the app)
|
|
414
|
-
for (const dir of appDirs) {
|
|
415
|
-
const src = resolve(pkgDir, dir)
|
|
416
|
-
if (existsSync(src)) {
|
|
417
|
-
cpSync(src, resolve(targetAppDir, dir), { recursive: true, force: true })
|
|
418
|
-
console.log(` ✓ Copied ${dir}/ → custom/apps/${slug}/${dir}/`)
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
console.log(` ✓ Ejected app files → custom/apps/${slug}/`)
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
async function installApp(slug: string, options: InstallOptions): Promise<void> {
|
|
426
|
-
console.log(`\n📦 Installing app '${slug}'...\n`)
|
|
427
|
-
|
|
428
|
-
let resolvedSlug = slug
|
|
429
|
-
let packageName: string | null = null
|
|
430
|
-
|
|
431
|
-
if (isNpmPackage(slug)) {
|
|
432
|
-
// Strip version specifier (e.g. spine-framework-cortex@latest → spine-framework-cortex)
|
|
433
|
-
// Handles: pkg@latest, pkg@1.2.3, @scope/pkg@latest (preserve leading @ for scoped packages)
|
|
434
|
-
const barePackageName = slug.startsWith('@')
|
|
435
|
-
? slug.replace(/(@[^/]+\/[^@]+)@.*$/, '$1') // @scope/pkg@version → @scope/pkg
|
|
436
|
-
: slug.replace(/@.*$/, '') // pkg@version → pkg
|
|
437
|
-
packageName = barePackageName
|
|
438
|
-
console.log(`📥 Step 1: Installing npm package...`)
|
|
439
|
-
const pkgDir = resolve(PROJECT_ROOT, `node_modules/${barePackageName}`)
|
|
440
|
-
if (!existsSync(pkgDir)) {
|
|
441
|
-
try {
|
|
442
|
-
console.log(` Running: npm install ${slug}`)
|
|
443
|
-
execSync(`npm install ${slug}`, { cwd: PROJECT_ROOT, stdio: 'inherit' })
|
|
444
|
-
console.log(` ✓ Package installed`)
|
|
445
|
-
} catch (err: any) {
|
|
446
|
-
console.error(` ❌ npm install failed: ${err.message}`)
|
|
447
|
-
process.exit(1)
|
|
448
|
-
}
|
|
449
|
-
} else {
|
|
450
|
-
console.log(` ⏭️ Package already in node_modules`)
|
|
451
|
-
}
|
|
452
|
-
resolvedSlug = resolveSlugFromPackage(barePackageName)
|
|
453
|
-
console.log(` ✓ App slug: ${resolvedSlug}`)
|
|
454
|
-
|
|
455
|
-
console.log(`\n📁 Step 2: Ejecting app files...`)
|
|
456
|
-
copyAppFiles(barePackageName, resolvedSlug)
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Check prerequisites (e.g. portal requires cortex)
|
|
460
|
-
await checkPrerequisites(resolvedSlug)
|
|
461
|
-
|
|
462
|
-
const appDir = resolve(PROJECT_ROOT, `custom/apps/${resolvedSlug}`)
|
|
463
|
-
if (!existsSync(appDir)) {
|
|
464
|
-
console.error(` ❌ App directory not found: custom/apps/${resolvedSlug}`)
|
|
465
|
-
console.error(` Install the npm package first or create the app with 'spine-framework create-app ${resolvedSlug}'`)
|
|
466
|
-
process.exit(1)
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// 2. Read manifest for display name
|
|
470
|
-
const manifestPath = resolve(appDir, 'manifest.json')
|
|
471
|
-
let appName = slug
|
|
472
|
-
if (existsSync(manifestPath)) {
|
|
473
|
-
try {
|
|
474
|
-
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
475
|
-
appName = manifest.name || slug
|
|
476
|
-
console.log(` 📋 Manifest: ${appName} v${manifest.version || '0.0.0'}`)
|
|
477
|
-
} catch {
|
|
478
|
-
console.log(` ⚠️ Could not parse manifest.json, using slug as name`)
|
|
479
|
-
}
|
|
480
|
-
} else {
|
|
481
|
-
console.log(` ⚠️ No manifest.json found, using slug as name`)
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// 3. Ensure app record in DB
|
|
485
|
-
const appId = await ensureAppRecord(resolvedSlug, appName)
|
|
486
|
-
|
|
487
|
-
// 4. Process seed files
|
|
488
|
-
const seedDir = resolve(appDir, 'seed')
|
|
489
|
-
if (!existsSync(seedDir)) {
|
|
490
|
-
console.log(` ⏭️ No seed/ directory — skipping data seeding`)
|
|
491
|
-
} else {
|
|
492
|
-
console.log(`\n🌱 Applying seed data...`)
|
|
493
|
-
const seedFiles = readdirSync(seedDir).filter(f => f.endsWith('.json'))
|
|
494
|
-
|
|
495
|
-
let totalInserted = 0
|
|
496
|
-
let totalErrors = 0
|
|
497
|
-
|
|
498
|
-
for (const file of seedFiles) {
|
|
499
|
-
const handler = SEED_HANDLERS[file]
|
|
500
|
-
if (!handler) {
|
|
501
|
-
console.log(` ⏭️ Skipping unknown seed file: ${file}`)
|
|
502
|
-
continue
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
const filePath = resolve(seedDir, file)
|
|
506
|
-
let records: any[]
|
|
507
|
-
try {
|
|
508
|
-
records = JSON.parse(readFileSync(filePath, 'utf8'))
|
|
509
|
-
if (!Array.isArray(records)) {
|
|
510
|
-
console.log(` ⚠️ ${file}: expected array, skipping`)
|
|
511
|
-
continue
|
|
512
|
-
}
|
|
513
|
-
} catch (err: any) {
|
|
514
|
-
console.error(` ❌ ${file}: parse error — ${err.message}`)
|
|
515
|
-
continue
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
console.log(` 📄 ${file} (${records.length} records)...`)
|
|
519
|
-
const result = await handler(appId, records, options.dryRun)
|
|
520
|
-
|
|
521
|
-
if (result.errors.length > 0) {
|
|
522
|
-
for (const e of result.errors) {
|
|
523
|
-
console.error(` ❌ ${e}`)
|
|
524
|
-
}
|
|
525
|
-
totalErrors += result.errors.length
|
|
526
|
-
}
|
|
527
|
-
totalInserted += result.inserted
|
|
528
|
-
console.log(` ✓ ${result.inserted} upserted`)
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
console.log(`\n Total: ${totalInserted} records upserted, ${totalErrors} errors`)
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// 5. Record installation
|
|
535
|
-
console.log(`\n📝 Recording installation...`)
|
|
536
|
-
await recordInstallation(resolvedSlug, options.account || null, options.dryRun)
|
|
537
|
-
|
|
538
|
-
console.log(`\n✅ App '${appName}' installed successfully!`)
|
|
539
|
-
console.log(`\n Next steps:`)
|
|
540
|
-
console.log(` 1. npm run assemble && netlify dev`)
|
|
541
|
-
console.log(` 2. Navigate to /${resolvedSlug} in your browser`)
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
export function registerInstallAppCommands(program: Command) {
|
|
545
|
-
program
|
|
546
|
-
.command('install-app <slug>')
|
|
547
|
-
.description('Install an app by applying its seed data to the database')
|
|
548
|
-
.option('--account <id>', 'Account ID to associate the installation with')
|
|
549
|
-
.option('--force', 'Overwrite existing seed data', false)
|
|
550
|
-
.option('--dry-run', 'Show what would happen without making changes', false)
|
|
551
|
-
.action(async (slug, opts) => {
|
|
552
|
-
try {
|
|
553
|
-
await installApp(slug, opts)
|
|
554
|
-
} catch (err: any) {
|
|
555
|
-
console.error('Error:', err.message)
|
|
556
|
-
if (process.env.SPINE_CLI_DEBUG) console.error(err.stack)
|
|
557
|
-
process.exit(1)
|
|
558
|
-
}
|
|
559
|
-
})
|
|
560
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
/**
|
|
3
|
-
* @module cli/commands/migrate
|
|
4
|
-
* @audience installer
|
|
5
|
-
* @layer cli
|
|
6
|
-
* @stability stable
|
|
7
|
-
*
|
|
8
|
-
* `spine-framework migrate` — Apply SQL migrations to a Supabase project
|
|
9
|
-
* via a direct Postgres connection.
|
|
10
|
-
*
|
|
11
|
-
* Requires --db-password (the Supabase database password).
|
|
12
|
-
* Get it from: https://supabase.com/dashboard/project/_/settings/database
|
|
13
|
-
*
|
|
14
|
-
* **Usage:**
|
|
15
|
-
* ```bash
|
|
16
|
-
* spine-framework migrate --db-password <password>
|
|
17
|
-
* spine-framework migrate --db-password <password> --dry-run
|
|
18
|
-
* ```
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import type { Command } from 'commander'
|
|
22
|
-
import { existsSync, readdirSync, readFileSync } from 'fs'
|
|
23
|
-
import { resolve, dirname } from 'path'
|
|
24
|
-
import { fileURLToPath } from 'url'
|
|
25
|
-
|
|
26
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
27
|
-
const __dirname = dirname(__filename)
|
|
28
|
-
const MIGRATIONS_DIR = resolve(__dirname, '../../migrations')
|
|
29
|
-
|
|
30
|
-
interface MigrateOptions {
|
|
31
|
-
dbPassword?: string
|
|
32
|
-
dryRun: boolean
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function migrateCommand(options: MigrateOptions): Promise<void> {
|
|
36
|
-
console.log('\n🗄️ Spine Framework — Migrate\n')
|
|
37
|
-
|
|
38
|
-
const supabaseUrl = process.env.SUPABASE_URL
|
|
39
|
-
if (!supabaseUrl || supabaseUrl === 'https://your-project.supabase.co') {
|
|
40
|
-
console.error('❌ SUPABASE_URL not set. Fill in your .env file first.')
|
|
41
|
-
process.exit(1)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const dbPassword = options.dbPassword || process.env.SUPABASE_DB_PASSWORD
|
|
45
|
-
if (!dbPassword || dbPassword === 'your-db-password') {
|
|
46
|
-
console.error('❌ SUPABASE_DB_PASSWORD not set.')
|
|
47
|
-
console.error(' Add it to .env or pass --db-password <password>')
|
|
48
|
-
console.error(' Get it from: https://supabase.com/dashboard/project/_/settings/database')
|
|
49
|
-
process.exit(1)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const projectRef = supabaseUrl.replace('https://', '').split('.')[0]
|
|
53
|
-
const connectionString = `postgresql://postgres:${dbPassword}@db.${projectRef}.supabase.co:5432/postgres`
|
|
54
|
-
|
|
55
|
-
if (!existsSync(MIGRATIONS_DIR)) {
|
|
56
|
-
console.error(`❌ Migrations directory not found: ${MIGRATIONS_DIR}`)
|
|
57
|
-
process.exit(1)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const files = readdirSync(MIGRATIONS_DIR)
|
|
61
|
-
.filter(f => f.endsWith('.sql'))
|
|
62
|
-
.sort()
|
|
63
|
-
|
|
64
|
-
if (files.length === 0) {
|
|
65
|
-
console.log(' No migration files found.')
|
|
66
|
-
return
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
console.log(` Found ${files.length} migration file(s)\n`)
|
|
70
|
-
|
|
71
|
-
if (options.dryRun) {
|
|
72
|
-
for (const file of files) {
|
|
73
|
-
console.log(` [dry-run] Would apply: ${file}`)
|
|
74
|
-
}
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Dynamically import pg to avoid loading it at module init time
|
|
79
|
-
const { default: pg } = await import('pg')
|
|
80
|
-
const client = new pg.Client({ connectionString })
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
await client.connect()
|
|
84
|
-
console.log(' ✓ Connected to database\n')
|
|
85
|
-
} catch (err: any) {
|
|
86
|
-
console.error(`❌ Could not connect to database: ${err.message}`)
|
|
87
|
-
console.error(' Check your --db-password and that the project is active.')
|
|
88
|
-
process.exit(1)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
let applied = 0
|
|
92
|
-
let failed = 0
|
|
93
|
-
|
|
94
|
-
for (const file of files) {
|
|
95
|
-
const sql = readFileSync(resolve(MIGRATIONS_DIR, file), 'utf8')
|
|
96
|
-
process.stdout.write(` Applying ${file}... `)
|
|
97
|
-
try {
|
|
98
|
-
await client.query(sql)
|
|
99
|
-
console.log('✓')
|
|
100
|
-
applied++
|
|
101
|
-
} catch (err: any) {
|
|
102
|
-
console.log('❌')
|
|
103
|
-
console.error(` ${err.message}`)
|
|
104
|
-
failed++
|
|
105
|
-
// Continue applying remaining migrations
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
await client.end()
|
|
110
|
-
|
|
111
|
-
console.log(`\n ${applied} applied, ${failed} failed`)
|
|
112
|
-
|
|
113
|
-
if (failed > 0) {
|
|
114
|
-
console.error('\n❌ Some migrations failed. Check errors above.')
|
|
115
|
-
process.exit(1)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
console.log('\n✅ All migrations applied successfully!')
|
|
119
|
-
console.log('\n Next steps:')
|
|
120
|
-
console.log(' 1. spine-framework install-app <app-slug>')
|
|
121
|
-
console.log(' 2. npm run assemble && netlify dev')
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function registerMigrateCommands(program: Command) {
|
|
125
|
-
program
|
|
126
|
-
.command('migrate')
|
|
127
|
-
.description('Apply SQL migrations via direct Postgres connection')
|
|
128
|
-
.option('--db-password <password>', 'DB password override (default: reads SUPABASE_DB_PASSWORD from .env)')
|
|
129
|
-
.option('--dry-run', 'Show what would happen without making changes', false)
|
|
130
|
-
.action(async (opts) => {
|
|
131
|
-
try {
|
|
132
|
-
await migrateCommand(opts)
|
|
133
|
-
} catch (err: any) {
|
|
134
|
-
console.error('Error:', err.message)
|
|
135
|
-
if (process.env.SPINE_CLI_DEBUG) console.error(err.stack)
|
|
136
|
-
process.exit(1)
|
|
137
|
-
}
|
|
138
|
-
})
|
|
139
|
-
}
|