spine-framework 0.3.144 → 0.4.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 (451) hide show
  1. package/.env.example +76 -0
  2. package/.framework/.spine-manifest.json +19 -0
  3. package/.framework/cli/commands/agents.ts +61 -33
  4. package/.framework/cli/commands/auth.ts +60 -28
  5. package/.framework/cli/commands/create-app.ts +212 -80
  6. package/.framework/cli/commands/dev.ts +184 -68
  7. package/.framework/cli/commands/doctor.ts +324 -152
  8. package/.framework/cli/commands/generate.ts +119 -68
  9. package/.framework/cli/commands/init.ts +141 -242
  10. package/.framework/cli/commands/install-app.ts +1336 -314
  11. package/.framework/cli/commands/items.ts +31 -29
  12. package/.framework/cli/commands/migrate.ts +299 -156
  13. package/.framework/cli/commands/migrations.ts +62 -30
  14. package/.framework/cli/commands/pipelines.ts +31 -29
  15. package/.framework/cli/commands/status.ts +105 -45
  16. package/.framework/cli/commands/system.ts +47 -42
  17. package/.framework/cli/commands/test.ts +161 -102
  18. package/.framework/cli/commands/upgrade.ts +260 -0
  19. package/.framework/cli/commands/validate-app.ts +517 -161
  20. package/.framework/cli/context.ts +117 -85
  21. package/.framework/cli/env-loader.ts +74 -43
  22. package/.framework/cli/index.ts +70 -46
  23. package/.framework/cli/lib/project-files.ts +404 -0
  24. package/.framework/docs/guides/testing-guide.md +1 -1
  25. package/.framework/docs/runtime-types.md +72 -72
  26. package/.framework/functions/_shared/access.ts +84 -0
  27. package/.framework/functions/_shared/agent-runner.ts +5051 -1026
  28. package/.framework/functions/_shared/app-manifest.ts +214 -94
  29. package/.framework/functions/_shared/ast.ts +214 -0
  30. package/.framework/functions/_shared/audit.ts +91 -55
  31. package/.framework/functions/_shared/cil.ts +368 -127
  32. package/.framework/functions/_shared/db.ts +337 -68
  33. package/.framework/functions/_shared/funnel.ts +228 -0
  34. package/.framework/functions/_shared/geo.ts +105 -0
  35. package/.framework/functions/_shared/hash-api-key.ts +94 -17
  36. package/.framework/functions/_shared/index.ts +22 -224
  37. package/.framework/functions/_shared/kb.ts +832 -58
  38. package/.framework/functions/_shared/llm.ts +118 -18
  39. package/.framework/functions/_shared/logging.ts +317 -19
  40. package/.framework/functions/_shared/middleware.ts +701 -131
  41. package/.framework/functions/_shared/notify.ts +70 -0
  42. package/.framework/functions/_shared/outcomes.ts +34 -19
  43. package/.framework/functions/_shared/permissions.ts +501 -315
  44. package/.framework/functions/_shared/pipeline-runner.ts +704 -155
  45. package/.framework/functions/_shared/principal.ts +436 -263
  46. package/.framework/functions/_shared/rate-limit.ts +233 -0
  47. package/.framework/functions/_shared/resolve-credentials.ts +86 -19
  48. package/.framework/functions/_shared/resolve-ids.ts +203 -139
  49. package/.framework/functions/_shared/retry-deliveries.ts +51 -16
  50. package/.framework/functions/_shared/schema-utils.ts +602 -521
  51. package/.framework/functions/_shared/security.ts +345 -0
  52. package/.framework/functions/_shared/testing.ts +197 -172
  53. package/.framework/functions/_shared/tracing.ts +351 -21
  54. package/.framework/functions/_shared/trigger-engine.ts +425 -136
  55. package/.framework/functions/_shared/webhook-registration.ts +71 -70
  56. package/.framework/functions/_shared/webhook-registry.ts +126 -83
  57. package/.framework/functions/account-nodes.ts +30 -30
  58. package/.framework/functions/adapters/generic-webhook-inbound.ts +49 -16
  59. package/.framework/functions/adapters/generic-webhook-outbound.ts +17 -17
  60. package/.framework/functions/adapters/openai-outbound.ts +15 -16
  61. package/.framework/functions/admin-data.ts +596 -173
  62. package/.framework/functions/admin-funnel.ts +64 -60
  63. package/.framework/functions/admin-install-funnel.ts +84 -66
  64. package/.framework/functions/admin-signals.ts +69 -51
  65. package/.framework/functions/ai-agents.ts +431 -106
  66. package/.framework/functions/ai-interactions.ts +186 -112
  67. package/.framework/functions/api-keys.ts +107 -107
  68. package/.framework/functions/apps.ts +154 -136
  69. package/.framework/functions/auth.ts +83 -65
  70. package/.framework/functions/cil-agent-ingest.ts +189 -232
  71. package/.framework/functions/cil-agent.ts +433 -177
  72. package/.framework/functions/cil-callmap.ts +642 -430
  73. package/.framework/functions/cil-config.ts +139 -135
  74. package/.framework/functions/cil-context.ts +214 -127
  75. package/.framework/functions/cil-doc.ts +587 -458
  76. package/.framework/functions/cil-embeddings.ts +453 -364
  77. package/.framework/functions/cil-graph.ts +427 -142
  78. package/.framework/functions/cil-ingest.ts +1418 -1154
  79. package/.framework/functions/cil-pipeline.ts +276 -21
  80. package/.framework/functions/cil-project.ts +659 -132
  81. package/.framework/functions/cil-validate.ts +198 -164
  82. package/.framework/functions/debug-auth.ts +57 -55
  83. package/.framework/functions/deliveries.ts +80 -21
  84. package/.framework/functions/embeddings.ts +1023 -318
  85. package/.framework/functions/eval-harness.ts +146 -108
  86. package/.framework/functions/integration-outbound.ts +205 -75
  87. package/.framework/functions/integration-routes.ts +128 -23
  88. package/.framework/functions/integrations.ts +125 -100
  89. package/.framework/functions/invites.ts +1095 -460
  90. package/.framework/functions/item-progress.ts +158 -102
  91. package/.framework/functions/kb.ts +34 -10
  92. package/.framework/functions/logs.ts +133 -133
  93. package/.framework/functions/observability.ts +152 -122
  94. package/.framework/functions/pipeline-executions.ts +147 -132
  95. package/.framework/functions/pipelines.ts +134 -119
  96. package/.framework/functions/prompt-configs.ts +96 -80
  97. package/.framework/functions/roles.ts +112 -82
  98. package/.framework/functions/security.ts +450 -0
  99. package/.framework/functions/system-cron.ts +117 -80
  100. package/.framework/functions/system.ts +158 -98
  101. package/.framework/functions/tests.ts +29 -29
  102. package/.framework/functions/timers.ts +94 -95
  103. package/.framework/functions/trigger-scheduler.ts +10 -0
  104. package/.framework/functions/triggers.ts +179 -149
  105. package/.framework/functions/types.ts +137 -125
  106. package/.framework/mcp/index.ts +133 -0
  107. package/.framework/mcp/load-env.ts +60 -0
  108. package/.framework/mcp/shared/app-manifest.ts +275 -0
  109. package/.framework/mcp/shared/db.ts +477 -0
  110. package/.framework/mcp/shared/kb.ts +118 -0
  111. package/.framework/mcp/tools/architect.ts +431 -0
  112. package/.framework/mcp/tools/cil/context.ts +345 -0
  113. package/.framework/mcp/tools/cil/graph.ts +242 -0
  114. package/.framework/mcp/tools/cil/ingest.ts +1217 -0
  115. package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +693 -0
  116. package/.framework/mcp/tools/cil/pipeline.ts +767 -0
  117. package/.framework/mcp/tools/cil/project.ts +791 -0
  118. package/.framework/mcp/tools/cil/retrieval.ts +336 -0
  119. package/.framework/mcp/tools/cil/search-core.ts +454 -0
  120. package/.framework/mcp/tools/cil/search.ts +97 -0
  121. package/.framework/mcp/tools/cil/suggest.ts +186 -0
  122. package/.framework/mcp/tools/cil/validate.ts +1369 -0
  123. package/.framework/mcp/tools/kb/delete.ts +139 -0
  124. package/.framework/mcp/tools/kb/index.ts +52 -0
  125. package/.framework/mcp/tools/kb/ingest.ts +84 -0
  126. package/.framework/mcp/tools/kb/list-types.ts +102 -0
  127. package/.framework/mcp/tools/kb/publish.ts +100 -0
  128. package/.framework/mcp/tools/kb/reindex.ts +86 -0
  129. package/.framework/mcp/tools/kb/revoke.ts +85 -0
  130. package/.framework/mcp/tools/kb/search.ts +101 -0
  131. package/.framework/mcp/tools/kb/upsert.ts +125 -0
  132. package/.framework/mcp/tools/observability.ts +510 -0
  133. package/.framework/migrations/001_core_schema.sql +15 -0
  134. package/.framework/migrations/002_core_rls_and_functions.sql +15 -0
  135. package/.framework/migrations/003_core_system_data.sql +16 -1
  136. package/.framework/migrations/004_core_rpc_and_broker.sql +17 -1
  137. package/.framework/migrations/005_core_observability.sql +17 -1
  138. package/.framework/migrations/006_security_hardening_rls.sql +15 -0
  139. package/.framework/migrations/007_kb_article_list.sql +17 -1
  140. package/.framework/migrations/008_items_target.sql +15 -0
  141. package/.framework/migrations/009_roles_slug_unique.sql +15 -0
  142. package/.framework/migrations/010_auth_user_trigger.sql +15 -0
  143. package/.framework/migrations/011_spine_system_account.sql +16 -0
  144. package/.framework/migrations/012_service_role_grants.sql +16 -0
  145. package/.framework/migrations/013_get_account_apps_role_filter.sql +15 -0
  146. package/.framework/migrations/014_apps_required_roles.sql +16 -0
  147. package/.framework/migrations/015_types_kind_slug_unique.sql +20 -0
  148. package/.framework/migrations/016_cil_types.sql +281 -0
  149. package/.framework/migrations/017_cil_stable_ids.sql +37 -0
  150. package/.framework/migrations/018_runtime_tables.sql +129 -0
  151. package/.framework/migrations/019_agent_prompt_columns.sql +56 -0
  152. package/.framework/migrations/020_schema_reconciliation.sql +67 -0
  153. package/.framework/migrations/021_kb_embedding_lifecycle.sql +31 -0
  154. package/.framework/migrations/032_cil_status_migration.sql +73 -0
  155. package/.framework/migrations/033_cil_versioning.sql +73 -0
  156. package/.framework/migrations/034_match_embeddings_knn.sql +115 -0
  157. package/.framework/migrations/035_rate_limits.sql +55 -0
  158. package/.framework/migrations/036_demo_record_flags.sql +72 -0
  159. package/.framework/migrations/037_kb_types_canonical.sql +244 -0
  160. package/.framework/migrations/038_drop_demo_record_flags.sql +38 -0
  161. package/.framework/migrations/042_registration_hierarchy.sql +309 -0
  162. package/.framework/migrations/043_policy_actor_cleanup.sql +39 -0
  163. package/.framework/migrations/044_match_embeddings_filtered_knn.sql +151 -0
  164. package/.framework/migrations/045_security_critical_rls.sql +139 -0
  165. package/.framework/migrations/046_security_missing_rls.sql +79 -0
  166. package/.framework/migrations/047_people_updated_by.sql +27 -0
  167. package/.framework/migrations/048_watcher_last_seen_at.sql +24 -0
  168. package/.framework/migrations/049_security_blocklist.sql +148 -0
  169. package/.framework/migrations/050_notifications.sql +49 -0
  170. package/.framework/migrations/051_message_agent_fields.sql +74 -0
  171. package/.framework/migrations/052_registration_security.sql +421 -0
  172. package/.framework/migrations/053_builtin_action_seeds.sql +66 -0
  173. package/.framework/migrations/054_kb_ingest_lease.sql +157 -0
  174. package/.framework/migrations/055_type_id_drift_reconciliation.sql +33 -0
  175. package/.framework/migrations/056_slug_column_reconciliation.sql +38 -0
  176. package/.framework/migrations/057_pipelines_slug_identity.sql +14 -0
  177. package/.framework/migrations/058_account_data_field_declarations.sql +53 -0
  178. package/.framework/migrations/059_registration_placement_fixes.sql +269 -0
  179. package/.framework/migrations/060_account_person_record_permissions.sql +38 -0
  180. package/.framework/migrations/061_fk_dedup.sql +32 -0
  181. package/.framework/public/spine-logo.svg +10 -3
  182. package/.framework/scripts/app-install-cli.ts +190 -93
  183. package/.framework/scripts/assemble-frontend.sh +127 -44
  184. package/.framework/scripts/assemble-functions.sh +128 -30
  185. package/.framework/scripts/boundary-check.sh +6 -2
  186. package/.framework/scripts/build-manifest.sh +24 -68
  187. package/.framework/scripts/check-core-integrity.sh +15 -76
  188. package/.framework/scripts/checksum-manifest.mts +343 -0
  189. package/.framework/scripts/cil-migrate-markers.ts +403 -0
  190. package/.framework/scripts/fix-cil-markers.mjs +176 -0
  191. package/.framework/scripts/verify-integrity.sh +21 -57
  192. package/.framework/scripts/verify-manifest.mts +305 -0
  193. package/.framework/seeds/types/funnel_signal_use.json +74 -21
  194. package/.framework/seeds/types/spine_instance.json +73 -21
  195. package/.framework/src/App.tsx +118 -72
  196. package/.framework/src/apps/admin/index.tsx +855 -58
  197. package/.framework/src/components/AppWrapper.tsx +43 -45
  198. package/.framework/src/components/CustomAppLoader.tsx +160 -116
  199. package/.framework/src/components/admin/AdminListPage.tsx +46 -44
  200. package/.framework/src/components/admin/AdminSidebar.tsx +57 -47
  201. package/.framework/src/components/admin/AdminStatsCard.tsx +46 -44
  202. package/.framework/src/components/admin/PageInstructions.tsx +98 -2
  203. package/.framework/src/components/admin/SortableTableHeader.tsx +44 -45
  204. package/.framework/src/components/app-shell/GenericAppShell.tsx +64 -65
  205. package/.framework/src/components/app-shell/GenericDetailPage.tsx +44 -47
  206. package/.framework/src/components/app-shell/GenericListPage.tsx +45 -49
  207. package/.framework/src/components/app-sidebar.tsx +42 -45
  208. package/.framework/src/components/auth/ProtectedRoute.tsx +48 -41
  209. package/.framework/src/components/auth/TurnstileWidget.tsx +27 -0
  210. package/.framework/src/components/kb/KBArticleEditor.tsx +231 -22
  211. package/.framework/src/components/kb/KBArticleList.tsx +251 -10
  212. package/.framework/src/components/layout/AppShell.tsx +51 -45
  213. package/.framework/src/components/layout/Header.tsx +42 -43
  214. package/.framework/src/components/layout/Layout.tsx +46 -46
  215. package/.framework/src/components/layout/Sidebar.tsx +196 -96
  216. package/.framework/src/components/runtime/DataDetailHeader.tsx +55 -30
  217. package/.framework/src/components/runtime/DataDetailPage.tsx +31 -269
  218. package/.framework/src/components/runtime/DataFilters.tsx +49 -74
  219. package/.framework/src/components/runtime/DataHeader.tsx +53 -32
  220. package/.framework/src/components/runtime/DataListPage.tsx +51 -155
  221. package/.framework/src/components/runtime/DataStats.tsx +66 -44
  222. package/.framework/src/components/runtime/DataTable.tsx +49 -149
  223. package/.framework/src/components/runtime/SchemaDetailForm.tsx +45 -75
  224. package/.framework/src/components/runtime/index.ts +119 -16
  225. package/.framework/src/components/search-form.tsx +46 -29
  226. package/.framework/src/components/shared/AgentView.tsx +65 -48
  227. package/.framework/src/components/shared/FieldRenderer.tsx +144 -50
  228. package/.framework/src/components/shared/InviteManager.tsx +207 -97
  229. package/.framework/src/components/shared/MemberList.tsx +97 -0
  230. package/.framework/src/components/shared/SchemaFields.tsx +107 -86
  231. package/.framework/src/components/ui/DataTable.tsx +80 -68
  232. package/.framework/src/components/ui/Form.tsx +157 -146
  233. package/.framework/src/components/ui/ItemCard.tsx +74 -67
  234. package/.framework/src/components/ui/ItemListView.tsx +73 -68
  235. package/.framework/src/components/ui/LoadingSpinner.tsx +43 -48
  236. package/.framework/src/components/ui/Modal.tsx +78 -68
  237. package/.framework/src/components/ui/RichTextEditor.tsx +89 -69
  238. package/.framework/src/components/ui/accordion.tsx +85 -86
  239. package/.framework/src/components/ui/alert-dialog.tsx +197 -184
  240. package/.framework/src/components/ui/alert.tsx +105 -94
  241. package/.framework/src/components/ui/aspect-ratio.tsx +57 -43
  242. package/.framework/src/components/ui/avatar.tsx +122 -107
  243. package/.framework/src/components/ui/badge.tsx +60 -55
  244. package/.framework/src/components/ui/breadcrumb.tsx +127 -128
  245. package/.framework/src/components/ui/button-group.tsx +86 -72
  246. package/.framework/src/components/ui/button.tsx +71 -50
  247. package/.framework/src/components/ui/calendar.tsx +72 -57
  248. package/.framework/src/components/ui/card.tsx +136 -129
  249. package/.framework/src/components/ui/carousel.tsx +226 -107
  250. package/.framework/src/components/ui/chart.tsx +252 -147
  251. package/.framework/src/components/ui/checkbox.tsx +43 -44
  252. package/.framework/src/components/ui/collapsible.tsx +86 -71
  253. package/.framework/src/components/ui/combobox.tsx +282 -249
  254. package/.framework/src/components/ui/command.tsx +158 -146
  255. package/.framework/src/components/ui/context-menu.tsx +239 -240
  256. package/.framework/src/components/ui/dialog.tsx +174 -157
  257. package/.framework/src/components/ui/direction.tsx +72 -56
  258. package/.framework/src/components/ui/drawer.tsx +168 -169
  259. package/.framework/src/components/ui/dropdown-menu.tsx +254 -223
  260. package/.framework/src/components/ui/empty.tsx +131 -120
  261. package/.framework/src/components/ui/field.tsx +205 -173
  262. package/.framework/src/components/ui/hover-card.tsx +71 -72
  263. package/.framework/src/components/ui/input-group.tsx +148 -133
  264. package/.framework/src/components/ui/input-otp.tsx +99 -86
  265. package/.framework/src/components/ui/input.tsx +43 -41
  266. package/.framework/src/components/ui/item.tsx +207 -185
  267. package/.framework/src/components/ui/kbd.tsx +57 -56
  268. package/.framework/src/components/ui/label.tsx +45 -41
  269. package/.framework/src/components/ui/menubar.tsx +268 -236
  270. package/.framework/src/components/ui/native-select.tsx +85 -86
  271. package/.framework/src/components/ui/navigation-menu.tsx +166 -156
  272. package/.framework/src/components/ui/pagination.tsx +131 -141
  273. package/.framework/src/components/ui/popover.tsx +125 -128
  274. package/.framework/src/components/ui/progress.tsx +47 -44
  275. package/.framework/src/components/ui/radio-group.tsx +60 -55
  276. package/.framework/src/components/ui/resizable.tsx +85 -72
  277. package/.framework/src/components/ui/scroll-area.tsx +57 -58
  278. package/.framework/src/components/ui/select.tsx +182 -169
  279. package/.framework/src/components/ui/separator.tsx +42 -43
  280. package/.framework/src/components/ui/sheet.tsx +170 -159
  281. package/.framework/src/components/ui/sidebar.tsx +395 -398
  282. package/.framework/src/components/ui/skeleton.tsx +42 -43
  283. package/.framework/src/components/ui/slider.tsx +61 -44
  284. package/.framework/src/components/ui/sonner.tsx +44 -44
  285. package/.framework/src/components/ui/spinner.tsx +47 -42
  286. package/.framework/src/components/ui/switch.tsx +56 -42
  287. package/.framework/src/components/ui/table-primitives.tsx +272 -132
  288. package/.framework/src/components/ui/table.tsx +140 -142
  289. package/.framework/src/components/ui/tabs.tsx +110 -93
  290. package/.framework/src/components/ui/textarea.tsx +43 -44
  291. package/.framework/src/components/ui/toggle-group.tsx +87 -71
  292. package/.framework/src/components/ui/toggle.tsx +56 -56
  293. package/.framework/src/components/ui/tooltip.tsx +99 -85
  294. package/.framework/src/contexts/AppContext.tsx +166 -114
  295. package/.framework/src/contexts/AuthContext.tsx +232 -137
  296. package/.framework/src/hooks/use-mobile.ts +43 -44
  297. package/.framework/src/hooks/useApi.ts +185 -172
  298. package/.framework/src/hooks/useAppPath.ts +31 -29
  299. package/.framework/src/hooks/useApps.ts +77 -42
  300. package/.framework/src/hooks/useEntityList.ts +66 -29
  301. package/.framework/src/hooks/useEntityRecord.ts +76 -32
  302. package/.framework/src/hooks/useForm.ts +62 -30
  303. package/.framework/src/hooks/useListSchema.ts +60 -43
  304. package/.framework/src/hooks/useSchemaRecord.ts +115 -95
  305. package/.framework/src/lib/api.ts +258 -134
  306. package/.framework/src/lib/registration.ts +181 -0
  307. package/.framework/src/lib/supabase.ts +118 -81
  308. package/.framework/src/lib/utils.ts +266 -266
  309. package/.framework/src/main.tsx +43 -44
  310. package/.framework/src/pages/DashboardPage.tsx +44 -43
  311. package/.framework/src/pages/NoAppPage.tsx +50 -0
  312. package/.framework/src/pages/NotFoundPage.tsx +32 -29
  313. package/.framework/src/pages/SetupPage.tsx +34 -33
  314. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +69 -58
  315. package/.framework/src/pages/admin/AIAgentsPage.tsx +43 -40
  316. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +63 -55
  317. package/.framework/src/pages/admin/APIKeysPage.tsx +44 -42
  318. package/.framework/src/pages/admin/AlertsConfigPage.tsx +117 -94
  319. package/.framework/src/pages/admin/AppDetailPage.tsx +48 -42
  320. package/.framework/src/pages/admin/AppsPage.tsx +45 -42
  321. package/.framework/src/pages/admin/DeliveriesPage.tsx +49 -17
  322. package/.framework/src/pages/admin/DesignedPage.tsx +31 -30
  323. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +58 -0
  324. package/.framework/src/pages/admin/EmbeddingsPage.tsx +49 -0
  325. package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +31 -29
  326. package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +46 -29
  327. package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -29
  328. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +68 -55
  329. package/.framework/src/pages/admin/IntegrationsPage.tsx +45 -42
  330. package/.framework/src/pages/admin/InvitesPage.tsx +60 -44
  331. package/.framework/src/pages/admin/LogsPage.tsx +87 -137
  332. package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +31 -29
  333. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +150 -89
  334. package/.framework/src/pages/admin/PipelineDetailPage.tsx +77 -104
  335. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +44 -43
  336. package/.framework/src/pages/admin/PipelinesPage.tsx +59 -145
  337. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +53 -42
  338. package/.framework/src/pages/admin/PromptConfigsPage.tsx +47 -117
  339. package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +31 -44
  340. package/.framework/src/pages/admin/RoleDetailPage.tsx +49 -92
  341. package/.framework/src/pages/admin/RolesPage.tsx +47 -164
  342. package/.framework/src/pages/admin/SecurityPage.tsx +310 -0
  343. package/.framework/src/pages/admin/SelectTestPage.tsx +30 -32
  344. package/.framework/src/pages/admin/ShadcnTestPage.tsx +37 -44
  345. package/.framework/src/pages/admin/SimpleDashboard.tsx +32 -44
  346. package/.framework/src/pages/admin/TestRunDetailPage.tsx +97 -44
  347. package/.framework/src/pages/admin/TestingDashboard.tsx +126 -71
  348. package/.framework/src/pages/admin/TimerDetailPage.tsx +67 -55
  349. package/.framework/src/pages/admin/TimersPage.tsx +44 -42
  350. package/.framework/src/pages/admin/TriggerDetailPage.tsx +47 -42
  351. package/.framework/src/pages/admin/TriggersPage.tsx +58 -57
  352. package/.framework/src/pages/admin/TypeDetailPage.tsx +91 -55
  353. package/.framework/src/pages/admin/TypesPage.tsx +45 -42
  354. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +258 -128
  355. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +127 -123
  356. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +308 -235
  357. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +159 -110
  358. package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +432 -0
  359. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +200 -178
  360. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +286 -239
  361. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +211 -90
  362. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +156 -143
  363. package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +379 -0
  364. package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +337 -0
  365. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +219 -214
  366. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
  367. package/.framework/src/pages/auth/AuthConfirmPage.tsx +135 -65
  368. package/.framework/src/pages/auth/ForgotPasswordPage.tsx +39 -31
  369. package/.framework/src/pages/auth/LoginPage.tsx +38 -32
  370. package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -28
  371. package/.framework/src/pages/auth/RegisterPage.tsx +429 -155
  372. package/.framework/src/pages/spine-framework/APIPage.tsx +33 -30
  373. package/.framework/src/pages/spine-framework/CLIPage.tsx +32 -30
  374. package/.framework/src/types/auth.ts +101 -93
  375. package/.framework/src/types/types.ts +283 -262
  376. package/.framework/templates/project.json +87 -0
  377. package/.framework/templates/spine.config.neutral.json +7 -0
  378. package/.framework/tests/api/api-surface.test.ts +381 -0
  379. package/.framework/tests/fixtures/seed.ts +427 -0
  380. package/.framework/tests/fixtures/teardown.ts +228 -0
  381. package/.framework/tests/reporter.ts +351 -0
  382. package/.framework/tests/setup.ts +135 -0
  383. package/.framework/tests/ui/ui-sweep.ts +278 -0
  384. package/.framework/tests/unit/admin-data-logger.test.ts +310 -0
  385. package/.framework/tests/unit/agent-runner.test.ts +2389 -0
  386. package/.framework/tests/unit/ai-interactions.test.ts +81 -0
  387. package/.framework/tests/unit/app-manifest.test.ts +359 -0
  388. package/.framework/tests/unit/assembly-scripts.test.ts +172 -0
  389. package/.framework/tests/unit/cil-determinism.test.ts +85 -0
  390. package/.framework/tests/unit/core-isolation.test.ts +153 -0
  391. package/.framework/tests/unit/funnel-signals.test.ts +135 -0
  392. package/.framework/tests/unit/install-app.test.ts +1759 -0
  393. package/.framework/tests/unit/invites-security.test.ts +572 -0
  394. package/.framework/tests/unit/kb-ingest-lease.test.ts +287 -0
  395. package/.framework/tests/unit/kb-search.test.ts +119 -0
  396. package/.framework/tests/unit/migrate.test.ts +509 -0
  397. package/.framework/tests/unit/notify.test.ts +136 -0
  398. package/.framework/tests/unit/permissions.test.ts +269 -0
  399. package/.framework/tests/unit/pipeline-runner.test.ts +755 -0
  400. package/.framework/tests/unit/principal-cache.test.ts +138 -0
  401. package/.framework/tests/unit/retrieval-golden.test.ts +520 -0
  402. package/.framework/tests/unit/runtime-isolation.test.ts +642 -0
  403. package/.framework/tests/unit/schema-utils.test.ts +225 -0
  404. package/.framework/tests/unit/trigger-engine.test.ts +513 -0
  405. package/.framework/tests/unit/validate-app.test.ts +527 -0
  406. package/README.md +34 -3
  407. package/bin/spine-framework.cjs +1 -0
  408. package/config/tailwind.config.cjs +7 -0
  409. package/config/tsconfig.functions.json +42 -0
  410. package/config/tsconfig.json +37 -9
  411. package/config/vite.config.ts +31 -2
  412. package/netlify.toml +31 -3
  413. package/package.json +46 -29
  414. package/scripts/assemble-frontend.sh +127 -44
  415. package/scripts/assemble-functions.sh +127 -29
  416. package/scripts/boundary-check.sh +6 -2
  417. package/scripts/build-manifest.sh +24 -68
  418. package/scripts/checksum-manifest.mts +345 -0
  419. package/scripts/dev-clean.sh +86 -0
  420. package/scripts/dev-netlify.sh +49 -0
  421. package/scripts/netlify-dev-wrapper.sh +2 -0
  422. package/scripts/verify-install.ts +358 -0
  423. package/scripts/verify-integrity.sh +21 -57
  424. package/scripts/verify-manifest.mts +305 -0
  425. package/templates/project/gitignore +45 -0
  426. package/{config → templates/project}/package-lock.json +22335 -9366
  427. package/vitest.config.ts +46 -0
  428. package/.framework/public/spine.config.json +0 -91
  429. package/config/deno.lock +0 -108
  430. package/config/tsconfig.node.tsbuildinfo +0 -1
  431. package/config/tsconfig.tsbuildinfo +0 -1
  432. package/config/vite.config.d.ts +0 -2
  433. package/custom/.devin/AGENT.md +0 -357
  434. package/custom/.devin/skills/install-app/SKILL.md +0 -39
  435. package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +0 -39
  436. package/custom/.devin/skills/spine-workbench-idea/SKILL.md +0 -36
  437. package/custom/.devin/skills/spine-workbench-implement/SKILL.md +0 -46
  438. package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +0 -28
  439. package/custom/.devin/skills/spine-workbench-plan/SKILL.md +0 -38
  440. package/custom/.devin/workflows/app-install.yaml +0 -83
  441. package/custom/README.md +0 -79
  442. package/docs/README.md +0 -15
  443. package/scripts/app-install-cli.ts +0 -286
  444. package/scripts/check-core-integrity.sh +0 -82
  445. package/scripts/ingest-chunks.cjs +0 -202
  446. package/scripts/kb-chunk-parser.cjs +0 -312
  447. package/scripts/kb-chunk-parser.ts +0 -330
  448. package/scripts/kb-ingest-docs.ts +0 -203
  449. package/scripts/load-test-app-install.ts +0 -484
  450. package/scripts/test-login.js +0 -33
  451. package/spine.config.json +0 -91
