spine-framework 0.3.145 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +76 -0
- package/.framework/.spine-manifest.json +19 -0
- package/.framework/cli/commands/agents.ts +61 -33
- package/.framework/cli/commands/auth.ts +60 -28
- package/.framework/cli/commands/create-app.ts +212 -80
- package/.framework/cli/commands/dev.ts +184 -68
- package/.framework/cli/commands/doctor.ts +324 -152
- package/.framework/cli/commands/generate.ts +119 -68
- package/.framework/cli/commands/init.ts +141 -242
- package/.framework/cli/commands/install-app.ts +1336 -314
- package/.framework/cli/commands/items.ts +31 -29
- package/.framework/cli/commands/migrate.ts +299 -156
- package/.framework/cli/commands/migrations.ts +62 -30
- package/.framework/cli/commands/pipelines.ts +31 -29
- package/.framework/cli/commands/status.ts +105 -45
- package/.framework/cli/commands/system.ts +47 -42
- package/.framework/cli/commands/test.ts +161 -102
- package/.framework/cli/commands/upgrade.ts +260 -0
- package/.framework/cli/commands/validate-app.ts +517 -161
- package/.framework/cli/context.ts +117 -85
- package/.framework/cli/env-loader.ts +74 -43
- package/.framework/cli/index.ts +70 -46
- package/.framework/cli/lib/project-files.ts +404 -0
- package/.framework/docs/guides/testing-guide.md +1 -1
- package/.framework/docs/runtime-types.md +72 -72
- package/.framework/functions/_shared/access.ts +84 -0
- package/.framework/functions/_shared/agent-runner.ts +5067 -1026
- package/.framework/functions/_shared/app-manifest.ts +214 -94
- package/.framework/functions/_shared/ast.ts +214 -0
- package/.framework/functions/_shared/audit.ts +91 -55
- package/.framework/functions/_shared/cil.ts +368 -127
- package/.framework/functions/_shared/db.ts +337 -68
- package/.framework/functions/_shared/funnel.ts +228 -0
- package/.framework/functions/_shared/geo.ts +105 -0
- package/.framework/functions/_shared/hash-api-key.ts +94 -17
- package/.framework/functions/_shared/index.ts +22 -224
- package/.framework/functions/_shared/kb.ts +832 -58
- package/.framework/functions/_shared/llm.ts +118 -18
- package/.framework/functions/_shared/logging.ts +317 -19
- package/.framework/functions/_shared/middleware.ts +701 -131
- package/.framework/functions/_shared/notify.ts +70 -0
- package/.framework/functions/_shared/outcomes.ts +34 -19
- package/.framework/functions/_shared/permissions.ts +501 -315
- package/.framework/functions/_shared/pipeline-runner.ts +704 -155
- package/.framework/functions/_shared/principal.ts +436 -263
- package/.framework/functions/_shared/rate-limit.ts +233 -0
- package/.framework/functions/_shared/resolve-credentials.ts +86 -19
- package/.framework/functions/_shared/resolve-ids.ts +203 -139
- package/.framework/functions/_shared/retry-deliveries.ts +51 -16
- package/.framework/functions/_shared/schema-utils.ts +602 -521
- package/.framework/functions/_shared/security.ts +345 -0
- package/.framework/functions/_shared/testing.ts +197 -172
- package/.framework/functions/_shared/tracing.ts +351 -21
- package/.framework/functions/_shared/trigger-engine.ts +425 -136
- package/.framework/functions/_shared/webhook-registration.ts +71 -70
- package/.framework/functions/_shared/webhook-registry.ts +126 -83
- package/.framework/functions/account-nodes.ts +30 -30
- package/.framework/functions/adapters/generic-webhook-inbound.ts +49 -16
- package/.framework/functions/adapters/generic-webhook-outbound.ts +17 -17
- package/.framework/functions/adapters/openai-outbound.ts +15 -16
- package/.framework/functions/admin-data.ts +596 -173
- package/.framework/functions/admin-funnel.ts +64 -60
- package/.framework/functions/admin-install-funnel.ts +84 -66
- package/.framework/functions/admin-signals.ts +69 -51
- package/.framework/functions/ai-agents.ts +431 -106
- package/.framework/functions/ai-interactions.ts +186 -112
- package/.framework/functions/api-keys.ts +107 -107
- package/.framework/functions/apps.ts +154 -136
- package/.framework/functions/auth.ts +83 -65
- package/.framework/functions/cil-agent-ingest.ts +189 -232
- package/.framework/functions/cil-agent.ts +433 -177
- package/.framework/functions/cil-callmap.ts +642 -430
- package/.framework/functions/cil-config.ts +139 -135
- package/.framework/functions/cil-context.ts +214 -127
- package/.framework/functions/cil-doc.ts +587 -458
- package/.framework/functions/cil-embeddings.ts +453 -364
- package/.framework/functions/cil-graph.ts +427 -142
- package/.framework/functions/cil-ingest.ts +1418 -1154
- package/.framework/functions/cil-pipeline.ts +276 -21
- package/.framework/functions/cil-project.ts +659 -132
- package/.framework/functions/cil-validate.ts +198 -164
- package/.framework/functions/debug-auth.ts +57 -55
- package/.framework/functions/deliveries.ts +80 -21
- package/.framework/functions/embeddings.ts +1023 -318
- package/.framework/functions/eval-harness.ts +146 -108
- package/.framework/functions/integration-outbound.ts +205 -75
- package/.framework/functions/integration-routes.ts +128 -23
- package/.framework/functions/integrations.ts +125 -100
- package/.framework/functions/invites.ts +1095 -460
- package/.framework/functions/item-progress.ts +158 -102
- package/.framework/functions/kb.ts +34 -10
- package/.framework/functions/logs.ts +133 -133
- package/.framework/functions/observability.ts +152 -122
- package/.framework/functions/pipeline-executions.ts +147 -132
- package/.framework/functions/pipelines.ts +134 -119
- package/.framework/functions/prompt-configs.ts +96 -80
- package/.framework/functions/roles.ts +112 -82
- package/.framework/functions/security.ts +450 -0
- package/.framework/functions/system-cron.ts +117 -80
- package/.framework/functions/system.ts +158 -98
- package/.framework/functions/tests.ts +29 -29
- package/.framework/functions/timers.ts +94 -95
- package/.framework/functions/trigger-scheduler.ts +10 -0
- package/.framework/functions/triggers.ts +179 -149
- package/.framework/functions/types.ts +137 -125
- package/.framework/mcp/index.ts +133 -0
- package/.framework/mcp/load-env.ts +60 -0
- package/.framework/mcp/shared/app-manifest.ts +275 -0
- package/.framework/mcp/shared/db.ts +477 -0
- package/.framework/mcp/shared/kb.ts +118 -0
- package/.framework/mcp/tools/architect.ts +431 -0
- package/.framework/mcp/tools/cil/context.ts +345 -0
- package/.framework/mcp/tools/cil/graph.ts +242 -0
- package/.framework/mcp/tools/cil/ingest.ts +1217 -0
- package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +693 -0
- package/.framework/mcp/tools/cil/pipeline.ts +767 -0
- package/.framework/mcp/tools/cil/project.ts +791 -0
- package/.framework/mcp/tools/cil/retrieval.ts +336 -0
- package/.framework/mcp/tools/cil/search-core.ts +454 -0
- package/.framework/mcp/tools/cil/search.ts +97 -0
- package/.framework/mcp/tools/cil/suggest.ts +186 -0
- package/.framework/mcp/tools/cil/validate.ts +1369 -0
- package/.framework/mcp/tools/kb/delete.ts +139 -0
- package/.framework/mcp/tools/kb/index.ts +52 -0
- package/.framework/mcp/tools/kb/ingest.ts +84 -0
- package/.framework/mcp/tools/kb/list-types.ts +102 -0
- package/.framework/mcp/tools/kb/publish.ts +100 -0
- package/.framework/mcp/tools/kb/reindex.ts +86 -0
- package/.framework/mcp/tools/kb/revoke.ts +85 -0
- package/.framework/mcp/tools/kb/search.ts +101 -0
- package/.framework/mcp/tools/kb/upsert.ts +125 -0
- package/.framework/mcp/tools/observability.ts +510 -0
- package/.framework/migrations/001_core_schema.sql +15 -0
- package/.framework/migrations/002_core_rls_and_functions.sql +15 -0
- package/.framework/migrations/003_core_system_data.sql +16 -1
- package/.framework/migrations/004_core_rpc_and_broker.sql +17 -1
- package/.framework/migrations/005_core_observability.sql +17 -1
- package/.framework/migrations/006_security_hardening_rls.sql +15 -0
- package/.framework/migrations/007_kb_article_list.sql +17 -1
- package/.framework/migrations/008_items_target.sql +15 -0
- package/.framework/migrations/009_roles_slug_unique.sql +15 -0
- package/.framework/migrations/010_auth_user_trigger.sql +15 -0
- package/.framework/migrations/011_spine_system_account.sql +16 -0
- package/.framework/migrations/012_service_role_grants.sql +16 -0
- package/.framework/migrations/013_get_account_apps_role_filter.sql +15 -0
- package/.framework/migrations/014_apps_required_roles.sql +16 -0
- package/.framework/migrations/015_types_kind_slug_unique.sql +15 -0
- package/.framework/migrations/016_cil_types.sql +281 -0
- package/.framework/migrations/017_cil_stable_ids.sql +37 -0
- package/.framework/migrations/018_runtime_tables.sql +129 -0
- package/.framework/migrations/019_agent_prompt_columns.sql +56 -0
- package/.framework/migrations/020_schema_reconciliation.sql +67 -0
- package/.framework/migrations/021_kb_embedding_lifecycle.sql +31 -0
- package/.framework/migrations/032_cil_status_migration.sql +73 -0
- package/.framework/migrations/033_cil_versioning.sql +73 -0
- package/.framework/migrations/034_match_embeddings_knn.sql +115 -0
- package/.framework/migrations/035_rate_limits.sql +55 -0
- package/.framework/migrations/036_demo_record_flags.sql +72 -0
- package/.framework/migrations/037_kb_types_canonical.sql +244 -0
- package/.framework/migrations/038_drop_demo_record_flags.sql +38 -0
- package/.framework/migrations/042_registration_hierarchy.sql +309 -0
- package/.framework/migrations/043_policy_actor_cleanup.sql +39 -0
- package/.framework/migrations/044_match_embeddings_filtered_knn.sql +151 -0
- package/.framework/migrations/045_security_critical_rls.sql +139 -0
- package/.framework/migrations/046_security_missing_rls.sql +79 -0
- package/.framework/migrations/047_people_updated_by.sql +27 -0
- package/.framework/migrations/048_watcher_last_seen_at.sql +24 -0
- package/.framework/migrations/049_security_blocklist.sql +148 -0
- package/.framework/migrations/050_notifications.sql +49 -0
- package/.framework/migrations/051_message_agent_fields.sql +74 -0
- package/.framework/migrations/052_registration_security.sql +421 -0
- package/.framework/migrations/053_builtin_action_seeds.sql +66 -0
- package/.framework/migrations/054_kb_ingest_lease.sql +157 -0
- package/.framework/migrations/055_type_id_drift_reconciliation.sql +33 -0
- package/.framework/migrations/056_slug_column_reconciliation.sql +38 -0
- package/.framework/migrations/057_pipelines_slug_identity.sql +14 -0
- package/.framework/migrations/058_account_data_field_declarations.sql +53 -0
- package/.framework/migrations/059_registration_placement_fixes.sql +269 -0
- package/.framework/migrations/060_account_person_record_permissions.sql +38 -0
- package/.framework/migrations/061_fk_dedup.sql +32 -0
- package/.framework/public/spine-logo.svg +10 -3
- package/.framework/scripts/app-install-cli.ts +190 -93
- package/.framework/scripts/assemble-frontend.sh +127 -44
- package/.framework/scripts/assemble-functions.sh +128 -30
- package/.framework/scripts/boundary-check.sh +6 -2
- package/.framework/scripts/build-manifest.sh +24 -68
- package/.framework/scripts/check-core-integrity.sh +15 -76
- package/.framework/scripts/checksum-manifest.mts +343 -0
- package/.framework/scripts/cil-migrate-markers.ts +403 -0
- package/.framework/scripts/fix-cil-markers.mjs +176 -0
- package/.framework/scripts/verify-integrity.sh +21 -57
- package/.framework/scripts/verify-manifest.mts +305 -0
- package/.framework/seeds/types/funnel_signal_use.json +74 -21
- package/.framework/seeds/types/spine_instance.json +73 -21
- package/.framework/src/App.tsx +118 -72
- package/.framework/src/apps/admin/index.tsx +855 -58
- package/.framework/src/components/AppWrapper.tsx +43 -45
- package/.framework/src/components/CustomAppLoader.tsx +160 -116
- package/.framework/src/components/admin/AdminListPage.tsx +46 -44
- package/.framework/src/components/admin/AdminSidebar.tsx +57 -47
- package/.framework/src/components/admin/AdminStatsCard.tsx +46 -44
- package/.framework/src/components/admin/PageInstructions.tsx +98 -2
- package/.framework/src/components/admin/SortableTableHeader.tsx +44 -45
- package/.framework/src/components/app-shell/GenericAppShell.tsx +64 -65
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +44 -47
- package/.framework/src/components/app-shell/GenericListPage.tsx +45 -49
- package/.framework/src/components/app-sidebar.tsx +42 -45
- package/.framework/src/components/auth/ProtectedRoute.tsx +48 -41
- package/.framework/src/components/auth/TurnstileWidget.tsx +27 -0
- package/.framework/src/components/kb/KBArticleEditor.tsx +231 -22
- package/.framework/src/components/kb/KBArticleList.tsx +251 -10
- package/.framework/src/components/layout/AppShell.tsx +51 -45
- package/.framework/src/components/layout/Header.tsx +42 -43
- package/.framework/src/components/layout/Layout.tsx +46 -46
- package/.framework/src/components/layout/Sidebar.tsx +196 -96
- package/.framework/src/components/runtime/DataDetailHeader.tsx +55 -30
- package/.framework/src/components/runtime/DataDetailPage.tsx +31 -269
- package/.framework/src/components/runtime/DataFilters.tsx +49 -74
- package/.framework/src/components/runtime/DataHeader.tsx +53 -32
- package/.framework/src/components/runtime/DataListPage.tsx +51 -155
- package/.framework/src/components/runtime/DataStats.tsx +66 -44
- package/.framework/src/components/runtime/DataTable.tsx +49 -149
- package/.framework/src/components/runtime/SchemaDetailForm.tsx +45 -75
- package/.framework/src/components/runtime/index.ts +119 -16
- package/.framework/src/components/search-form.tsx +46 -29
- package/.framework/src/components/shared/AgentView.tsx +65 -48
- package/.framework/src/components/shared/FieldRenderer.tsx +144 -50
- package/.framework/src/components/shared/InviteManager.tsx +207 -97
- package/.framework/src/components/shared/MemberList.tsx +97 -0
- package/.framework/src/components/shared/SchemaFields.tsx +107 -86
- package/.framework/src/components/ui/DataTable.tsx +80 -68
- package/.framework/src/components/ui/Form.tsx +157 -146
- package/.framework/src/components/ui/ItemCard.tsx +74 -67
- package/.framework/src/components/ui/ItemListView.tsx +73 -68
- package/.framework/src/components/ui/LoadingSpinner.tsx +43 -48
- package/.framework/src/components/ui/Modal.tsx +78 -68
- package/.framework/src/components/ui/RichTextEditor.tsx +89 -69
- package/.framework/src/components/ui/accordion.tsx +85 -86
- package/.framework/src/components/ui/alert-dialog.tsx +197 -184
- package/.framework/src/components/ui/alert.tsx +105 -94
- package/.framework/src/components/ui/aspect-ratio.tsx +57 -43
- package/.framework/src/components/ui/avatar.tsx +122 -107
- package/.framework/src/components/ui/badge.tsx +60 -55
- package/.framework/src/components/ui/breadcrumb.tsx +127 -128
- package/.framework/src/components/ui/button-group.tsx +86 -72
- package/.framework/src/components/ui/button.tsx +71 -50
- package/.framework/src/components/ui/calendar.tsx +72 -57
- package/.framework/src/components/ui/card.tsx +136 -129
- package/.framework/src/components/ui/carousel.tsx +226 -107
- package/.framework/src/components/ui/chart.tsx +252 -147
- package/.framework/src/components/ui/checkbox.tsx +43 -44
- package/.framework/src/components/ui/collapsible.tsx +86 -71
- package/.framework/src/components/ui/combobox.tsx +282 -249
- package/.framework/src/components/ui/command.tsx +158 -146
- package/.framework/src/components/ui/context-menu.tsx +239 -240
- package/.framework/src/components/ui/dialog.tsx +174 -157
- package/.framework/src/components/ui/direction.tsx +72 -56
- package/.framework/src/components/ui/drawer.tsx +168 -169
- package/.framework/src/components/ui/dropdown-menu.tsx +254 -223
- package/.framework/src/components/ui/empty.tsx +131 -120
- package/.framework/src/components/ui/field.tsx +205 -173
- package/.framework/src/components/ui/hover-card.tsx +71 -72
- package/.framework/src/components/ui/input-group.tsx +148 -133
- package/.framework/src/components/ui/input-otp.tsx +99 -86
- package/.framework/src/components/ui/input.tsx +43 -41
- package/.framework/src/components/ui/item.tsx +207 -185
- package/.framework/src/components/ui/kbd.tsx +57 -56
- package/.framework/src/components/ui/label.tsx +45 -41
- package/.framework/src/components/ui/menubar.tsx +268 -236
- package/.framework/src/components/ui/native-select.tsx +85 -86
- package/.framework/src/components/ui/navigation-menu.tsx +166 -156
- package/.framework/src/components/ui/pagination.tsx +131 -141
- package/.framework/src/components/ui/popover.tsx +125 -128
- package/.framework/src/components/ui/progress.tsx +47 -44
- package/.framework/src/components/ui/radio-group.tsx +60 -55
- package/.framework/src/components/ui/resizable.tsx +85 -72
- package/.framework/src/components/ui/scroll-area.tsx +57 -58
- package/.framework/src/components/ui/select.tsx +182 -169
- package/.framework/src/components/ui/separator.tsx +42 -43
- package/.framework/src/components/ui/sheet.tsx +170 -159
- package/.framework/src/components/ui/sidebar.tsx +395 -398
- package/.framework/src/components/ui/skeleton.tsx +42 -43
- package/.framework/src/components/ui/slider.tsx +61 -44
- package/.framework/src/components/ui/sonner.tsx +44 -44
- package/.framework/src/components/ui/spinner.tsx +47 -42
- package/.framework/src/components/ui/switch.tsx +56 -42
- package/.framework/src/components/ui/table-primitives.tsx +272 -132
- package/.framework/src/components/ui/table.tsx +140 -142
- package/.framework/src/components/ui/tabs.tsx +110 -93
- package/.framework/src/components/ui/textarea.tsx +43 -44
- package/.framework/src/components/ui/toggle-group.tsx +87 -71
- package/.framework/src/components/ui/toggle.tsx +56 -56
- package/.framework/src/components/ui/tooltip.tsx +99 -85
- package/.framework/src/contexts/AppContext.tsx +166 -114
- package/.framework/src/contexts/AuthContext.tsx +232 -137
- package/.framework/src/hooks/use-mobile.ts +43 -44
- package/.framework/src/hooks/useApi.ts +185 -172
- package/.framework/src/hooks/useAppPath.ts +31 -29
- package/.framework/src/hooks/useApps.ts +77 -42
- package/.framework/src/hooks/useEntityList.ts +66 -29
- package/.framework/src/hooks/useEntityRecord.ts +76 -32
- package/.framework/src/hooks/useForm.ts +62 -30
- package/.framework/src/hooks/useListSchema.ts +60 -43
- package/.framework/src/hooks/useSchemaRecord.ts +115 -95
- package/.framework/src/lib/api.ts +258 -134
- package/.framework/src/lib/registration.ts +181 -0
- package/.framework/src/lib/supabase.ts +118 -81
- package/.framework/src/lib/utils.ts +266 -266
- package/.framework/src/main.tsx +43 -44
- package/.framework/src/pages/DashboardPage.tsx +44 -43
- package/.framework/src/pages/NoAppPage.tsx +50 -0
- package/.framework/src/pages/NotFoundPage.tsx +32 -29
- package/.framework/src/pages/SetupPage.tsx +34 -33
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +69 -58
- package/.framework/src/pages/admin/AIAgentsPage.tsx +43 -40
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +63 -55
- package/.framework/src/pages/admin/APIKeysPage.tsx +44 -42
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +117 -94
- package/.framework/src/pages/admin/AppDetailPage.tsx +48 -42
- package/.framework/src/pages/admin/AppsPage.tsx +45 -42
- package/.framework/src/pages/admin/DeliveriesPage.tsx +49 -17
- package/.framework/src/pages/admin/DesignedPage.tsx +31 -30
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +58 -0
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +49 -0
- package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +31 -29
- package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +46 -29
- package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -29
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +68 -55
- package/.framework/src/pages/admin/IntegrationsPage.tsx +45 -42
- package/.framework/src/pages/admin/InvitesPage.tsx +60 -44
- package/.framework/src/pages/admin/LogsPage.tsx +87 -137
- package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +31 -29
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +150 -89
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +77 -104
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +44 -43
- package/.framework/src/pages/admin/PipelinesPage.tsx +59 -145
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +53 -42
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +47 -117
- package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +31 -44
- package/.framework/src/pages/admin/RoleDetailPage.tsx +49 -92
- package/.framework/src/pages/admin/RolesPage.tsx +47 -164
- package/.framework/src/pages/admin/SecurityPage.tsx +310 -0
- package/.framework/src/pages/admin/SelectTestPage.tsx +30 -32
- package/.framework/src/pages/admin/ShadcnTestPage.tsx +37 -44
- package/.framework/src/pages/admin/SimpleDashboard.tsx +32 -44
- package/.framework/src/pages/admin/TestRunDetailPage.tsx +97 -44
- package/.framework/src/pages/admin/TestingDashboard.tsx +126 -71
- package/.framework/src/pages/admin/TimerDetailPage.tsx +67 -55
- package/.framework/src/pages/admin/TimersPage.tsx +44 -42
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +47 -42
- package/.framework/src/pages/admin/TriggersPage.tsx +58 -57
- package/.framework/src/pages/admin/TypeDetailPage.tsx +91 -55
- package/.framework/src/pages/admin/TypesPage.tsx +45 -42
- package/.framework/src/pages/admin/cil/CILAgentPage.tsx +258 -128
- package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +127 -123
- package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +308 -235
- package/.framework/src/pages/admin/cil/CILFileListPage.tsx +159 -110
- package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +432 -0
- package/.framework/src/pages/admin/cil/CILGraphPage.tsx +200 -178
- package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +286 -239
- package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +211 -90
- package/.framework/src/pages/admin/cil/CILSearchPage.tsx +156 -143
- package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +379 -0
- package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +337 -0
- package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +219 -214
- package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
- package/.framework/src/pages/auth/AuthConfirmPage.tsx +135 -65
- package/.framework/src/pages/auth/ForgotPasswordPage.tsx +39 -31
- package/.framework/src/pages/auth/LoginPage.tsx +38 -32
- package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -28
- package/.framework/src/pages/auth/RegisterPage.tsx +429 -155
- package/.framework/src/pages/spine-framework/APIPage.tsx +33 -30
- package/.framework/src/pages/spine-framework/CLIPage.tsx +32 -30
- package/.framework/src/types/auth.ts +101 -93
- package/.framework/src/types/types.ts +283 -262
- package/.framework/templates/project.json +87 -0
- package/.framework/templates/spine.config.neutral.json +7 -0
- package/.framework/tests/api/api-surface.test.ts +381 -0
- package/.framework/tests/fixtures/seed.ts +427 -0
- package/.framework/tests/fixtures/teardown.ts +228 -0
- package/.framework/tests/reporter.ts +351 -0
- package/.framework/tests/setup.ts +135 -0
- package/.framework/tests/ui/ui-sweep.ts +278 -0
- package/.framework/tests/unit/admin-data-logger.test.ts +310 -0
- package/.framework/tests/unit/agent-runner.test.ts +2389 -0
- package/.framework/tests/unit/ai-interactions.test.ts +81 -0
- package/.framework/tests/unit/app-manifest.test.ts +359 -0
- package/.framework/tests/unit/assembly-scripts.test.ts +172 -0
- package/.framework/tests/unit/cil-determinism.test.ts +85 -0
- package/.framework/tests/unit/core-isolation.test.ts +153 -0
- package/.framework/tests/unit/funnel-signals.test.ts +135 -0
- package/.framework/tests/unit/install-app.test.ts +1759 -0
- package/.framework/tests/unit/invites-security.test.ts +572 -0
- package/.framework/tests/unit/kb-ingest-lease.test.ts +287 -0
- package/.framework/tests/unit/kb-search.test.ts +119 -0
- package/.framework/tests/unit/migrate.test.ts +509 -0
- package/.framework/tests/unit/notify.test.ts +136 -0
- package/.framework/tests/unit/permissions.test.ts +269 -0
- package/.framework/tests/unit/pipeline-runner.test.ts +755 -0
- package/.framework/tests/unit/principal-cache.test.ts +138 -0
- package/.framework/tests/unit/retrieval-golden.test.ts +520 -0
- package/.framework/tests/unit/runtime-isolation.test.ts +642 -0
- package/.framework/tests/unit/schema-utils.test.ts +225 -0
- package/.framework/tests/unit/trigger-engine.test.ts +513 -0
- package/.framework/tests/unit/validate-app.test.ts +527 -0
- package/README.md +34 -3
- package/bin/spine-framework.cjs +1 -0
- package/config/tailwind.config.cjs +7 -0
- package/config/tsconfig.functions.json +42 -0
- package/config/tsconfig.json +37 -9
- package/config/vite.config.ts +31 -2
- package/netlify.toml +31 -3
- package/package.json +46 -29
- package/scripts/assemble-frontend.sh +127 -44
- package/scripts/assemble-functions.sh +127 -29
- package/scripts/boundary-check.sh +6 -2
- package/scripts/build-manifest.sh +24 -68
- package/scripts/checksum-manifest.mts +345 -0
- package/scripts/dev-clean.sh +86 -0
- package/scripts/dev-netlify.sh +49 -0
- package/scripts/netlify-dev-wrapper.sh +2 -0
- package/scripts/verify-install.ts +358 -0
- package/scripts/verify-integrity.sh +21 -57
- package/scripts/verify-manifest.mts +305 -0
- package/templates/project/gitignore +45 -0
- package/{config → templates/project}/package-lock.json +22335 -9366
- package/vitest.config.ts +46 -0
- package/.framework/public/spine.config.json +0 -91
- package/config/deno.lock +0 -108
- package/config/tsconfig.node.tsbuildinfo +0 -1
- package/config/tsconfig.tsbuildinfo +0 -1
- package/config/vite.config.d.ts +0 -2
- package/custom/.devin/AGENT.md +0 -357
- package/custom/.devin/skills/install-app/SKILL.md +0 -39
- package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +0 -39
- package/custom/.devin/skills/spine-workbench-idea/SKILL.md +0 -36
- package/custom/.devin/skills/spine-workbench-implement/SKILL.md +0 -46
- package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +0 -28
- package/custom/.devin/skills/spine-workbench-plan/SKILL.md +0 -38
- package/custom/.devin/workflows/app-install.yaml +0 -83
- package/custom/README.md +0 -79
- package/docs/README.md +0 -15
- package/scripts/app-install-cli.ts +0 -286
- package/scripts/check-core-integrity.sh +0 -82
- package/scripts/ingest-chunks.cjs +0 -202
- package/scripts/kb-chunk-parser.cjs +0 -312
- package/scripts/kb-chunk-parser.ts +0 -330
- package/scripts/kb-ingest-docs.ts +0 -203
- package/scripts/load-test-app-install.ts +0 -484
- package/scripts/test-login.js +0 -33
- package/spine.config.json +0 -91
|
@@ -1,52 +1,48 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// CIL[
|
|
12
|
-
// CIL[DOC-
|
|
13
|
-
// CIL[DOC-
|
|
14
|
-
// CIL[DOC-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// CIL[
|
|
42
|
-
// CIL[DOC-
|
|
43
|
-
// CIL[DOC-
|
|
44
|
-
// CIL[DOC-
|
|
45
|
-
// CIL[DOC-
|
|
46
|
-
// CIL[DOC-
|
|
47
|
-
// CIL[DOC-
|
|
48
|
-
// CIL[DOC-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// CIL[
|
|
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
|
|
97
|
-
// CIL[
|
|
98
|
-
// CIL[DOC-
|
|
99
|
-
// CIL[DOC-
|
|
100
|
-
// CIL[DOC-
|
|
101
|
-
// CIL[DOC-
|
|
102
|
-
// CIL[DOC-
|
|
103
|
-
// CIL[DOC-
|
|
104
|
-
// CIL[DOC-
|
|
105
|
-
// CIL[DOC-
|
|
106
|
-
// CIL[DOC-
|
|
107
|
-
// CIL[DOC-VECTOR type="
|
|
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'
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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 =
|
|
330
|
-
} catch {
|
|
331
|
-
console.error(`❌
|
|
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
|
-
|
|
372
|
-
|
|
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
|
|
380
|
-
// CIL[
|
|
381
|
-
// CIL[DOC-
|
|
382
|
-
// CIL[DOC-
|
|
383
|
-
// CIL[DOC-
|
|
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
|
|
404
|
-
// CIL[
|
|
405
|
-
// CIL[DOC-
|
|
406
|
-
// CIL[DOC-
|
|
407
|
-
// CIL[DOC-
|
|
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
|
|
433
|
-
// CIL[
|
|
434
|
-
// CIL[DOC-
|
|
435
|
-
// CIL[DOC-
|
|
436
|
-
// CIL[DOC-
|
|
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
|
-
|
|
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
|
-
|
|
457
|
-
return
|
|
783
|
+
throw new Error('SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY must be set to seed')
|
|
458
784
|
}
|
|
459
785
|
|
|
460
|
-
const
|
|
461
|
-
|
|
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
|
-
?
|
|
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 =
|
|
540
|
-
if (!
|
|
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:
|
|
835
|
+
let records: unknown
|
|
546
836
|
try {
|
|
547
837
|
records = JSON.parse(readFileSync(filePath, 'utf8'))
|
|
548
838
|
} catch {
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
1033
|
+
design_schema: mergedSchema,
|
|
1034
|
+
validation_schema: generateValidationSchema(mergedSchema),
|
|
608
1035
|
})
|
|
609
1036
|
.eq('id', existing.id)
|
|
610
1037
|
|
|
611
1038
|
if (patchErr) {
|
|
612
|
-
|
|
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
|
-
|
|
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
|
|
643
|
-
// CIL[
|
|
644
|
-
// CIL[DOC-
|
|
645
|
-
// CIL[DOC-
|
|
646
|
-
// CIL[DOC-
|
|
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
|
|
667
|
-
// CIL[
|
|
668
|
-
// CIL[DOC-
|
|
669
|
-
// CIL[DOC-
|
|
670
|
-
// CIL[DOC-
|
|
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
|
-
|
|
682
|
-
|
|
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
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
|
692
|
-
// CIL[
|
|
693
|
-
// CIL[DOC-
|
|
694
|
-
// CIL[DOC-
|
|
695
|
-
// CIL[DOC-
|
|
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
|
-
|
|
707
|
-
|
|
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
|
|
710
|
-
// CIL[
|
|
711
|
-
// CIL[DOC-
|
|
712
|
-
// CIL[DOC-
|
|
713
|
-
// CIL[DOC-
|
|
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
|
-
|
|
726
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: ${
|
|
1779
|
+
console.log(` ✓ Available roles: ${entry.roles.join(', ')}`)
|
|
764
1780
|
}
|
|
765
|
-
// CIL[END
|
|
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"]
|