spine-framework 0.4.3 → 0.5.0-rc.4
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 +13 -3
- package/.framework/cli/commands/bootstrap.ts +392 -0
- package/.framework/cli/commands/dev.ts +91 -218
- package/.framework/cli/commands/doctor.ts +132 -26
- package/.framework/cli/commands/generate.ts +38 -298
- package/.framework/cli/commands/init.ts +20 -1
- package/.framework/cli/commands/migrate.ts +36 -30
- package/.framework/cli/commands/migrations.ts +16 -56
- package/.framework/cli/commands/upgrade.ts +35 -0
- package/.framework/cli/commands/validate-schema.ts +147 -0
- package/.framework/cli/context.ts +223 -110
- package/.framework/cli/index.ts +6 -0
- package/.framework/cli/lib/project-files.ts +241 -1
- package/.framework/functions/_shared/agent-runner.ts +1000 -81
- package/.framework/functions/_shared/cil.ts +2 -1
- package/.framework/functions/_shared/db.ts +3 -6
- package/.framework/functions/_shared/hash-api-key.ts +183 -79
- package/.framework/functions/_shared/kb.ts +61 -7
- package/.framework/functions/_shared/logging.ts +562 -224
- package/.framework/functions/_shared/message-sequence.ts +53 -0
- package/.framework/functions/_shared/middleware.ts +47 -44
- package/.framework/functions/_shared/pipeline-runner.ts +150 -37
- package/.framework/functions/_shared/principal.ts +9 -6
- package/.framework/functions/_shared/trigger-engine.ts +149 -40
- package/.framework/functions/admin-data.ts +48 -31
- package/.framework/functions/ai-agents.ts +103 -33
- package/.framework/functions/api-keys.ts +301 -122
- package/.framework/functions/apps.ts +43 -26
- package/.framework/functions/cil-ingest.ts +48 -15
- package/.framework/functions/debug-auth.ts +14 -8
- package/.framework/functions/deliveries.ts +39 -12
- package/.framework/functions/embeddings.ts +145 -76
- package/.framework/functions/integration-outbound.ts +67 -27
- package/.framework/functions/integrations.ts +254 -127
- package/.framework/functions/logs.ts +273 -133
- package/.framework/functions/observability.ts +277 -147
- package/.framework/functions/pipeline-executions.ts +1 -1
- package/.framework/functions/pipeline-runner-background.ts +120 -0
- package/.framework/functions/pipelines.ts +14 -9
- package/.framework/functions/prompt-configs.ts +7 -4
- package/.framework/functions/roles.ts +8 -5
- package/.framework/functions/security.ts +30 -17
- package/.framework/functions/system-cron.ts +250 -128
- package/.framework/functions/tests.ts +7 -6
- package/.framework/functions/timers.ts +310 -107
- package/.framework/functions/trigger-scheduler.ts +65 -5
- package/.framework/functions/triggers.ts +28 -13
- package/.framework/functions/types.ts +13 -10
- package/.framework/migrations/064_episode_leases.sql +196 -0
- package/.framework/migrations/065_execution_tables_updated_at.sql +20 -0
- package/.framework/migrations/066_episode_steal_thread_merge.sql +148 -0
- package/.framework/migrations/067_episode_lease_stale_steal.sql +93 -0
- package/.framework/migrations/068_cron_schedule_timer_rpcs.sql +251 -0
- package/.framework/migrations/069_log_stats_window.sql +54 -0
- package/.framework/migrations/070_app_type_schema_rpcs.sql +166 -0
- package/.framework/migrations/071_timer_interval_keys.sql +96 -0
- package/.framework/migrations/072_slug_autofill.sql +77 -0
- package/.framework/migrations/073_seed_member_roles.sql +26 -0
- package/.framework/schemas/spine.config.schema.json +62 -0
- package/.framework/src/App.tsx +3 -1
- package/.framework/src/apps/admin/index.tsx +1141 -775
- package/.framework/src/components/ErrorBoundary.tsx +136 -0
- package/.framework/src/components/admin/AdminListPage.tsx +81 -73
- package/.framework/src/components/admin/AdminSidebar.tsx +92 -81
- package/.framework/src/components/admin/EntitySelect.tsx +181 -0
- package/.framework/src/components/admin/PageInstructions.tsx +86 -127
- package/.framework/src/components/app-shell/GenericAppShell.tsx +3 -1
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
- package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
- package/.framework/src/components/auth/TurnstileWidget.tsx +9 -6
- package/.framework/src/components/kb/KBArticleEditor.tsx +49 -6
- package/.framework/src/components/layout/Layout.tsx +4 -2
- package/.framework/src/components/layout/Sidebar.tsx +304 -179
- package/.framework/src/components/runtime/DataDetailPage.tsx +67 -35
- package/.framework/src/components/runtime/DataHeader.tsx +96 -48
- package/.framework/src/components/runtime/DataListPage.tsx +3 -26
- package/.framework/src/components/runtime/DataStats.tsx +10 -10
- package/.framework/src/components/runtime/DataTable.tsx +12 -10
- package/.framework/src/components/ui/sheet.tsx +24 -26
- package/.framework/src/components/ui/textarea.tsx +74 -48
- package/.framework/src/hooks/useBranding.ts +47 -0
- package/.framework/src/hooks/useEntityList.ts +12 -9
- package/.framework/src/hooks/useListSchema.ts +108 -58
- package/.framework/src/instructions/README.md +57 -0
- package/.framework/src/instructions/ai-agents.md +69 -0
- package/.framework/src/instructions/alerts.md +80 -0
- package/.framework/src/instructions/api-keys.md +87 -0
- package/.framework/src/instructions/apps.md +77 -0
- package/.framework/src/instructions/concepts/ai-and-prompts.md +150 -0
- package/.framework/src/instructions/concepts/apps-and-renderers.md +162 -0
- package/.framework/src/instructions/concepts/auth-and-api-keys.md +133 -0
- package/.framework/src/instructions/concepts/automation.md +186 -0
- package/.framework/src/instructions/concepts/concurrency-and-idempotency.md +125 -0
- package/.framework/src/instructions/concepts/core-vs-custom.md +124 -0
- package/.framework/src/instructions/concepts/design-schema.md +152 -0
- package/.framework/src/instructions/concepts/integrations-and-webhooks.md +127 -0
- package/.framework/src/instructions/concepts/observability.md +116 -0
- package/.framework/src/instructions/concepts/permissions.md +150 -0
- package/.framework/src/instructions/concepts/runtime-records.md +111 -0
- package/.framework/src/instructions/concepts/scope-and-accounts.md +112 -0
- package/.framework/src/instructions/concepts/spine-way.md +172 -0
- package/.framework/src/instructions/deliveries.md +59 -0
- package/.framework/src/instructions/guide/00-orientation.md +106 -0
- package/.framework/src/instructions/guide/01-install-and-first-run.md +253 -0
- package/.framework/src/instructions/guide/02-project-map.md +147 -0
- package/.framework/src/instructions/guide/03-domain-model.md +223 -0
- package/.framework/src/instructions/guide/04-first-app.md +227 -0
- package/.framework/src/instructions/guide/05-glossary.md +183 -0
- package/.framework/src/instructions/integrations.md +96 -0
- package/.framework/src/instructions/internal/cil-markers.md +83 -0
- package/.framework/src/instructions/invites.md +60 -0
- package/.framework/src/instructions/kb.md +80 -0
- package/.framework/src/instructions/logs.md +77 -0
- package/.framework/src/instructions/observability.md +61 -0
- package/.framework/src/instructions/operations/deployment.md +115 -0
- package/.framework/src/instructions/operations/migrations.md +125 -0
- package/.framework/src/instructions/operations/security-checklist.md +148 -0
- package/.framework/src/instructions/operations/troubleshooting.md +170 -0
- package/.framework/src/instructions/operations/upgrade.md +116 -0
- package/.framework/src/instructions/pipeline-executions.md +62 -0
- package/.framework/src/instructions/pipelines.md +78 -0
- package/.framework/src/instructions/prompts.md +95 -0
- package/.framework/src/instructions/reference/api/admin-data.md +162 -0
- package/.framework/src/instructions/reference/api/auth.md +113 -0
- package/.framework/src/instructions/reference/api/errors-and-envelopes.md +113 -0
- package/.framework/src/instructions/reference/app-manifest.md +150 -0
- package/.framework/src/instructions/reference/cli.md +124 -0
- package/.framework/src/instructions/reference/database.md +132 -0
- package/.framework/src/instructions/reference/design-schema.md +280 -0
- package/.framework/src/instructions/reference/env-vars.md +133 -0
- package/.framework/src/instructions/reference/functions/account-nodes.md +75 -0
- package/.framework/src/instructions/reference/functions/admin-data.md +195 -0
- package/.framework/src/instructions/reference/functions/admin-funnel.md +90 -0
- package/.framework/src/instructions/reference/functions/admin-install-funnel.md +107 -0
- package/.framework/src/instructions/reference/functions/admin-signals.md +102 -0
- package/.framework/src/instructions/reference/functions/ai-agents.md +148 -0
- package/.framework/src/instructions/reference/functions/ai-interactions.md +156 -0
- package/.framework/src/instructions/reference/functions/api-keys.md +130 -0
- package/.framework/src/instructions/reference/functions/apps.md +140 -0
- package/.framework/src/instructions/reference/functions/auth.md +99 -0
- package/.framework/src/instructions/reference/functions/debug-auth.md +91 -0
- package/.framework/src/instructions/reference/functions/deliveries.md +110 -0
- package/.framework/src/instructions/reference/functions/embeddings.md +166 -0
- package/.framework/src/instructions/reference/functions/eval-harness.md +149 -0
- package/.framework/src/instructions/reference/functions/index.md +103 -0
- package/.framework/src/instructions/reference/functions/integration-outbound.md +200 -0
- package/.framework/src/instructions/reference/functions/integration-routes.md +151 -0
- package/.framework/src/instructions/reference/functions/integrations.md +130 -0
- package/.framework/src/instructions/reference/functions/invites.md +182 -0
- package/.framework/src/instructions/reference/functions/item-progress.md +107 -0
- package/.framework/src/instructions/reference/functions/kb.md +166 -0
- package/.framework/src/instructions/reference/functions/logs.md +141 -0
- package/.framework/src/instructions/reference/functions/observability.md +105 -0
- package/.framework/src/instructions/reference/functions/pipeline-executions.md +132 -0
- package/.framework/src/instructions/reference/functions/pipeline-runner-background.md +121 -0
- package/.framework/src/instructions/reference/functions/pipelines.md +136 -0
- package/.framework/src/instructions/reference/functions/prompt-configs.md +121 -0
- package/.framework/src/instructions/reference/functions/roles.md +121 -0
- package/.framework/src/instructions/reference/functions/security.md +140 -0
- package/.framework/src/instructions/reference/functions/system-cron.md +136 -0
- package/.framework/src/instructions/reference/functions/system.md +106 -0
- package/.framework/src/instructions/reference/functions/tests.md +97 -0
- package/.framework/src/instructions/reference/functions/timers.md +117 -0
- package/.framework/src/instructions/reference/functions/trigger-scheduler.md +102 -0
- package/.framework/src/instructions/reference/functions/triggers.md +140 -0
- package/.framework/src/instructions/reference/functions/types.md +146 -0
- package/.framework/src/instructions/reference/seeds.md +126 -0
- package/.framework/src/instructions/reference/shared-modules.md +127 -0
- package/.framework/src/instructions/reference/spine-config.md +114 -0
- package/.framework/src/instructions/roles.md +80 -0
- package/.framework/src/instructions/runtime-detail.md +70 -0
- package/.framework/src/instructions/runtime-list.md +77 -0
- package/.framework/src/instructions/security.md +67 -0
- package/.framework/src/instructions/tasks/add-a-pipeline.md +110 -0
- package/.framework/src/instructions/tasks/add-a-role.md +96 -0
- package/.framework/src/instructions/tasks/add-a-timer.md +114 -0
- package/.framework/src/instructions/tasks/add-a-trigger.md +106 -0
- package/.framework/src/instructions/tasks/add-a-type.md +151 -0
- package/.framework/src/instructions/tasks/add-a-webhook.md +108 -0
- package/.framework/src/instructions/tasks/add-an-ai-agent.md +108 -0
- package/.framework/src/instructions/tasks/add-an-integration.md +124 -0
- package/.framework/src/instructions/tasks/build-a-custom-page.md +118 -0
- package/.framework/src/instructions/tasks/package-and-install-an-app.md +127 -0
- package/.framework/src/instructions/tasks/test-your-code.md +108 -0
- package/.framework/src/instructions/tasks/write-a-function.md +132 -0
- package/.framework/src/instructions/tasks/write-a-migration.md +110 -0
- package/.framework/src/instructions/testing.md +59 -0
- package/.framework/src/instructions/timers.md +82 -0
- package/.framework/src/instructions/triggers.md +69 -0
- package/.framework/src/instructions/types.md +93 -0
- package/.framework/src/lib/branding.ts +128 -0
- package/.framework/src/pages/NoAppPage.tsx +3 -1
- package/.framework/src/pages/NotFoundPage.tsx +62 -31
- package/.framework/src/pages/SetupPage.tsx +5 -2
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +117 -106
- package/.framework/src/pages/admin/AIAgentsPage.tsx +86 -48
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +153 -152
- package/.framework/src/pages/admin/APIKeysPage.tsx +91 -90
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +29 -18
- package/.framework/src/pages/admin/AppDetailPage.tsx +6 -60
- package/.framework/src/pages/admin/AppsPage.tsx +104 -66
- package/.framework/src/pages/admin/DeliveriesPage.tsx +5 -5
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +5 -23
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +5 -26
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +7 -7
- package/.framework/src/pages/admin/IntegrationsPage.tsx +85 -43
- package/.framework/src/pages/admin/LogsPage.tsx +144 -76
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +223 -140
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +134 -113
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +108 -45
- package/.framework/src/pages/admin/PipelinesPage.tsx +160 -115
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +96 -98
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +101 -59
- package/.framework/src/pages/admin/RoleDetailPage.tsx +90 -79
- package/.framework/src/pages/admin/RolesPage.tsx +86 -43
- package/.framework/src/pages/admin/SecurityPage.tsx +1 -1
- package/.framework/src/pages/admin/TestingDashboard.tsx +16 -28
- package/.framework/src/pages/admin/TimerDetailPage.tsx +117 -100
- package/.framework/src/pages/admin/TimersPage.tsx +90 -62
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +90 -83
- package/.framework/src/pages/admin/TriggersPage.tsx +124 -99
- package/.framework/src/pages/admin/TypeDetailPage.tsx +152 -138
- package/.framework/src/pages/admin/TypesPage.tsx +90 -48
- package/.framework/src/pages/admin/cil/CILAgentPage.tsx +17 -25
- package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +17 -27
- package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +72 -40
- package/.framework/src/pages/admin/cil/CILFileListPage.tsx +321 -129
- package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +18 -29
- package/.framework/src/pages/admin/cil/CILGraphPage.tsx +26 -38
- package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +34 -46
- package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +189 -103
- package/.framework/src/pages/admin/cil/CILSearchPage.tsx +246 -136
- package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +11 -22
- package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +9 -20
- package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +26 -38
- package/.framework/src/pages/admin/kb/KBAdminPage.tsx +7 -26
- package/.framework/src/pages/auth/AuthConfirmPage.tsx +3 -1
- package/.framework/src/pages/auth/ForgotPasswordPage.tsx +3 -1
- package/.framework/src/pages/auth/LoginPage.tsx +4 -2
- package/.framework/src/pages/auth/RegisterPage.tsx +3 -1
- package/.framework/templates/agent/AGENTS.md +150 -0
- package/.framework/templates/agent/skills/spine-add-ai-agent/SKILL.md +58 -0
- package/.framework/templates/agent/skills/spine-add-automation/SKILL.md +62 -0
- package/.framework/templates/agent/skills/spine-add-integration/SKILL.md +63 -0
- package/.framework/templates/agent/skills/spine-architect/SKILL.md +86 -0
- package/.framework/templates/agent/skills/spine-install-app/SKILL.md +72 -0
- package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
- package/.framework/templates/agent/skills/spine-new-app/SKILL.md +67 -0
- package/.framework/templates/agent/skills/spine-new-function/SKILL.md +65 -0
- package/.framework/templates/agent/skills/spine-new-type/SKILL.md +65 -0
- package/.framework/templates/agent/skills/spine-schema/SKILL.md +77 -0
- package/.framework/templates/agent/skills/spine-test/SKILL.md +67 -0
- package/.framework/templates/agent/skills/spine-troubleshoot/SKILL.md +74 -0
- package/.framework/templates/agent/skills/spine-upgrade/SKILL.md +69 -0
- package/.framework/templates/project.json +5 -1
- package/.framework/tests/unit/account-scoping-gates.test.ts +392 -0
- package/.framework/tests/unit/agent-files.test.ts +355 -0
- package/.framework/tests/unit/agent-runner.test.ts +496 -47
- package/.framework/tests/unit/api-keys-create-scope-gate.test.ts +160 -0
- package/.framework/tests/unit/api-keys-omit-secret-fields.test.ts +38 -0
- package/.framework/tests/unit/bootstrap.test.ts +242 -0
- package/.framework/tests/unit/cli-context-api-key.test.ts +114 -0
- package/.framework/tests/unit/debug-auth.test.ts +38 -0
- package/.framework/tests/unit/deliveries-account-scoping.test.ts +185 -0
- package/.framework/tests/unit/episode-lease.test.ts +695 -0
- package/.framework/tests/unit/integration-outbound-credentials.test.ts +167 -0
- package/.framework/tests/unit/integrations-audit-redaction.test.ts +130 -0
- package/.framework/tests/unit/kb-ingest-lease.test.ts +19 -0
- package/.framework/tests/unit/logging-app-id.test.ts +75 -0
- package/.framework/tests/unit/pipeline-runner-background.test.ts +120 -0
- package/.framework/tests/unit/pipeline-runner.test.ts +153 -1
- package/.framework/tests/unit/principal-cache.test.ts +15 -0
- package/.framework/tests/unit/system-context-denial.test.ts +145 -0
- package/.framework/tests/unit/system-cron.test.ts +240 -0
- package/.framework/tests/unit/timers.test.ts +159 -0
- package/.framework/tests/unit/trigger-engine.test.ts +278 -91
- package/.framework/tests/unit/trigger-scheduler.test.ts +78 -0
- package/README.md +125 -166
- package/config/tsconfig.cli.json +2 -2
- package/netlify.toml +5 -0
- package/package.json +15 -4
- package/templates/project/package-lock.json +2 -2
- package/.framework/docs/admin-routes.md +0 -76
- package/.framework/docs/api-docs-progress.md +0 -38
- package/.framework/docs/api-governance.md +0 -146
- package/.framework/docs/api-testing-results.md +0 -212
- package/.framework/docs/assembly-launch-guide.md +0 -138
- package/.framework/docs/audit-results.md +0 -590
- package/.framework/docs/db-api-inventory.md +0 -95
- package/.framework/docs/gap-remediation-backlog.md +0 -103
- package/.framework/docs/ui-adoption-verification.md +0 -111
- package/.framework/docs/ui-api-coverage.md +0 -84
- package/.framework/docs/v2-compatibility-audit.md +0 -228
- package/.framework/mcp/index.ts +0 -133
- package/.framework/mcp/load-env.ts +0 -60
- package/.framework/mcp/shared/app-manifest.ts +0 -275
- package/.framework/mcp/shared/db.ts +0 -477
- package/.framework/mcp/shared/kb.ts +0 -118
- package/.framework/mcp/tools/architect.ts +0 -431
- package/.framework/mcp/tools/cil/context.ts +0 -345
- package/.framework/mcp/tools/cil/graph.ts +0 -242
- package/.framework/mcp/tools/cil/ingest.ts +0 -1217
- package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +0 -693
- package/.framework/mcp/tools/cil/pipeline.ts +0 -767
- package/.framework/mcp/tools/cil/project.ts +0 -791
- package/.framework/mcp/tools/cil/retrieval.ts +0 -336
- package/.framework/mcp/tools/cil/search-core.ts +0 -454
- package/.framework/mcp/tools/cil/search.ts +0 -97
- package/.framework/mcp/tools/cil/suggest.ts +0 -186
- package/.framework/mcp/tools/cil/validate.ts +0 -1369
- package/.framework/mcp/tools/kb/delete.ts +0 -139
- package/.framework/mcp/tools/kb/index.ts +0 -52
- package/.framework/mcp/tools/kb/ingest.ts +0 -84
- package/.framework/mcp/tools/kb/list-types.ts +0 -102
- package/.framework/mcp/tools/kb/publish.ts +0 -100
- package/.framework/mcp/tools/kb/reindex.ts +0 -86
- package/.framework/mcp/tools/kb/revoke.ts +0 -85
- package/.framework/mcp/tools/kb/search.ts +0 -101
- package/.framework/mcp/tools/kb/upsert.ts +0 -125
- package/.framework/mcp/tools/observability.ts +0 -510
package/.env.example
CHANGED
|
@@ -6,20 +6,30 @@ VITE_APP_NAME=Spine
|
|
|
6
6
|
VITE_ENVIRONMENT=dev
|
|
7
7
|
|
|
8
8
|
# ── Supabase (frontend — public keys only) ───────────────────────
|
|
9
|
+
# All values in both Supabase blocks come from one page:
|
|
10
|
+
# Supabase Dashboard → Project Settings → API Keys
|
|
11
|
+
# (older dashboards: Settings → API / Data API)
|
|
12
|
+
# Key naming: the "Publishable key" (sb_publishable_…) is the anon key, and
|
|
13
|
+
# a "Secret key" (sb_secret_…) is the service-role key. Legacy anon /
|
|
14
|
+
# service_role JWTs work identically — use whichever your project shows.
|
|
9
15
|
VITE_SUPABASE_URL=https://your-project.supabase.co
|
|
10
|
-
VITE_SUPABASE_ANON_KEY=your-anon-key
|
|
16
|
+
VITE_SUPABASE_ANON_KEY=your-anon-key-or-publishable-key
|
|
11
17
|
|
|
12
18
|
# ── Supabase (backend — Netlify Functions) ───────────────────────
|
|
13
19
|
SUPABASE_URL=https://your-project.supabase.co
|
|
14
|
-
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
|
|
20
|
+
SUPABASE_SERVICE_ROLE_KEY=your-service-role-or-secret-key
|
|
15
21
|
# Functions build user-scoped PostgREST clients with the anon key.
|
|
16
|
-
SUPABASE_ANON_KEY=your-anon-key
|
|
22
|
+
SUPABASE_ANON_KEY=your-anon-key-or-publishable-key
|
|
17
23
|
|
|
18
24
|
# ── Migrations / CLI ─────────────────────────────────────────────
|
|
19
25
|
# Postgres password used by `spine-framework migrate` and `install-app`
|
|
20
26
|
# (connects directly to db.<project-ref>.supabase.co:5432).
|
|
21
27
|
# Get from: Supabase Dashboard → Project Settings → Database.
|
|
22
28
|
SUPABASE_DB_PASSWORD=
|
|
29
|
+
# Alternative to SUPABASE_DB_PASSWORD: a full Postgres connection string.
|
|
30
|
+
# Use the Session Pooler host when your network can't reach db.<ref>.supabase.co:5432
|
|
31
|
+
# (direct connections are IPv6-only on most Supabase projects).
|
|
32
|
+
# SUPABASE_DB_URL=postgres://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:5432/postgres
|
|
23
33
|
|
|
24
34
|
# ── App URLs ─────────────────────────────────────────────────────
|
|
25
35
|
# Base URL for invite links and auth redirects; also the allowlist
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
// CIL[BEGIN stable_id="~/.framework/cli/commands/bootstrap.ts:module_header:module_header" type="module_header" identity="module_header" parent="~/.framework/cli/commands/bootstrap.ts"]
|
|
2
|
+
/**
|
|
3
|
+
* @module bootstrap
|
|
4
|
+
* @audience engineers
|
|
5
|
+
* @layer cli
|
|
6
|
+
* @stability stable
|
|
7
|
+
*
|
|
8
|
+
* `spine-framework bootstrap` — scriptable first-run provisioning. Creates
|
|
9
|
+
* the first `system_admin` person (bound to the `spine-system` account,
|
|
10
|
+
* mirroring the `handle_new_auth_user` trigger columns) and mints the first
|
|
11
|
+
* `sk_` API key, entirely without a browser. With `--email`/`--password` it
|
|
12
|
+
* additionally provisions a login-able Supabase auth user via the admin API.
|
|
13
|
+
*
|
|
14
|
+
* INVARIANT: the raw API key is printed exactly once and only its PBKDF2 hash
|
|
15
|
+
* is persisted — identical to the `api-keys` create path.
|
|
16
|
+
*
|
|
17
|
+
* @seeAlso .framework/migrations/052_registration_security.sql (trigger row shape)
|
|
18
|
+
* @seeAlso .framework/functions/api-keys.ts (key minting/hash logic)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { Command } from 'commander'
|
|
22
|
+
import { existsSync, readFileSync } from 'fs'
|
|
23
|
+
import { join } from 'path'
|
|
24
|
+
import { createClient } from '@supabase/supabase-js'
|
|
25
|
+
import { hashApiKey } from '../../functions/_shared/hash-api-key.ts'
|
|
26
|
+
import { emitLog } from '../../functions/_shared/audit.ts'
|
|
27
|
+
import { SYSTEM_PRINCIPAL } from '../../functions/_shared/principal.ts'
|
|
28
|
+
// CIL[END stable_id="~/.framework/cli/commands/bootstrap.ts:module_header:module_header"]
|
|
29
|
+
|
|
30
|
+
// CIL[BEGIN stable_id="~/.framework/cli/commands/bootstrap.ts:PROJECT_ROOT:constant" type="constant" identity="PROJECT_ROOT" parent="~/.framework/cli/commands/bootstrap.ts"]
|
|
31
|
+
const PROJECT_ROOT = process.cwd()
|
|
32
|
+
// CIL[END stable_id="~/.framework/cli/commands/bootstrap.ts:PROJECT_ROOT:constant"]
|
|
33
|
+
|
|
34
|
+
// CIL[BEGIN stable_id="~/.framework/cli/commands/bootstrap.ts:BootstrapOptions:interface" type="interface" identity="BootstrapOptions" parent="~/.framework/cli/commands/bootstrap.ts"]
|
|
35
|
+
/**
|
|
36
|
+
* Options accepted by `runBootstrap`. `scopes` entries of `system_admin`
|
|
37
|
+
* normalize to the `*:*` wildcard — `resolveMachinePrincipal` maps `*:*` to
|
|
38
|
+
* the `system_admin` role on the machine principal.
|
|
39
|
+
*/
|
|
40
|
+
export interface BootstrapOptions {
|
|
41
|
+
email?: string
|
|
42
|
+
password?: string
|
|
43
|
+
fullName?: string
|
|
44
|
+
keyName?: string
|
|
45
|
+
scopes?: string[]
|
|
46
|
+
}
|
|
47
|
+
// CIL[END stable_id="~/.framework/cli/commands/bootstrap.ts:BootstrapOptions:interface"]
|
|
48
|
+
|
|
49
|
+
// CIL[BEGIN stable_id="~/.framework/cli/commands/bootstrap.ts:BootstrapResult:interface" type="interface" identity="BootstrapResult" parent="~/.framework/cli/commands/bootstrap.ts"]
|
|
50
|
+
export interface BootstrapResult {
|
|
51
|
+
/** 'created' when a new admin person was provisioned, 'existing' when a
|
|
52
|
+
system_admin already occupied the spine-system account (idempotent skip). */
|
|
53
|
+
admin: 'created' | 'existing'
|
|
54
|
+
personId: string
|
|
55
|
+
personEmail: string
|
|
56
|
+
/** True when a Supabase auth user was created via the admin API — without
|
|
57
|
+
it the person row exists but cannot log in through the UI. */
|
|
58
|
+
authUserCreated: boolean
|
|
59
|
+
apiKey: {
|
|
60
|
+
id: string
|
|
61
|
+
name: string
|
|
62
|
+
key_prefix: string
|
|
63
|
+
scopes: string[]
|
|
64
|
+
/** Raw `sk_` key — returned once, never stored. */
|
|
65
|
+
raw_key: string
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// CIL[END stable_id="~/.framework/cli/commands/bootstrap.ts:BootstrapResult:interface"]
|
|
69
|
+
|
|
70
|
+
// CIL[BEGIN stable_id="~/.framework/cli/commands/bootstrap.ts:loadEnv:function" type="function" identity="loadEnv" parent="~/.framework/cli/commands/bootstrap.ts"]
|
|
71
|
+
/**
|
|
72
|
+
* Loads key=value pairs from the project .env into process.env without
|
|
73
|
+
* overwriting already-set variables. Mirrors the helper in migrate.ts so the
|
|
74
|
+
* command works when its module is exercised outside cli/index.ts.
|
|
75
|
+
*/
|
|
76
|
+
function loadEnv() {
|
|
77
|
+
const envPath = join(PROJECT_ROOT, '.env')
|
|
78
|
+
if (!existsSync(envPath)) return
|
|
79
|
+
const lines = readFileSync(envPath, 'utf8').split('\n')
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
const trimmed = line.trim()
|
|
82
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
83
|
+
const eqIdx = trimmed.indexOf('=')
|
|
84
|
+
if (eqIdx === -1) continue
|
|
85
|
+
const key = trimmed.slice(0, eqIdx).trim()
|
|
86
|
+
const val = trimmed.slice(eqIdx + 1).trim()
|
|
87
|
+
if (key && !(key in process.env)) process.env[key] = val
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// CIL[END stable_id="~/.framework/cli/commands/bootstrap.ts:loadEnv:function"]
|
|
91
|
+
|
|
92
|
+
// CIL[BEGIN stable_id="~/.framework/cli/commands/bootstrap.ts:normalizeScopes:function" type="function" identity="normalizeScopes" parent="~/.framework/cli/commands/bootstrap.ts"]
|
|
93
|
+
/** Normalize requested scopes; default is the system_admin wildcard. */
|
|
94
|
+
function normalizeScopes(scopes?: string[]): string[] {
|
|
95
|
+
const requested = (scopes || []).filter(Boolean)
|
|
96
|
+
if (requested.length === 0) return ['*:*']
|
|
97
|
+
return requested.map((s) => (s === 'system_admin' ? '*:*' : s))
|
|
98
|
+
}
|
|
99
|
+
// CIL[END stable_id="~/.framework/cli/commands/bootstrap.ts:normalizeScopes:function"]
|
|
100
|
+
|
|
101
|
+
// CIL[BEGIN stable_id="~/.framework/cli/commands/bootstrap.ts:runBootstrap:function" type="function" identity="runBootstrap" parent="~/.framework/cli/commands/bootstrap.ts"]
|
|
102
|
+
/**
|
|
103
|
+
* Provision the first system admin and mint the first API key.
|
|
104
|
+
*
|
|
105
|
+
* Idempotent on the admin side: when a `people` row carrying the
|
|
106
|
+
* `system_admin` role already exists in the `spine-system` account, creation
|
|
107
|
+
* is skipped and that person is reused as the key's `created_by`. A new API
|
|
108
|
+
* key is minted on every run — re-running produces an additional key, never
|
|
109
|
+
* an error, and the raw key is only ever returned to the caller once.
|
|
110
|
+
*
|
|
111
|
+
* @throws Error when the spine-system account, system_admin role, or person
|
|
112
|
+
* type seeds are missing (i.e. `migrate` has not run), or when the auth
|
|
113
|
+
* admin API / people upsert fails.
|
|
114
|
+
*/
|
|
115
|
+
export async function runBootstrap(db: any, options: BootstrapOptions = {}): Promise<BootstrapResult> {
|
|
116
|
+
if (options.password && !options.email) {
|
|
117
|
+
throw new Error('--password requires --email')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- Required seeds (created by migrate / framework seeds) ---
|
|
121
|
+
const { data: account, error: accountErr } = await db
|
|
122
|
+
.from('accounts')
|
|
123
|
+
.select('id')
|
|
124
|
+
.eq('slug', 'spine-system')
|
|
125
|
+
.maybeSingle()
|
|
126
|
+
if (accountErr) throw accountErr
|
|
127
|
+
if (!account) {
|
|
128
|
+
throw new Error('spine-system account not found — run `npx spine-framework migrate` first')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const { data: role, error: roleErr } = await db
|
|
132
|
+
.from('roles')
|
|
133
|
+
.select('id')
|
|
134
|
+
.eq('slug', 'system_admin')
|
|
135
|
+
.maybeSingle()
|
|
136
|
+
if (roleErr) throw roleErr
|
|
137
|
+
if (!role) {
|
|
138
|
+
throw new Error('system_admin role not found — run `npx spine-framework migrate` first')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const { data: personType, error: typeErr } = await db
|
|
142
|
+
.from('types')
|
|
143
|
+
.select('id, design_schema, validation_schema')
|
|
144
|
+
.eq('kind', 'person')
|
|
145
|
+
.eq('slug', 'person')
|
|
146
|
+
.maybeSingle()
|
|
147
|
+
if (typeErr) throw typeErr
|
|
148
|
+
if (!personType) {
|
|
149
|
+
throw new Error('person type (kind=person, slug=person) not found — run framework seeds first')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const auditCtx = {
|
|
153
|
+
principal: SYSTEM_PRINCIPAL,
|
|
154
|
+
accountId: account.id,
|
|
155
|
+
db,
|
|
156
|
+
requestId: crypto.randomUUID(),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// --- Idempotent admin check ---
|
|
160
|
+
const { data: existingAdmins, error: existErr } = await db
|
|
161
|
+
.from('people')
|
|
162
|
+
.select('id, email')
|
|
163
|
+
.eq('account_id', account.id)
|
|
164
|
+
.eq('role_id', role.id)
|
|
165
|
+
.limit(1)
|
|
166
|
+
if (existErr) throw existErr
|
|
167
|
+
|
|
168
|
+
let admin: 'created' | 'existing' = 'existing'
|
|
169
|
+
let personId: string | undefined = existingAdmins?.[0]?.id
|
|
170
|
+
let personEmail: string = existingAdmins?.[0]?.email
|
|
171
|
+
let authUserCreated = false
|
|
172
|
+
|
|
173
|
+
if (!personId) {
|
|
174
|
+
admin = 'created'
|
|
175
|
+
const email = options.email || 'admin@spine.local'
|
|
176
|
+
const fullName = options.fullName || email.split('@')[0]
|
|
177
|
+
|
|
178
|
+
// Optional login-able auth user. handle_new_auth_user fires on insert and
|
|
179
|
+
// may create the people row itself (first user gets system_admin); the
|
|
180
|
+
// upsert below converges the row to the same binding either way, so the
|
|
181
|
+
// command does not depend on the trigger being installed.
|
|
182
|
+
let authUid: string | null = null
|
|
183
|
+
if (options.email && options.password) {
|
|
184
|
+
const { data: created, error: createErr } = await db.auth.admin.createUser({
|
|
185
|
+
email,
|
|
186
|
+
password: options.password,
|
|
187
|
+
email_confirm: true,
|
|
188
|
+
user_metadata: { full_name: fullName },
|
|
189
|
+
})
|
|
190
|
+
if (createErr) {
|
|
191
|
+
// An already-registered email still gets a bound person record — the
|
|
192
|
+
// auth user can be re-linked or password-reset later.
|
|
193
|
+
if (/already|exists|registered/i.test(createErr.message || '')) {
|
|
194
|
+
console.warn(` ⚠ auth user for ${email} already exists — binding a person record without touching auth`)
|
|
195
|
+
} else {
|
|
196
|
+
throw new Error(`Failed to create auth user: ${createErr.message}`)
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
authUid = created?.user?.id || null
|
|
200
|
+
authUserCreated = !!authUid
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Mirror the handle_new_auth_user INSERT columns exactly (052), keyed on
|
|
205
|
+
// people.email's UNIQUE constraint: claim/upgrade an existing row or
|
|
206
|
+
// insert a fresh one stamped with the person type's schemas.
|
|
207
|
+
const { data: existingPerson, error: personLookupErr } = await db
|
|
208
|
+
.from('people')
|
|
209
|
+
.select('id')
|
|
210
|
+
.eq('email', email)
|
|
211
|
+
.maybeSingle()
|
|
212
|
+
if (personLookupErr) throw personLookupErr
|
|
213
|
+
|
|
214
|
+
if (existingPerson) {
|
|
215
|
+
const { error: updateErr } = await db
|
|
216
|
+
.from('people')
|
|
217
|
+
.update({
|
|
218
|
+
account_id: account.id,
|
|
219
|
+
role_id: role.id,
|
|
220
|
+
type_id: personType.id,
|
|
221
|
+
status: 'active',
|
|
222
|
+
is_active: true,
|
|
223
|
+
design_schema: personType.design_schema || {},
|
|
224
|
+
validation_schema: personType.validation_schema || {},
|
|
225
|
+
...(authUid ? { auth_uid: authUid } : {}),
|
|
226
|
+
updated_at: new Date().toISOString(),
|
|
227
|
+
})
|
|
228
|
+
.eq('id', existingPerson.id)
|
|
229
|
+
if (updateErr) throw updateErr
|
|
230
|
+
personId = existingPerson.id
|
|
231
|
+
} else {
|
|
232
|
+
const { data: inserted, error: insertErr } = await db
|
|
233
|
+
.from('people')
|
|
234
|
+
.insert({
|
|
235
|
+
id: authUid || crypto.randomUUID(),
|
|
236
|
+
auth_uid: authUid,
|
|
237
|
+
email,
|
|
238
|
+
full_name: fullName,
|
|
239
|
+
account_id: account.id,
|
|
240
|
+
type_id: personType.id,
|
|
241
|
+
role_id: role.id,
|
|
242
|
+
status: 'active',
|
|
243
|
+
is_active: true,
|
|
244
|
+
design_schema: personType.design_schema || {},
|
|
245
|
+
validation_schema: personType.validation_schema || {},
|
|
246
|
+
})
|
|
247
|
+
.select('id')
|
|
248
|
+
.single()
|
|
249
|
+
if (insertErr) throw insertErr
|
|
250
|
+
personId = inserted.id
|
|
251
|
+
}
|
|
252
|
+
personEmail = email
|
|
253
|
+
|
|
254
|
+
await emitLog(auditCtx as any, 'person.created', { type: 'person', id: personId! }, {
|
|
255
|
+
after: { email, role: 'system_admin', account: 'spine-system', auth_user: authUserCreated },
|
|
256
|
+
}, { source: 'cli bootstrap' })
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// --- Mint the API key (always — each run yields one new credential) ---
|
|
260
|
+
const scopes = normalizeScopes(options.scopes)
|
|
261
|
+
const keyName = options.keyName || 'bootstrap-admin'
|
|
262
|
+
const slug = `${keyName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'bootstrap'}-${crypto.randomUUID().slice(0, 6)}`
|
|
263
|
+
|
|
264
|
+
const rawKey = `sk_${crypto.randomUUID().replace(/-/g, '')}`
|
|
265
|
+
const { key_prefix, key_hash } = hashApiKey(rawKey)
|
|
266
|
+
|
|
267
|
+
const { data: key, error: keyErr } = await db
|
|
268
|
+
.from('api_keys')
|
|
269
|
+
.insert({
|
|
270
|
+
integration_id: null,
|
|
271
|
+
app_id: null,
|
|
272
|
+
account_id: account.id,
|
|
273
|
+
slug,
|
|
274
|
+
name: keyName,
|
|
275
|
+
key_prefix,
|
|
276
|
+
key_hash,
|
|
277
|
+
key_type: 'cli',
|
|
278
|
+
permissions: [],
|
|
279
|
+
scopes,
|
|
280
|
+
rate_limit: 100,
|
|
281
|
+
expires_at: null,
|
|
282
|
+
metadata: { source: 'cli bootstrap' },
|
|
283
|
+
machine_type: 'service_account',
|
|
284
|
+
is_internal: false,
|
|
285
|
+
is_active: true,
|
|
286
|
+
created_by: personId,
|
|
287
|
+
})
|
|
288
|
+
.select('id, name, key_prefix, scopes')
|
|
289
|
+
.single()
|
|
290
|
+
if (keyErr) throw keyErr
|
|
291
|
+
|
|
292
|
+
await emitLog(auditCtx as any, 'api_key.created', { type: 'api_key', id: key.id }, {
|
|
293
|
+
after: { name: keyName, key_type: 'cli', scopes, machine_type: 'service_account' },
|
|
294
|
+
}, { source: 'cli bootstrap' })
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
admin,
|
|
298
|
+
personId: personId!,
|
|
299
|
+
personEmail,
|
|
300
|
+
authUserCreated,
|
|
301
|
+
apiKey: {
|
|
302
|
+
id: key.id,
|
|
303
|
+
name: key.name,
|
|
304
|
+
key_prefix: key.key_prefix,
|
|
305
|
+
scopes: key.scopes,
|
|
306
|
+
raw_key: rawKey,
|
|
307
|
+
},
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// CIL[END stable_id="~/.framework/cli/commands/bootstrap.ts:runBootstrap:function"]
|
|
311
|
+
|
|
312
|
+
// CIL[BEGIN stable_id="~/.framework/cli/commands/bootstrap.ts:bootstrapCommand:function" type="function" identity="bootstrapCommand" parent="~/.framework/cli/commands/bootstrap.ts"]
|
|
313
|
+
async function bootstrapCommand(options: {
|
|
314
|
+
email?: string
|
|
315
|
+
password?: string
|
|
316
|
+
fullName?: string
|
|
317
|
+
name?: string
|
|
318
|
+
scope?: string[]
|
|
319
|
+
json?: boolean
|
|
320
|
+
}): Promise<void> {
|
|
321
|
+
console.log('\nBootstrapping Spine instance...\n')
|
|
322
|
+
|
|
323
|
+
loadEnv()
|
|
324
|
+
|
|
325
|
+
const supabaseUrl = process.env.SUPABASE_URL
|
|
326
|
+
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY
|
|
327
|
+
if (!supabaseUrl || !serviceRoleKey) {
|
|
328
|
+
console.error('❌ SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY must be set')
|
|
329
|
+
console.error(' Set them in .env or export them before running this command.')
|
|
330
|
+
process.exit(1)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const db = createClient(supabaseUrl, serviceRoleKey, { auth: { persistSession: false } })
|
|
334
|
+
const result = await runBootstrap(db, {
|
|
335
|
+
email: options.email,
|
|
336
|
+
password: options.password,
|
|
337
|
+
fullName: options.fullName,
|
|
338
|
+
keyName: options.name,
|
|
339
|
+
scopes: options.scope,
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
if (options.json) {
|
|
343
|
+
console.log(JSON.stringify(result, null, 2))
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (result.admin === 'existing') {
|
|
348
|
+
console.log(` ✓ system_admin already exists (${result.personEmail}) — skipped person creation`)
|
|
349
|
+
} else {
|
|
350
|
+
console.log(` ✓ Created system_admin person ${result.personEmail} in the spine-system account`)
|
|
351
|
+
if (result.authUserCreated) {
|
|
352
|
+
console.log(' ✓ Auth user created and email confirmed — sign in at /login')
|
|
353
|
+
} else {
|
|
354
|
+
console.log(' ℹ No auth user created. UI login requires registration or an')
|
|
355
|
+
console.log(' admin-supplied auth user (rerun with --email/--password, or')
|
|
356
|
+
console.log(' create one in Supabase Dashboard → Authentication → Users).')
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
console.log(`\n ✓ Minted API key "${result.apiKey.name}" (scopes: ${result.apiKey.scopes.join(', ')})`)
|
|
361
|
+
console.log('\n ' + '─'.repeat(52))
|
|
362
|
+
console.log(` API key (shown once — store it now): ${result.apiKey.raw_key}`)
|
|
363
|
+
console.log(' ' + '─'.repeat(52))
|
|
364
|
+
console.log('\n Use it as: SPINE_CLI_API_KEY / x-api-key header / SPINE_API_KEY')
|
|
365
|
+
console.log('\n✅ Bootstrap complete.\n')
|
|
366
|
+
}
|
|
367
|
+
// CIL[END stable_id="~/.framework/cli/commands/bootstrap.ts:bootstrapCommand:function"]
|
|
368
|
+
|
|
369
|
+
// CIL[BEGIN stable_id="~/.framework/cli/commands/bootstrap.ts:registerBootstrapCommands:function" type="function" identity="registerBootstrapCommands" parent="~/.framework/cli/commands/bootstrap.ts"]
|
|
370
|
+
export function registerBootstrapCommands(program: Command) {
|
|
371
|
+
const collect = (value: string, previous: string[]) => previous.concat([value])
|
|
372
|
+
|
|
373
|
+
program
|
|
374
|
+
.command('bootstrap')
|
|
375
|
+
.description('Create the first system_admin and mint the first API key — no browser required')
|
|
376
|
+
.option('--email <email>', 'Email for the admin person (default: admin@spine.local)')
|
|
377
|
+
.option('--password <password>', 'With --email, also creates a confirmed login-able auth user')
|
|
378
|
+
.option('--full-name <name>', 'Display name for the admin person (default: email local part)')
|
|
379
|
+
.option('--name <name>', 'API key name', 'bootstrap-admin')
|
|
380
|
+
.option('--scope <scope>', 'API key scope, repeatable (default: system_admin i.e. *:*)', collect, [] as string[])
|
|
381
|
+
.option('--json', 'Output as JSON (includes the raw key — capture it now)')
|
|
382
|
+
.action(async (opts) => {
|
|
383
|
+
try {
|
|
384
|
+
await bootstrapCommand(opts)
|
|
385
|
+
} catch (err: any) {
|
|
386
|
+
console.error('Error:', err.message)
|
|
387
|
+
if (process.env.SPINE_CLI_DEBUG) console.error(err.stack)
|
|
388
|
+
process.exit(1)
|
|
389
|
+
}
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
// CIL[END stable_id="~/.framework/cli/commands/bootstrap.ts:registerBootstrapCommands:function"]
|