@@ -1,52 +1,48 @@
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
- // CIL[BEGIN chunk_id="install-app_registrationconfig_1784069813" type="interface" identity="RegistrationConfig" parent="none"]
12
- // CIL[DOC-MACRO purpose="Define registration configuration for an installed app"]
13
- // CIL[DOC-RESPONSIBILITY responsibility="Own the RegistrationConfig interface definition"]
14
- // CIL[DOC-CONTEXT module="cli.commands.install-app" layer="cli" related_files="[]"]
15
- // CIL[DOC-CONTRACT inputs="none" outputs="interface definition" errors="none" side_effects="none"]
16
- // CIL[DOC-MICRO logic_summary="Defines registration settings including default role, redirect path, and account strategy" notable_decisions="[]"]
17
- // CIL[DOC-CONFIDENCE pass="first" score="0.95" flags="none"]
1
+ /**
2
+ * The RegistrationConfig interface defines the configuration options for user registration in the application. It allows customization of registration behavior, including role assignment and account strategies.
3
+ *
4
+ * @sideEffects none
5
+ * @stability stable
6
+ * @cil-generated
7
+ */
8
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:registrationconfig:interface" type="interface" identity="RegistrationConfig" parent="~/.framework/cli/commands/install-app.ts"]
9
+ // CIL[DOC-MACRO purpose="Manifest-declared registration options for an app"]
10
+ // CIL[DOC-RESPONSIBILITY responsibility="Declare the manifest.json registration block shape: enabled, default_role, redirect_path, account_strategy, target_account, parent_account, default_workspace_name, allow_domain_join"]
11
+ // CIL[DOC-CONTEXT module="install-app" layer="cli" related_files="spine.config.json"]
12
+ // CIL[DOC-CONTRACT inputs="manifest.json fields" outputs="typed registration config" errors="none" side_effects="none"]
13
+ // CIL[DOC-MICRO logic_summary="Field bag consumed by updateRegistrationConfig and mirrored into apps.config.registration" notable_decisions="parent_account stamps parent_id on accounts created via the 'new' strategy"]
14
+ // CIL[DOC-CONFIDENCE pass="second" score="0.9" flags="none"]
18
15
  // CIL[DOC-STABILITY stability="stable"]
19
- // CIL[DOC-AUDIENCE audience="developer"]
20
- // CIL[DOC-VECTOR type="context" content="Layer: cli. Direct callers: 0. Direct calls: 0. Related files: none."]
21
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
22
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
23
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
24
- // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
25
- // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
26
16
  interface RegistrationConfig {
27
17
  enabled: boolean
28
18
  default_role: string
29
19
  redirect_path: string
30
20
  account_strategy?: 'existing' | 'new' | 'choice'
31
21
  target_account?: string
22
+ /** Account slug that becomes parent_id for accounts created via the 'new'
23
+ strategy — e.g. 'spine-system' gives staff hierarchical visibility. */
24
+ parent_account?: string
25
+ /** When true, registration skips the personal/company choice and
26
+ auto-names the workspace from the user's name/email. */
27
+ default_workspace_name?: boolean
28
+ allow_domain_join?: boolean
32
29
  }
33
- // CIL[END chunk_id="install-app_registrationconfig_1784069813"]
34
-
35
- // CIL[BEGIN chunk_id="install-app_spineconfig_1784069813" type="interface" identity="SpineConfig" parent="none"]
36
- // CIL[DOC-MACRO purpose="Define spine.config.json structure for registration"]
37
- // CIL[DOC-RESPONSIBILITY responsibility="Own the SpineConfig interface definition"]
38
- // CIL[DOC-CONTEXT module="cli.commands.install-app" layer="cli" related_files="[]"]
39
- // CIL[DOC-CONTRACT inputs="none" outputs="interface definition" errors="none" side_effects="none"]
40
- // CIL[DOC-MICRO logic_summary="Defines registration config with default app/role, URL override flag, and apps map" notable_decisions="[]"]
41
- // CIL[DOC-CONFIDENCE pass="first" score="0.95" flags="none"]
42
- // CIL[DOC-STABILITY stability="stable"]
43
- // CIL[DOC-AUDIENCE audience="developer"]
44
- // CIL[DOC-VECTOR type="context" content="Layer: cli. Direct callers: 0. Direct calls: 0. Related files: none."]
45
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
46
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
47
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
48
- // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
49
- // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
30
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:registrationconfig:interface"]
31
+ /**
32
+ * The SpineConfig interface defines the configuration structure for application registration and role management within the CLI framework. It allows for flexible configuration of applications, roles, and their associated settings.
33
+ *
34
+ * @sideEffects none
35
+ * @stability stable
36
+ * @cil-generated
37
+ */
38
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:spineconfig:interface" type="interface" identity="SpineConfig" parent="~/.framework/cli/commands/install-app.ts"]
39
+ // CIL[DOC-MACRO purpose="Runtime shape of spine.config.json registration section"]
40
+ // CIL[DOC-RESPONSIBILITY responsibility="Declare the spine.config.json registration structure, including the per-app runtime entry (roles, default_role, path, enabled, account_strategy, target_account, parent_account, default_workspace_name, allow_domain_join)"]
41
+ // CIL[DOC-CONTEXT module="install-app" layer="cli" related_files="spine.config.json,.framework/src/pages/auth/RegisterPage.tsx"]
42
+ // CIL[DOC-CONTRACT inputs="spine.config.json on disk" outputs="typed config object" errors="none" side_effects="none"]
43
+ // CIL[DOC-MICRO logic_summary="Mirrors the runtime config the RegisterPage fetches and completeRegistration resolves via apps.config.registration" notable_decisions="[]"]
44
+ // CIL[DOC-CONFIDENCE pass="second" score="0.9" flags="none"]
45
+ // CIL[DOC-STABILITY stability="stable"]
50
46
  interface SpineConfig {
51
47
  registration?: {
52
48
  default_app?: string
@@ -60,62 +56,306 @@ interface SpineConfig {
60
56
  enabled: boolean
61
57
  account_strategy: 'existing' | 'new' | 'choice'
62
58
  target_account?: string
59
+ parent_account?: string
60
+ default_workspace_name?: boolean
61
+ allow_domain_join?: boolean
63
62
  }>
64
63
  }
65
64
  }
