spine-framework 0.1.61 → 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.
Files changed (246) hide show
  1. package/.framework/cli/commands/create-app.ts +1 -1
  2. package/.framework/cli/commands/dev.ts +215 -33
  3. package/.framework/cli/index.ts +37 -63
  4. package/.framework/functions/_shared/app-manifest.ts +8 -15
  5. package/.framework/functions/_shared/db.ts +11 -15
  6. package/.framework/functions/_shared/index.ts +0 -9
  7. package/.framework/functions/_shared/principal.ts +4 -62
  8. package/.framework/functions/_shared/resolve-ids.ts +6 -6
  9. package/.framework/functions/_shared/webhook-registry.ts +1 -1
  10. package/.framework/functions/admin-funnel.ts +104 -0
  11. package/.framework/functions/admin-install-funnel.ts +225 -0
  12. package/.framework/functions/admin-signals.ts +241 -0
  13. package/.framework/migrations/001_schema.sql +406 -0
  14. package/.framework/migrations/002_rls_and_functions.sql +710 -0
  15. package/.framework/migrations/003_system_data.sql +520 -0
  16. package/.framework/migrations/004_enhancements.sql +43 -0
  17. package/.framework/public/_redirects +3 -0
  18. package/.framework/public/spine-logo.jpg +0 -0
  19. package/.framework/public/spine-logo.svg +3 -0
  20. package/{scripts → .framework/scripts}/assemble-frontend.sh +1 -1
  21. package/{scripts → .framework/scripts}/assemble-functions.sh +1 -1
  22. package/{scripts → .framework/scripts}/assemble.sh +2 -8
  23. package/{scripts → .framework/scripts}/check-core-integrity.sh +0 -0
  24. package/.framework/seeds/link_types/account_cli_instances.json +9 -0
  25. package/.framework/seeds/types/funnel_signal_use.json +98 -0
  26. package/.framework/seeds/types/spine_instance.json +109 -0
  27. package/.framework/src/App.tsx +6 -4
  28. package/.framework/src/components/ui/accordion.tsx +1 -1
  29. package/.framework/src/components/ui/alert-dialog.tsx +1 -1
  30. package/.framework/src/components/ui/aspect-ratio.tsx +1 -1
  31. package/.framework/src/components/ui/avatar.tsx +1 -1
  32. package/.framework/src/components/ui/badge.tsx +2 -2
  33. package/.framework/src/components/ui/breadcrumb.tsx +2 -2
  34. package/.framework/src/components/ui/button-group.tsx +2 -2
  35. package/.framework/src/components/ui/button.tsx +2 -2
  36. package/.framework/src/components/ui/chart.tsx +6 -1
  37. package/.framework/src/components/ui/checkbox.tsx +1 -1
  38. package/.framework/src/components/ui/collapsible.tsx +1 -1
  39. package/.framework/src/components/ui/context-menu.tsx +1 -1
  40. package/.framework/src/components/ui/dialog.tsx +1 -1
  41. package/.framework/src/components/ui/direction.tsx +17 -1
  42. package/.framework/src/components/ui/dropdown-menu.tsx +1 -1
  43. package/.framework/src/components/ui/hover-card.tsx +1 -1
  44. package/.framework/src/components/ui/item.tsx +2 -2
  45. package/.framework/src/components/ui/label.tsx +1 -1
  46. package/.framework/src/components/ui/menubar.tsx +1 -1
  47. package/.framework/src/components/ui/navigation-menu.tsx +1 -1
  48. package/.framework/src/components/ui/popover.tsx +1 -1
  49. package/.framework/src/components/ui/progress.tsx +1 -1
  50. package/.framework/src/components/ui/radio-group.tsx +1 -1
  51. package/.framework/src/components/ui/scroll-area.tsx +1 -1
  52. package/.framework/src/components/ui/select.tsx +1 -1
  53. package/.framework/src/components/ui/separator.tsx +1 -1
  54. package/.framework/src/components/ui/sheet.tsx +1 -1
  55. package/.framework/src/components/ui/sidebar.tsx +6 -6
  56. package/.framework/src/components/ui/slider.tsx +1 -1
  57. package/.framework/src/components/ui/switch.tsx +1 -1
  58. package/.framework/src/components/ui/tabs.tsx +1 -1
  59. package/.framework/src/components/ui/toggle-group.tsx +1 -1
  60. package/.framework/src/components/ui/toggle.tsx +1 -1
  61. package/.framework/src/components/ui/tooltip.tsx +1 -1
  62. package/.framework/src/pages/auth/LoginPage.tsx +10 -9
  63. package/LICENSE.md +193 -0
  64. package/README.md +254 -0
  65. package/custom/.devin/skills/install-app/SKILL.md +39 -0
  66. package/custom/.devin/workflows/app-install.yaml +83 -0
  67. package/custom/README.md +79 -0
  68. package/docs/APPS.md +232 -0
  69. package/docs/ARCHITECTURE.md +246 -0
  70. package/docs/INSTALL.md +155 -0
  71. package/docs/README.md +170 -0
  72. package/docs/design-schema-spec.md +1837 -0
  73. package/docs/dev-guides/01-create-an-app.md +183 -0
  74. package/docs/dev-guides/02-create-a-function.md +178 -0
  75. package/docs/dev-guides/03-create-a-component.md +178 -0
  76. package/docs/dev-guides/04-webhook-handlers.md +129 -0
  77. package/docs/dev-guides/05-testing.md +168 -0
  78. package/docs/dev-guides/README.md +40 -0
  79. package/docs/npm-packages-plan.md +288 -0
  80. package/docs/spine-core--api-reference.md +822 -0
  81. package/package.json +78 -144
  82. package/.framework/cli/bin.cjs +0 -14
  83. package/.framework/cli/commands/init.ts +0 -186
  84. package/.framework/cli/commands/install-app.ts +0 -565
  85. package/.framework/cli/commands/migrate.ts +0 -139
  86. package/.framework/cli/commands/status.ts +0 -197
  87. package/.framework/cli/commands/uninstall-app.ts +0 -166
  88. package/.framework/cli/welcome.cjs +0 -45
  89. package/.framework/docs/STABILITY.md +0 -52
  90. package/.framework/docs/examples/custom-app/README.md +0 -77
  91. package/.framework/docs/examples/custom-function/README.md +0 -27
  92. package/.framework/docs/examples/custom-function/handler.ts +0 -48
  93. package/.framework/docs/examples/custom-webhook/README.md +0 -68
  94. package/.framework/migrations/000_foundation.sql +0 -1266
  95. package/.framework/migrations/001_seed.sql +0 -163
  96. package/.framework/migrations/002_seed_constraints.sql +0 -19
  97. package/.framework/migrations/003_auth_user_trigger.sql +0 -67
  98. package/.framework/src/pages/auth/RegisterPage.tsx +0 -163
  99. package/STRUCTURE.md +0 -150
  100. package/bin/spine-framework.cjs +0 -62
  101. package/bin/welcome.cjs +0 -45
  102. package/bin/ws-shim.cjs +0 -8
  103. package/bin/ws-shim.ts +0 -10
  104. package/config/components.json +0 -25
  105. package/config/deno.lock +0 -108
  106. package/config/package-lock.json +0 -17183
  107. package/config/postcss.config.cjs +0 -10
  108. package/config/tailwind.config.cjs +0 -78
  109. package/config/tsconfig.build.json +0 -32
  110. package/config/tsconfig.cli.json +0 -18
  111. package/config/tsconfig.json +0 -41
  112. package/config/tsconfig.node.json +0 -17
  113. package/config/tsconfig.node.tsbuildinfo +0 -1
  114. package/config/tsconfig.tsbuildinfo +0 -1
  115. package/config/typedoc.json +0 -16
  116. package/config/vite.config.d.ts +0 -2
  117. package/config/vite.config.ts +0 -72
  118. package/dist/cli/commands/agents.d.ts +0 -39
  119. package/dist/cli/commands/agents.d.ts.map +0 -1
  120. package/dist/cli/commands/auth.d.ts +0 -36
  121. package/dist/cli/commands/auth.d.ts.map +0 -1
  122. package/dist/cli/commands/create-app.d.ts +0 -23
  123. package/dist/cli/commands/create-app.d.ts.map +0 -1
  124. package/dist/cli/commands/dev.d.ts +0 -24
  125. package/dist/cli/commands/dev.d.ts.map +0 -1
  126. package/dist/cli/commands/doctor.d.ts +0 -42
  127. package/dist/cli/commands/doctor.d.ts.map +0 -1
  128. package/dist/cli/commands/generate.d.ts +0 -36
  129. package/dist/cli/commands/generate.d.ts.map +0 -1
  130. package/dist/cli/commands/init.d.ts +0 -20
  131. package/dist/cli/commands/init.d.ts.map +0 -1
  132. package/dist/cli/commands/install-app.d.ts +0 -30
  133. package/dist/cli/commands/install-app.d.ts.map +0 -1
  134. package/dist/cli/commands/items.d.ts +0 -45
  135. package/dist/cli/commands/items.d.ts.map +0 -1
  136. package/dist/cli/commands/migrate.d.ts +0 -21
  137. package/dist/cli/commands/migrate.d.ts.map +0 -1
  138. package/dist/cli/commands/migrations.d.ts +0 -41
  139. package/dist/cli/commands/migrations.d.ts.map +0 -1
  140. package/dist/cli/commands/pipelines.d.ts +0 -40
  141. package/dist/cli/commands/pipelines.d.ts.map +0 -1
  142. package/dist/cli/commands/status.d.ts +0 -23
  143. package/dist/cli/commands/status.d.ts.map +0 -1
  144. package/dist/cli/commands/system.d.ts +0 -29
  145. package/dist/cli/commands/system.d.ts.map +0 -1
  146. package/dist/cli/commands/test.d.ts +0 -46
  147. package/dist/cli/commands/test.d.ts.map +0 -1
  148. package/dist/cli/commands/uninstall-app.d.ts +0 -23
  149. package/dist/cli/commands/uninstall-app.d.ts.map +0 -1
  150. package/dist/cli/context.d.ts +0 -88
  151. package/dist/cli/context.d.ts.map +0 -1
  152. package/dist/cli/env-loader.d.ts +0 -14
  153. package/dist/cli/env-loader.d.ts.map +0 -1
  154. package/dist/cli/index.d.ts +0 -41
  155. package/dist/cli/index.d.ts.map +0 -1
  156. package/dist/functions/_shared/agent-runner.d.ts +0 -156
  157. package/dist/functions/_shared/agent-runner.d.ts.map +0 -1
  158. package/dist/functions/_shared/app-manifest.d.ts +0 -68
  159. package/dist/functions/_shared/app-manifest.d.ts.map +0 -1
  160. package/dist/functions/_shared/audit.d.ts +0 -91
  161. package/dist/functions/_shared/audit.d.ts.map +0 -1
  162. package/dist/functions/_shared/db.d.ts +0 -125
  163. package/dist/functions/_shared/db.d.ts.map +0 -1
  164. package/dist/functions/_shared/index.d.ts +0 -299
  165. package/dist/functions/_shared/index.d.ts.map +0 -1
  166. package/dist/functions/_shared/middleware.d.ts +0 -315
  167. package/dist/functions/_shared/middleware.d.ts.map +0 -1
  168. package/dist/functions/_shared/permissions.d.ts +0 -626
  169. package/dist/functions/_shared/permissions.d.ts.map +0 -1
  170. package/dist/functions/_shared/pipeline-runner.d.ts +0 -124
  171. package/dist/functions/_shared/pipeline-runner.d.ts.map +0 -1
  172. package/dist/functions/_shared/principal.d.ts +0 -284
  173. package/dist/functions/_shared/principal.d.ts.map +0 -1
  174. package/dist/functions/_shared/resolve-ids.d.ts +0 -10
  175. package/dist/functions/_shared/resolve-ids.d.ts.map +0 -1
  176. package/dist/functions/_shared/schema-utils.d.ts +0 -181
  177. package/dist/functions/_shared/schema-utils.d.ts.map +0 -1
  178. package/dist/functions/_shared/testing.d.ts +0 -172
  179. package/dist/functions/_shared/testing.d.ts.map +0 -1
  180. package/dist/functions/_shared/trigger-engine.d.ts +0 -140
  181. package/dist/functions/_shared/trigger-engine.d.ts.map +0 -1
  182. package/dist/functions/_shared/webhook-registration.d.ts +0 -81
  183. package/dist/functions/_shared/webhook-registration.d.ts.map +0 -1
  184. package/dist/functions/_shared/webhook-registry.d.ts +0 -57
  185. package/dist/functions/_shared/webhook-registry.d.ts.map +0 -1
  186. package/dist/functions/account-nodes.d.ts +0 -48
  187. package/dist/functions/account-nodes.d.ts.map +0 -1
  188. package/dist/functions/admin-data.d.ts +0 -178
  189. package/dist/functions/admin-data.d.ts.map +0 -1
  190. package/dist/functions/ai-agents.d.ts +0 -125
  191. package/dist/functions/ai-agents.d.ts.map +0 -1
  192. package/dist/functions/api-keys.d.ts +0 -140
  193. package/dist/functions/api-keys.d.ts.map +0 -1
  194. package/dist/functions/apps.d.ts +0 -163
  195. package/dist/functions/apps.d.ts.map +0 -1
  196. package/dist/functions/auth.d.ts +0 -74
  197. package/dist/functions/auth.d.ts.map +0 -1
  198. package/dist/functions/debug-auth.d.ts +0 -33
  199. package/dist/functions/debug-auth.d.ts.map +0 -1
  200. package/dist/functions/embeddings.d.ts +0 -205
  201. package/dist/functions/embeddings.d.ts.map +0 -1
  202. package/dist/functions/integration-routes.d.ts +0 -45
  203. package/dist/functions/integration-routes.d.ts.map +0 -1
  204. package/dist/functions/integrations.d.ts +0 -124
  205. package/dist/functions/integrations.d.ts.map +0 -1
  206. package/dist/functions/item-progress.d.ts +0 -41
  207. package/dist/functions/item-progress.d.ts.map +0 -1
  208. package/dist/functions/logs.d.ts +0 -162
  209. package/dist/functions/logs.d.ts.map +0 -1
  210. package/dist/functions/observability.d.ts +0 -123
  211. package/dist/functions/observability.d.ts.map +0 -1
  212. package/dist/functions/pipeline-executions.d.ts +0 -190
  213. package/dist/functions/pipeline-executions.d.ts.map +0 -1
  214. package/dist/functions/pipelines.d.ts +0 -171
  215. package/dist/functions/pipelines.d.ts.map +0 -1
  216. package/dist/functions/prompt-configs.d.ts +0 -125
  217. package/dist/functions/prompt-configs.d.ts.map +0 -1
  218. package/dist/functions/roles.d.ts +0 -118
  219. package/dist/functions/roles.d.ts.map +0 -1
  220. package/dist/functions/system-cron.d.ts +0 -65
  221. package/dist/functions/system-cron.d.ts.map +0 -1
  222. package/dist/functions/system.d.ts +0 -29
  223. package/dist/functions/system.d.ts.map +0 -1
  224. package/dist/functions/tests.d.ts +0 -28
  225. package/dist/functions/tests.d.ts.map +0 -1
  226. package/dist/functions/timers.d.ts +0 -139
  227. package/dist/functions/timers.d.ts.map +0 -1
  228. package/dist/functions/triggers.d.ts +0 -203
  229. package/dist/functions/triggers.d.ts.map +0 -1
  230. package/dist/functions/types.d.ts +0 -151
  231. package/dist/functions/types.d.ts.map +0 -1
  232. package/dist/src/types/types.d.ts +0 -364
  233. package/dist/src/types/types.d.ts.map +0 -1
  234. package/index.html +0 -13
  235. package/netlify.toml +0 -36
  236. package/package-project.json +0 -64
  237. package/scripts/ingest-chunks.cjs +0 -202
  238. package/scripts/kb-chunk-parser.cjs +0 -312
  239. package/scripts/kb-chunk-parser.ts +0 -330
  240. package/scripts/load-test-app-install.ts +0 -484
  241. package/scripts/netlify-dev-wrapper.sh +0 -22
  242. package/vitest.config.ts +0 -45
  243. /package/{scripts → .framework/scripts}/app-install-cli.ts +0 -0
  244. /package/{scripts → .framework/scripts}/boundary-check.sh +0 -0
  245. /package/{scripts → .framework/scripts}/build-manifest.sh +0 -0
  246. /package/{scripts → .framework/scripts}/verify-integrity.sh +0 -0
