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.
Files changed (269) 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 -75
  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 +7 -16
  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 +43 -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/functions/integration-routes.ts +3 -7
  14. package/.framework/migrations/001_schema.sql +406 -0
  15. package/.framework/migrations/002_rls_and_functions.sql +710 -0
  16. package/.framework/migrations/003_system_data.sql +520 -0
  17. package/.framework/migrations/004_enhancements.sql +43 -0
  18. package/.framework/public/_redirects +3 -0
  19. package/.framework/public/spine-logo.jpg +0 -0
  20. package/.framework/public/spine-logo.svg +3 -0
  21. package/{scripts → .framework/scripts}/assemble-frontend.sh +8 -5
  22. package/{scripts → .framework/scripts}/assemble-functions.sh +4 -47
  23. package/{scripts → .framework/scripts}/assemble.sh +2 -8
  24. package/{scripts → .framework/scripts}/boundary-check.sh +5 -9
  25. package/{scripts → .framework/scripts}/check-core-integrity.sh +0 -0
  26. package/.framework/seeds/link_types/account_cli_instances.json +9 -0
  27. package/.framework/seeds/types/funnel_signal_use.json +98 -0
  28. package/.framework/seeds/types/spine_instance.json +109 -0
  29. package/.framework/src/App.tsx +11 -21
  30. package/.framework/src/components/CustomAppLoader.tsx +2 -19
  31. package/.framework/src/components/ui/accordion.tsx +1 -1
  32. package/.framework/src/components/ui/alert-dialog.tsx +1 -1
  33. package/.framework/src/components/ui/aspect-ratio.tsx +1 -1
  34. package/.framework/src/components/ui/avatar.tsx +1 -1
  35. package/.framework/src/components/ui/badge.tsx +2 -2
  36. package/.framework/src/components/ui/breadcrumb.tsx +2 -2
  37. package/.framework/src/components/ui/button-group.tsx +2 -2
  38. package/.framework/src/components/ui/button.tsx +2 -2
  39. package/.framework/src/components/ui/chart.tsx +6 -1
  40. package/.framework/src/components/ui/checkbox.tsx +1 -1
  41. package/.framework/src/components/ui/collapsible.tsx +1 -1
  42. package/.framework/src/components/ui/context-menu.tsx +1 -1
  43. package/.framework/src/components/ui/dialog.tsx +1 -1
  44. package/.framework/src/components/ui/direction.tsx +17 -1
  45. package/.framework/src/components/ui/dropdown-menu.tsx +1 -1
  46. package/.framework/src/components/ui/hover-card.tsx +1 -1
  47. package/.framework/src/components/ui/item.tsx +2 -2
  48. package/.framework/src/components/ui/label.tsx +1 -1
  49. package/.framework/src/components/ui/menubar.tsx +1 -1
  50. package/.framework/src/components/ui/navigation-menu.tsx +1 -1
  51. package/.framework/src/components/ui/popover.tsx +1 -1
  52. package/.framework/src/components/ui/progress.tsx +1 -1
  53. package/.framework/src/components/ui/radio-group.tsx +1 -1
  54. package/.framework/src/components/ui/scroll-area.tsx +1 -1
  55. package/.framework/src/components/ui/select.tsx +1 -1
  56. package/.framework/src/components/ui/separator.tsx +1 -1
  57. package/.framework/src/components/ui/sheet.tsx +1 -1
  58. package/.framework/src/components/ui/sidebar.tsx +6 -6
  59. package/.framework/src/components/ui/slider.tsx +1 -1
  60. package/.framework/src/components/ui/switch.tsx +1 -1
  61. package/.framework/src/components/ui/tabs.tsx +1 -1
  62. package/.framework/src/components/ui/toggle-group.tsx +1 -1
  63. package/.framework/src/components/ui/toggle.tsx +1 -1
  64. package/.framework/src/components/ui/tooltip.tsx +1 -1
  65. package/.framework/src/pages/auth/LoginPage.tsx +10 -9
  66. package/LICENSE.md +63 -93
  67. package/README.md +254 -129
  68. package/custom/.devin/skills/install-app/SKILL.md +39 -0
  69. package/custom/.devin/workflows/app-install.yaml +83 -0
  70. package/custom/README.md +79 -0
  71. package/docs/APPS.md +232 -0
  72. package/docs/ARCHITECTURE.md +246 -0
  73. package/docs/INSTALL.md +155 -0
  74. package/docs/README.md +170 -0
  75. package/docs/design-schema-spec.md +1837 -0
  76. package/docs/dev-guides/01-create-an-app.md +183 -0
  77. package/docs/dev-guides/02-create-a-function.md +178 -0
  78. package/docs/dev-guides/03-create-a-component.md +178 -0
  79. package/docs/dev-guides/04-webhook-handlers.md +129 -0
  80. package/docs/dev-guides/05-testing.md +168 -0
  81. package/docs/dev-guides/README.md +40 -0
  82. package/docs/npm-packages-plan.md +288 -0
  83. package/docs/spine-core--api-reference.md +822 -0
  84. package/package.json +78 -150
  85. package/.framework/cli/bin.cjs +0 -14
  86. package/.framework/cli/commands/claim.ts +0 -430
  87. package/.framework/cli/commands/init.ts +0 -219
  88. package/.framework/cli/commands/install-app.ts +0 -685
  89. package/.framework/cli/commands/migrate.ts +0 -178
  90. package/.framework/cli/commands/publish.ts +0 -191
  91. package/.framework/cli/commands/status.ts +0 -268
  92. package/.framework/cli/commands/uninstall-app.ts +0 -166
  93. package/.framework/cli/commands/update-app.ts +0 -397
  94. package/.framework/cli/welcome.cjs +0 -45
  95. package/.framework/config/signal-client.ts +0 -147
  96. package/.framework/config/signals.ts +0 -70
  97. package/.framework/docs/STABILITY.md +0 -52
  98. package/.framework/docs/examples/custom-app/README.md +0 -77
  99. package/.framework/docs/examples/custom-function/README.md +0 -27
  100. package/.framework/docs/examples/custom-function/handler.ts +0 -48
  101. package/.framework/docs/examples/custom-webhook/README.md +0 -68
  102. package/.framework/functions/handler-registry.ts +0 -22
  103. package/.framework/functions/signal.ts +0 -168
  104. package/.framework/migrations/000_foundation.sql +0 -1302
  105. package/.framework/migrations/001_seed.sql +0 -163
  106. package/.framework/migrations/002_seed_constraints.sql +0 -19
  107. package/.framework/migrations/003_auth_user_trigger.sql +0 -67
  108. package/.framework/migrations/004_app_version_tracking.sql +0 -8
  109. package/.framework/migrations/005_pipelines_slug_stages.sql +0 -21
  110. package/.framework/src/pages/auth/RegisterPage.tsx +0 -163
  111. package/CHANGELOG.md +0 -68
  112. package/STRUCTURE.md +0 -150
  113. package/bin/spine-framework.cjs +0 -62
  114. package/bin/welcome.cjs +0 -58
  115. package/bin/ws-shim.ts +0 -10
  116. package/config/components.json +0 -25
  117. package/config/deno.lock +0 -108
  118. package/config/package-lock.json +0 -17183
  119. package/config/postcss.config.cjs +0 -10
  120. package/config/tailwind.config.cjs +0 -78
  121. package/config/tsconfig.build.json +0 -32
  122. package/config/tsconfig.cli.json +0 -18
  123. package/config/tsconfig.json +0 -41
  124. package/config/tsconfig.node.json +0 -17
  125. package/config/tsconfig.node.tsbuildinfo +0 -1
  126. package/config/tsconfig.tsbuildinfo +0 -1
  127. package/config/typedoc.json +0 -16
  128. package/config/vite.config.d.ts +0 -2
  129. package/config/vite.config.ts +0 -71
  130. package/dist/cli/commands/agents.d.ts +0 -39
  131. package/dist/cli/commands/agents.d.ts.map +0 -1
  132. package/dist/cli/commands/auth.d.ts +0 -36
  133. package/dist/cli/commands/auth.d.ts.map +0 -1
  134. package/dist/cli/commands/claim.d.ts +0 -38
  135. package/dist/cli/commands/claim.d.ts.map +0 -1
  136. package/dist/cli/commands/create-app.d.ts +0 -23
  137. package/dist/cli/commands/create-app.d.ts.map +0 -1
  138. package/dist/cli/commands/dev.d.ts +0 -24
  139. package/dist/cli/commands/dev.d.ts.map +0 -1
  140. package/dist/cli/commands/doctor.d.ts +0 -42
  141. package/dist/cli/commands/doctor.d.ts.map +0 -1
  142. package/dist/cli/commands/generate.d.ts +0 -36
  143. package/dist/cli/commands/generate.d.ts.map +0 -1
  144. package/dist/cli/commands/init.d.ts +0 -20
  145. package/dist/cli/commands/init.d.ts.map +0 -1
  146. package/dist/cli/commands/install-app.d.ts +0 -30
  147. package/dist/cli/commands/install-app.d.ts.map +0 -1
  148. package/dist/cli/commands/items.d.ts +0 -45
  149. package/dist/cli/commands/items.d.ts.map +0 -1
  150. package/dist/cli/commands/migrate.d.ts +0 -21
  151. package/dist/cli/commands/migrate.d.ts.map +0 -1
  152. package/dist/cli/commands/migrations.d.ts +0 -41
  153. package/dist/cli/commands/migrations.d.ts.map +0 -1
  154. package/dist/cli/commands/pipelines.d.ts +0 -40
  155. package/dist/cli/commands/pipelines.d.ts.map +0 -1
  156. package/dist/cli/commands/publish.d.ts +0 -23
  157. package/dist/cli/commands/publish.d.ts.map +0 -1
  158. package/dist/cli/commands/status.d.ts +0 -23
  159. package/dist/cli/commands/status.d.ts.map +0 -1
  160. package/dist/cli/commands/system.d.ts +0 -29
  161. package/dist/cli/commands/system.d.ts.map +0 -1
  162. package/dist/cli/commands/test.d.ts +0 -46
  163. package/dist/cli/commands/test.d.ts.map +0 -1
  164. package/dist/cli/commands/uninstall-app.d.ts +0 -23
  165. package/dist/cli/commands/uninstall-app.d.ts.map +0 -1
  166. package/dist/cli/commands/update-app.d.ts +0 -24
  167. package/dist/cli/commands/update-app.d.ts.map +0 -1
  168. package/dist/cli/context.d.ts +0 -88
  169. package/dist/cli/context.d.ts.map +0 -1
  170. package/dist/cli/env-loader.d.ts +0 -14
  171. package/dist/cli/env-loader.d.ts.map +0 -1
  172. package/dist/cli/index.d.ts +0 -44
  173. package/dist/cli/index.d.ts.map +0 -1
  174. package/dist/config/signals.d.ts +0 -82
  175. package/dist/config/signals.d.ts.map +0 -1
  176. package/dist/functions/_shared/agent-runner.d.ts +0 -156
  177. package/dist/functions/_shared/agent-runner.d.ts.map +0 -1
  178. package/dist/functions/_shared/app-manifest.d.ts +0 -68
  179. package/dist/functions/_shared/app-manifest.d.ts.map +0 -1
  180. package/dist/functions/_shared/audit.d.ts +0 -91
  181. package/dist/functions/_shared/audit.d.ts.map +0 -1
  182. package/dist/functions/_shared/db.d.ts +0 -125
  183. package/dist/functions/_shared/db.d.ts.map +0 -1
  184. package/dist/functions/_shared/index.d.ts +0 -299
  185. package/dist/functions/_shared/index.d.ts.map +0 -1
  186. package/dist/functions/_shared/middleware.d.ts +0 -315
  187. package/dist/functions/_shared/middleware.d.ts.map +0 -1
  188. package/dist/functions/_shared/permissions.d.ts +0 -626
  189. package/dist/functions/_shared/permissions.d.ts.map +0 -1
  190. package/dist/functions/_shared/pipeline-runner.d.ts +0 -124
  191. package/dist/functions/_shared/pipeline-runner.d.ts.map +0 -1
  192. package/dist/functions/_shared/principal.d.ts +0 -284
  193. package/dist/functions/_shared/principal.d.ts.map +0 -1
  194. package/dist/functions/_shared/resolve-ids.d.ts +0 -10
  195. package/dist/functions/_shared/resolve-ids.d.ts.map +0 -1
  196. package/dist/functions/_shared/schema-utils.d.ts +0 -181
  197. package/dist/functions/_shared/schema-utils.d.ts.map +0 -1
  198. package/dist/functions/_shared/testing.d.ts +0 -172
  199. package/dist/functions/_shared/testing.d.ts.map +0 -1
  200. package/dist/functions/_shared/trigger-engine.d.ts +0 -140
  201. package/dist/functions/_shared/trigger-engine.d.ts.map +0 -1
  202. package/dist/functions/_shared/webhook-registration.d.ts +0 -81
  203. package/dist/functions/_shared/webhook-registration.d.ts.map +0 -1
  204. package/dist/functions/_shared/webhook-registry.d.ts +0 -41
  205. package/dist/functions/_shared/webhook-registry.d.ts.map +0 -1
  206. package/dist/functions/account-nodes.d.ts +0 -48
  207. package/dist/functions/account-nodes.d.ts.map +0 -1
  208. package/dist/functions/admin-data.d.ts +0 -178
  209. package/dist/functions/admin-data.d.ts.map +0 -1
  210. package/dist/functions/ai-agents.d.ts +0 -125
  211. package/dist/functions/ai-agents.d.ts.map +0 -1
  212. package/dist/functions/api-keys.d.ts +0 -140
  213. package/dist/functions/api-keys.d.ts.map +0 -1
  214. package/dist/functions/apps.d.ts +0 -163
  215. package/dist/functions/apps.d.ts.map +0 -1
  216. package/dist/functions/auth.d.ts +0 -74
  217. package/dist/functions/auth.d.ts.map +0 -1
  218. package/dist/functions/debug-auth.d.ts +0 -33
  219. package/dist/functions/debug-auth.d.ts.map +0 -1
  220. package/dist/functions/embeddings.d.ts +0 -205
  221. package/dist/functions/embeddings.d.ts.map +0 -1
  222. package/dist/functions/handler-registry.d.ts +0 -22
  223. package/dist/functions/handler-registry.d.ts.map +0 -1
  224. package/dist/functions/integration-routes.d.ts +0 -45
  225. package/dist/functions/integration-routes.d.ts.map +0 -1
  226. package/dist/functions/integrations.d.ts +0 -124
  227. package/dist/functions/integrations.d.ts.map +0 -1
  228. package/dist/functions/item-progress.d.ts +0 -41
  229. package/dist/functions/item-progress.d.ts.map +0 -1
  230. package/dist/functions/logs.d.ts +0 -162
  231. package/dist/functions/logs.d.ts.map +0 -1
  232. package/dist/functions/observability.d.ts +0 -123
  233. package/dist/functions/observability.d.ts.map +0 -1
  234. package/dist/functions/pipeline-executions.d.ts +0 -190
  235. package/dist/functions/pipeline-executions.d.ts.map +0 -1
  236. package/dist/functions/pipelines.d.ts +0 -171
  237. package/dist/functions/pipelines.d.ts.map +0 -1
  238. package/dist/functions/prompt-configs.d.ts +0 -125
  239. package/dist/functions/prompt-configs.d.ts.map +0 -1
  240. package/dist/functions/roles.d.ts +0 -118
  241. package/dist/functions/roles.d.ts.map +0 -1
  242. package/dist/functions/signal.d.ts +0 -73
  243. package/dist/functions/signal.d.ts.map +0 -1
  244. package/dist/functions/system-cron.d.ts +0 -65
  245. package/dist/functions/system-cron.d.ts.map +0 -1
  246. package/dist/functions/system.d.ts +0 -29
  247. package/dist/functions/system.d.ts.map +0 -1
  248. package/dist/functions/tests.d.ts +0 -28
  249. package/dist/functions/tests.d.ts.map +0 -1
  250. package/dist/functions/timers.d.ts +0 -139
  251. package/dist/functions/timers.d.ts.map +0 -1
  252. package/dist/functions/triggers.d.ts +0 -203
  253. package/dist/functions/triggers.d.ts.map +0 -1
  254. package/dist/functions/types.d.ts +0 -151
  255. package/dist/functions/types.d.ts.map +0 -1
  256. package/dist/src/types/types.d.ts +0 -364
  257. package/dist/src/types/types.d.ts.map +0 -1
  258. package/index.html +0 -13
  259. package/netlify.toml +0 -36
  260. package/package-project.json +0 -71
  261. package/scripts/ingest-chunks.cjs +0 -202
  262. package/scripts/kb-chunk-parser.cjs +0 -312
  263. package/scripts/kb-chunk-parser.ts +0 -330
  264. package/scripts/load-test-app-install.ts +0 -484
  265. package/scripts/netlify-dev-wrapper.sh +0 -22
  266. package/vitest.config.ts +0 -45
  267. /package/{scripts → .framework/scripts}/app-install-cli.ts +0 -0
  268. /package/{scripts → .framework/scripts}/build-manifest.sh +0 -0
  269. /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
- }