66
- // CIL[END chunk_id="install-app_spineconfig_1784069813"]
67
-
68
- // CIL[BEGIN chunk_id="install-app_moduleheader_1784069813" type="module_header" identity="module_header" parent="none"]
69
- // CIL[DOC-MACRO purpose="Module imports for app installation CLI"]
70
- // CIL[DOC-RESPONSIBILITY responsibility="Provide filesystem, database, and CLI utilities"]
71
- // CIL[DOC-CONTEXT module="cli.commands.install-app" layer="cli" related_files="[\".framework/migrations/015_simplify_apps_table.sql\",\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\"custom/apps/portal/pages/ContentPage.tsx\",\".framework/src/pages/admin/TypeDetailPage.tsx\",\"\\\"]"]
72
- // CIL[DOC-CONTRACT inputs="none" outputs="imports" errors="none" side_effects="none"]
73
- // CIL[DOC-MICRO logic_summary="Imports Node.js modules, Supabase client, and migration utilities" notable_decisions="[]"]
74
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
75
- // CIL[DOC-STABILITY stability="stable"]
76
- // CIL[DOC-AUDIENCE audience="developer"]
77
- // CIL[DOC-VECTOR type="context" content="Layer: cli. Direct callers: 0. Direct calls: 0. Related files: .framework/migrations/015_simplify_apps_table.sql, custom/apps/cortex/pages/operations/OperationsDashboard.tsx, custom/apps/portal/pages/ContentPage.tsx, .framework/src/pages/admin/TypeDetailPage.tsx, \."]
78
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
79
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
80
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
65
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:spineconfig:interface"]
66
+ /**
67
+ * @module install-app
68
+ * @audience internal
69
+ * @layer app-specific
70
+ * @stability stable
71
+ *
72
+ * This module handles the installation of applications by managing file operations and executing necessary migrations. It serves as a command-line interface (CLI) command that integrates with the Supabase client for database interactions. INVARIANT: The application must exist in the specified directory before installation can proceed.
73
+ *
74
+ * @seeAlso migrate.js (provides migration functionality).
75
+ * @seeAlso supabase-js (used for database interactions).
76
+ *
77
+ * @cil-generated
78
+ */
79
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:module_header:module_header" type="module_header" identity="module_header" parent="~/.framework/cli/commands/install-app.ts"]
80
+ // CIL[DOC-MACRO purpose="The RegistrationConfig interface defines the configuration options for user registration in the application."]
81
+ // CIL[DOC-RESPONSIBILITY responsibility="The RegistrationConfig interface defines the configuration options for user registration in the application. It allows customization of registration behavior, including role assignment and account strategies."]
82
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/migrate.js"]
83
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
84
+ // CIL[DOC-MICRO logic_summary="The RegistrationConfig interface defines the configuration options for user registration in the application. It allows customization of registration behavior, including role assignment and account strategies." notable_decisions="none"]
85
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
86
+ // CIL[DOC-STABILITY stability="stable"]
87
+
88
+
89
+
90
+
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+ // CIL[DOC-MACRO purpose="RegistrationConfig interface"]
100
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the RegistrationConfig interface."]
101
+ // CIL[DOC-CONTEXT module=".framework/cli/commands/install-app.ts" layer="app-specific" related_files=""]
102
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
103
+ // CIL[DOC-MICRO logic_summary="Implements the RegistrationConfig interface." notable_decisions="[]"]
104
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
105
+ // CIL[DOC-STABILITY stability="stable"]
106
+ // CIL[DOC-VECTOR type="context" content="purpose='The RegistrationConfig interface defines the configuration options for user registration in the application.'; No verified external callers" status="ready"]
107
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
108
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
109
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
81
110
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
82
111
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
112
+
113
+
114
+ // CIL[DOC-MACRO purpose="The SpineConfig interface defines the configuration structure for application registration and role management within the CLI framework."]
115
+ // CIL[DOC-RESPONSIBILITY responsibility="The SpineConfig interface defines the configuration structure for application registration and role management within the CLI framework. It allows for flexible configuration of applications, roles, and their associated settings."]
116
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/migrate.js"]
117
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
118
+ // CIL[DOC-MICRO logic_summary="The SpineConfig interface defines the configuration structure for application registration and role management within the CLI framework. It allows for flexible configuration of applications, roles, and their associated settings." notable_decisions="none"]
119
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
120
+ // CIL[DOC-STABILITY stability="stable"]
121
+ // CIL[DOC-MACRO purpose="SpineConfig interface"]
122
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the SpineConfig interface."]
123
+ // CIL[DOC-CONTEXT module=".framework/cli/commands/install-app.ts" layer="app-specific" related_files=""]
124
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
125
+ // CIL[DOC-MICRO logic_summary="Implements the SpineConfig interface." notable_decisions="[]"]
126
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
127
+ // CIL[DOC-STABILITY stability="stable"]
128
+ // CIL[DOC-VECTOR type="context" content="purpose='The SpineConfig interface defines the configuration structure for application registration and role management within the CLI framework.'; No verified external callers" status="ready"]
129
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
130
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
131
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
132
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
133
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
134
+
135
+ // CIL[DOC-MACRO purpose="This module handles the installation of applications by managing file operations and executing necessary migrations."]
136
+ // CIL[DOC-RESPONSIBILITY responsibility="This module handles the installation of applications by managing file operations and executing necessary migrations. It serves as a command-line interface (CLI) command that integrates with the Supabase client for database interactions. INVARIANT: The application must exist in the specified directory before installation can proceed."]
137
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files="migrate.js (provides migration functionality).,supabase-js (used for database interactions)."]
138
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
139
+ // CIL[DOC-MICRO logic_summary="This module handles the installation of applications by managing file operations and executing necessary migrations. It serves as a command-line interface (CLI) command that integrates with the Supabase client for database interactions. INVARIANT: The application must exist in the specified directory before installation can proceed." notable_decisions="none"]
140
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
141
+ // CIL[DOC-STABILITY stability="stable"]
142
+ // CIL[DOC-AUDIENCE audience="internal"]
143
+
83
144
  import type { Command } from 'commander'
84
- import { existsSync, readFileSync, mkdirSync, cpSync, writeFileSync } from 'fs'
85
- import { resolve, dirname, join, basename } from 'path'
145
+ import { existsSync, readFileSync, mkdirSync, cpSync, writeFileSync, realpathSync, statSync } from 'fs'
146
+ import { resolve, dirname, join, basename, isAbsolute, sep } from 'path'
86
147
  import { fileURLToPath } from 'url'
87
148
  import { execSync } from 'child_process'
88
149
  import * as readline from 'readline'
89
150
  import { createClient } from '@supabase/supabase-js'
90
151
  import { runMigrations } from './migrate.js'
152
+ import { validateAppStructure } from './validate-app.js'
153
+ import { generateValidationSchema } from '../../functions/_shared/schema-utils.ts'
154
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:module_header:module_header"]
155
+ // CIL[DOC-VECTOR type="context" content="purpose='This module handles the installation of applications by managing file operations and executing necessary migrations.'; No verified external callers" status="ready"]
156
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
157
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
158
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
159
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
160
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
161
+
91
162
 
163
+ // CIL[DOC-MACRO purpose="__filename is a constant that holds the resolved path of the current module's file URL."]
164
+ // CIL[DOC-RESPONSIBILITY responsibility="__filename is a constant that holds the resolved path of the current module's file URL. It exists to provide a reliable reference to the file's location in the filesystem."]
165
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/migrate.js"]
166
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none dependencies="fileURLToPath"]
167
+ // CIL[DOC-MICRO logic_summary="__filename is a constant that holds the resolved path of the current module's file URL. It exists to provide a reliable reference to the file's location in the filesystem." notable_decisions="none"]
168
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
169
+ // CIL[DOC-STABILITY stability="stable"]
170
+ /**
171
+ * __filename is a constant that holds the resolved path of the current module's file URL. It exists to provide a reliable reference to the file's location in the filesystem.
172
+ *
173
+ * @sideEffects none
174
+ * @stability stable
175
+ * @calls fileURLToPath
176
+ * @cil-generated
177
+ */
178
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:__filename:constant" type="constant" identity="__filename" parent="~/.framework/cli/commands/install-app.ts"]
179
+ // CIL[DOC-MACRO purpose="__filename constant"]
180
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the __filename constant."]
181
+ // CIL[DOC-CONTEXT module=".framework/cli/commands/install-app.ts" layer="app-specific" related_files=""]
182
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
183
+ // CIL[DOC-MICRO logic_summary="Implements the __filename constant." notable_decisions="[]"]
184
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
185
+ // CIL[DOC-STABILITY stability="stable"]
92
186
  const __filename = fileURLToPath(import.meta.url)
187
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:__filename:constant"]
188
+ // CIL[DOC-MACRO purpose="__filename constant"]
189
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the __filename constant."]
190
+ // CIL[DOC-CONTEXT module=".framework/cli/commands/install-app.ts" layer="app-specific" related_files=""]
191
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
192
+ // CIL[DOC-MICRO logic_summary="Implements the __filename constant." notable_decisions="[]"]
193
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
194
+ // CIL[DOC-STABILITY stability="stable"]
195
+ // CIL[DOC-VECTOR type="context" content="purpose='__filename is a constant that holds the resolved path of the current module's file URL.'; No verified external callers" status="ready"]
196
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
197
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
198
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
199
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
200
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
201
+
202
+
203
+ // CIL[DOC-MACRO purpose="__dirname is a constant that holds the directory name of the current module, allowing for relative path resolution."]
204
+ // CIL[DOC-RESPONSIBILITY responsibility="__dirname is a constant that holds the directory name of the current module, allowing for relative path resolution. It exists to facilitate file operations within the module's directory context."]
205
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/migrate.js"]
206
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none dependencies="dirname"]
207
+ // CIL[DOC-MICRO logic_summary="__dirname is a constant that holds the directory name of the current module, allowing for relative path resolution. It exists to facilitate file operations within the module's directory context." notable_decisions="none"]
208
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
209
+ // CIL[DOC-STABILITY stability="stable"]
210
+ /**
211
+ * __dirname is a constant that holds the directory name of the current module, allowing for relative path resolution. It exists to facilitate file operations within the module's directory context.
212
+ *
213
+ * @sideEffects none
214
+ * @stability stable
215
+ * @calls dirname
216
+ * @cil-generated
217
+ */
218
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:__dirname:constant" type="constant" identity="__dirname" parent="~/.framework/cli/commands/install-app.ts"]
219
+ // CIL[DOC-MACRO purpose="__dirname constant"]
220
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the __dirname constant."]
221
+ // CIL[DOC-CONTEXT module=".framework/cli/commands/install-app.ts" layer="app-specific" related_files=""]
222
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
223
+ // CIL[DOC-MICRO logic_summary="Implements the __dirname constant." notable_decisions="[]"]
224
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
225
+ // CIL[DOC-STABILITY stability="stable"]
93
226
  const __dirname = dirname(__filename)
227
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:__dirname:constant"]
228
+ // CIL[DOC-MACRO purpose="__dirname constant"]
229
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the __dirname constant."]
230
+ // CIL[DOC-CONTEXT module=".framework/cli/commands/install-app.ts" layer="app-specific" related_files=""]
231
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
232
+ // CIL[DOC-MICRO logic_summary="Implements the __dirname constant." notable_decisions="[]"]
233
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
234
+ // CIL[DOC-STABILITY stability="stable"]
235
+ // CIL[DOC-VECTOR type="context" content="purpose='__dirname is a constant that holds the directory name of the current module, allowing for relative path resolution.'; No verified external callers" status="ready"]
236
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
237
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
238
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
239
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
240
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
241
+
242
+
243
+ // CIL[DOC-MACRO purpose="PROJECT_ROOT holds the current working directory of the process."]
244
+ // CIL[DOC-RESPONSIBILITY responsibility="PROJECT_ROOT holds the current working directory of the process. It is used to determine the base path for file operations within the application."]
245
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/migrate.js"]
246
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
247
+ // CIL[DOC-MICRO logic_summary="PROJECT_ROOT holds the current working directory of the process. It is used to determine the base path for file operations within the application." notable_decisions="none"]
248
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
249
+ // CIL[DOC-STABILITY stability="stable"]
250
+ /**
251
+ * PROJECT_ROOT holds the current working directory of the process. It is used to determine the base path for file operations within the application.
252
+ *
253
+ * @sideEffects none
254
+ * @stability stable
255
+ * @cil-generated
256
+ */
257
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:project_root:constant" type="constant" identity="PROJECT_ROOT" parent="~/.framework/cli/commands/install-app.ts"]
258
+ // CIL[DOC-MACRO purpose="PROJECT_ROOT constant"]
259
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the PROJECT_ROOT constant."]
260
+ // CIL[DOC-CONTEXT module=".framework/cli/commands/install-app.ts" layer="app-specific" related_files=""]
261
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
262
+ // CIL[DOC-MICRO logic_summary="Implements the PROJECT_ROOT constant." notable_decisions="[]"]
263
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
264
+ // CIL[DOC-STABILITY stability="stable"]
94
265
  const PROJECT_ROOT = process.cwd()
266
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:project_root:constant"]
267
+ // CIL[DOC-MACRO purpose="PROJECT_ROOT constant"]
268
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the PROJECT_ROOT constant."]
269
+ // CIL[DOC-CONTEXT module=".framework/cli/commands/install-app.ts" layer="app-specific" related_files=""]
270
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
271
+ // CIL[DOC-MICRO logic_summary="Implements the PROJECT_ROOT constant." notable_decisions="[]"]
272
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
273
+ // CIL[DOC-STABILITY stability="stable"]
274
+ // CIL[DOC-VECTOR type="context" content="purpose='PROJECT_ROOT holds the current working directory of the process.'; No verified external callers" status="ready"]
275
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
276
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
277
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
278
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
279
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
280
+
281
+
282
+ // CIL[DOC-MACRO purpose="SPINE_CONFIG_PATH defines the path to the spine configuration file, which is essential for the application's setup process."]
283
+ // CIL[DOC-RESPONSIBILITY responsibility="SPINE_CONFIG_PATH defines the path to the spine configuration file, which is essential for the application's setup process. This constant ensures that the application can locate the configuration file reliably."]
284
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/migrate.js"]
285
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none dependencies="join"]
286
+ // CIL[DOC-MICRO logic_summary="SPINE_CONFIG_PATH defines the path to the spine configuration file, which is essential for the application's setup process. This constant ensures that the application can locate the configuration file reliably." notable_decisions="none"]
287
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
288
+ // CIL[DOC-STABILITY stability="stable"]
289
+ /**
290
+ * SPINE_CONFIG_PATH defines the path to the spine configuration file, which is essential for the application's setup process. This constant ensures that the application can locate the configuration file reliably.
291
+ *
292
+ * @sideEffects none
293
+ * @stability stable
294
+ * @calls join
295
+ * @cil-generated
296
+ */
297
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:spine_config_path:constant" type="constant" identity="SPINE_CONFIG_PATH" parent="~/.framework/cli/commands/install-app.ts"]
298
+ // CIL[DOC-MACRO purpose="SPINE_CONFIG_PATH constant"]
299
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the SPINE_CONFIG_PATH constant."]
300
+ // CIL[DOC-CONTEXT module=".framework/cli/commands/install-app.ts" layer="app-specific" related_files=""]
301
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
302
+ // CIL[DOC-MICRO logic_summary="Implements the SPINE_CONFIG_PATH constant." notable_decisions="[]"]
303
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
304
+ // CIL[DOC-STABILITY stability="stable"]
95
305
  const SPINE_CONFIG_PATH = join(PROJECT_ROOT, 'spine.config.json')