@@ -1,565 +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 SEED_ORDER = ['accounts.json', 'types.json', 'link-types.json', 'roles.json', 'pipelines.json', 'triggers.json']
494
- const allFiles = readdirSync(seedDir).filter(f => f.endsWith('.json'))
495
- const seedFiles = [
496
- ...SEED_ORDER.filter(f => allFiles.includes(f)),
497
- ...allFiles.filter(f => !SEED_ORDER.includes(f)),
498
- ]
499
-
500
- let totalInserted = 0
501
- let totalErrors = 0
502
-
503
- for (const file of seedFiles) {
504
- const handler = SEED_HANDLERS[file]
505
- if (!handler) {
506
- console.log(` ⏭️ Skipping unknown seed file: ${file}`)
507
- continue
508
- }
509
-
510
- const filePath = resolve(seedDir, file)
511
- let records: any[]
512
- try {
513
- records = JSON.parse(readFileSync(filePath, 'utf8'))
514
- if (!Array.isArray(records)) {
515
- console.log(` ⚠️ ${file}: expected array, skipping`)
516
- continue
517
- }
518
- } catch (err: any) {
519
- console.error(` ❌ ${file}: parse error — ${err.message}`)
520
- continue
521
- }
522
-
523
- console.log(` 📄 ${file} (${records.length} records)...`)
524
- const result = await handler(appId, records, options.dryRun)
525
-
526
- if (result.errors.length > 0) {
527
- for (const e of result.errors) {
528
- console.error(` ❌ ${e}`)
529
- }
530
- totalErrors += result.errors.length
531
- }
532
- totalInserted += result.inserted
533
- console.log(` ✓ ${result.inserted} upserted`)
534
- }
535
-
536
- console.log(`\n Total: ${totalInserted} records upserted, ${totalErrors} errors`)
537
- }
538
-
539
- // 5. Record installation
540
- console.log(`\n📝 Recording installation...`)
541
- await recordInstallation(resolvedSlug, options.account || null, options.dryRun)
542
-
543
- console.log(`\n✅ App '${appName}' installed successfully!`)
544
- console.log(`\n Next steps:`)
545
- console.log(` 1. npm run assemble && netlify dev`)
546
- console.log(` 2. Navigate to /${resolvedSlug} in your browser`)
547
- }
548
-
549
- export function registerInstallAppCommands(program: Command) {
550
- program
551
- .command('install-app <slug>')
552
- .description('Install an app by applying its seed data to the database')
553
- .option('--account <id>', 'Account ID to associate the installation with')
554
- .option('--force', 'Overwrite existing seed data', false)
555
- .option('--dry-run', 'Show what would happen without making changes', false)
556
- .action(async (slug, opts) => {
557
- try {
558
- await installApp(slug, opts)
559
- } catch (err: any) {
560
- console.error('Error:', err.message)
561
- if (process.env.SPINE_CLI_DEBUG) console.error(err.stack)
562
- process.exit(1)
563
- }
564
- })
565
- }
@@ -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
- }