spine-framework 0.3.16 → 0.3.18
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 -75
- package/.framework/functions/_shared/app-manifest.ts +8 -15
- package/.framework/functions/_shared/db.ts +11 -15
- package/.framework/functions/_shared/index.ts +7 -16
- package/.framework/functions/_shared/principal.ts +4 -62
- package/.framework/functions/_shared/resolve-ids.ts +6 -6
- package/.framework/functions/_shared/webhook-registry.ts +43 -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/functions/integration-routes.ts +3 -7
- 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 +8 -5
- package/{scripts → .framework/scripts}/assemble-functions.sh +4 -47
- package/{scripts → .framework/scripts}/assemble.sh +2 -8
- package/{scripts → .framework/scripts}/boundary-check.sh +5 -9
- 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 +11 -21
- package/.framework/src/components/CustomAppLoader.tsx +2 -19
- 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 +63 -93
- package/README.md +254 -129
- 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 -150
- package/.framework/cli/bin.cjs +0 -14
- package/.framework/cli/commands/claim.ts +0 -430
- package/.framework/cli/commands/init.ts +0 -219
- package/.framework/cli/commands/install-app.ts +0 -685
- package/.framework/cli/commands/migrate.ts +0 -178
- package/.framework/cli/commands/publish.ts +0 -191
- package/.framework/cli/commands/status.ts +0 -268
- package/.framework/cli/commands/uninstall-app.ts +0 -166
- package/.framework/cli/commands/update-app.ts +0 -397
- package/.framework/cli/welcome.cjs +0 -45
- package/.framework/config/signal-client.ts +0 -147
- package/.framework/config/signals.ts +0 -70
- 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/functions/handler-registry.ts +0 -22
- package/.framework/functions/signal.ts +0 -168
- package/.framework/migrations/000_foundation.sql +0 -1302
- 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/migrations/004_app_version_tracking.sql +0 -8
- package/.framework/migrations/005_pipelines_slug_stages.sql +0 -21
- package/.framework/src/pages/auth/RegisterPage.tsx +0 -163
- package/CHANGELOG.md +0 -68
- package/STRUCTURE.md +0 -150
- package/bin/spine-framework.cjs +0 -62
- package/bin/welcome.cjs +0 -58
- 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 -71
- 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/claim.d.ts +0 -38
- package/dist/cli/commands/claim.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/publish.d.ts +0 -23
- package/dist/cli/commands/publish.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/commands/update-app.d.ts +0 -24
- package/dist/cli/commands/update-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 -44
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/config/signals.d.ts +0 -82
- package/dist/config/signals.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 -41
- 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/handler-registry.d.ts +0 -22
- package/dist/functions/handler-registry.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/signal.d.ts +0 -73
- package/dist/functions/signal.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 -71
- 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}/build-manifest.sh +0 -0
- /package/{scripts → .framework/scripts}/verify-integrity.sh +0 -0
|
@@ -1,685 +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, statSync } 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
|
-
import { recordEngagementSignal, sendAppInstallSignal, ensureSerialData } from './claim.ts'
|
|
37
|
-
|
|
38
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
39
|
-
const __dirname = dirname(__filename)
|
|
40
|
-
const PROJECT_ROOT = process.cwd()
|
|
41
|
-
|
|
42
|
-
interface InstallOptions {
|
|
43
|
-
account?: string
|
|
44
|
-
force: boolean
|
|
45
|
-
dryRun: boolean
|
|
46
|
-
packageName?: string
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface SeedResult {
|
|
50
|
-
table: string
|
|
51
|
-
inserted: number
|
|
52
|
-
skipped: number
|
|
53
|
-
errors: string[]
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function resolveAppId(slug: string): Promise<string | null> {
|
|
57
|
-
const { data } = await adminDb
|
|
58
|
-
.from('apps')
|
|
59
|
-
.select('id')
|
|
60
|
-
.eq('slug', slug)
|
|
61
|
-
.single()
|
|
62
|
-
|
|
63
|
-
return data?.id || null
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function resolveSystemAccountId(): Promise<string | null> {
|
|
67
|
-
const { data } = await adminDb
|
|
68
|
-
.from('accounts')
|
|
69
|
-
.select('id')
|
|
70
|
-
.eq('slug', 'spine-system')
|
|
71
|
-
.maybeSingle()
|
|
72
|
-
return data?.id || null
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function ensureAppRecord(slug: string, name: string): Promise<string> {
|
|
76
|
-
let appId = await resolveAppId(slug)
|
|
77
|
-
|
|
78
|
-
if (!appId) {
|
|
79
|
-
const ownerAccountId = await resolveSystemAccountId()
|
|
80
|
-
const { error: insertErr, data } = await adminDb
|
|
81
|
-
.from('apps')
|
|
82
|
-
.insert({
|
|
83
|
-
slug,
|
|
84
|
-
name,
|
|
85
|
-
route_prefix: `/${slug}`,
|
|
86
|
-
renderer: 'custom',
|
|
87
|
-
is_active: true,
|
|
88
|
-
is_system: false,
|
|
89
|
-
owner_account_id: ownerAccountId,
|
|
90
|
-
})
|
|
91
|
-
.select('id')
|
|
92
|
-
.single()
|
|
93
|
-
|
|
94
|
-
if (insertErr) {
|
|
95
|
-
throw new Error(`Failed to create app record: ${insertErr.message}`)
|
|
96
|
-
}
|
|
97
|
-
appId = data.id
|
|
98
|
-
console.log(` ✓ Created app record for '${slug}'`)
|
|
99
|
-
} else {
|
|
100
|
-
console.log(` ⏭️ App '${slug}' already in database`)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return appId!
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function upsertTypes(appId: string, records: any[], dryRun: boolean): Promise<SeedResult> {
|
|
107
|
-
const result: SeedResult = { table: 'types', inserted: 0, skipped: 0, errors: [] }
|
|
108
|
-
|
|
109
|
-
for (const record of records) {
|
|
110
|
-
if (dryRun) {
|
|
111
|
-
console.log(` [dry-run] Would upsert type: ${record.slug}`)
|
|
112
|
-
result.inserted++
|
|
113
|
-
continue
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const { error } = await adminDb
|
|
117
|
-
.from('types')
|
|
118
|
-
.upsert({
|
|
119
|
-
...record,
|
|
120
|
-
app_id: appId,
|
|
121
|
-
}, {
|
|
122
|
-
onConflict: 'app_id,kind,slug',
|
|
123
|
-
ignoreDuplicates: false
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
if (error) {
|
|
127
|
-
result.errors.push(`${record.slug}: ${error.message}`)
|
|
128
|
-
} else {
|
|
129
|
-
result.inserted++
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return result
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function upsertLinkTypes(appId: string, records: any[], dryRun: boolean): Promise<SeedResult> {
|
|
137
|
-
const result: SeedResult = { table: 'link_types', inserted: 0, skipped: 0, errors: [] }
|
|
138
|
-
|
|
139
|
-
for (const record of records) {
|
|
140
|
-
if (dryRun) {
|
|
141
|
-
console.log(` [dry-run] Would upsert link type: ${record.slug}`)
|
|
142
|
-
result.inserted++
|
|
143
|
-
continue
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const { error } = await adminDb
|
|
147
|
-
.from('link_types')
|
|
148
|
-
.upsert({
|
|
149
|
-
...record,
|
|
150
|
-
app_id: appId,
|
|
151
|
-
}, {
|
|
152
|
-
onConflict: 'app_id,slug',
|
|
153
|
-
ignoreDuplicates: false
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
if (error) {
|
|
157
|
-
result.errors.push(`${record.slug}: ${error.message}`)
|
|
158
|
-
} else {
|
|
159
|
-
result.inserted++
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return result
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async function upsertTriggers(appId: string, records: any[], dryRun: boolean): Promise<SeedResult> {
|
|
167
|
-
const result: SeedResult = { table: 'triggers', inserted: 0, skipped: 0, errors: [] }
|
|
168
|
-
|
|
169
|
-
for (const record of records) {
|
|
170
|
-
if (dryRun) {
|
|
171
|
-
console.log(` [dry-run] Would upsert trigger: ${record.name}`)
|
|
172
|
-
result.inserted++
|
|
173
|
-
continue
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Clone config to avoid mutating the original
|
|
177
|
-
const config = record.config ? JSON.parse(JSON.stringify(record.config)) : {}
|
|
178
|
-
|
|
179
|
-
// Resolve type_slug references in filters to type_id
|
|
180
|
-
if (config.filters && Array.isArray(config.filters)) {
|
|
181
|
-
for (const filter of config.filters) {
|
|
182
|
-
if (filter.type_slug && !filter.value) {
|
|
183
|
-
const { data: typeData, error: typeError } = await adminDb
|
|
184
|
-
.from('types')
|
|
185
|
-
.select('id')
|
|
186
|
-
.eq('slug', filter.type_slug)
|
|
187
|
-
.eq('app_id', appId)
|
|
188
|
-
.single()
|
|
189
|
-
|
|
190
|
-
if (typeError || !typeData) {
|
|
191
|
-
result.errors.push(`${record.name}: type_slug '${filter.type_slug}' not found`)
|
|
192
|
-
continue
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
filter.value = typeData.id
|
|
196
|
-
delete filter.type_slug
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Resolve pipeline_slug → pipeline_id for trigger→pipeline wiring
|
|
202
|
-
let pipelineId: string | undefined = record.pipeline_id
|
|
203
|
-
if (!pipelineId && record.pipeline_slug) {
|
|
204
|
-
const { data: pipelineData, error: pipelineError } = await adminDb
|
|
205
|
-
.from('pipelines')
|
|
206
|
-
.select('id')
|
|
207
|
-
.eq('slug', record.pipeline_slug)
|
|
208
|
-
.eq('app_id', appId)
|
|
209
|
-
.maybeSingle()
|
|
210
|
-
|
|
211
|
-
if (pipelineError || !pipelineData) {
|
|
212
|
-
result.errors.push(`${record.name}: pipeline_slug '${record.pipeline_slug}' not found — trigger will be inactive until pipeline is created`)
|
|
213
|
-
} else {
|
|
214
|
-
pipelineId = pipelineData.id
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Build the row — strip pipeline_slug (not a db column), inject resolved pipeline_id
|
|
219
|
-
const { pipeline_slug: _pslug, ...recordWithoutPipelineSlug } = record
|
|
220
|
-
const triggerRow = { ...recordWithoutPipelineSlug, app_id: appId, config }
|
|
221
|
-
if (pipelineId) triggerRow.pipeline_id = pipelineId
|
|
222
|
-
|
|
223
|
-
const { error } = await adminDb
|
|
224
|
-
.from('triggers')
|
|
225
|
-
.upsert(triggerRow, {
|
|
226
|
-
onConflict: 'app_id,name',
|
|
227
|
-
ignoreDuplicates: false
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
if (error) {
|
|
231
|
-
result.errors.push(`${record.name}: ${error.message}`)
|
|
232
|
-
} else {
|
|
233
|
-
result.inserted++
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return result
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
async function upsertAccounts(appId: string, records: any[], dryRun: boolean): Promise<SeedResult> {
|
|
241
|
-
const result: SeedResult = { table: 'accounts', inserted: 0, skipped: 0, errors: [] }
|
|
242
|
-
|
|
243
|
-
for (const record of records) {
|
|
244
|
-
if (dryRun) {
|
|
245
|
-
console.log(` [dry-run] Would upsert account: ${record.slug}`)
|
|
246
|
-
result.inserted++
|
|
247
|
-
continue
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Resolve type_slug → type_id if present
|
|
251
|
-
const row = { ...record, app_id: appId }
|
|
252
|
-
if (row.type_slug && !row.type_id) {
|
|
253
|
-
const { data: typeData } = await adminDb
|
|
254
|
-
.from('types')
|
|
255
|
-
.select('id')
|
|
256
|
-
.eq('kind', 'account')
|
|
257
|
-
.eq('slug', row.type_slug)
|
|
258
|
-
.limit(1)
|
|
259
|
-
.single()
|
|
260
|
-
if (typeData) {
|
|
261
|
-
row.type_id = typeData.id
|
|
262
|
-
} else {
|
|
263
|
-
result.errors.push(`${record.slug}: type_slug '${row.type_slug}' not found`)
|
|
264
|
-
continue
|
|
265
|
-
}
|
|
266
|
-
delete row.type_slug
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const { error } = await adminDb
|
|
270
|
-
.from('accounts')
|
|
271
|
-
.upsert(row, {
|
|
272
|
-
onConflict: 'slug',
|
|
273
|
-
ignoreDuplicates: false
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
if (error) {
|
|
277
|
-
result.errors.push(`${record.slug}: ${error.message}`)
|
|
278
|
-
} else {
|
|
279
|
-
result.inserted++
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return result
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
async function upsertPipelines(appId: string, records: any[], dryRun: boolean): Promise<SeedResult> {
|
|
287
|
-
const result: SeedResult = { table: 'pipelines', inserted: 0, skipped: 0, errors: [] }
|
|
288
|
-
|
|
289
|
-
for (const record of records) {
|
|
290
|
-
if (dryRun) {
|
|
291
|
-
console.log(` [dry-run] Would upsert pipeline: ${record.slug || record.name}`)
|
|
292
|
-
result.inserted++
|
|
293
|
-
continue
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Prefer slug-based conflict resolution; fall back to name if no slug
|
|
297
|
-
const conflictKey = record.slug ? 'app_id,slug' : 'app_id,name'
|
|
298
|
-
|
|
299
|
-
const { error } = await adminDb
|
|
300
|
-
.from('pipelines')
|
|
301
|
-
.upsert({
|
|
302
|
-
...record,
|
|
303
|
-
app_id: appId,
|
|
304
|
-
}, {
|
|
305
|
-
onConflict: conflictKey,
|
|
306
|
-
ignoreDuplicates: false
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
if (error) {
|
|
310
|
-
result.errors.push(`${record.slug}: ${error.message}`)
|
|
311
|
-
} else {
|
|
312
|
-
result.inserted++
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return result
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
async function upsertRoles(appId: string, records: any[], dryRun: boolean): Promise<SeedResult> {
|
|
320
|
-
const result: SeedResult = { table: 'roles', inserted: 0, skipped: 0, errors: [] }
|
|
321
|
-
|
|
322
|
-
for (const record of records) {
|
|
323
|
-
if (dryRun) {
|
|
324
|
-
console.log(` [dry-run] Would upsert role: ${record.slug}`)
|
|
325
|
-
result.inserted++
|
|
326
|
-
continue
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const { error } = await adminDb
|
|
330
|
-
.from('roles')
|
|
331
|
-
.upsert({
|
|
332
|
-
...record,
|
|
333
|
-
app_id: appId,
|
|
334
|
-
}, {
|
|
335
|
-
onConflict: 'app_id,slug',
|
|
336
|
-
ignoreDuplicates: false
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
if (error) {
|
|
340
|
-
result.errors.push(`${record.slug}: ${error.message}`)
|
|
341
|
-
} else {
|
|
342
|
-
result.inserted++
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return result
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
async function upsertIntegrations(appId: string, records: any[], dryRun: boolean): Promise<SeedResult> {
|
|
350
|
-
const result: SeedResult = { table: 'integrations', inserted: 0, skipped: 0, errors: [] }
|
|
351
|
-
|
|
352
|
-
for (const record of records) {
|
|
353
|
-
if (dryRun) {
|
|
354
|
-
console.log(` [dry-run] Would upsert integration: ${record.name}`)
|
|
355
|
-
result.inserted++
|
|
356
|
-
continue
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Only include fields that exist in the database schema
|
|
360
|
-
const cleanRecord = {
|
|
361
|
-
name: record.name,
|
|
362
|
-
slug: record.slug,
|
|
363
|
-
provider: record.provider,
|
|
364
|
-
status: record.status,
|
|
365
|
-
is_active: record.is_active,
|
|
366
|
-
config: record.config,
|
|
367
|
-
app_id: appId,
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const { error } = await adminDb
|
|
371
|
-
.from('integrations')
|
|
372
|
-
.upsert(cleanRecord, {
|
|
373
|
-
onConflict: 'slug',
|
|
374
|
-
ignoreDuplicates: false
|
|
375
|
-
})
|
|
376
|
-
|
|
377
|
-
if (error) {
|
|
378
|
-
result.errors.push(`${record.slug}: ${error.message}`)
|
|
379
|
-
} else {
|
|
380
|
-
result.inserted++
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
return result
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const SEED_HANDLERS: Record<string, (appId: string, records: any[], dryRun: boolean) => Promise<SeedResult>> = {
|
|
388
|
-
'types.json': upsertTypes,
|
|
389
|
-
'link-types.json': upsertLinkTypes,
|
|
390
|
-
'triggers.json': upsertTriggers,
|
|
391
|
-
'accounts.json': upsertAccounts,
|
|
392
|
-
'pipelines.json': upsertPipelines,
|
|
393
|
-
'roles.json': upsertRoles,
|
|
394
|
-
'integrations.json': upsertIntegrations,
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
async function recordInstallation(
|
|
398
|
-
appSlug: string,
|
|
399
|
-
accountId: string | null,
|
|
400
|
-
dryRun: boolean,
|
|
401
|
-
installedVersion?: string,
|
|
402
|
-
packageName?: string,
|
|
403
|
-
): Promise<void> {
|
|
404
|
-
if (dryRun) {
|
|
405
|
-
console.log(` [dry-run] Would record installation for '${appSlug}' (version: ${installedVersion || 'unknown'})`)
|
|
406
|
-
return
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const { error } = await adminDb
|
|
410
|
-
.from('app_installations')
|
|
411
|
-
.upsert({
|
|
412
|
-
app_slug: appSlug,
|
|
413
|
-
account_id: accountId,
|
|
414
|
-
is_enabled: true,
|
|
415
|
-
installed_version: installedVersion || null,
|
|
416
|
-
package_name: packageName || null,
|
|
417
|
-
updated_at: new Date().toISOString(),
|
|
418
|
-
}, {
|
|
419
|
-
onConflict: 'app_slug,account_id',
|
|
420
|
-
})
|
|
421
|
-
|
|
422
|
-
if (error) {
|
|
423
|
-
console.log(` ⚠️ Failed to record installation: ${error.message}`)
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function isNpmPackage(slug: string): boolean {
|
|
428
|
-
return slug.includes('/') || slug.startsWith('spine-framework-') || slug.startsWith('@')
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function resolveSlugFromPackage(pkg: string): string {
|
|
432
|
-
const manifest = resolve(PROJECT_ROOT, `node_modules/${pkg}/spine-manifest.json`)
|
|
433
|
-
if (existsSync(manifest)) {
|
|
434
|
-
try {
|
|
435
|
-
return JSON.parse(readFileSync(manifest, 'utf8')).slug
|
|
436
|
-
} catch { /* fall through */ }
|
|
437
|
-
}
|
|
438
|
-
const pkgJson = resolve(PROJECT_ROOT, `node_modules/${pkg}/package.json`)
|
|
439
|
-
if (existsSync(pkgJson)) {
|
|
440
|
-
try {
|
|
441
|
-
const meta = JSON.parse(readFileSync(pkgJson, 'utf8'))
|
|
442
|
-
if (meta.spine?.slug) return meta.spine.slug
|
|
443
|
-
} catch { /* fall through */ }
|
|
444
|
-
}
|
|
445
|
-
return pkg.replace(/^spine-framework-/, '').replace(/^@[^/]+\//, '')
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
async function checkPrerequisites(_slug: string): Promise<void> {
|
|
449
|
-
// Apps are independently installable — no hardcoded prereqs.
|
|
450
|
-
// Future: read prereqs from the app's manifest.json if needed.
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function copyAppFiles(pkg: string, slug: string, force: boolean): void {
|
|
454
|
-
const pkgDir = resolve(PROJECT_ROOT, `node_modules/${pkg}`)
|
|
455
|
-
const targetAppDir = resolve(PROJECT_ROOT, `custom/apps/${slug}`)
|
|
456
|
-
|
|
457
|
-
// Guard: abort if already installed and --force not passed
|
|
458
|
-
if (existsSync(targetAppDir) && statSync(targetAppDir).isDirectory()) {
|
|
459
|
-
const hasFiles = readdirSync(targetAppDir).length > 0
|
|
460
|
-
if (hasFiles && !force) {
|
|
461
|
-
console.error(` ❌ App '${slug}' is already installed at custom/apps/${slug}/`)
|
|
462
|
-
console.error(` Use 'update-app ${slug}' to upgrade, or 'install-app --force' to overwrite.`)
|
|
463
|
-
process.exit(1)
|
|
464
|
-
}
|
|
465
|
-
if (hasFiles && force) {
|
|
466
|
-
console.log(` ⚠️ --force: overwriting existing custom/apps/${slug}/`)
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
mkdirSync(targetAppDir, { recursive: true })
|
|
471
|
-
|
|
472
|
-
// Always copy core app files
|
|
473
|
-
const appFiles = ['index.tsx', 'manifest.json', 'package.json']
|
|
474
|
-
for (const file of appFiles) {
|
|
475
|
-
const src = resolve(pkgDir, file)
|
|
476
|
-
if (existsSync(src)) {
|
|
477
|
-
cpSync(src, resolve(targetAppDir, file), { force: true })
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Read directories from manifest (self-contained apps)
|
|
482
|
-
const manifestPath = resolve(pkgDir, 'manifest.json')
|
|
483
|
-
let appDirs = ['pages', 'components', 'hooks', 'config', 'seed', 'api', 'functions', 'migrations', 'tests']
|
|
484
|
-
|
|
485
|
-
if (existsSync(manifestPath)) {
|
|
486
|
-
try {
|
|
487
|
-
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'))
|
|
488
|
-
if (manifest.directories && Array.isArray(manifest.directories)) {
|
|
489
|
-
appDirs = manifest.directories
|
|
490
|
-
}
|
|
491
|
-
} catch (e) {
|
|
492
|
-
console.warn(` ⚠️ Could not read manifest.json, using default directories`)
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Copy app directories (all self-contained within the app)
|
|
497
|
-
for (const dir of appDirs) {
|
|
498
|
-
const src = resolve(pkgDir, dir)
|
|
499
|
-
if (existsSync(src)) {
|
|
500
|
-
cpSync(src, resolve(targetAppDir, dir), { recursive: true, force: true })
|
|
501
|
-
console.log(` ✓ Copied ${dir}/ → custom/apps/${slug}/${dir}/`)
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
console.log(` ✓ Ejected app files → custom/apps/${slug}/`)
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
async function installApp(slug: string, options: InstallOptions): Promise<void> {
|
|
509
|
-
console.log(`\n📦 Installing app '${slug}'...\n`)
|
|
510
|
-
|
|
511
|
-
let resolvedSlug = slug
|
|
512
|
-
let packageName: string | null = null
|
|
513
|
-
|
|
514
|
-
if (isNpmPackage(slug)) {
|
|
515
|
-
// Strip version specifier (e.g. spine-framework-cortex@latest → spine-framework-cortex)
|
|
516
|
-
// Handles: pkg@latest, pkg@1.2.3, @scope/pkg@latest (preserve leading @ for scoped packages)
|
|
517
|
-
const barePackageName = slug.startsWith('@')
|
|
518
|
-
? slug.replace(/(@[^/]+\/[^@]+)@.*$/, '$1') // @scope/pkg@version → @scope/pkg
|
|
519
|
-
: slug.replace(/@.*$/, '') // pkg@version → pkg
|
|
520
|
-
packageName = barePackageName
|
|
521
|
-
console.log(`📥 Step 1: Installing npm package...`)
|
|
522
|
-
const pkgDir = resolve(PROJECT_ROOT, `node_modules/${barePackageName}`)
|
|
523
|
-
// Always run npm install when a version specifier is given (e.g. @1.2.3 or @latest)
|
|
524
|
-
// so the correct version is fetched even if an older one is already in node_modules.
|
|
525
|
-
const hasVersionSpec = slug.includes('@') && !slug.startsWith('@') || (slug.startsWith('@') && slug.split('@').length > 2)
|
|
526
|
-
const shouldInstall = !existsSync(pkgDir) || hasVersionSpec
|
|
527
|
-
if (shouldInstall) {
|
|
528
|
-
try {
|
|
529
|
-
console.log(` Running: npm install ${slug}`)
|
|
530
|
-
execSync(`npm install ${slug}`, { cwd: PROJECT_ROOT, stdio: 'inherit' })
|
|
531
|
-
console.log(` ✓ Package installed`)
|
|
532
|
-
} catch (err: any) {
|
|
533
|
-
console.error(` ❌ npm install failed: ${err.message}`)
|
|
534
|
-
process.exit(1)
|
|
535
|
-
}
|
|
536
|
-
} else {
|
|
537
|
-
console.log(` ⏭️ Package already in node_modules`)
|
|
538
|
-
}
|
|
539
|
-
resolvedSlug = resolveSlugFromPackage(barePackageName)
|
|
540
|
-
console.log(` ✓ App slug: ${resolvedSlug}`)
|
|
541
|
-
|
|
542
|
-
console.log(`\n📁 Step 2: Ejecting app files...`)
|
|
543
|
-
copyAppFiles(barePackageName, resolvedSlug, options.force)
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// Check prerequisites (e.g. portal requires cortex)
|
|
547
|
-
await checkPrerequisites(resolvedSlug)
|
|
548
|
-
|
|
549
|
-
const appDir = resolve(PROJECT_ROOT, `custom/apps/${resolvedSlug}`)
|
|
550
|
-
if (!existsSync(appDir)) {
|
|
551
|
-
console.error(` ❌ App directory not found: custom/apps/${resolvedSlug}`)
|
|
552
|
-
console.error(` Install the npm package first or create the app with 'spine-framework create-app ${resolvedSlug}'`)
|
|
553
|
-
process.exit(1)
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// 2. Read manifest for display name
|
|
557
|
-
const manifestPath = resolve(appDir, 'manifest.json')
|
|
558
|
-
let appName = slug
|
|
559
|
-
if (existsSync(manifestPath)) {
|
|
560
|
-
try {
|
|
561
|
-
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
562
|
-
appName = manifest.name || slug
|
|
563
|
-
console.log(` 📋 Manifest: ${appName} v${manifest.version || '0.0.0'}`)
|
|
564
|
-
} catch {
|
|
565
|
-
console.log(` ⚠️ Could not parse manifest.json, using slug as name`)
|
|
566
|
-
}
|
|
567
|
-
} else {
|
|
568
|
-
console.log(` ⚠️ No manifest.json found, using slug as name`)
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// 3. Ensure app record in DB
|
|
572
|
-
const appId = await ensureAppRecord(resolvedSlug, appName)
|
|
573
|
-
|
|
574
|
-
// 4. Process seed files
|
|
575
|
-
const seedDir = resolve(appDir, 'seed')
|
|
576
|
-
if (!existsSync(seedDir)) {
|
|
577
|
-
console.log(` ⏭️ No seed/ directory — skipping data seeding`)
|
|
578
|
-
} else {
|
|
579
|
-
console.log(`\n🌱 Applying seed data...`)
|
|
580
|
-
const SEED_ORDER = ['accounts.json', 'types.json', 'link-types.json', 'roles.json', 'pipelines.json', 'triggers.json']
|
|
581
|
-
const allFiles = readdirSync(seedDir).filter(f => f.endsWith('.json'))
|
|
582
|
-
const seedFiles = [
|
|
583
|
-
...SEED_ORDER.filter(f => allFiles.includes(f)),
|
|
584
|
-
...allFiles.filter(f => !SEED_ORDER.includes(f)),
|
|
585
|
-
]
|
|
586
|
-
|
|
587
|
-
let totalInserted = 0
|
|
588
|
-
let totalErrors = 0
|
|
589
|
-
|
|
590
|
-
for (const file of seedFiles) {
|
|
591
|
-
const handler = SEED_HANDLERS[file]
|
|
592
|
-
if (!handler) {
|
|
593
|
-
console.log(` ⏭️ Skipping unknown seed file: ${file}`)
|
|
594
|
-
continue
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const filePath = resolve(seedDir, file)
|
|
598
|
-
let records: any[]
|
|
599
|
-
try {
|
|
600
|
-
records = JSON.parse(readFileSync(filePath, 'utf8'))
|
|
601
|
-
if (!Array.isArray(records)) {
|
|
602
|
-
console.log(` ⚠️ ${file}: expected array, skipping`)
|
|
603
|
-
continue
|
|
604
|
-
}
|
|
605
|
-
} catch (err: any) {
|
|
606
|
-
console.error(` ❌ ${file}: parse error — ${err.message}`)
|
|
607
|
-
continue
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
console.log(` 📄 ${file} (${records.length} records)...`)
|
|
611
|
-
const result = await handler(appId, records, options.dryRun)
|
|
612
|
-
|
|
613
|
-
if (result.errors.length > 0) {
|
|
614
|
-
for (const e of result.errors) {
|
|
615
|
-
console.error(` ❌ ${e}`)
|
|
616
|
-
}
|
|
617
|
-
totalErrors += result.errors.length
|
|
618
|
-
}
|
|
619
|
-
totalInserted += result.inserted
|
|
620
|
-
console.log(` ✓ ${result.inserted} upserted`)
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
console.log(`\n Total: ${totalInserted} records upserted, ${totalErrors} errors`)
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// 5. Record installation
|
|
627
|
-
console.log(`\n📝 Recording installation...`)
|
|
628
|
-
let installedVersion: string | undefined
|
|
629
|
-
const manifestForVersion = resolve(appDir, 'manifest.json')
|
|
630
|
-
if (existsSync(manifestForVersion)) {
|
|
631
|
-
try {
|
|
632
|
-
installedVersion = JSON.parse(readFileSync(manifestForVersion, 'utf8')).version
|
|
633
|
-
} catch { /* ignore */ }
|
|
634
|
-
}
|
|
635
|
-
await recordInstallation(resolvedSlug, options.account || null, options.dryRun, installedVersion, packageName || undefined)
|
|
636
|
-
|
|
637
|
-
// 6. Signal integration (only on new installs, not --force overwrites)
|
|
638
|
-
if (!options.force && !options.dryRun) {
|
|
639
|
-
console.log(`\n📡 Sending install signal...`)
|
|
640
|
-
try {
|
|
641
|
-
// Ensure framework serial exists
|
|
642
|
-
ensureSerialData()
|
|
643
|
-
// Record engagement
|
|
644
|
-
recordEngagementSignal('app_install')
|
|
645
|
-
// Send app install signal
|
|
646
|
-
if (packageName) {
|
|
647
|
-
await sendAppInstallSignal(packageName, installedVersion || '0.0.0')
|
|
648
|
-
}
|
|
649
|
-
console.log(` ✓ Install signal sent`)
|
|
650
|
-
|
|
651
|
-
// Check for claim prompt
|
|
652
|
-
const { shouldOfferClaim } = await import('./claim.ts')
|
|
653
|
-
const prompt = shouldOfferClaim()
|
|
654
|
-
if (prompt) {
|
|
655
|
-
console.log(`\n💡 ${prompt.message}`)
|
|
656
|
-
console.log(` Run: spine-framework claim`)
|
|
657
|
-
}
|
|
658
|
-
} catch {
|
|
659
|
-
// Silent fail - signal tracking is best-effort
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
console.log(`\n✅ App '${appName}' installed successfully!`)
|
|
664
|
-
console.log(`\n Next steps:`)
|
|
665
|
-
console.log(` 1. npm run assemble && netlify dev`)
|
|
666
|
-
console.log(` 2. Navigate to /${resolvedSlug} in your browser`)
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
export function registerInstallAppCommands(program: Command) {
|
|
670
|
-
program
|
|
671
|
-
.command('install-app <slug>')
|
|
672
|
-
.description('Install an app by applying its seed data to the database')
|
|
673
|
-
.option('--account <id>', 'Account ID to associate the installation with')
|
|
674
|
-
.option('--force', 'Overwrite existing seed data', false)
|
|
675
|
-
.option('--dry-run', 'Show what would happen without making changes', false)
|
|
676
|
-
.action(async (slug, opts) => {
|
|
677
|
-
try {
|
|
678
|
-
await installApp(slug, opts)
|
|
679
|
-
} catch (err: any) {
|
|
680
|
-
console.error('Error:', err.message)
|
|
681
|
-
if (process.env.SPINE_CLI_DEBUG) console.error(err.stack)
|
|
682
|
-
process.exit(1)
|
|
683
|
-
}
|
|
684
|
-
})
|
|
685
|
-
}
|