96
- // CIL[END chunk_id="install-app_moduleheader_1784069813"]
97
- // CIL[BEGIN chunk_id="install-app_registerinstallappcommands_1784069813" type="function" identity="registerInstallAppCommands" parent="none"]
98
- // CIL[DOC-LEARNING topic="install-app:two_phase_install" content="install-app is two phases: npm copy then DB seed. If seed fails or is skipped, custom/apps/<slug> files may exist while the app is absent from public.apps, so it will not appear in the app registry."]
99
- // CIL[DOC-MACRO purpose="Register app management CLI commands"]
100
- // CIL[DOC-RESPONSIBILITY responsibility="Provide install, uninstall, update, and sync operations"]
101
- // CIL[DOC-CONTEXT module="cli.commands.install-app" layer="cli" related_files="[\".framework/migrations/015_simplify_apps_table.sql\",\"custom/apps/portal/pages/ContentPage.tsx\",\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\"custom/apps/cortex/functions/custom_cil-embeddings.ts\",\"\\\"]"]
102
- // CIL[DOC-CONTRACT inputs="none" outputs="CLI commands" errors="npm, filesystem, database errors" side_effects="filesystem writes, database writes"]
103
- // CIL[DOC-NOTE type="manifest_config_sync" content="install-app and update-db-app now persist manifest.config and manifest.metadata into the apps table's config/metadata JSONB columns."]
104
- // CIL[DOC-MICRO logic_summary="Registers install-app, uninstall-app, update-app, and update-db-app commands; syncs manifest config/metadata into apps table" notable_decisions="[]"]
105
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
106
- // CIL[DOC-STABILITY stability="stable"]
107
- // CIL[DOC-VECTOR type="context" content="Layer: cli. Direct callers: 0. Direct calls: 0. Related files: .framework/migrations/015_simplify_apps_table.sql, custom/apps/portal/pages/ContentPage.tsx, custom/apps/cortex/pages/operations/OperationsDashboard.tsx, custom/apps/cortex/functions/custom_cil-embeddings.ts, \."]
108
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
109
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
110
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
306
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:spine_config_path:constant"]
307
+ // CIL[DOC-MACRO purpose="SPINE_CONFIG_PATH constant"]
308
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the SPINE_CONFIG_PATH constant."]
309
+ // CIL[DOC-CONTEXT module=".framework/cli/commands/install-app.ts" layer="app-specific" related_files=""]
310
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
311
+ // CIL[DOC-MICRO logic_summary="Implements the SPINE_CONFIG_PATH constant." notable_decisions="[]"]
312
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
313
+ // CIL[DOC-STABILITY stability="stable"]
314
+ // CIL[DOC-VECTOR type="context" content="purpose='SPINE_CONFIG_PATH defines the path to the spine configuration file, which is essential for the application's setup process.'; No verified external callers" status="ready"]
315
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
316
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
317
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
111
318
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
112
319
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
320
+
321
+
322
+ // CIL[DOC-MACRO purpose="Command registration and installation handler for Spine Framework apps."]
323
+ // CIL[DOC-RESPONSIBILITY responsibility="Registers a command to install a Spine Framework app from npm and handles the installation process, including file copying and database migrations. This function ensures that the app is properly set up within the project environment."]
324
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/migrate.js"]
325
+ // CIL[DOC-CONTRACT inputs="program: Command (required) — the command program instance to register the install-app command." outputs="Promise<void>" errors="Error — if the package is not found at the expected path." side_effects="Installs npm packages, creates directories, copies files, and potentially modifies the database. dependencies="execSync, existsSync, mkdirSync, cpSync, runMigrations, seedApp"]
326
+ // CIL[DOC-MICRO logic_summary="Executes npm install, manages file copying, runs database migrations, and aborts before seeding when migrations throw or report failures." notable_decisions="Migration failures abort the install; --no-seed skips all DB work; without credentials only an explicit --no-seed install may proceed."]
327
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
328
+ // CIL[DOC-STABILITY stability="stable"]
329
+ /**
330
+ * Registers a command to install a Spine Framework app from npm and handles the installation process, including file copying and database migrations. This function ensures that the app is properly set up within the project environment.
331
+ *
332
+ * @macro Command registration and installation handler for Spine Framework apps.
333
+ * @micro Executes npm install, manages file copying, and runs database migrations based on the installation outcome.
334
+ * @inputSpec
335
+ * program: Command (required) — the command program instance to register the install-app command.
336
+ * @outputSpec Promise<void>
337
+ * @throws Error — if the package is not found at the expected path.
338
+ * @sideEffects Installs npm packages, creates directories, copies files, and potentially modifies the database.
339
+ * @stability stable
340
+ * @calls execSync, existsSync, mkdirSync, cpSync, runMigrations, seedApp
341
+ * @errorHandling Logs migration errors but continues execution.
342
+ * @cil-generated
343
+ */
344
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:registerinstallappcommands:function" type="function" identity="registerInstallAppCommands" parent="~/.framework/cli/commands/install-app.ts"]
345
+ // CIL[DOC-MACRO purpose="Register app installation, manifest synchronization, and local seeding commands with fail-closed execution."]
346
+ // CIL[DOC-RESPONSIBILITY responsibility="Register app installation, manifest synchronization, and local seeding commands with fail-closed execution."]
347
+ // CIL[DOC-CONTEXT module="install-app" layer="cli" related_files=".framework/cli/commands/migrate.ts,spine.config.json"]
348
+ // CIL[DOC-CONTRACT inputs="program: Command" outputs="void" errors="Action errors exit nonzero instead of reporting a successful incomplete installation." side_effects="Registers CLI actions; invoked actions may install packages, copy files, migrate, seed, and synchronize config."]
349
+ // CIL[DOC-MICRO logic_summary="Register app installation, manifest synchronization, and local seeding commands with fail-closed execution; install-app and seed-app accept --reconcile-owned-types to interactively update manifest-owned types whose DB design_schema drifted from the seed." notable_decisions="[]"]
350
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
351
+ // CIL[DOC-STABILITY stability="stable"]
113
352
  export function registerInstallAppCommands(program: Command) {
114
353
 
115
354
  program
116
355
  .command('install-app <package>')
117
356
  .description('Install a Spine Framework app from npm')
118
- .option('--no-seed', 'Skip seeding types and roles into the database', false)
357
+ .option('--no-seed', 'Skip seeding types and roles into the database')
358
+ .option('--reconcile-owned-types', 'Prompt to update manifest-owned types whose DB design_schema differs from the seed')
119
359
  .action(async (packageName: string, opts) => {
120
360
  console.log(`\nInstalling app: ${packageName}\n`)
121
361
 
@@ -176,33 +416,34 @@ export function registerInstallAppCommands(program: Command) {
176
416
 
177
417
 
178
418
  console.log(' Running migrations...')
179
- try {
180
- const migrateResult = await runMigrations({ frameworkOnly: false })
181
- if (migrateResult === null) {
182
- console.log(' ○ Skipping migrations (SUPABASE_DB_PASSWORD not set)')
183
- } else if (migrateResult.applied > 0) {
184
- console.log(` ✓ Migrations: ${migrateResult.applied} applied, ${migrateResult.skipped} skipped`)
185
- } else {
186
- console.log(` ✓ Migrations: up to date (${migrateResult.skipped} already applied)`)
187
- }
188
- if (migrateResult && migrateResult.failed > 0) {
189
- console.warn(` ⚠ ${migrateResult.failed} migration(s) failed — seed may fail`)
419
+ const migrateResult = await runMigrations({ frameworkOnly: false })
420
+ if (migrateResult === null) {
421
+ if (opts.seed !== false) {
422
+ throw new Error('SUPABASE_URL or SUPABASE_DB_PASSWORD not set — cannot run migrations or seed (rerun with --no-seed to install files only)')
190
423
  }
191
- } catch (migrateErr: any) {
192
- console.warn(` ⚠ Migration error: ${migrateErr.message} — continuing with seed`)
424
+ console.log(' ○ Skipping migrations (SUPABASE_DB_PASSWORD not set)')
425
+ } else if (migrateResult.applied > 0) {
426
+ console.log(` ✓ Migrations: ${migrateResult.applied} applied, ${migrateResult.skipped} skipped`)
427
+ } else {
428
+ console.log(` ✓ Migrations: up to date (${migrateResult.skipped} already applied)`)
429
+ }
430
+ if (migrateResult && migrateResult.failed > 0) {
431
+ throw new Error(`${migrateResult.failed} migration(s) failed — aborting install before seed`)
193
432
  }
194
433
 
195
434
 
196
- if (!opts.seed === false) {
197
- await seedApp(appDest, appSlug)
435
+ if (opts.seed !== false) {
436
+ await seedApp(appDest, appSlug, SPINE_CONFIG_PATH, { reconcileOwnedTypes: opts.reconcileOwnedTypes === true })
437
+ } else {
438
+ console.log(' ○ Seed skipped (--no-seed)')
198
439
  }
199
440
 
200
441
 
201
442
  const manifestPath = join(appDest, 'manifest.json')
202
443
  let manifest: Record<string, any> = {}
203
444
  try { manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) } catch {}
204
- if (manifest.registration?.enabled) {
205
- updateRegistrationConfig(appSlug, manifest.registration, manifest.required_roles ?? [])
445
+ if (opts.seed === false && manifest.registration && typeof manifest.registration.enabled === 'boolean') {
446
+ updateRegistrationConfig(appSlug, manifest.registration, manifest.required_roles ?? [], SPINE_CONFIG_PATH, manifest.route_prefix ?? '/')
206
447
  }
207
448
 
208
449
  let installedVersion = 'unknown'
@@ -285,8 +526,8 @@ export function registerInstallAppCommands(program: Command) {
285
526
  const manifestPath = join(appDest, 'manifest.json')
286
527
  let manifest: Record<string, any> = {}
287
528
  try { manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) } catch {}
288
- if (manifest.registration?.enabled) {
289
- updateRegistrationConfig(appSlug, manifest.registration, manifest.required_roles ?? [])
529
+ if (manifest.registration && typeof manifest.registration.enabled === 'boolean') {
530
+ updateRegistrationConfig(appSlug, manifest.registration, manifest.required_roles ?? [], SPINE_CONFIG_PATH, manifest.route_prefix ?? '/')
290
531
  }
291
532
 
292
533
  let updatedVersion = 'unknown'
@@ -324,37 +565,34 @@ export function registerInstallAppCommands(program: Command) {
324
565
  process.exit(1)
325
566
  }
326
567
 
327
- let manifest: Record<string, any> = {}
568
+ let manifest: Record<string, any>
328
569
  try {
329
- manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
330
- } catch {
331
- console.error(`❌ Could not parse manifest.json`)
570
+ manifest = preflightAppInputs(appDir, slug, SPINE_CONFIG_PATH)
571
+ } catch (err: any) {
572
+ console.error(`❌ ${err.message}`)
332
573
  process.exit(1)
333
574
  }
334
575
 
335
576
  const db = createClient(supabaseUrl, serviceRoleKey, { auth: { persistSession: false } })
336
577
 
578
+ const { data: existingApp, error: existingErr } = await db
579
+ .from('apps')
580
+ .select('config')
581
+ .eq('slug', slug)
582
+ .maybeSingle()
583
+ if (existingErr) {
584
+ console.error(`❌ Failed to read existing app config: ${existingErr.message}`)
585
+ process.exit(1)
586
+ }
587
+ if (existingApp?.config !== undefined && existingApp?.config !== null && !isPlainObject(existingApp.config)) {
588
+ console.error(`❌ apps.config for ${slug} is not an object — refusing to merge`)
589
+ process.exit(1)
590
+ }
591
+
337
592
  const { data: appRow, error: appErr } = await db
338
593
  .from('apps')
339
594
  .upsert(
340
- {
341
- slug,
342
- name: manifest.name ?? slug,
343
- description: manifest.description ?? '',
344
- version: manifest.version ?? '1.0.0',
345
- nav_items: manifest.nav_items ?? [],
346
- app_type: 'custom',
347
- source: 'npm',
348
- is_active: true,
349
- route_prefix: manifest.route_prefix ?? ('/' + slug),
350
- renderer: manifest.renderer ?? 'custom',
351
- required_roles: manifest.required_roles ?? (manifest.min_role ? [manifest.min_role] : []),
352
- min_role: manifest.required_roles?.[0] ?? manifest.min_role ?? null,
353
- preview_domain: manifest.preview_domain ?? null,
354
- prod_domain: manifest.prod_domain ?? null,
355
- config: manifest.config ?? undefined,
356
- metadata: manifest.metadata ?? undefined,
357
- },
595
+ appRowPayload(manifest, slug, existingApp?.config ?? undefined),
358
596
  { onConflict: 'slug' }
359
597
  )
360
598
  .select('id')
@@ -368,29 +606,77 @@ export function registerInstallAppCommands(program: Command) {
368
606
  console.log(` ✓ apps row updated (route_prefix: ${manifest.route_prefix ?? '/' + slug}, config: ${manifest.config ? 'synced' : 'none'})`)
369
607
 
370
608
 
371
- if (manifest.registration?.enabled) {
372
- updateRegistrationConfig(slug, manifest.registration, manifest.required_roles ?? [])
609
+ try {
610
+ await syncAppRegistration(db, slug, manifest)
611
+ } catch (err: any) {
612
+ console.error(`❌ Registration sync failed: ${err.message}`)
613
+ process.exit(1)
373
614
  }
374
615
 
375
616
  console.log(`\n✅ ${slug} DB synced from local manifest.\n`)
376
617
  console.log(`Next: npm run assemble && netlify dev\n`)
377
618
  })
619
+
620
+ program
621
+ .command('seed-app <slug>')
622
+ .description('Run the manifest seed block for a local app (no npm, no file changes)')
623
+ .option('--reconcile-owned-types', 'Prompt to update manifest-owned types whose DB design_schema differs from the seed')
624
+ .action(async (slug: string, opts) => {
625
+ console.log(`\nSeeding app: ${slug}\n`)
626
+ loadEnv()
627
+
628
+ const appDir = join(PROJECT_ROOT, 'custom/apps', slug)
629
+ if (!existsSync(join(appDir, 'manifest.json'))) {
630
+ console.error(`❌ No app found at custom/apps/${slug}/manifest.json`)
631
+ process.exit(1)
632
+ }
633
+
634
+ try {
635
+ await seedApp(appDir, slug, SPINE_CONFIG_PATH, { reconcileOwnedTypes: opts.reconcileOwnedTypes === true })
636
+ } catch (err: any) {
637
+ console.error(`\n❌ Seed failed: ${err.message}`)
638
+ if (process.env.SPINE_CLI_DEBUG) console.error(err.stack)
639
+ process.exit(1)
640
+ }
641
+
642
+ console.log(`\n✅ ${slug} seeded.\n`)
643
+ })
378
644
  }
379
- // CIL[END chunk_id="install-app_registerinstallappcommands_1784069813"]
380
- // CIL[BEGIN chunk_id="install-app_resolveappslug_1784069813" type="function" identity="resolveAppSlug" parent="none"]
381
- // CIL[DOC-MACRO purpose="Resolve app slug from package name or manifest"]
382
- // CIL[DOC-RESPONSIBILITY responsibility="Extract app identifier from package metadata"]
383
- // CIL[DOC-CONTEXT module="cli.commands.install-app" layer="cli" related_files="[\".framework/migrations/015_simplify_apps_table.sql\",\"custom/apps/portal/pages/ContentPage.tsx\",\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\"custom/apps/cortex/functions/custom_cil-embeddings.ts\",\"\\\"]"]
384
- // CIL[DOC-CONTRACT inputs="none" outputs="app slug" errors="file read errors" side_effects="none"]
385
- // CIL[DOC-MICRO logic_summary="Reads manifest.json slug field or strips spine-framework- prefix" notable_decisions="[]"]
386
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
387
- // CIL[DOC-STABILITY stability="stable"]
388
- // CIL[DOC-VECTOR type="context" content="Layer: cli. Direct callers: 1. Direct calls: 0. Related files: .framework/migrations/015_simplify_apps_table.sql, custom/apps/portal/pages/ContentPage.tsx, custom/apps/cortex/pages/operations/OperationsDashboard.tsx, custom/apps/cortex/functions/custom_cil-embeddings.ts, \."]
389
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 1. Total affected: 1."]
390
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
391
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
645
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:registerinstallappcommands:function"]
646
+ // CIL[DOC-VECTOR type="context" content="purpose='Registers a command to install a Spine Framework app from npm and handles the installation process, including file copying and database migrations.'; No verified external callers" status="ready"]
647
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
648
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
649
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
392
650
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
393
651
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
652
+
653
+ // CIL[DOC-MACRO purpose="Resolves the application slug from a given package path and package name."]
654
+ // CIL[DOC-RESPONSIBILITY responsibility="Resolves the application slug from a given package path and package name. It attempts to read the slug from a manifest file, falling back to a modified package name if not found."]
655
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/migrate.js"]
656
+ // CIL[DOC-CONTRACT inputs="pkgPath: string (required) — the file path to the package manifest.\\npkgName: string (required) — the name of the package to derive the slug from." outputs="string — the resolved application slug." errors="none" side_effects="file I/O dependencies="readFileSync, JSON.parse, join"]
657
+ // CIL[DOC-MICRO logic_summary="Resolves the application slug from a given package path and package name. It attempts to read the slug from a manifest file, falling back to a modified package name if not found." notable_decisions="none"]
658
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
659
+ // CIL[DOC-STABILITY stability="stable"]
660
+ /**
661
+ * Resolves the application slug from a given package path and package name. It attempts to read the slug from a manifest file, falling back to a modified package name if not found.
662
+ *
663
+ * @inputSpec
664
+ * pkgPath: string (required) — the file path to the package manifest.
665
+ * pkgName: string (required) — the name of the package to derive the slug from.
666
+ * @outputSpec string — the resolved application slug.
667
+ * @sideEffects file I/O
668
+ * @stability stable
669
+ * @calls readFileSync, JSON.parse, join
670
+ * @cil-generated
671
+ */
672
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:resolveappslug:function" type="function" identity="resolveAppSlug" parent="~/.framework/cli/commands/install-app.ts"]
673
+ // CIL[DOC-MACRO purpose="resolveAppSlug function"]
674
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the resolveAppSlug function."]
675
+ // CIL[DOC-CONTEXT module=".framework/cli/commands/install-app.ts" layer="app-specific" related_files=""]
676
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
677
+ // CIL[DOC-MICRO logic_summary="Implements the resolveAppSlug function." notable_decisions="[]"]
678
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
679
+ // CIL[DOC-STABILITY stability="stable"]
394
680
  function resolveAppSlug(pkgPath: string, pkgName: string): string {
395
681
  try {
396
682
  const manifest = JSON.parse(readFileSync(join(pkgPath, 'manifest.json'), 'utf8'))
@@ -400,21 +686,38 @@ function resolveAppSlug(pkgPath: string, pkgName: string): string {
400
686
  const slug = pkgName.replace(/^spine-framework-/, '')
401
687
  return slug || pkgName // fallback to full name if replacement results in empty
402
688
  }
403
- // CIL[END chunk_id="install-app_resolveappslug_1784069813"]
404
- // CIL[BEGIN chunk_id="install-app_loadenv_1784069813" type="function" identity="loadEnv" parent="none"]
405
- // CIL[DOC-MACRO purpose="Load environment variables from .env file"]
406
- // CIL[DOC-RESPONSIBILITY responsibility="Parse and set environment variables"]
407
- // CIL[DOC-CONTEXT module="cli.commands.install-app" layer="cli" related_files="[\"\\\"]"]
408
- // CIL[DOC-CONTRACT inputs="none" outputs="process.env updates" errors="file read errors" side_effects="process.env mutation"]
409
- // CIL[DOC-MICRO logic_summary="Reads .env file, parses key=value pairs, sets process.env" notable_decisions="[]"]
410
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
411
- // CIL[DOC-STABILITY stability="stable"]
412
- // CIL[DOC-VECTOR type="context" content="Layer: cli. Direct callers: 1. Direct calls: 0. Related files: \."]
413
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 1. Total affected: 1."]
414
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
415
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
689
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:resolveappslug:function"]
690
+ // CIL[DOC-VECTOR type="context" content="purpose='Resolves the application slug from a given package path and package name.'; No verified external callers" status="ready"]
691
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
692
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
693
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
416
694
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
417
695
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
696
+
697
+ // CIL[DOC-MACRO purpose="Loads environment variables from a .env file located at the project root if it exists."]
698
+ // CIL[DOC-RESPONSIBILITY responsibility="Loads environment variables from a .env file located at the project root if it exists. This function ensures that environment variables are set in the process environment for the application to use."]
699
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/migrate.js"]
700
+ // CIL[DOC-CONTRACT inputs="none" outputs="void" errors="none" side_effects="Reads from the file system to load environment variables. dependencies="join, existsSync, readFileSync"]
701
+ // CIL[DOC-MICRO logic_summary="Loads environment variables from a .env file located at the project root if it exists. This function ensures that environment variables are set in the process environment for the application to use." notable_decisions="none"]
702
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
703
+ // CIL[DOC-STABILITY stability="stable"]
704
+ /**
705
+ * Loads environment variables from a .env file located at the project root if it exists. This function ensures that environment variables are set in the process environment for the application to use.
706
+ *
707
+ * @outputSpec void
708
+ * @sideEffects Reads from the file system to load environment variables.
709
+ * @stability stable
710
+ * @calls join, existsSync, readFileSync
711
+ * @cil-generated
712
+ */
713
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:loadenv:function" type="function" identity="loadEnv" parent="~/.framework/cli/commands/install-app.ts"]
714
+ // CIL[DOC-MACRO purpose="loadEnv function"]
715
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the loadEnv function."]
716
+ // CIL[DOC-CONTEXT module=".framework/cli/commands/install-app.ts" layer="app-specific" related_files=""]
717
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
718
+ // CIL[DOC-MICRO logic_summary="Implements the loadEnv function." notable_decisions="[]"]
719
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
720
+ // CIL[DOC-STABILITY stability="stable"]
418
721
  function loadEnv() {
419
722
  const envPath = join(PROJECT_ROOT, '.env')
420
723
  if (!existsSync(envPath)) return
@@ -429,81 +732,59 @@ function loadEnv() {
429
732
  if (key && !(key in process.env)) process.env[key] = val
430
733
  }
431
734
  }
432
- // CIL[END chunk_id="install-app_loadenv_1784069813"]
433
- // CIL[BEGIN chunk_id="install-app_seedapp_1784069813" type="function" identity="seedApp" parent="none"]
434
- // CIL[DOC-LEARNING topic="install-app:seed_requires_env" content="DB seed requires SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in .env; missing env vars cause install-app to copy files without registering the app in public.apps."]
435
- // CIL[DOC-LEARNING topic="install-app:types_unique_slug" content="public.types must enforce a unique slug. Seeding types per app (inject_app_id with conflict 'app_id,kind,slug') created duplicate funnel_signal, support_ticket, etc. rows, breaking resolveTypeId and .single() lookups. Types are now seeded with conflict 'kind,slug', inject_app_id: false, and ignoreDuplicates to keep them global."]
436
- // CIL[DOC-NOTE type="caution" content="The apps upsert uses onConflict: 'slug' and can still fail on the active route_prefix unique index if another active app already owns the same route_prefix."]
437
- // CIL[DOC-NOTE type="caution" content="types seeds must not inject app_id. The types table uses slug as the global type identifier; app-scoped duplicates cause lookup ambiguity."]
438
- // CIL[DOC-MACRO purpose="Seed database with app types, roles, and permissions"]
439
- // CIL[DOC-RESPONSIBILITY responsibility="Populate database with app-specific seed data"]
440
- // CIL[DOC-CONTEXT module="cli.commands.install-app" layer="cli" related_files="[]"]
441
- // CIL[DOC-CONTRACT inputs="none" outputs="database records" errors="database errors" side_effects="database writes, app config sync from manifest"]
442
- // CIL[DOC-MICRO logic_summary="Registers app (including manifest.config/metadata), seeds roles/types, handles permission patches with user prompts" notable_decisions="[]"]
443
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="complex_database_operations"]
444
- // CIL[DOC-STABILITY stability="stable"]
445
- // CIL[DOC-VECTOR type="context" content="Layer: cli. Direct callers: 0. Direct calls: 0. Related files: none."]
446
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
447
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
448
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
735
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:loadenv:function"]
736
+ // CIL[DOC-VECTOR type="context" content="purpose='Loads environment variables from a .env file located at the project root if it exists.'; No verified external callers" status="ready"]
737
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
738
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
739
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
449
740
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
450
741
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
451
- async function seedApp(appDir: string, appSlug: string) {
742
+
743
+ // CIL[DOC-MACRO purpose="Database seeding function for application setup."]
744
+ // CIL[DOC-RESPONSIBILITY responsibility="Seeds the application database with initial data based on the provided manifest file. It ensures that necessary environment variables are set and handles the upsertion of application details into the database."]
745
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/migrate.js"]
746
+ // CIL[DOC-CONTRACT inputs="appDir: string (required) — the directory where the application files are located.\\nappSlug: string (required) — a unique identifier for the application." outputs="Promise<void>" errors="none" side_effects="Writes to the database and reads from the file system. dependencies="createClient, db.from, db.upsert, db.select, readFileSync, existsSync, join"]
747
+ // CIL[DOC-MICRO logic_summary="Validates the app structure, preloads declared seed files with root confinement, performs fail-closed upsert operations, and syncs registration policy to DB and spine.config.json." notable_decisions="throws on missing environment variables and database errors instead of warning and continuing."]
748
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
749
+ // CIL[DOC-STABILITY stability="stable"]
750
+ /**
751
+ * Seeds the application database with initial data based on the provided manifest file. It ensures that necessary environment variables are set and handles the upsertion of application details into the database.
752
+ *
753
+ * @macro Database seeding function for application setup.
754
+ * @micro Reads a manifest file to determine seed entries and performs upsert operations on the database.
755
+ * @inputSpec
756
+ * appDir: string (required) — the directory where the application files are located.
757
+ * appSlug: string (required) — a unique identifier for the application.
758
+ * @outputSpec Promise<void>
759
+ * @sideEffects Writes to the database and reads from the file system.
760
+ * @stability stable
761
+ * @calls createClient, db.from, db.upsert, db.select, readFileSync, existsSync, join
762
+ * @errorHandling logs warnings for missing environment variables and database errors without throwing.
763
+ * @cil-generated
764
+ */
765
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:seedapp:function" type="function" identity="seedApp" parent="~/.framework/cli/commands/install-app.ts"]
766
+ // CIL[DOC-MACRO purpose="Preflight source and config before seeding declared records, then synchronize one registration policy."]
767
+ // CIL[DOC-RESPONSIBILITY responsibility="Preflight source and config before seeding declared records, then synchronize one registration policy."]
768
+ // CIL[DOC-CONTEXT module="install-app" layer="cli" related_files=".framework/cli/commands/validate-app.ts,.framework/functions/_shared/schema-utils.ts,spine.config.json"]
769
+ // CIL[DOC-CONTRACT inputs="appDir: string, appSlug: string, configPath: string defaults to SPINE_CONFIG_PATH, opts.reconcileOwnedTypes?: boolean" outputs="Promise<void>" errors="Throws for credentials, source/config validation, row writes, missing references, type design_schema conflicts with existing rows, invalid owned_types declarations, unknown pipeline stage_types, invalid notification recipients, or policy synchronization errors." side_effects="Reads source files, writes app seed records, optionally asks permission-patch and owned-type reconcile choices, and synchronizes local/server policy."]
770
+ // CIL[DOC-MICRO logic_summary="Preflight the app structure, manifest, registration block, and local config, preload declared seeds with root confinement while validating type design_schema and permission-patch rows and generating validation_schema on normalized copies, stable-sort prepared entries into dependency order (roles, types, link_types, actions, ai_agents, prompt_configs, pipelines, triggers; permissions_patch ranks just after types; unknown tables last in declared order), then upsert the app row with merged config, apply each seed entry fail-closed — types rows are read-then-compared so identical rows skip and differing rows throw unless reconciled via --reconcile-owned-types, pipeline stages are checked against public.actions and send_notification recipients against roles — and sync registration policy to DB and file" notable_decisions="All input validation happens before createClient so malformed manifests, seeds, or config produce zero DB clients or writes; declared seed entries are mandatory; legacy fallback and auto-added type_permissions files are optional but must be well-formed when present; types rows are never silently overwritten — a DB-vs-seed design_schema diff throws unless the manifest owns the slug and --reconcile-owned-types was passed; action and role slug lookups are fetched once per run and only when pipelines demand them"]
771
+ // CIL[DOC-CONFIDENCE pass="second" score="0.9" flags="none"]
772
+ // CIL[DOC-STABILITY stability="stable"]
773
+ export async function seedApp(
774
+ appDir: string,
775
+ appSlug: string,
776
+ configPath: string = SPINE_CONFIG_PATH,
777
+ opts: { reconcileOwnedTypes?: boolean } = {},
778
+ ): Promise<void> {
452
779
  const supabaseUrl = process.env.SUPABASE_URL
453
780
  const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY
454
781
 
455
782
  if (!supabaseUrl || !serviceRoleKey) {
456
- console.log(' ○ Skipping DB seed (SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY not set)')
457
- return
783
+ throw new Error('SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY must be set to seed')
458
784
  }
459
785
 
460
- const db = createClient(supabaseUrl, serviceRoleKey, {
461
- auth: { persistSession: false },
462
- })
463
-
464
-
465
- const manifestPath = join(appDir, 'manifest.json')
466
- let manifest: Record<string, any> = {}
467
- try { manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) } catch {}
468
-
469
- const { data: appRow, error: appErr } = await db
470
- .from('apps')
471
- .upsert(
472
- {
473
- slug: appSlug,
474
- name: manifest.name ?? appSlug,
475
- description: manifest.description ?? '',
476
- version: manifest.version ?? '1.0.0',
477
- nav_items: manifest.nav_items ?? [],
478
- app_type: 'custom',
479
- source: 'npm',
480
- is_active: true,
481
- route_prefix: manifest.route_prefix ?? ('/' + appSlug),
482
- renderer: manifest.renderer ?? 'custom',
483
-
484
- required_roles: manifest.required_roles ?? (manifest.min_role ? [manifest.min_role] : []),
485
-
486
- min_role: manifest.required_roles?.[0] ?? manifest.min_role ?? null,
487
- preview_domain: manifest.preview_domain ?? null,
488
- prod_domain: manifest.prod_domain ?? null,
489
- config: manifest.config ?? undefined,
490
- metadata: manifest.metadata ?? undefined,
491
- },
492
- { onConflict: 'slug' }
493
- )
494
- .select('id')
495
- .single()
496
-
497
- if (appErr || !appRow?.id) {
498
- console.warn(` ⚠ Could not upsert app row for ${appSlug}: ${appErr?.message ?? 'no id returned'}`)
499
- console.warn(' ○ Skipping seed — fix the apps table and retry with: npx spine-framework install-app ' + appSlug)
500
- return
501
- }
502
-
503
- const appId = appRow.id
504
- console.log(` ✓ App registered (id: ${appId}, config: ${manifest.config ? 'synced' : 'none'})`)
505
-
506
-
786
+ const reconcileOwnedTypes = opts.reconcileOwnedTypes === true
787
+ const manifest = preflightAppInputs(appDir, appSlug, configPath)
507
788
 
508
789
  interface SeedEntry {
509
790
  file: string
@@ -514,16 +795,21 @@ async function seedApp(appDir: string, appSlug: string) {
514
795
  permissions_patch?: boolean
515
796
  }
516
797
 
798
+ const declaredEntries: SeedEntry[] = Array.isArray(manifest.seed)
799
+ ? manifest.seed.map((entry: SeedEntry) => ({ ...entry }))
800
+ : []
801
+ const declared = new Set<SeedEntry>(declaredEntries)
802
+
517
803
  const seedEntries: SeedEntry[] = Array.isArray(manifest.seed)
518
- ? manifest.seed
804
+ ? declaredEntries
519
805
  : [
520
-
806
+
521
807
  { file: 'seed/roles.json', table: 'roles', conflict: 'slug' },
522
808
  { file: 'seed/types.json', table: 'types', conflict: 'kind,slug', inject_app_id: false },
523
809
  { file: 'seed/link-types.json', table: 'link_types', conflict: 'app_id,slug' },
524
810
  ]
525
811
 
526
-
812
+
527
813
  const hasPermissionsPatch = seedEntries.some(e => e.permissions_patch)
528
814
  if (!hasPermissionsPatch) {
529
815
  seedEntries.push({
@@ -535,59 +821,198 @@ async function seedApp(appDir: string, appSlug: string) {
535
821
  })
536
822
  }
537
823
 
824
+ const prepared: { entry: SeedEntry; records: Record<string, any>[] }[] = []
538
825
  for (const entry of seedEntries) {
539
- const filePath = join(appDir, entry.file)
540
- if (!existsSync(filePath)) {
826
+ const filePath = canonicalFileWithin(appDir, entry.file)
827
+ if (!filePath) {
828
+ if (declared.has(entry)) {
829
+ throw new Error(`Declared seed file not found or escapes the app root: ${entry.file}`)
830
+ }
541
831
  console.log(` ○ ${entry.file} not found, skipping`)
542
832
  continue
543
833
  }
544
834
 
545
- let records: Record<string, any>[]
835
+ let records: unknown
546
836
  try {
547
837
  records = JSON.parse(readFileSync(filePath, 'utf8'))
548
838
  } catch {
549
- console.warn(` ⚠ Could not parse ${entry.file}, skipping`)
550
- continue
839
+ throw new Error(`Could not parse seed file: ${entry.file}`)
840
+ }
841
+ if (!Array.isArray(records)) {
842
+ throw new Error(`Seed file must contain a JSON array of records: ${entry.file}`)
843
+ }
844
+ for (const record of records) {
845
+ if (!record || typeof record !== 'object' || Array.isArray(record)) {
846
+ throw new Error(`Seed file contains a non-object record: ${entry.file}`)
847
+ }
848
+ }
849
+
850
+ let preparedRecords = records as Record<string, any>[]
851
+ if (entry.permissions_patch) {
852
+ for (const record of preparedRecords) {
853
+ validatePermissionsPatchRow(record, entry.file)
854
+ }
855
+ } else if (entry.table === 'types') {
856
+ preparedRecords = preparedRecords.map(record => {
857
+ if (!('design_schema' in record)) return { ...record }
858
+ if (!isPlainObject(record.design_schema)) {
859
+ const label = record.slug ?? record.name ?? JSON.stringify(record).slice(0, 40)
860
+ throw new Error(`types seed "${label}": design_schema must be a non-null object`)
861
+ }
862
+ return { ...record, validation_schema: generateValidationSchema(record.design_schema) }
863
+ })
864
+ }
865
+ prepared.push({ entry, records: preparedRecords })
866
+ }
867
+
868
+ // Deterministic dependency ordering — seeds apply in SEED_TABLE_ORDER rather
869
+ // than manifest declaration order, so e.g. a trigger's pipeline_slug lookup
870
+ // can never race its pipeline. permissions_patch ranks just after types (it
871
+ // patches types rows) and unknown tables keep declared relative order last.
872
+ // Array.prototype.sort is stable, so equal ranks preserve declaration order.
873
+ prepared.sort((a, b) => seedTableRank(a.entry) - seedTableRank(b.entry))
874
+
875
+ // Type slugs this app seeds (non-patch types entries) plus the conventional
876
+ // seed/types.json — owned_types declarations resolve against this set, and
877
+ // role slugs from this run's roles seeds backstop recipient validation.
878
+ const seedTypeSlugs = new Set<string>()
879
+ const seedRoleSlugs = new Set<string>()
880
+ for (const { entry, records } of prepared) {
881
+ const target = entry.table === 'types' && !entry.permissions_patch ? seedTypeSlugs
882
+ : entry.table === 'roles' ? seedRoleSlugs
883
+ : null
884
+ if (!target) continue
885
+ for (const record of records) {
886
+ if (typeof record.slug === 'string' && record.slug) target.add(record.slug)
551
887
  }
888
+ }
889
+ const typesSeedPath = canonicalFileWithin(appDir, 'seed/types.json')
890
+ if (typesSeedPath) {
891
+ try {
892
+ const extra: unknown = JSON.parse(readFileSync(typesSeedPath, 'utf8'))
893
+ if (Array.isArray(extra)) {
894
+ for (const row of extra) {
895
+ if (row && typeof row === 'object' && typeof row.slug === 'string') seedTypeSlugs.add(row.slug)
896
+ }
897
+ }
898
+ } catch { /* an undeclared malformed file is not this check's concern */ }
899
+ }
900
+
901
+ const ownedTypeSlugs = new Set<string>()
902
+ if (reconcileOwnedTypes) {
903
+ const owned = manifest.owned_types
904
+ if (!Array.isArray(owned) || owned.length === 0) {
905
+ throw new Error(`--reconcile-owned-types requires manifest.json to declare "owned_types" — a non-empty array of type slugs this app owns`)
906
+ }
907
+ for (const slug of owned) {
908
+ if (!seedTypeSlugs.has(slug)) {
909
+ throw new Error(`manifest.json owned_types declares "${slug}" but no record with that slug exists in the app's types seed (seed/types.json)`)
910
+ }
911
+ ownedTypeSlugs.add(slug)
912
+ }
913
+ }
914
+
915
+ const db = createClient(supabaseUrl, serviceRoleKey, {
916
+ auth: { persistSession: false },
917
+ })
918
+
919
+ const { data: existingApp, error: existingErr } = await db
920
+ .from('apps')
921
+ .select('config')
922
+ .eq('slug', appSlug)
923
+ .maybeSingle()
924
+ if (existingErr) {
925
+ throw new Error(`Could not read existing app config for ${appSlug}: ${existingErr.message}`)
926
+ }
927
+ if (existingApp?.config !== undefined && existingApp?.config !== null && !isPlainObject(existingApp.config)) {
928
+ throw new Error(`apps.config for ${appSlug} is not an object — refusing to merge`)
929
+ }
552
930
 
553
- if (!Array.isArray(records) || records.length === 0) {
931
+ const { data: appRow, error: appErr } = await db
932
+ .from('apps')
933
+ .upsert(
934
+ appRowPayload(manifest, appSlug, existingApp?.config ?? undefined),
935
+ { onConflict: 'slug' }
936
+ )
937
+ .select('id')
938
+ .single()
939
+
940
+ if (appErr || !appRow?.id) {
941
+ throw new Error(`Could not upsert app row for ${appSlug}: ${appErr?.message ?? 'no id returned'}`)
942
+ }
943
+
944
+ const appId = appRow.id
945
+ console.log(` ✓ App registered (id: ${appId}, config: ${manifest.config ? 'synced' : 'none'})`)
946
+
947
+ // Lazily fetched once per run — stage_type and recipient lookups only happen
948
+ // when a pipelines seed actually demands them.
949
+ let cachedActionSlugs: Set<string> | null = null
950
+ const getActionSlugs = async (): Promise<Set<string>> => {
951
+ if (!cachedActionSlugs) {
952
+ const { data, error } = await db.from('actions').select('slug')
953
+ if (error) {
954
+ throw new Error(`Could not read public.actions for pipeline stage validation: ${error.message}`)
955
+ }
956
+ cachedActionSlugs = new Set(
957
+ (data ?? []).map((r: any) => r?.slug).filter((s: any) => typeof s === 'string')
958
+ )
959
+ }
960
+ return cachedActionSlugs
961
+ }
962
+ let cachedRoleSlugs: Set<string> | null = null
963
+ const getRoleSlugs = async (): Promise<Set<string>> => {
964
+ if (!cachedRoleSlugs) {
965
+ const { data, error } = await db.from('roles').select('slug')
966
+ if (error) {
967
+ throw new Error(`Could not read public.roles for recipient validation: ${error.message}`)
968
+ }
969
+ cachedRoleSlugs = new Set(
970
+ (data ?? []).map((r: any) => r?.slug).filter((s: any) => typeof s === 'string')
971
+ )
972
+ // Roles seeded earlier in this same run are valid references even if the
973
+ // read raced the write.
974
+ for (const slug of seedRoleSlugs) cachedRoleSlugs.add(slug)
975
+ }
976
+ return cachedRoleSlugs
977
+ }
978
+
979
+ for (const { entry, records } of prepared) {
980
+ if (records.length === 0) {
554
981
  console.log(` ○ ${entry.file} is empty, skipping`)
555
982
  continue
556
983
  }
557
984
 
558
-
985
+
559
986
  if (entry.permissions_patch) {
560
987
  console.log(` Patching record_permissions for ${records.length} type(s) from ${entry.file}...`)
561
988
  for (const record of records) {
562
989
  const { kind, slug, record_permissions: newPerms } = record
563
- if (!kind || !slug || !newPerms) {
564
- console.warn(` ⚠ Skipping malformed entry (requires kind, slug, record_permissions)`)
565
- continue
566
- }
567
990
 
568
-
569
- const { data: existing } = await db
991
+
992
+ const { data: existing, error: existingTypeErr } = await db
570
993
  .from('types')
571
994
  .select('id, design_schema')
572
995
  .eq('kind', kind)
573
996
  .eq('slug', slug)
574
997
  .maybeSingle()
575
998
 
999
+ if (existingTypeErr) {
1000
+ throw new Error(`Could not read type ${kind}/${slug}: ${existingTypeErr.message}`)
1001
+ }
576
1002
  if (!existing) {
577
- console.log(` ○ ${kind}/${slug} not found in DB — skipping (install the owning app first)`)
578
- continue
1003
+ throw new Error(`Type ${kind}/${slug} not found in DB — cannot apply permissions patch (install the owning app first)`)
579
1004
  }
580
1005
 
581
1006
  const existingSchema = (existing.design_schema ?? {}) as Record<string, any>
582
1007
  const existingPerms = existingSchema.record_permissions ?? null
583
1008
 
584
-
1009
+
585
1010
  if (JSON.stringify(existingPerms) === JSON.stringify(newPerms)) {
586
1011
  console.log(` ✓ ${kind}/${slug} record_permissions unchanged — skipped`)
587
1012
  continue
588
1013
  }
589
1014
 
590
-
1015
+
591
1016
  console.log(`\n Type: ${kind}/${slug}`)
592
1017
  console.log(` Existing record_permissions:`)
593
1018
  console.log(' ' + JSON.stringify(existingPerms, null, 2).replace(/\n/g, '\n '))
@@ -600,30 +1025,89 @@ async function seedApp(appDir: string, appSlug: string) {
600
1025
  continue
601
1026
  }
602
1027
 
603
-
1028
+
1029
+ const mergedSchema = { ...existingSchema, record_permissions: newPerms }
604
1030
  const { error: patchErr } = await db
605
1031
  .from('types')
606
1032
  .update({
607
- design_schema: { ...existingSchema, record_permissions: newPerms },
1033
+ design_schema: mergedSchema,
1034
+ validation_schema: generateValidationSchema(mergedSchema),
608
1035
  })
609
1036
  .eq('id', existing.id)
610
1037
 
611
1038
  if (patchErr) {
612
- console.warn(` ⚠ ${kind}/${slug}: ${patchErr.message}`)
613
- } else {
614
- console.log(` ✓ ${kind}/${slug} record_permissions updated`)
1039
+ throw new Error(`Could not patch ${kind}/${slug}: ${patchErr.message}`)
615
1040
  }
1041
+ console.log(` ✓ ${kind}/${slug} record_permissions updated`)
616
1042
  }
617
1043
  console.log(` ✓ type_permissions patch complete`)
618
1044
  continue
619
1045
  }
620
1046
 
621
-
1047
+
622
1048
  const injectAppId = entry.inject_app_id !== false // default true
623
1049
  console.log(` Seeding ${records.length} record(s) from ${entry.file} → ${entry.table}...`)
624
1050
 
625
1051
  for (const record of records) {
626
1052
  const row = injectAppId ? { ...record, app_id: appId } : { ...record }
1053
+
1054
+ // Types are global (conflict kind,slug) — fetch the existing row first so
1055
+ // a drifted design_schema fails loudly instead of being silently skipped
1056
+ // by ignoreDuplicates. Identical rows skip cleanly; differing rows throw
1057
+ // unless the slug is manifest-owned and --reconcile-owned-types was set.
1058
+ if (entry.table === 'types') {
1059
+ const { data: existingType, error: typeReadErr } = await db
1060
+ .from('types')
1061
+ .select('id, design_schema')
1062
+ .eq('kind', row.kind)
1063
+ .eq('slug', row.slug)
1064
+ .maybeSingle()
1065
+ if (typeReadErr) {
1066
+ throw new Error(`types "${row.kind}/${row.slug}": could not read existing row: ${typeReadErr.message}`)
1067
+ }
1068
+ if (existingType) {
1069
+ if (canonicalJson(existingType.design_schema) === canonicalJson(row.design_schema)) {
1070
+ console.log(` ✓ ${row.kind}/${row.slug} already seeded — identical`)
1071
+ continue
1072
+ }
1073
+ if (ownedTypeSlugs.has(row.slug)) {
1074
+ await reconcileOwnedType(db, existingType, row)
1075
+ continue
1076
+ }
1077
+ throw new Error(
1078
+ `Type conflict on ${row.kind}/${row.slug} (${entry.file}): the DB row's design_schema differs from the seed — ` +
1079
+ `refusing to overwrite. Inspect the diff and rerun with --reconcile-owned-types if this app owns the type.`
1080
+ )
1081
+ }
1082
+ // Absent — fall through to the upsert; ignoreDuplicates stays as the
1083
+ // race guard for a concurrent insert between the read and the write.
1084
+ }
1085
+
1086
+ // Pipeline seeds are validated at install time so a bad stage_type or
1087
+ // notification recipient fails here, not at first trigger fire.
1088
+ if (entry.table === 'pipelines') {
1089
+ await validatePipelineSeedStages(row, entry.file, getActionSlugs, getRoleSlugs)
1090
+ }
1091
+
1092
+ // Trigger seeds reference pipelines by slug — resolve to pipeline_id so
1093
+ // the trigger engine can dispatch. Pipelines must be seeded first.
1094
+ if (entry.table === 'triggers' && row.pipeline_slug && !row.pipeline_id) {
1095
+ const { data: pipeline, error: pipelineErr } = await db
1096
+ .from('pipelines')
1097
+ .select('id')
1098
+ .eq('slug', row.pipeline_slug)
1099
+ .eq('app_id', appId)
1100
+ .maybeSingle()
1101
+ if (pipelineErr) {
1102
+ throw new Error(`trigger "${row.name || row.slug}": could not resolve pipeline '${row.pipeline_slug}': ${pipelineErr.message}`)
1103
+ }
1104
+ if (!pipeline?.id) {
1105
+ throw new Error(`trigger "${row.name || row.slug}": pipeline '${row.pipeline_slug}' not found — seed pipelines first`)
1106
+ }
1107
+ row.pipeline_id = pipeline.id
1108
+ delete row.pipeline_slug
1109
+ }
1110
+
627
1111
  const { error } = await db
628
1112
  .from(entry.table)
629
1113
  .upsert(row, {
@@ -632,28 +1116,504 @@ async function seedApp(appDir: string, appSlug: string) {
632
1116
  })
633
1117
  if (error) {
634
1118
  const label = record.slug ?? record.name ?? JSON.stringify(record).slice(0, 40)
635
- console.warn(` ⚠ ${entry.table} "${label}": ${error.message}`)
1119
+ throw new Error(`${entry.table} "${label}": ${error.message}`)
636
1120
  }
637
1121
  }
638
1122
 
639
1123
  console.log(` ✓ ${entry.table} seeded`)
640
1124
  }
1125
+
1126
+ await syncAppRegistration(db, appSlug, manifest, configPath)
641
1127
  }
642
- // CIL[END chunk_id="install-app_seedapp_1784069813"]
643
- // CIL[BEGIN chunk_id="install-app_prompt_1784069813" type="function" identity="prompt" parent="none"]
644
- // CIL[DOC-MACRO purpose="Prompt user for input via command line"]
645
- // CIL[DOC-RESPONSIBILITY responsibility="Handle interactive user input"]
646
- // CIL[DOC-CONTEXT module="cli.commands.install-app" layer="cli" related_files="[]"]
647
- // CIL[DOC-CONTRACT inputs="none" outputs="user answer" errors="readline errors" side_effects="stdin interaction"]
648
- // CIL[DOC-MICRO logic_summary="Creates readline interface and returns user input as Promise" notable_decisions="[]"]
649
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
650
- // CIL[DOC-STABILITY stability="stable"]
651
- // CIL[DOC-VECTOR type="context" content="Layer: cli. Direct callers: 0. Direct calls: 0. Related files: none."]
652
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
653
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
654
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
1128
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:seedapp:function"]
1129
+ // CIL[DOC-VECTOR type="context" content="purpose='Seeds the application database with initial data based on the provided manifest file.'; No verified external callers" status="ready"]
1130
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
1131
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
1132
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
655
1133
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
656
1134
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
1135
+
1136
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:canonicalfilewithin:function" type="function" identity="canonicalFileWithin" parent="~/.framework/cli/commands/install-app.ts"]
1137
+ // CIL[DOC-MACRO purpose="Resolve a root-contained file path for manifest-referenced seed files"]
1138
+ // CIL[DOC-RESPONSIBILITY responsibility="Return the canonical path of an existing regular file inside the app root, or null"]
1139
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/validate-app.ts"]
1140
+ // CIL[DOC-CONTRACT inputs="appDir: string, relPath: string" outputs="string canonical path or null" errors="none" side_effects="reads filesystem metadata via realpathSync and statSync"]
1141
+ // CIL[DOC-MICRO logic_summary="Rejects absolute paths, lexical escapes, symlink escapes, and non-file targets by canonicalizing app root and target with realpathSync; mirrors the reviewed validate-app helper" notable_decisions="local copy because the validate-app helper is module-private"]
1142
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
1143
+ // CIL[DOC-STABILITY stability="stable"]
1144
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
1145
+ function canonicalFileWithin(appDir: string, relPath: string): string | null {
1146
+ try {
1147
+ if (isAbsolute(relPath)) return null
1148
+ const root = realpathSync(appDir)
1149
+ const lexical = resolve(root, relPath)
1150
+ if (lexical !== root && !lexical.startsWith(root + sep)) return null
1151
+ const canonical = realpathSync(lexical)
1152
+ if (canonical !== root && !canonical.startsWith(root + sep)) return null
1153
+ if (!statSync(canonical).isFile()) return null
1154
+ return canonical
1155
+ } catch {
1156
+ return null
1157
+ }
1158
+ }
1159
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:canonicalfilewithin:function"]
1160
+
1161
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:isplainobject:function" type="function" identity="isPlainObject" parent="~/.framework/cli/commands/install-app.ts"]
1162
+ // CIL[DOC-MACRO purpose="Narrow a value to a non-null non-array plain object"]
1163
+ // CIL[DOC-RESPONSIBILITY responsibility="Return true only for JSON-object-shaped values, excluding null and arrays"]
1164
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/install-app.ts"]
1165
+ // CIL[DOC-CONTRACT inputs="v: any" outputs="boolean type guard for Record<string,any>" errors="none" side_effects="none"]
1166
+ // CIL[DOC-MICRO logic_summary="Checks typeof object while excluding null and Array.isArray" notable_decisions="used to validate manifest.config, registration blocks, seed rows, and stored apps.config before merging"]
1167
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
1168
+ // CIL[DOC-STABILITY stability="stable"]
1169
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
1170
+ function isPlainObject(v: any): v is Record<string, any> {
1171
+ return v !== null && typeof v === 'object' && !Array.isArray(v)
1172
+ }
1173
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:isplainobject:function"]
1174
+
1175
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:preflightappinputs:function" type="function" identity="preflightAppInputs" parent="~/.framework/cli/commands/install-app.ts"]
1176
+ // CIL[DOC-MACRO purpose="Validate an app directory, manifest, registration block, and local config before any DB work"]
1177
+ // CIL[DOC-RESPONSIBILITY responsibility="Run validateAppStructure, require slug match, check manifest.config and registration shapes, and preflight the local spine config — all before createClient"]
1178
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/validate-app.ts,spine.config.json"]
1179
+ // CIL[DOC-CONTRACT inputs="appDir: string, appSlug: string, configPath: string" outputs="parsed manifest Record<string,any>" errors="Throws on invalid app slug, invalid app structure, slug mismatch, non-object manifest.config, malformed registration block, or malformed local config" side_effects="reads manifest, seed files, and local config from disk"]
1180
+ // CIL[DOC-MICRO logic_summary="Requires the app slug to match /^[a-z][a-z0-9-]*$/ before anything else, delegates structural checks to validateAppStructure, then enforces slug equality, object-typed manifest.config when non-null, a plain-object registration block with boolean enabled and required nonempty fields when enabled, and loads the local config to surface corruption early" notable_decisions="the local config is only read when the manifest declares a registration block, so data-only seeds never touch unrelated config; slug validation runs before all filesystem reads so hostile slugs like __proto__ can never reach config indexing"]
1181
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
1182
+ // CIL[DOC-STABILITY stability="stable"]
1183
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
1184
+ function preflightAppInputs(appDir: string, appSlug: string, configPath: string): Record<string, any> {
1185
+ if (typeof appSlug !== 'string' || !/^[a-z][a-z0-9-]*$/.test(appSlug)) {
1186
+ throw new Error(`Invalid app slug "${appSlug}" — expected /^[a-z][a-z0-9-]*$/`)
1187
+ }
1188
+
1189
+ validateAppStructure(appDir)
1190
+
1191
+ const manifest: Record<string, any> = JSON.parse(readFileSync(join(appDir, 'manifest.json'), 'utf8'))
1192
+ if (manifest.slug !== appSlug) {
1193
+ throw new Error(`manifest.json slug "${manifest.slug}" does not match app slug "${appSlug}"`)
1194
+ }
1195
+ if (manifest.config !== undefined && manifest.config !== null && !isPlainObject(manifest.config)) {
1196
+ throw new Error(`manifest.json config for "${appSlug}" must be an object`)
1197
+ }
1198
+ if (manifest.owned_types !== undefined) {
1199
+ if (!Array.isArray(manifest.owned_types) || manifest.owned_types.some((s: any) => typeof s !== 'string' || !s.trim())) {
1200
+ throw new Error(`manifest.json owned_types for "${appSlug}" must be an array of non-empty strings`)
1201
+ }
1202
+ }
1203
+ if (manifest.registration !== undefined && manifest.registration !== null) {
1204
+ const reg = manifest.registration
1205
+ if (!isPlainObject(reg)) {
1206
+ throw new Error(`manifest.json registration for "${appSlug}" must be an object`)
1207
+ }
1208
+ if (reg.enabled !== undefined && typeof reg.enabled !== 'boolean') {
1209
+ throw new Error(`manifest.json registration.enabled for "${appSlug}" must be a boolean`)
1210
+ }
1211
+ if (reg.enabled === true) {
1212
+ if (typeof reg.default_role !== 'string' || !reg.default_role.trim()) {
1213
+ throw new Error(`manifest.json registration.default_role for "${appSlug}" must be a non-empty string when registration is enabled`)
1214
+ }
1215
+ if (typeof reg.redirect_path !== 'string' || !reg.redirect_path.trim()) {
1216
+ throw new Error(`manifest.json registration.redirect_path for "${appSlug}" must be a non-empty string when registration is enabled`)
1217
+ }
1218
+ }
1219
+ loadSpineConfig(configPath)
1220
+ }
1221
+ return manifest
1222
+ }
1223
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:preflightappinputs:function"]
1224
+
1225
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:validatepermissionspatchrow:function" type="function" identity="validatePermissionsPatchRow" parent="~/.framework/cli/commands/install-app.ts"]
1226
+ // CIL[DOC-MACRO purpose="Validate a type_permissions patch seed row before any DB work"]
1227
+ // CIL[DOC-RESPONSIBILITY responsibility="Require nonempty string kind and slug plus a record_permissions map whose values are arrays of CRUD action strings"]
1228
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/install-app.ts"]
1229
+ // CIL[DOC-CONTRACT inputs="record: Record<string,any>, file: string" outputs="void" errors="Throws on missing/blank kind or slug, a non-object record_permissions, or grants that are not arrays of create/read/update/delete" side_effects="none"]
1230
+ // CIL[DOC-MICRO logic_summary="Checks kind and slug as nonempty trimmed strings, record_permissions as a plain object, and every grant list as an array whose members are within the CRUD action set" notable_decisions="empty maps and empty grant arrays remain valid explicit no-grant declarations"]
1231
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
1232
+ // CIL[DOC-STABILITY stability="stable"]
1233
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
1234
+ function validatePermissionsPatchRow(record: Record<string, any>, file: string): void {
1235
+ const crudActions = new Set(['create', 'read', 'update', 'delete'])
1236
+ const { kind, slug, record_permissions: perms } = record
1237
+ if (typeof kind !== 'string' || !kind.trim() || typeof slug !== 'string' || !slug.trim()) {
1238
+ throw new Error(`Malformed permissions patch entry in ${file} (requires kind, slug, record_permissions)`)
1239
+ }
1240
+ if (!isPlainObject(perms)) {
1241
+ throw new Error(`Malformed permissions patch entry in ${file} (record_permissions must be an object)`)
1242
+ }
1243
+ for (const [role, actions] of Object.entries(perms)) {
1244
+ if (!Array.isArray(actions) || actions.some(a => !crudActions.has(a))) {
1245
+ throw new Error(`Malformed permissions patch entry in ${file} (record_permissions.${role} must be an array of create/read/update/delete)`)
1246
+ }
1247
+ }
1248
+ }
1249
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:validatepermissionspatchrow:function"]
1250
+
1251
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:seed_table_order:constant" type="constant" identity="SEED_TABLE_ORDER" parent="~/.framework/cli/commands/install-app.ts"]
1252
+ // CIL[DOC-MACRO purpose="Declare the deterministic dependency order for manifest seed tables"]
1253
+ // CIL[DOC-RESPONSIBILITY responsibility="Rank known seed tables so dependent rows are written after the rows they reference"]
1254
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/install-app.ts"]
1255
+ // CIL[DOC-CONTRACT inputs="none" outputs="ordered table name list" errors="none" side_effects="none"]
1256
+ // CIL[DOC-MICRO logic_summary="roles before types before link_types before actions before ai_agents before prompt_configs before pipelines before triggers" notable_decisions="permissions_patch entries rank just after types via seedTableRank; unknown tables sort after the known set while keeping declared relative order"]
1257
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
1258
+ // CIL[DOC-STABILITY stability="stable"]
1259
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
1260
+ const SEED_TABLE_ORDER: readonly string[] = [
1261
+ 'roles', 'types', 'link_types', 'actions',
1262
+ 'ai_agents', 'prompt_configs', 'pipelines', 'triggers',
1263
+ ]
1264
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:seed_table_order:constant"]
1265
+
1266
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:seedtablerank:function" type="function" identity="seedTableRank" parent="~/.framework/cli/commands/install-app.ts"]
1267
+ // CIL[DOC-MACRO purpose="Map a seed entry to its deterministic apply-order rank"]
1268
+ // CIL[DOC-RESPONSIBILITY responsibility="Return the SEED_TABLE_ORDER index for the entry's table, 1.5 for permissions_patch entries, or past-the-end for unknown tables"]
1269
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/install-app.ts"]
1270
+ // CIL[DOC-CONTRACT inputs="entry: { table: string; permissions_patch?: boolean }" outputs="number rank" errors="none" side_effects="none"]
1271
+ // CIL[DOC-MICRO logic_summary="permissions_patch always ranks between types and link_types so it still runs after every types seed regardless of declaration order; stable sort keeps equal ranks in declared order" notable_decisions="fractional rank keeps the patch slot expressible without renumbering the known-table list"]
1272
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
1273
+ // CIL[DOC-STABILITY stability="stable"]
1274
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
1275
+ function seedTableRank(entry: { table: string; permissions_patch?: boolean }): number {
1276
+ if (entry.permissions_patch) return 1.5
1277
+ const idx = SEED_TABLE_ORDER.indexOf(entry.table)
1278
+ return idx === -1 ? SEED_TABLE_ORDER.length : idx
1279
+ }
1280
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:seedtablerank:function"]
1281
+
1282
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:canonicaljson:function" type="function" identity="canonicalJson" parent="~/.framework/cli/commands/install-app.ts"]
1283
+ // CIL[DOC-MACRO purpose="Serialize JSON with recursively sorted object keys for order-insensitive comparison"]
1284
+ // CIL[DOC-RESPONSIBILITY responsibility="Produce a canonical string form so jsonb key order never counts as a schema difference"]
1285
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/install-app.ts"]
1286
+ // CIL[DOC-CONTRACT inputs="value: any" outputs="canonical JSON string" errors="none" side_effects="none"]
1287
+ // CIL[DOC-MICRO logic_summary="Sort object keys recursively, serialize arrays in order, and skip undefined-valued keys the way JSON.stringify does" notable_decisions="undefined serializes as null so a missing design_schema compares equal to a NULL column"]
1288
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
1289
+ // CIL[DOC-STABILITY stability="stable"]
1290
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
1291
+ function canonicalJson(value: any): string {
1292
+ if (Array.isArray(value)) {
1293
+ return `[${value.map(canonicalJson).join(',')}]`
1294
+ }
1295
+ if (value !== null && typeof value === 'object') {
1296
+ const keys = Object.keys(value).filter(k => value[k] !== undefined).sort()
1297
+ return `{${keys.map(k => `${JSON.stringify(k)}:${canonicalJson(value[k])}`).join(',')}}`
1298
+ }
1299
+ return JSON.stringify(value) ?? 'null'
1300
+ }
1301
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:canonicaljson:function"]
1302
+
1303
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:designschemadifflines:function" type="function" identity="designSchemaDiffLines" parent="~/.framework/cli/commands/install-app.ts"]
1304
+ // CIL[DOC-MACRO purpose="Describe a field-level diff between a stored and a seeded design_schema"]
1305
+ // CIL[DOC-RESPONSIBILITY responsibility="List added, changed, and DB-only field names plus record_permissions and functionality changes"]
1306
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/install-app.ts,.framework/functions/_shared/schema-utils.ts"]
1307
+ // CIL[DOC-CONTRACT inputs="dbSchema: any, seedSchema: any" outputs="string[] of human-readable diff lines" errors="none" side_effects="none"]
1308
+ // CIL[DOC-MICRO logic_summary="Compare the fields maps key-by-key with canonicalJson, then flag record_permissions and functionality changes; DB-only fields are noted as preserved because the owned-type merge keeps them" notable_decisions="views and other top-level keys are not itemized — the diff stays semantic rather than a raw JSON dump"]
1309
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
1310
+ // CIL[DOC-STABILITY stability="stable"]
1311
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
1312
+ function designSchemaDiffLines(dbSchema: any, seedSchema: any): string[] {
1313
+ const lines: string[] = []
1314
+ const dbFields = isPlainObject(dbSchema?.fields) ? dbSchema.fields : {}
1315
+ const seedFields = isPlainObject(seedSchema?.fields) ? seedSchema.fields : {}
1316
+ for (const name of Object.keys(seedFields)) {
1317
+ if (!(name in dbFields)) lines.push(`+ field "${name}" (added by seed)`)
1318
+ else if (canonicalJson(dbFields[name]) !== canonicalJson(seedFields[name])) lines.push(`~ field "${name}" (changed)`)
1319
+ }
1320
+ for (const name of Object.keys(dbFields)) {
1321
+ if (!(name in seedFields)) lines.push(`- field "${name}" (DB-only — preserved)`)
1322
+ }
1323
+ for (const key of ['record_permissions', 'functionality']) {
1324
+ if (canonicalJson(dbSchema?.[key] ?? null) !== canonicalJson(seedSchema?.[key] ?? null)) {
1325
+ lines.push(`~ ${key} changed`)
1326
+ }
1327
+ }
1328
+ return lines
1329
+ }
1330
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:designschemadifflines:function"]
1331
+
1332
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:recipient_regexes:block" type="block" identity="recipient_regexes" parent="~/.framework/cli/commands/install-app.ts"]
1333
+ // CIL[DOC-MACRO purpose="Mirror the recipient shapes _shared/notify.ts resolveNotificationRecipients accepts"]
1334
+ // CIL[DOC-RESPONSIBILITY responsibility="Provide the person-UUID and role:<slug> patterns used by send_notification stage validation"]
1335
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/functions/_shared/notify.ts"]
1336
+ // CIL[DOC-CONTRACT inputs="none" outputs="regex constants" errors="none" side_effects="none"]
1337
+ // CIL[DOC-MICRO logic_summary="NOTIFICATION_UUID_RE matches a person UUID; NOTIFICATION_ROLE_RE captures the slug from role:<slug>; the literal 'self' is also accepted" notable_decisions="kept in sync with notify.ts — anything else must fail install rather than warn-skip at send time"]
1338
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
1339
+ // CIL[DOC-STABILITY stability="stable"]
1340
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
1341
+ const NOTIFICATION_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
1342
+ const NOTIFICATION_ROLE_RE = /^role:(.+)$/
1343
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:recipient_regexes:block"]
1344
+
1345
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:validatepipelineseedstages:function" type="function" identity="validatePipelineSeedStages" parent="~/.framework/cli/commands/install-app.ts"]
1346
+ // CIL[DOC-MACRO purpose="Validate a pipelines seed record's stages against actions and notification recipients against roles"]
1347
+ // CIL[DOC-RESPONSIBILITY responsibility="Require every stages[] stage_type to resolve to a public.actions slug, and every send_notification config.recipients entry to be 'role:<existing slug>', 'self', or a person UUID"]
1348
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/functions/_shared/pipeline-runner.ts,.framework/functions/_shared/notify.ts,.framework/migrations/053_builtin_action_seeds.sql"]
1349
+ // CIL[DOC-CONTRACT inputs="record: Record<string,any>, file: string, getActionSlugs/getRoleSlugs: lazy slug-set fetchers" outputs="Promise<void>" errors="Throws naming the pipeline slug, stage index, and offending stage_type or recipient" side_effects="May fetch public.actions or public.roles slug sets once per run via the provided fetchers"]
1350
+ // CIL[DOC-MICRO logic_summary="Return early when stages is absent, throw on a non-array stages, then check each stage_type against the actions set and each send_notification recipient against role/self/UUID shapes with role existence verified through getRoleSlugs" notable_decisions="actions and roles are fetched lazily so apps without pipelines never pay the lookups; a missing action slug means migration 053/054 has not run"]
1351
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
1352
+ // CIL[DOC-STABILITY stability="stable"]
1353
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
1354
+ async function validatePipelineSeedStages(
1355
+ record: Record<string, any>,
1356
+ file: string,
1357
+ getActionSlugs: () => Promise<Set<string>>,
1358
+ getRoleSlugs: () => Promise<Set<string>>,
1359
+ ): Promise<void> {
1360
+ const stages = record.stages
1361
+ if (stages === undefined || stages === null) return
1362
+ const label = record.slug ?? record.name ?? '(unnamed)'
1363
+ if (!Array.isArray(stages)) {
1364
+ throw new Error(`pipelines "${label}" (${file}): stages must be an array`)
1365
+ }
1366
+ if (stages.length === 0) return
1367
+
1368
+ const actionSlugs = await getActionSlugs()
1369
+ let roleSlugs: Set<string> | null = null
1370
+ for (let i = 0; i < stages.length; i++) {
1371
+ const stage = stages[i]
1372
+ const stageType = stage?.stage_type
1373
+ if (typeof stageType !== 'string' || !actionSlugs.has(stageType)) {
1374
+ throw new Error(
1375
+ `pipelines "${label}" (${file}) stage ${i}: unknown stage_type ${JSON.stringify(stageType)} — ` +
1376
+ `no matching row in public.actions (migrations 053/054 seed the built-ins; run migrations first)`
1377
+ )
1378
+ }
1379
+
1380
+ if (stageType !== 'send_notification') continue
1381
+ const recipients = stage.config?.recipients
1382
+ if (!Array.isArray(recipients)) {
1383
+ throw new Error(`pipelines "${label}" (${file}) stage ${i} (send_notification): config.recipients must be an array`)
1384
+ }
1385
+ for (const recipient of recipients) {
1386
+ const roleMatch = typeof recipient === 'string' ? recipient.match(NOTIFICATION_ROLE_RE) : null
1387
+ if (roleMatch) {
1388
+ roleSlugs ??= await getRoleSlugs()
1389
+ if (!roleSlugs.has(roleMatch[1])) {
1390
+ throw new Error(`pipelines "${label}" (${file}) stage ${i} (send_notification): recipient "${recipient}" references a role that does not exist`)
1391
+ }
1392
+ continue
1393
+ }
1394
+ if (recipient === 'self' || (typeof recipient === 'string' && NOTIFICATION_UUID_RE.test(recipient))) continue
1395
+ throw new Error(
1396
+ `pipelines "${label}" (${file}) stage ${i} (send_notification): invalid recipient ${JSON.stringify(recipient)} — ` +
1397
+ `expected 'role:<slug>', 'self', or a person UUID`
1398
+ )
1399
+ }
1400
+ }
1401
+ }
1402
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:validatepipelineseedstages:function"]
1403
+
1404
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:reconcileownedtype:function" type="function" identity="reconcileOwnedType" parent="~/.framework/cli/commands/install-app.ts"]
1405
+ // CIL[DOC-MACRO purpose="Interactively reconcile a manifest-owned type whose DB design_schema differs from the seed"]
1406
+ // CIL[DOC-RESPONSIBILITY responsibility="Print the field-level diff, prompt [U]pdate / [K]eep, and on update write the merged design_schema with a regenerated validation_schema"]
1407
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/functions/_shared/schema-utils.ts"]
1408
+ // CIL[DOC-CONTRACT inputs="db: any, existingType: Record<string,any>, record: Record<string,any>" outputs="Promise<void>" errors="Throws on the types update failing" side_effects="Prompts on stdin; may update the types row"]
1409
+ // CIL[DOC-MICRO logic_summary="Show designSchemaDiffLines output, then merge as {...dbSchema, ...seedSchema, fields: {...dbFields, ...seedFields}} so DB-only fields survive while the seed wins shared fields and all other keys" notable_decisions="only reachable for slugs declared in manifest.owned_types under --reconcile-owned-types; 'keep' leaves the DB row untouched"]
1410
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
1411
+ // CIL[DOC-STABILITY stability="stable"]
1412
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
1413
+ async function reconcileOwnedType(db: any, existingType: Record<string, any>, record: Record<string, any>): Promise<void> {
1414
+ const { kind, slug } = record
1415
+ const dbSchema = isPlainObject(existingType.design_schema) ? existingType.design_schema : {}
1416
+ const seedSchema = isPlainObject(record.design_schema) ? record.design_schema : {}
1417
+ const dbFields = isPlainObject(dbSchema.fields) ? dbSchema.fields : {}
1418
+ const seedFields = isPlainObject(seedSchema.fields) ? seedSchema.fields : {}
1419
+
1420
+ console.log(`\n Owned type ${kind}/${slug} differs from the seed:`)
1421
+ for (const line of designSchemaDiffLines(dbSchema, seedSchema)) {
1422
+ console.log(` ${line}`)
1423
+ }
1424
+
1425
+ const choice = await prompt(' Apply seed schema? [U]pdate / [K]eep → ')
1426
+ if (choice.trim().toLowerCase() !== 'u') {
1427
+ console.log(` ○ Kept existing ${kind}/${slug}`)
1428
+ return
1429
+ }
1430
+
1431
+ // DB-only fields survive; the seed wins shared fields and every other key.
1432
+ const mergedSchema = {
1433
+ ...dbSchema,
1434
+ ...seedSchema,
1435
+ fields: { ...dbFields, ...seedFields },
1436
+ }
1437
+ const { error } = await db
1438
+ .from('types')
1439
+ .update({
1440
+ design_schema: mergedSchema,
1441
+ validation_schema: generateValidationSchema(mergedSchema),
1442
+ })
1443
+ .eq('id', existingType.id)
1444
+ if (error) {
1445
+ throw new Error(`Could not reconcile type ${kind}/${slug}: ${error.message}`)
1446
+ }
1447
+ console.log(` ✓ ${kind}/${slug} reconciled to seed schema`)
1448
+ }
1449
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:reconcileownedtype:function"]
1450
+
1451
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:approwpayload:function" type="function" identity="appRowPayload" parent="~/.framework/cli/commands/install-app.ts"]
1452
+ // CIL[DOC-MACRO purpose="Build the apps-table upsert payload shared by seedApp and update-db-app"]
1453
+ // CIL[DOC-RESPONSIBILITY responsibility="Merge the existing apps.config with manifest.config so unrelated keys survive, and emit the fixed manifest-derived column set"]
1454
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/install-app.ts"]
1455
+ // CIL[DOC-CONTRACT inputs="manifest: Record<string,any>, appSlug: string, existingConfig?: Record<string,any>" outputs="apps upsert payload object" errors="none" side_effects="none"]
1456
+ // CIL[DOC-MICRO logic_summary="Spread existingConfig then manifest.config when either is present, leaving config undefined when both are absent; all other payload keys derive from the manifest with the same defaults as before" notable_decisions="config is omitted entirely when neither side declares one so upserts never erase existing config"]
1457
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
1458
+ // CIL[DOC-STABILITY stability="stable"]
1459
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
1460
+ function appRowPayload(manifest: Record<string, any>, appSlug: string, existingConfig?: Record<string, any>) {
1461
+ const manifestConfig = manifest.config ?? undefined
1462
+ const mergedConfig = manifestConfig !== undefined || existingConfig !== undefined
1463
+ ? { ...(existingConfig ?? {}), ...(manifestConfig ?? {}) }
1464
+ : undefined
1465
+ return {
1466
+ slug: appSlug,
1467
+ name: manifest.name ?? appSlug,
1468
+ description: manifest.description ?? '',
1469
+ version: manifest.version ?? '1.0.0',
1470
+ nav_items: manifest.nav_items ?? [],
1471
+ app_type: 'custom',
1472
+ source: 'npm',
1473
+ is_active: true,
1474
+ route_prefix: manifest.route_prefix ?? ('/' + appSlug),
1475
+ renderer: manifest.renderer ?? 'custom',
1476
+
1477
+ required_roles: manifest.required_roles ?? (manifest.min_role ? [manifest.min_role] : []),
1478
+
1479
+ min_role: manifest.required_roles?.[0] ?? manifest.min_role ?? null,
1480
+ preview_domain: manifest.preview_domain ?? null,
1481
+ prod_domain: manifest.prod_domain ?? null,
1482
+ config: mergedConfig,
1483
+ metadata: manifest.metadata ?? undefined,
1484
+ }
1485
+ }
1486
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:approwpayload:function"]
1487
+
1488
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:registrationentry:function" type="function" identity="registrationEntry" parent="~/.framework/cli/commands/install-app.ts"]
1489
+ // CIL[DOC-MACRO purpose="Compute the per-app registration policy entry shared by file and DB writes"]
1490
+ // CIL[DOC-RESPONSIBILITY responsibility="Derive the roles/default_role/path/strategy/target/parent/workspace/domain-join entry from the manifest registration block"]
1491
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files="spine.config.json"]
1492
+ // CIL[DOC-CONTRACT inputs="regConfig: RegistrationConfig, requiredRoles: string[], previous?: Record<string,any>, fallbackPath?: string" outputs="per-app registration entry object" errors="none" side_effects="none"]
1493
+ // CIL[DOC-MICRO logic_summary="Roles come from requiredRoles when nonempty otherwise default_role; account_strategy defaults to 'new'; allow_domain_join is copied only when declared so other apps are never forced false; when enabled is not true the entry merges declared fields over the previous policy (mapping previous.path to redirect_path) with a neutral member/route_prefix fallback" notable_decisions="undefined optional fields serialize as absent keys; explicit enabled:false persists a disabled policy instead of skipping sync"]
1494
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
1495
+ // CIL[DOC-STABILITY stability="stable"]
1496
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
1497
+ function registrationEntry(
1498
+ regConfig: RegistrationConfig,
1499
+ requiredRoles: string[],
1500
+ previous?: Record<string, any>,
1501
+ fallbackPath: string = '/',
1502
+ ) {
1503
+ if (regConfig.enabled !== true) {
1504
+ const prev = previous ?? {}
1505
+ const defaultRole = regConfig.default_role ?? prev.default_role ?? 'member'
1506
+ const roles = requiredRoles.length
1507
+ ? requiredRoles
1508
+ : (Array.isArray(prev.roles) && prev.roles.length ? prev.roles : [defaultRole])
1509
+ return {
1510
+ roles,
1511
+ default_role: defaultRole,
1512
+ path: regConfig.redirect_path ?? prev.path ?? fallbackPath,
1513
+ enabled: regConfig.enabled ?? prev.enabled ?? false,
1514
+ account_strategy: regConfig.account_strategy ?? prev.account_strategy ?? 'new',
1515
+ target_account: regConfig.target_account ?? prev.target_account,
1516
+ parent_account: regConfig.parent_account ?? prev.parent_account,
1517
+ default_workspace_name: regConfig.default_workspace_name ?? prev.default_workspace_name,
1518
+ allow_domain_join: regConfig.allow_domain_join ?? prev.allow_domain_join,
1519
+ }
1520
+ }
1521
+ const availableRoles: string[] = requiredRoles.length ? requiredRoles : [regConfig.default_role]
1522
+ return {
1523
+ roles: availableRoles,
1524
+ default_role: regConfig.default_role,
1525
+ path: regConfig.redirect_path,
1526
+ enabled: regConfig.enabled,
1527
+ account_strategy: regConfig.account_strategy || 'new',
1528
+ target_account: regConfig.target_account,
1529
+ parent_account: regConfig.parent_account,
1530
+ default_workspace_name: regConfig.default_workspace_name,
1531
+ allow_domain_join: regConfig.allow_domain_join,
1532
+ }
1533
+ }
1534
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:registrationentry:function"]
1535
+
1536
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:syncappregistration:function" type="function" identity="syncAppRegistration" parent="~/.framework/cli/commands/install-app.ts"]
1537
+ // CIL[DOC-MACRO purpose="Sync the resolved registration policy to apps.config.registration and spine.config.json"]
1538
+ // CIL[DOC-RESPONSIBILITY responsibility="Write the server-side registration block onto the app row config (merged with existing keys), then persist the same policy to the local config file"]
1539
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files="spine.config.json"]
1540
+ // CIL[DOC-CONTRACT inputs="db: any, appSlug: string, manifest: Record<string,any>, configPath?: string" outputs="Promise<void>" errors="Throws on config read or update failure, malformed existing apps.config, and malformed local config; no-ops only when the manifest has no registration block" side_effects="Reads and updates apps.config; writes spine.config.json; logs to stdout"]
1541
+ // CIL[DOC-MICRO logic_summary="Compute ONE canonical registration entry — merging declared fields over {...local previous, ...server previous} with server winning when enabled is not true — merge it into the existing apps.config via select(config).eq(slug).maybeSingle then update, and only after the DB write succeeds pass the same resolved entry to updateRegistrationConfig for the local file" notable_decisions="explicit enabled:false persists an identical disabled policy on both sides even when prior server/local policies diverged; server policy mirrors what complete-registration resolves so flows need not trust client-supplied values; local file save intentionally follows the DB write"]
1542
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
1543
+ // CIL[DOC-STABILITY stability="stable"]
1544
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
1545
+ async function syncAppRegistration(db: any, appSlug: string, manifest: Record<string, any>, configPath: string = SPINE_CONFIG_PATH): Promise<void> {
1546
+ const regConfig = manifest.registration as RegistrationConfig | undefined
1547
+ if (regConfig === undefined || regConfig === null) return
1548
+
1549
+ const requiredRoles: string[] = manifest.required_roles ?? []
1550
+ const fallbackPath: string = manifest.route_prefix ?? '/'
1551
+
1552
+ const localPrevious = (() => {
1553
+ const prev = loadSpineConfig(configPath).registration?.apps?.[appSlug]
1554
+ return isPlainObject(prev) ? prev : undefined
1555
+ })()
1556
+
1557
+ // Mirror the resolved registration policy onto apps.config.registration
1558
+ // so server-side flows (e.g. complete-registration) can derive
1559
+ // strategy/role/target/parent without trusting client-supplied values.
1560
+ const { data: existing, error: readErr } = await db
1561
+ .from('apps')
1562
+ .select('config')
1563
+ .eq('slug', appSlug)
1564
+ .maybeSingle()
1565
+ if (readErr) {
1566
+ throw new Error(`Could not read apps.config for ${appSlug}: ${readErr.message}`)
1567
+ }
1568
+
1569
+ const existingConfig = existing?.config ?? {}
1570
+ if (!isPlainObject(existingConfig)) {
1571
+ throw new Error(`apps.config for ${appSlug} is not an object — cannot merge registration policy`)
1572
+ }
1573
+ const serverPrevious = isPlainObject(existingConfig.registration) ? existingConfig.registration : undefined
1574
+ const previous = localPrevious || serverPrevious
1575
+ ? { ...(localPrevious ?? {}), ...(serverPrevious ?? {}) }
1576
+ : undefined
1577
+ const regEntry = registrationEntry(regConfig, requiredRoles, previous, fallbackPath)
1578
+
1579
+ const { error: cfgErr } = await db
1580
+ .from('apps')
1581
+ .update({ config: { ...existingConfig, registration: regEntry } })
1582
+ .eq('slug', appSlug)
1583
+ if (cfgErr) {
1584
+ throw new Error(`Could not persist registration config to apps.config: ${cfgErr.message}`)
1585
+ }
1586
+
1587
+ updateRegistrationConfig(appSlug, regConfig, requiredRoles, configPath, fallbackPath, regEntry)
1588
+ }
1589
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:syncappregistration:function"]
1590
+
1591
+ // CIL[DOC-MACRO purpose="Prompts the user with a question and returns their input as a promise."]
1592
+ // CIL[DOC-RESPONSIBILITY responsibility="Prompts the user with a question and returns their input as a promise. This function is used to facilitate interactive command-line interfaces."]
1593
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/migrate.js"]
1594
+ // CIL[DOC-CONTRACT inputs="question: string (required) — The question to prompt the user with." outputs="Promise<string> — A promise that resolves to the user's input as a string." errors="none" side_effects="none dependencies="readline.createInterface"]
1595
+ // CIL[DOC-MICRO logic_summary="Prompts the user with a question and returns their input as a promise. This function is used to facilitate interactive command-line interfaces." notable_decisions="none"]
1596
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
1597
+ // CIL[DOC-STABILITY stability="stable"]
1598
+ /**
1599
+ * Prompts the user with a question and returns their input as a promise. This function is used to facilitate interactive command-line interfaces.
1600
+ *
1601
+ * @inputSpec
1602
+ * question: string (required) — The question to prompt the user with.
1603
+ * @outputSpec Promise<string> — A promise that resolves to the user's input as a string.
1604
+ * @sideEffects none
1605
+ * @stability stable
1606
+ * @calls readline.createInterface
1607
+ * @cil-generated
1608
+ */
1609
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:prompt:function" type="function" identity="prompt" parent="~/.framework/cli/commands/install-app.ts"]
1610
+ // CIL[DOC-MACRO purpose="prompt function"]
1611
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the prompt function."]
1612
+ // CIL[DOC-CONTEXT module=".framework/cli/commands/install-app.ts" layer="app-specific" related_files=""]
1613
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
1614
+ // CIL[DOC-MICRO logic_summary="Implements the prompt function." notable_decisions="[]"]
1615
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
1616
+ // CIL[DOC-STABILITY stability="stable"]
657
1617
  function prompt(question: string): Promise<string> {
658
1618
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
659
1619
  return new Promise(resolve => {
@@ -663,67 +1623,130 @@ function prompt(question: string): Promise<string> {
663
1623
  })
664
1624
  })
665
1625
  }
666
- // CIL[END chunk_id="install-app_prompt_1784069813"]
667
- // CIL[BEGIN chunk_id="install-app_loadspineconfig_1784069813" type="function" identity="loadSpineConfig" parent="none"]
668
- // CIL[DOC-MACRO purpose="Load spine.config.json configuration file"]
669
- // CIL[DOC-RESPONSIBILITY responsibility="Read and parse Spine configuration"]
670
- // CIL[DOC-CONTEXT module="cli.commands.install-app" layer="cli" related_files="[]"]
671
- // CIL[DOC-CONTRACT inputs="none" outputs="SpineConfig object" errors="file read errors" side_effects="none"]
672
- // CIL[DOC-MICRO logic_summary="Reads spine.config.json with fallback to empty config" notable_decisions="[]"]
673
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
674
- // CIL[DOC-STABILITY stability="stable"]
675
- // CIL[DOC-VECTOR type="context" content="Layer: cli. Direct callers: 0. Direct calls: 0. Related files: none."]
676
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
677
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
678
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
1626
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:prompt:function"]
1627
+ // CIL[DOC-VECTOR type="context" content="purpose='Prompts the user with a question and returns their input as a promise.'; No verified external callers" status="ready"]
1628
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
1629
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
1630
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
679
1631
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
680
1632
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
681
- function loadSpineConfig(): SpineConfig {
682
- if (!existsSync(SPINE_CONFIG_PATH)) {
1633
+
1634
+ // CIL[DOC-MACRO purpose="Loads the Spine configuration from a specified file path, returning a default structure if the file does not exist or is invalid."]
1635
+ // CIL[DOC-RESPONSIBILITY responsibility="Loads the Spine configuration from a specified file path, returning a default structure if the file does not exist or is invalid. This function ensures that the application has a valid configuration to work with."]
1636
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/migrate.js"]
1637
+ // CIL[DOC-CONTRACT inputs="none" outputs="SpineConfig — an object representing the Spine configuration structure." errors="none" side_effects="none dependencies="existsSync, readFileSync, JSON.parse"]
1638
+ // CIL[DOC-MICRO logic_summary="Loads the Spine configuration from the given path (default SPINE_CONFIG_PATH), returning a neutral structure only when the file is absent." notable_decisions="malformed JSON propagates a parse error instead of silently resetting the config; non-object roots and non-object registration or registration.apps values throw a config-shape error."]
1639
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
1640
+ // CIL[DOC-STABILITY stability="stable"]
1641
+ /**
1642
+ * Loads the Spine configuration from a specified file path, returning a default structure if the file does not exist or is invalid. This function ensures that the application has a valid configuration to work with.
1643
+ *
1644
+ * @outputSpec SpineConfig — an object representing the Spine configuration structure.
1645
+ * @sideEffects none
1646
+ * @stability stable
1647
+ * @calls existsSync, readFileSync, JSON.parse
1648
+ * @errorHandling returns a default configuration on JSON parsing failure.
1649
+ * @cil-generated
1650
+ */
1651
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:loadspineconfig:function" type="function" identity="loadSpineConfig" parent="~/.framework/cli/commands/install-app.ts"]
1652
+ // CIL[DOC-MACRO purpose="Read a caller-selected Spine configuration, returning a neutral object only when the file is absent."]
1653
+ // CIL[DOC-RESPONSIBILITY responsibility="Read a caller-selected Spine configuration, returning a neutral object only when the file is absent."]
1654
+ // CIL[DOC-CONTEXT module="install-app" layer="cli" related_files="spine.config.json"]
1655
+ // CIL[DOC-CONTRACT inputs="configPath: string defaults to SPINE_CONFIG_PATH" outputs="SpineConfig" errors="Throws for JSON syntax or root/registration/apps container shape errors." side_effects="Reads a config file and parses JSON."]
1656
+ // CIL[DOC-MICRO logic_summary="Read a caller-selected Spine configuration, returning a neutral object only when the file is absent." notable_decisions="[]"]
1657
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
1658
+ // CIL[DOC-STABILITY stability="stable"]
1659
+ function loadSpineConfig(configPath: string = SPINE_CONFIG_PATH): SpineConfig {
1660
+ if (!existsSync(configPath)) {
683
1661
  return { registration: { apps: {} } }
684
1662
  }
685
- try {
686
- return JSON.parse(readFileSync(SPINE_CONFIG_PATH, 'utf8'))
687
- } catch {
688
- return { registration: { apps: {} } }
1663
+ const parsed = JSON.parse(readFileSync(configPath, 'utf8'))
1664
+ if (!isPlainObject(parsed)) {
1665
+ throw new Error(`Invalid spine.config.json at ${configPath}: root must be a JSON object`)
1666
+ }
1667
+ if (parsed.registration !== undefined && !isPlainObject(parsed.registration)) {
1668
+ throw new Error(`Invalid spine.config.json at ${configPath}: registration must be an object`)
1669
+ }
1670
+ if (parsed.registration?.apps !== undefined && !isPlainObject(parsed.registration.apps)) {
1671
+ throw new Error(`Invalid spine.config.json at ${configPath}: registration.apps must be an object`)
689
1672
  }
1673
+ return parsed
690
1674
  }
691
- // CIL[END chunk_id="install-app_loadspineconfig_1784069813"]
692
- // CIL[BEGIN chunk_id="install-app_savespineconfig_1784069813" type="function" identity="saveSpineConfig" parent="none"]
693
- // CIL[DOC-MACRO purpose="Save configuration to spine.config.json"]
694
- // CIL[DOC-RESPONSIBILITY responsibility="Write Spine configuration to file"]
695
- // CIL[DOC-CONTEXT module="cli.commands.install-app" layer="cli" related_files="[]"]
696
- // CIL[DOC-CONTRACT inputs="none" outputs="file write" errors="file write errors" side_effects="filesystem write"]
697
- // CIL[DOC-MICRO logic_summary="Writes configuration as formatted JSON to spine.config.json" notable_decisions="[]"]
698
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
699
- // CIL[DOC-STABILITY stability="stable"]
700
- // CIL[DOC-VECTOR type="context" content="Layer: cli. Direct callers: 0. Direct calls: 0. Related files: none."]
701
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
702
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
703
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
1675
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:loadspineconfig:function"]
1676
+ // CIL[DOC-VECTOR type="context" content="purpose='Loads the Spine configuration from a specified file path, returning a default structure if the file does not exist or is invalid.'; No verified external callers" status="ready"]
1677
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
1678
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
1679
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
704
1680
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
705
1681
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
706
- function saveSpineConfig(config: SpineConfig): void {
707
- writeFileSync(SPINE_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n')
1682
+
1683
+ // CIL[DOC-MACRO purpose="Saves the provided Spine configuration to a specified file path in JSON format."]
1684
+ // CIL[DOC-RESPONSIBILITY responsibility="Saves the provided Spine configuration to a specified file path in JSON format. This function ensures that the configuration is persistently stored for future use."]
1685
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/migrate.js"]
1686
+ // CIL[DOC-CONTRACT inputs="config: SpineConfig (required) — the configuration object to be saved." outputs="void" errors="none" side_effects="Writes the configuration to a file at SPINE_CONFIG_PATH. dependencies="writeFileSync"]
1687
+ // CIL[DOC-MICRO logic_summary="Saves the provided Spine configuration to a specified file path in JSON format. This function ensures that the configuration is persistently stored for future use." notable_decisions="none"]
1688
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
1689
+ // CIL[DOC-STABILITY stability="stable"]
1690
+ /**
1691
+ * Saves the provided Spine configuration to a specified file path in JSON format. This function ensures that the configuration is persistently stored for future use.
1692
+ *
1693
+ * @inputSpec
1694
+ * config: SpineConfig (required) — the configuration object to be saved.
1695
+ * @outputSpec void
1696
+ * @sideEffects Writes the configuration to a file at SPINE_CONFIG_PATH.
1697
+ * @stability stable
1698
+ * @calls writeFileSync
1699
+ * @cil-generated
1700
+ */
1701
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:savespineconfig:function" type="function" identity="saveSpineConfig" parent="~/.framework/cli/commands/install-app.ts"]
1702
+ // CIL[DOC-MACRO purpose="Serialize a Spine configuration to its selected file."]
1703
+ // CIL[DOC-RESPONSIBILITY responsibility="Serialize a Spine configuration to its selected file."]
1704
+ // CIL[DOC-CONTEXT module="install-app" layer="cli" related_files="spine.config.json"]
1705
+ // CIL[DOC-CONTRACT inputs="config: SpineConfig, configPath: string defaults to SPINE_CONFIG_PATH" outputs="void" errors="Propagates file write errors." side_effects="Writes the config file."]
1706
+ // CIL[DOC-MICRO logic_summary="Serialize a Spine configuration to its selected file." notable_decisions="[]"]
1707
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
1708
+ // CIL[DOC-STABILITY stability="stable"]
1709
+ function saveSpineConfig(config: SpineConfig, configPath: string = SPINE_CONFIG_PATH): void {
1710
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n')
708
1711
  }
709
- // CIL[END chunk_id="install-app_savespineconfig_1784069813"]
710
- // CIL[BEGIN chunk_id="install-app_updateregistrationconfig_1784069813" type="function" identity="updateRegistrationConfig" parent="none"]
711
- // CIL[DOC-LEARNING topic="spine.config:registration_slug_match" content="spine.config.json registration.apps is keyed by app slug and default_app points to that key; the slug must match the public.apps row or post-login redirects will point to a non-routable app."]
712
- // CIL[DOC-MACRO purpose="Update registration config for app in spine.config.json"]
713
- // CIL[DOC-RESPONSIBILITY responsibility="Configure app registration and role settings"]
714
- // CIL[DOC-CONTEXT module="cli.commands.install-app" layer="cli" related_files="[]"]
715
- // CIL[DOC-CONTRACT inputs="none" outputs="config file update" errors="file errors" side_effects="config file write"]
716
- // CIL[DOC-MICRO logic_summary="Updates spine.config.json with app registration settings and available roles" notable_decisions="[]"]
717
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
718
- // CIL[DOC-STABILITY stability="stable"]
719
- // CIL[DOC-VECTOR type="context" content="Layer: cli. Direct callers: 0. Direct calls: 0. Related files: none."]
720
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
721
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
722
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
1712
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:savespineconfig:function"]
1713
+ // CIL[DOC-VECTOR type="context" content="purpose='Saves the provided Spine configuration to a specified file path in JSON format.'; No verified external callers" status="ready"]
1714
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
1715
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
1716
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
723
1717
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
724
1718
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
725
- function updateRegistrationConfig(appSlug: string, regConfig: RegistrationConfig, requiredRoles: string[] = []): void {
726
- const config = loadSpineConfig()
1719
+
1720
+ // CIL[DOC-MACRO purpose="Updates the registration configuration for a specific application in the spine configuration file."]
1721
+ // CIL[DOC-RESPONSIBILITY responsibility="Updates the registration configuration for a specific application in the spine configuration file. This function ensures that the application registration is properly set up with roles and other relevant settings."]
1722
+ // CIL[DOC-CONTEXT module="install-app" layer="app-specific" related_files=".framework/cli/commands/migrate.js"]
1723
+ // CIL[DOC-CONTRACT inputs="appSlug: string (required) — the unique identifier for the application.\\nregConfig: RegistrationConfig (required) — configuration settings for the registration.\\nrequiredRoles: string[] (optional) — an array of roles required for the application." outputs="void" errors="none" side_effects="Writes to the spine configuration file and logs updates to the console. dependencies="loadSpineConfig, saveSpineConfig"]
1724
+ // CIL[DOC-MICRO logic_summary="Updates the registration configuration for a specific application in the spine configuration file. This function ensures that the application registration is properly set up with roles and other relevant settings." notable_decisions="none"]
1725
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
1726
+ // CIL[DOC-STABILITY stability="stable"]
1727
+ /**
1728
+ * Updates the registration configuration for a specific application in the spine configuration file. This function ensures that the application registration is properly set up with roles and other relevant settings.
1729
+ *
1730
+ * @inputSpec
1731
+ * appSlug: string (required) — the unique identifier for the application.
1732
+ * regConfig: RegistrationConfig (required) — configuration settings for the registration.
1733
+ * requiredRoles: string[] (optional) — an array of roles required for the application.
1734
+ * @outputSpec void
1735
+ * @sideEffects Writes to the spine configuration file and logs updates to the console.
1736
+ * @stability stable
1737
+ * @calls loadSpineConfig, saveSpineConfig
1738
+ * @cil-generated
1739
+ */
1740
+ // CIL[BEGIN stable_id="~/.framework/cli/commands/install-app.ts:updateregistrationconfig:function" type="function" identity="updateRegistrationConfig" parent="~/.framework/cli/commands/install-app.ts"]
1741
+ // CIL[DOC-MACRO purpose="Merge a computed or declared per-app policy into local config while preserving explicit defaults and other apps."]
1742
+ // CIL[DOC-RESPONSIBILITY responsibility="Merge a computed or declared per-app policy into local config while preserving explicit defaults and other apps."]
1743
+ // CIL[DOC-CONTEXT module="install-app" layer="cli" related_files="spine.config.json"]
1744
+ // CIL[DOC-CONTRACT inputs="appSlug: string, regConfig: RegistrationConfig, requiredRoles: string[] default empty, configPath: string defaults to SPINE_CONFIG_PATH, fallbackPath: string default slash, resolvedEntry?: Record<string, any>" outputs="void" errors="Propagates config read/shape and file write errors." side_effects="Reads and writes local config; logs results."]
1745
+ // CIL[DOC-MICRO logic_summary="Merge a computed or declared per-app policy into local config while preserving explicit defaults and other apps." notable_decisions="[]"]
1746
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
1747
+ // CIL[DOC-STABILITY stability="stable"]
1748
+ function updateRegistrationConfig(appSlug: string, regConfig: RegistrationConfig, requiredRoles: string[] = [], configPath: string = SPINE_CONFIG_PATH, fallbackPath: string = '/', resolvedEntry?: Record<string, any>): void {
1749
+ const config = loadSpineConfig(configPath)
727
1750
 
728
1751
 
729
1752
  if (!config.registration) {
@@ -734,21 +1757,14 @@ function updateRegistrationConfig(appSlug: string, regConfig: RegistrationConfig
734
1757
  }
735
1758
 
736
1759
 
737
- let availableRoles: string[] = requiredRoles.length ? requiredRoles : [regConfig.default_role]
1760
+ const previous = isPlainObject(config.registration.apps[appSlug]) ? config.registration.apps[appSlug] : undefined
1761
+ const entry = resolvedEntry ?? registrationEntry(regConfig, requiredRoles, previous, fallbackPath)
738
1762
 
739
1763
 
740
- config.registration.apps[appSlug] = {
741
- roles: availableRoles,
742
- default_role: regConfig.default_role,
743
- path: regConfig.redirect_path,
744
- enabled: regConfig.enabled,
745
- account_strategy: regConfig.account_strategy || 'new',
746
- target_account: regConfig.target_account,
747
- }
1764
+ config.registration.apps[appSlug] = entry
748
1765
 
749
1766
 
750
- const enabledApps = Object.entries(config.registration.apps).filter(([, cfg]) => cfg.enabled)
751
- if (enabledApps.length === 1 || !config.registration.default_app) {
1767
+ if (entry.enabled === true && !config.registration.default_app) {
752
1768
  config.registration.default_app = appSlug
753
1769
  config.registration.default_role = regConfig.default_role
754
1770
  }
@@ -758,8 +1774,14 @@ function updateRegistrationConfig(appSlug: string, regConfig: RegistrationConfig
758
1774
  config.registration.allow_url_override = true
759
1775
  }
760
1776
 
761
- saveSpineConfig(config)
1777
+ saveSpineConfig(config, configPath)
762
1778
  console.log(` ✓ Updated spine.config.json registration for ${appSlug}`)
763
- console.log(` ✓ Available roles: ${availableRoles.join(', ')}`)
1779
+ console.log(` ✓ Available roles: ${entry.roles.join(', ')}`)
764
1780
  }
765
- // CIL[END chunk_id="install-app_updateregistrationconfig_1784069813"]
1781
+ // CIL[END stable_id="~/.framework/cli/commands/install-app.ts:updateregistrationconfig:function"]
1782
+ // CIL[DOC-VECTOR type="context" content="purpose='Updates the registration configuration for a specific application in the spine configuration file.'; No verified external callers" status="ready"]
1783
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
1784
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
1785
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
1786
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
1787
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]