stagent 0.9.6 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -44
- package/dist/cli.js +66 -18
- package/docs/.coverage-gaps.json +144 -56
- package/docs/.last-generated +1 -1
- package/docs/features/agent-intelligence.md +12 -2
- package/docs/features/chat.md +40 -5
- package/docs/features/cost-usage.md +1 -1
- package/docs/features/documents.md +5 -2
- package/docs/features/inbox-notifications.md +10 -2
- package/docs/features/keyboard-navigation.md +12 -3
- package/docs/features/provider-runtimes.md +20 -2
- package/docs/features/schedules.md +32 -4
- package/docs/features/settings.md +28 -5
- package/docs/features/shared-components.md +7 -3
- package/docs/features/tables.md +11 -2
- package/docs/features/tool-permissions.md +6 -2
- package/docs/features/workflows.md +14 -4
- package/docs/index.md +1 -1
- package/docs/journeys/developer.md +39 -2
- package/docs/journeys/personal-use.md +32 -8
- package/docs/journeys/power-user.md +45 -14
- package/docs/journeys/work-use.md +17 -8
- package/docs/manifest.json +15 -15
- package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
- package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
- package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
- package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
- package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
- package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
- package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
- package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
- package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
- package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
- package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
- package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
- package/next.config.mjs +1 -0
- package/package.json +3 -2
- package/src/__tests__/instrumentation-smoke.test.ts +15 -0
- package/src/app/analytics/page.tsx +1 -21
- package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
- package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
- package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
- package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
- package/src/app/api/chat/export/route.ts +52 -0
- package/src/app/api/chat/files/search/route.ts +50 -0
- package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
- package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
- package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
- package/src/app/api/environment/skills/route.ts +13 -0
- package/src/app/api/instance/config/route.ts +41 -0
- package/src/app/api/instance/init/route.ts +34 -0
- package/src/app/api/instance/upgrade/check/route.ts +26 -0
- package/src/app/api/instance/upgrade/route.ts +96 -0
- package/src/app/api/instance/upgrade/status/route.ts +35 -0
- package/src/app/api/memory/route.ts +0 -11
- package/src/app/api/notifications/route.ts +4 -2
- package/src/app/api/projects/[id]/route.ts +5 -155
- package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
- package/src/app/api/schedules/[id]/execute/route.ts +111 -0
- package/src/app/api/schedules/[id]/route.ts +9 -1
- package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
- package/src/app/api/schedules/route.ts +3 -12
- package/src/app/api/settings/chat/pins/route.ts +94 -0
- package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
- package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
- package/src/app/api/settings/environment/route.ts +26 -0
- package/src/app/api/settings/openai/login/route.ts +22 -0
- package/src/app/api/settings/openai/logout/route.ts +7 -0
- package/src/app/api/settings/openai/route.ts +21 -1
- package/src/app/api/settings/providers/route.ts +35 -8
- package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
- package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
- package/src/app/api/tables/[id]/enrich/route.ts +147 -0
- package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
- package/src/app/api/tasks/[id]/execute/route.ts +52 -33
- package/src/app/api/tasks/[id]/respond/route.ts +31 -15
- package/src/app/api/tasks/[id]/resume/route.ts +24 -3
- package/src/app/api/workflows/[id]/resume/route.ts +59 -0
- package/src/app/api/workflows/[id]/status/route.ts +22 -8
- package/src/app/api/workspace/context/route.ts +2 -0
- package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
- package/src/app/chat/page.tsx +11 -0
- package/src/app/documents/page.tsx +4 -1
- package/src/app/inbox/page.tsx +12 -5
- package/src/app/layout.tsx +42 -21
- package/src/app/page.tsx +0 -2
- package/src/app/settings/page.tsx +8 -9
- package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +573 -0
- package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
- package/src/components/chat/capability-banner.tsx +68 -0
- package/src/components/chat/chat-command-popover.tsx +670 -49
- package/src/components/chat/chat-input.tsx +104 -10
- package/src/components/chat/chat-message.tsx +12 -3
- package/src/components/chat/chat-session-provider.tsx +790 -0
- package/src/components/chat/chat-shell.tsx +151 -401
- package/src/components/chat/command-tab-bar.tsx +68 -0
- package/src/components/chat/conversation-template-picker.tsx +421 -0
- package/src/components/chat/help-dialog.tsx +39 -0
- package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
- package/src/components/chat/skill-row.tsx +147 -0
- package/src/components/documents/document-browser.tsx +37 -19
- package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
- package/src/components/instance/instance-section.tsx +382 -0
- package/src/components/instance/upgrade-badge.tsx +219 -0
- package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
- package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
- package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
- package/src/components/notifications/batch-proposal-review.tsx +20 -5
- package/src/components/notifications/inbox-list.tsx +11 -2
- package/src/components/notifications/notification-item.tsx +56 -2
- package/src/components/notifications/pending-approval-host.tsx +56 -37
- package/src/components/notifications/permission-response-actions.tsx +155 -1
- package/src/components/schedules/schedule-create-sheet.tsx +19 -1
- package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
- package/src/components/schedules/schedule-form.tsx +31 -0
- package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
- package/src/components/settings/auth-method-selector.tsx +19 -4
- package/src/components/settings/auth-status-badge.tsx +28 -3
- package/src/components/settings/environment-section.tsx +102 -0
- package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
- package/src/components/settings/openai-runtime-section.tsx +7 -1
- package/src/components/settings/providers-runtimes-section.tsx +138 -19
- package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
- package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
- package/src/components/shared/app-sidebar.tsx +4 -3
- package/src/components/shared/command-palette.tsx +266 -7
- package/src/components/shared/filter-hint.tsx +70 -0
- package/src/components/shared/filter-input.tsx +59 -0
- package/src/components/shared/saved-searches-manager.tsx +199 -0
- package/src/components/shared/theme-toggle.tsx +5 -24
- package/src/components/shared/workspace-indicator.tsx +61 -2
- package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
- package/src/components/tables/table-create-sheet.tsx +4 -0
- package/src/components/tables/table-enrichment-runs.tsx +103 -0
- package/src/components/tables/table-enrichment-sheet.tsx +538 -0
- package/src/components/tables/table-spreadsheet.tsx +29 -5
- package/src/components/tables/table-toolbar.tsx +10 -1
- package/src/components/tasks/kanban-board.tsx +1 -0
- package/src/components/tasks/kanban-column.tsx +53 -14
- package/src/components/tasks/task-bento-grid.tsx +31 -2
- package/src/components/tasks/task-card.tsx +29 -3
- package/src/components/tasks/task-chip-bar.tsx +54 -1
- package/src/components/tasks/task-result-renderer.tsx +1 -1
- package/src/components/workflows/delay-step-body.tsx +109 -0
- package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
- package/src/components/workflows/loop-status-view.tsx +1 -1
- package/src/components/workflows/shared/step-result.tsx +78 -0
- package/src/components/workflows/shared/workflow-header.tsx +141 -0
- package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
- package/src/components/workflows/swarm-dashboard.tsx +2 -15
- package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
- package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
- package/src/components/workflows/workflow-form-view.tsx +133 -16
- package/src/components/workflows/workflow-status-view.tsx +30 -740
- package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
- package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
- package/src/hooks/use-active-skills.ts +110 -0
- package/src/hooks/use-chat-autocomplete.ts +120 -7
- package/src/hooks/use-enriched-skills.ts +19 -0
- package/src/hooks/use-pinned-entries.ts +104 -0
- package/src/hooks/use-recent-user-messages.ts +19 -0
- package/src/hooks/use-saved-searches.ts +142 -0
- package/src/instrumentation-node.ts +94 -0
- package/src/instrumentation.ts +4 -48
- package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +212 -0
- package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
- package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
- package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
- package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
- package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
- package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
- package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
- package/src/lib/agents/claude-agent.ts +217 -21
- package/src/lib/agents/execution-manager.ts +0 -35
- package/src/lib/agents/handoff/bus.ts +2 -2
- package/src/lib/agents/learned-context.ts +0 -12
- package/src/lib/agents/learning-session.ts +18 -5
- package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
- package/src/lib/agents/profiles/__tests__/registry.test.ts +53 -4
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +97 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +36 -0
- package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
- package/src/lib/agents/profiles/registry.ts +18 -0
- package/src/lib/agents/profiles/types.ts +7 -1
- package/src/lib/agents/router.ts +3 -6
- package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
- package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
- package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
- package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
- package/src/lib/agents/runtime/catalog.ts +121 -0
- package/src/lib/agents/runtime/claude-sdk.ts +32 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
- package/src/lib/agents/runtime/execution-target.ts +456 -0
- package/src/lib/agents/runtime/index.ts +4 -0
- package/src/lib/agents/runtime/launch-failure.ts +101 -0
- package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
- package/src/lib/agents/runtime/openai-codex.ts +64 -60
- package/src/lib/agents/runtime/openai-direct.ts +8 -0
- package/src/lib/agents/runtime/types.ts +8 -0
- package/src/lib/agents/task-dispatch.ts +220 -0
- package/src/lib/agents/tool-permissions.ts +16 -1
- package/src/lib/book/chapter-mapping.ts +11 -0
- package/src/lib/book/content.ts +10 -0
- package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
- package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
- package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
- package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
- package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
- package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
- package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
- package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
- package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
- package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
- package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
- package/src/lib/chat/__tests__/types.test.ts +28 -0
- package/src/lib/chat/active-skills.ts +31 -0
- package/src/lib/chat/active-streams.ts +27 -0
- package/src/lib/chat/clean-filter-input.ts +30 -0
- package/src/lib/chat/codex-engine.ts +46 -24
- package/src/lib/chat/command-tabs.ts +61 -0
- package/src/lib/chat/context-builder.ts +146 -4
- package/src/lib/chat/dismissals.ts +73 -0
- package/src/lib/chat/engine.ts +159 -18
- package/src/lib/chat/files/__tests__/search.test.ts +135 -0
- package/src/lib/chat/files/expand-mention.ts +76 -0
- package/src/lib/chat/files/search.ts +99 -0
- package/src/lib/chat/reconcile.ts +117 -0
- package/src/lib/chat/skill-composition.ts +210 -0
- package/src/lib/chat/skill-conflict.ts +105 -0
- package/src/lib/chat/stagent-tools.ts +7 -19
- package/src/lib/chat/stream-telemetry.ts +137 -0
- package/src/lib/chat/suggested-prompts.ts +28 -1
- package/src/lib/chat/system-prompt.ts +48 -1
- package/src/lib/chat/tool-catalog.ts +35 -4
- package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
- package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
- package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
- package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
- package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +399 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +351 -0
- package/src/lib/chat/tools/blueprint-tools.ts +190 -0
- package/src/lib/chat/tools/document-tools.ts +29 -13
- package/src/lib/chat/tools/helpers.ts +41 -0
- package/src/lib/chat/tools/notification-tools.ts +9 -5
- package/src/lib/chat/tools/profile-tools.ts +120 -23
- package/src/lib/chat/tools/project-tools.ts +33 -0
- package/src/lib/chat/tools/schedule-tools.ts +44 -11
- package/src/lib/chat/tools/skill-tools.ts +183 -0
- package/src/lib/chat/tools/table-tools.ts +71 -0
- package/src/lib/chat/tools/task-tools.ts +89 -21
- package/src/lib/chat/tools/workflow-tools.ts +275 -32
- package/src/lib/chat/types.ts +15 -0
- package/src/lib/constants/settings.ts +10 -18
- package/src/lib/data/__tests__/clear.test.ts +56 -2
- package/src/lib/data/clear.ts +17 -16
- package/src/lib/data/delete-project.ts +171 -0
- package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
- package/src/lib/db/bootstrap.ts +62 -16
- package/src/lib/db/index.ts +5 -0
- package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
- package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
- package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
- package/src/lib/db/migrations/0026_drop_license.sql +3 -0
- package/src/lib/db/migrations/meta/_journal.json +21 -0
- package/src/lib/db/schema.ts +94 -23
- package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
- package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
- package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
- package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
- package/src/lib/environment/data.ts +9 -0
- package/src/lib/environment/list-skills.ts +176 -0
- package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
- package/src/lib/environment/parsers/skill.ts +26 -5
- package/src/lib/environment/profile-generator.ts +54 -0
- package/src/lib/environment/skill-enrichment.ts +106 -0
- package/src/lib/environment/skill-recommendations.ts +66 -0
- package/src/lib/environment/workspace-context.ts +13 -1
- package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
- package/src/lib/filters/__tests__/parse.test.ts +135 -0
- package/src/lib/filters/parse.ts +86 -0
- package/src/lib/import/dedup.ts +4 -54
- package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
- package/src/lib/instance/__tests__/detect.test.ts +115 -0
- package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
- package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
- package/src/lib/instance/__tests__/settings.test.ts +83 -0
- package/src/lib/instance/__tests__/upgrade-poller.test.ts +181 -0
- package/src/lib/instance/bootstrap.ts +270 -0
- package/src/lib/instance/detect.ts +49 -0
- package/src/lib/instance/fingerprint.ts +76 -0
- package/src/lib/instance/git-ops.ts +95 -0
- package/src/lib/instance/settings.ts +61 -0
- package/src/lib/instance/types.ts +77 -0
- package/src/lib/instance/upgrade-poller.ts +205 -0
- package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
- package/src/lib/notifications/visibility.ts +33 -0
- package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
- package/src/lib/schedules/__tests__/config.test.ts +62 -0
- package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
- package/src/lib/schedules/__tests__/integration.test.ts +82 -0
- package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
- package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
- package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
- package/src/lib/schedules/collision-check.ts +105 -0
- package/src/lib/schedules/config.ts +53 -0
- package/src/lib/schedules/scheduler.ts +236 -17
- package/src/lib/schedules/slot-claim.ts +105 -0
- package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
- package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
- package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
- package/src/lib/settings/openai-auth.ts +105 -10
- package/src/lib/settings/openai-login-manager.ts +260 -0
- package/src/lib/settings/runtime-setup.ts +14 -4
- package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
- package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
- package/src/lib/tables/enrichment-planner.ts +454 -0
- package/src/lib/tables/enrichment.ts +328 -0
- package/src/lib/tables/query-builder.ts +5 -2
- package/src/lib/tables/trigger-evaluator.ts +3 -2
- package/src/lib/theme.ts +71 -0
- package/src/lib/usage/ledger.ts +2 -18
- package/src/lib/util/__tests__/similarity.test.ts +106 -0
- package/src/lib/util/similarity.ts +77 -0
- package/src/lib/utils/format-timestamp.ts +24 -0
- package/src/lib/utils/stagent-paths.ts +12 -0
- package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
- package/src/lib/validators/__tests__/settings.test.ts +10 -0
- package/src/lib/validators/blueprint.ts +70 -9
- package/src/lib/validators/profile.ts +2 -2
- package/src/lib/validators/settings.ts +3 -1
- package/src/lib/workflows/__tests__/delay.test.ts +196 -0
- package/src/lib/workflows/__tests__/engine.test.ts +8 -0
- package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
- package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
- package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
- package/src/lib/workflows/blueprints/instantiator.ts +22 -1
- package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
- package/src/lib/workflows/blueprints/types.ts +16 -2
- package/src/lib/workflows/delay.ts +106 -0
- package/src/lib/workflows/engine.ts +212 -7
- package/src/lib/workflows/loop-executor.ts +349 -24
- package/src/lib/workflows/post-action.ts +91 -0
- package/src/lib/workflows/types.ts +166 -1
- package/src/test/setup.ts +10 -0
- package/src/app/api/license/checkout/route.ts +0 -28
- package/src/app/api/license/portal/route.ts +0 -26
- package/src/app/api/license/route.ts +0 -89
- package/src/app/api/license/usage/route.ts +0 -63
- package/src/app/api/marketplace/browse/route.ts +0 -15
- package/src/app/api/marketplace/import/route.ts +0 -28
- package/src/app/api/marketplace/publish/route.ts +0 -40
- package/src/app/api/onboarding/email/route.ts +0 -53
- package/src/app/api/settings/telemetry/route.ts +0 -14
- package/src/app/api/sync/export/route.ts +0 -54
- package/src/app/api/sync/restore/route.ts +0 -37
- package/src/app/api/sync/sessions/route.ts +0 -24
- package/src/app/auth/callback/route.ts +0 -73
- package/src/app/marketplace/page.tsx +0 -19
- package/src/components/analytics/analytics-gate-card.tsx +0 -101
- package/src/components/marketplace/blueprint-card.tsx +0 -61
- package/src/components/marketplace/marketplace-browser.tsx +0 -131
- package/src/components/onboarding/email-capture-card.tsx +0 -104
- package/src/components/settings/activation-form.tsx +0 -95
- package/src/components/settings/cloud-account-section.tsx +0 -147
- package/src/components/settings/cloud-sync-section.tsx +0 -155
- package/src/components/settings/subscription-section.tsx +0 -410
- package/src/components/settings/telemetry-section.tsx +0 -80
- package/src/components/shared/premium-gate-overlay.tsx +0 -50
- package/src/components/shared/schedule-gate-dialog.tsx +0 -64
- package/src/components/shared/upgrade-banner.tsx +0 -112
- package/src/hooks/use-supabase-auth.ts +0 -79
- package/src/lib/billing/email.ts +0 -54
- package/src/lib/billing/products.ts +0 -80
- package/src/lib/billing/stripe.ts +0 -101
- package/src/lib/cloud/supabase-browser.ts +0 -32
- package/src/lib/cloud/supabase-client.ts +0 -56
- package/src/lib/license/__tests__/features.test.ts +0 -56
- package/src/lib/license/__tests__/key-format.test.ts +0 -88
- package/src/lib/license/__tests__/manager.test.ts +0 -64
- package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
- package/src/lib/license/cloud-validation.ts +0 -60
- package/src/lib/license/features.ts +0 -44
- package/src/lib/license/key-format.ts +0 -101
- package/src/lib/license/limit-check.ts +0 -111
- package/src/lib/license/limit-queries.ts +0 -51
- package/src/lib/license/manager.ts +0 -345
- package/src/lib/license/notifications.ts +0 -59
- package/src/lib/license/tier-limits.ts +0 -71
- package/src/lib/marketplace/marketplace-client.ts +0 -107
- package/src/lib/sync/cloud-sync.ts +0 -235
- package/src/lib/telemetry/conversion-events.ts +0 -71
- package/src/lib/telemetry/queue.ts +0 -122
- package/src/lib/validators/license.ts +0 -33
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, chmodSync, renameSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import type { EnsureStepResult, EnsureResult, GitOps, ConsentStatus } from "./types";
|
|
5
|
+
import { getInstanceConfig, setInstanceConfig, getGuardrails, setGuardrails } from "./settings";
|
|
6
|
+
import { isPrivateInstance, isDevMode, hasGitDir, detectRebaseInProgress } from "./detect";
|
|
7
|
+
import { createGitOps } from "./git-ops";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_BRANCH_NAME = "local";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Phase A step 1: ensure the instance config row exists with a stable instanceId.
|
|
13
|
+
* Idempotent — returns early if config already exists.
|
|
14
|
+
*/
|
|
15
|
+
export async function ensureInstanceConfig(): Promise<EnsureStepResult> {
|
|
16
|
+
const existing = getInstanceConfig();
|
|
17
|
+
if (existing) {
|
|
18
|
+
return { step: "instance-config", status: "skipped", reason: "already_exists" };
|
|
19
|
+
}
|
|
20
|
+
await setInstanceConfig({
|
|
21
|
+
instanceId: randomUUID(),
|
|
22
|
+
branchName: DEFAULT_BRANCH_NAME,
|
|
23
|
+
isPrivateInstance: isPrivateInstance(),
|
|
24
|
+
createdAt: Math.floor(Date.now() / 1000),
|
|
25
|
+
});
|
|
26
|
+
return { step: "instance-config", status: "ok" };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Phase A step 2: create the `local` branch at current HEAD if it doesn't exist.
|
|
31
|
+
* Non-destructive: `git checkout -b local` preserves whatever branch the user
|
|
32
|
+
* was on, including any local commits. Safe on drifted-main scenarios.
|
|
33
|
+
*/
|
|
34
|
+
export function ensureLocalBranch(git: GitOps): EnsureStepResult {
|
|
35
|
+
if (git.branchExists(DEFAULT_BRANCH_NAME)) {
|
|
36
|
+
return { step: "local-branch", status: "skipped", reason: "branch_exists" };
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
git.createAndCheckoutBranch(DEFAULT_BRANCH_NAME);
|
|
40
|
+
return { step: "local-branch", status: "ok" };
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return {
|
|
43
|
+
step: "local-branch",
|
|
44
|
+
status: "failed",
|
|
45
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const STAGENT_HOOK_VERSION = "1.0.0";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Pre-push hook template. Installed verbatim at .git/hooks/pre-push.
|
|
54
|
+
*
|
|
55
|
+
* Reads the blocked branch list from the stagent SQLite settings table
|
|
56
|
+
* via a bounded sqlite3 invocation. The query is hardcoded — no user
|
|
57
|
+
* input reaches the shell.
|
|
58
|
+
*
|
|
59
|
+
* Escape hatch: set ALLOW_PRIVATE_PUSH=1 in env to bypass the guardrail
|
|
60
|
+
* for legitimate cherry-pick pushes.
|
|
61
|
+
*/
|
|
62
|
+
const PRE_PUSH_HOOK_TEMPLATE = `#!/bin/sh
|
|
63
|
+
# STAGENT_HOOK_VERSION=${STAGENT_HOOK_VERSION}
|
|
64
|
+
# Blocks pushes of private instance branches to origin.
|
|
65
|
+
# Escape hatch: ALLOW_PRIVATE_PUSH=1 git push ...
|
|
66
|
+
#
|
|
67
|
+
# Generated by src/lib/instance/bootstrap.ts — do not edit manually.
|
|
68
|
+
|
|
69
|
+
if [ "$ALLOW_PRIVATE_PUSH" = "1" ]; then
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
current_branch=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")
|
|
74
|
+
if [ -z "$current_branch" ]; then
|
|
75
|
+
exit 0
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
data_dir="\${STAGENT_DATA_DIR:-$HOME/.stagent}"
|
|
79
|
+
db_path="$data_dir/stagent.db"
|
|
80
|
+
if [ ! -f "$db_path" ] || ! command -v sqlite3 >/dev/null 2>&1; then
|
|
81
|
+
exit 0
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
blocked_json=$(sqlite3 "$db_path" "SELECT value FROM settings WHERE key='instance.guardrails';" 2>/dev/null)
|
|
85
|
+
if [ -z "$blocked_json" ]; then
|
|
86
|
+
exit 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
if echo "$blocked_json" | grep -q "\\"$current_branch\\""; then
|
|
90
|
+
echo "stagent: refusing to push private instance branch '$current_branch' to origin." >&2
|
|
91
|
+
echo "stagent: set ALLOW_PRIVATE_PUSH=1 to override (not recommended)." >&2
|
|
92
|
+
exit 1
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
exit 0
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Phase B step 1: install the pre-push hook at .git/hooks/pre-push.
|
|
100
|
+
* Idempotent: checks version marker in existing file; backs up foreign hooks.
|
|
101
|
+
*/
|
|
102
|
+
export function ensurePrePushHook(git: GitOps): EnsureStepResult {
|
|
103
|
+
const hookPath = join(git.getGitDir(), "hooks", "pre-push");
|
|
104
|
+
const markerLine = `STAGENT_HOOK_VERSION=${STAGENT_HOOK_VERSION}`;
|
|
105
|
+
|
|
106
|
+
if (existsSync(hookPath)) {
|
|
107
|
+
const existing = readFileSync(hookPath, "utf-8");
|
|
108
|
+
if (existing.includes(markerLine)) {
|
|
109
|
+
return { step: "pre-push-hook", status: "skipped", reason: "already_installed" };
|
|
110
|
+
}
|
|
111
|
+
if (existing.includes("STAGENT_HOOK_VERSION=")) {
|
|
112
|
+
try {
|
|
113
|
+
writeFileSync(hookPath, PRE_PUSH_HOOK_TEMPLATE, { mode: 0o755 });
|
|
114
|
+
return { step: "pre-push-hook", status: "ok", reason: "upgraded" };
|
|
115
|
+
} catch (err) {
|
|
116
|
+
return {
|
|
117
|
+
step: "pre-push-hook",
|
|
118
|
+
status: "failed",
|
|
119
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
renameSync(hookPath, `${hookPath}.stagent-backup`);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
return {
|
|
127
|
+
step: "pre-push-hook",
|
|
128
|
+
status: "failed",
|
|
129
|
+
reason: `backup_failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
writeFileSync(hookPath, PRE_PUSH_HOOK_TEMPLATE, { mode: 0o755 });
|
|
136
|
+
chmodSync(hookPath, 0o755);
|
|
137
|
+
return { step: "pre-push-hook", status: "ok" };
|
|
138
|
+
} catch (err) {
|
|
139
|
+
return {
|
|
140
|
+
step: "pre-push-hook",
|
|
141
|
+
status: "failed",
|
|
142
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Phase B step 2: set branch.<name>.pushRemote=no_push for each blocked branch.
|
|
149
|
+
* Idempotent via git config semantics (setting the same value is a no-op).
|
|
150
|
+
*/
|
|
151
|
+
export function ensureBranchPushConfig(git: GitOps, branches: string[]): EnsureStepResult {
|
|
152
|
+
const failures: string[] = [];
|
|
153
|
+
for (const branch of branches) {
|
|
154
|
+
try {
|
|
155
|
+
git.setConfig(`branch.${branch}.pushRemote`, "no_push");
|
|
156
|
+
} catch (err) {
|
|
157
|
+
failures.push(`${branch}: ${err instanceof Error ? err.message : String(err)}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (failures.length > 0) {
|
|
161
|
+
return {
|
|
162
|
+
step: "branch-push-config",
|
|
163
|
+
status: "failed",
|
|
164
|
+
reason: failures.join("; "),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return { step: "branch-push-config", status: "ok" };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface ConsentDecision {
|
|
171
|
+
shouldRunPhaseB: boolean;
|
|
172
|
+
reason: ConsentStatus;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Reads the current consent status from settings and returns a decision
|
|
177
|
+
* about whether Phase B (destructive guardrail installation) should run.
|
|
178
|
+
*
|
|
179
|
+
* On first call, stamps firstBootCompletedAt so the system has a record
|
|
180
|
+
* that bootstrap has run at least once. This enables the upgrade-session
|
|
181
|
+
* feature to distinguish "never booted" from "booted but consent not yet
|
|
182
|
+
* given" in its Settings → Instance UI.
|
|
183
|
+
*
|
|
184
|
+
* Does NOT create any UI artifact. The prompt surface is owned by
|
|
185
|
+
* upgrade-session, which renders a "Enable guardrails" action in the
|
|
186
|
+
* Settings → Instance section reading from settings.instance.guardrails.
|
|
187
|
+
*/
|
|
188
|
+
export async function resolveConsentDecision(): Promise<ConsentDecision> {
|
|
189
|
+
const current = getGuardrails();
|
|
190
|
+
|
|
191
|
+
if (current.firstBootCompletedAt === null) {
|
|
192
|
+
await setGuardrails({
|
|
193
|
+
...current,
|
|
194
|
+
firstBootCompletedAt: Math.floor(Date.now() / 1000),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
shouldRunPhaseB: current.consentStatus === "enabled",
|
|
200
|
+
reason: current.consentStatus,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Main entry point called from src/instrumentation.ts.
|
|
206
|
+
* Idempotent — safe to run on every boot.
|
|
207
|
+
*
|
|
208
|
+
* Execution order:
|
|
209
|
+
* 1. Dev-mode gates (env + sentinel) — skip entirely if active
|
|
210
|
+
* 2. .git presence check — skip if absent (npx runtime)
|
|
211
|
+
* 3. Phase A: instanceId, local branch (non-destructive, always runs)
|
|
212
|
+
* 4. Consent: resolves consent decision (stamps firstBootCompletedAt on first call)
|
|
213
|
+
* 5. Phase B: pre-push hook, pushRemote config (only if consent=enabled)
|
|
214
|
+
*/
|
|
215
|
+
export async function ensureInstance(cwd: string = process.cwd()): Promise<EnsureResult> {
|
|
216
|
+
if (isDevMode(cwd)) {
|
|
217
|
+
const reason = process.env.STAGENT_DEV_MODE === "true" ? "dev_mode_env" : "dev_mode_sentinel";
|
|
218
|
+
return { skipped: reason, steps: [] };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!hasGitDir(cwd)) {
|
|
222
|
+
return { skipped: "no_git", steps: [] };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const steps: EnsureStepResult[] = [];
|
|
226
|
+
const git = createGitOps(cwd);
|
|
227
|
+
|
|
228
|
+
// Phase A step 1: instance config
|
|
229
|
+
steps.push(await ensureInstanceConfig());
|
|
230
|
+
|
|
231
|
+
// Phase A step 2: local branch — skip if rebase in progress
|
|
232
|
+
if (detectRebaseInProgress(cwd)) {
|
|
233
|
+
steps.push({ step: "local-branch", status: "skipped", reason: "rebase_in_progress" });
|
|
234
|
+
} else {
|
|
235
|
+
steps.push(ensureLocalBranch(git));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Resolve consent (stamps firstBootCompletedAt on first call, returns decision)
|
|
239
|
+
const decision = await resolveConsentDecision();
|
|
240
|
+
|
|
241
|
+
// Phase B — only if user has explicitly enabled guardrails
|
|
242
|
+
if (decision.shouldRunPhaseB) {
|
|
243
|
+
const hookResult = ensurePrePushHook(git);
|
|
244
|
+
steps.push(hookResult);
|
|
245
|
+
|
|
246
|
+
const config = getInstanceConfig();
|
|
247
|
+
const blockedBranches = config ? [config.branchName] : [];
|
|
248
|
+
if (blockedBranches.length > 0) {
|
|
249
|
+
const configResult = ensureBranchPushConfig(git, blockedBranches);
|
|
250
|
+
steps.push(configResult);
|
|
251
|
+
|
|
252
|
+
// Persist guardrail state back to settings so the pre-push hook can
|
|
253
|
+
// read the list of blocked branches at push time (the hook greps the
|
|
254
|
+
// serialized JSON of settings.instance.guardrails for the current
|
|
255
|
+
// branch name). Without this write, the hook would silently allow
|
|
256
|
+
// all pushes because pushRemoteBlocked would stay [].
|
|
257
|
+
if (hookResult.status !== "failed" && configResult.status !== "failed") {
|
|
258
|
+
const current = getGuardrails();
|
|
259
|
+
await setGuardrails({
|
|
260
|
+
...current,
|
|
261
|
+
prePushHookInstalled: true,
|
|
262
|
+
prePushHookVersion: STAGENT_HOOK_VERSION,
|
|
263
|
+
pushRemoteBlocked: blockedBranches,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return { steps };
|
|
270
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { join, resolve } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns true if the current environment is the canonical stagent dev repo
|
|
7
|
+
* and should skip all instance bootstrap operations.
|
|
8
|
+
*
|
|
9
|
+
* Layered gates:
|
|
10
|
+
* 1. STAGENT_DEV_MODE=true env var (primary, per-developer)
|
|
11
|
+
* 2. .git/stagent-dev-mode sentinel file (secondary, git-dir-scoped)
|
|
12
|
+
*
|
|
13
|
+
* Override: STAGENT_INSTANCE_MODE=true forces bootstrap to run even in dev
|
|
14
|
+
* mode, so contributors can test the feature in the main repo.
|
|
15
|
+
*/
|
|
16
|
+
export function isDevMode(cwd: string = process.cwd()): boolean {
|
|
17
|
+
if (process.env.STAGENT_INSTANCE_MODE === "true") return false;
|
|
18
|
+
if (process.env.STAGENT_DEV_MODE === "true") return true;
|
|
19
|
+
if (existsSync(join(cwd, ".git", "stagent-dev-mode"))) return true;
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Returns true if a .git directory exists at the given path. */
|
|
24
|
+
export function hasGitDir(cwd: string = process.cwd()): boolean {
|
|
25
|
+
return existsSync(join(cwd, ".git"));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns true if STAGENT_DATA_DIR is set to a non-default path,
|
|
30
|
+
* indicating this clone is running as an isolated private instance.
|
|
31
|
+
*/
|
|
32
|
+
export function isPrivateInstance(): boolean {
|
|
33
|
+
const override = process.env.STAGENT_DATA_DIR;
|
|
34
|
+
if (!override) return false;
|
|
35
|
+
const defaultDir = join(homedir(), ".stagent");
|
|
36
|
+
return resolve(override) !== resolve(defaultDir);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns true if a rebase is in progress in the current repo.
|
|
41
|
+
* Both rebase-merge (interactive) and rebase-apply (non-interactive) are detected.
|
|
42
|
+
*/
|
|
43
|
+
export function detectRebaseInProgress(cwd: string = process.cwd()): boolean {
|
|
44
|
+
const gitDir = join(cwd, ".git");
|
|
45
|
+
return (
|
|
46
|
+
existsSync(join(gitDir, "rebase-merge")) ||
|
|
47
|
+
existsSync(join(gitDir, "rebase-apply"))
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine fingerprint generator.
|
|
3
|
+
*
|
|
4
|
+
* Produces a stable, non-identifying hash that uniquely identifies the machine
|
|
5
|
+
* this stagent instance is running on. Used to give each stagent instance a
|
|
6
|
+
* durable identity (e.g., for telemetry correlation and multi-instance
|
|
7
|
+
* disambiguation); no billing or cloud-metering dependency.
|
|
8
|
+
*
|
|
9
|
+
* The fingerprint is derived from:
|
|
10
|
+
* 1. os.hostname() — e.g., "macbook-pro.local"
|
|
11
|
+
* 2. os.userInfo().username — e.g., "navam"
|
|
12
|
+
* 3. SHA-256 of the first non-internal MAC address
|
|
13
|
+
*
|
|
14
|
+
* The MAC is hashed before it leaves the process so the raw network identifier
|
|
15
|
+
* never appears in logs or telemetry. The combined inputs are SHA-256'd
|
|
16
|
+
* together to produce a 64-character hex string.
|
|
17
|
+
*
|
|
18
|
+
* Stability: the fingerprint is stable across reboots and stagent restarts
|
|
19
|
+
* on the same machine. It changes if the user renames their account, renames
|
|
20
|
+
* their machine, or swaps network hardware.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { createHash } from "crypto";
|
|
24
|
+
import { hostname, userInfo, networkInterfaces } from "os";
|
|
25
|
+
|
|
26
|
+
let cachedFingerprint: string | null = null;
|
|
27
|
+
|
|
28
|
+
export function getMachineFingerprint(): string {
|
|
29
|
+
if (cachedFingerprint !== null) return cachedFingerprint;
|
|
30
|
+
|
|
31
|
+
const host = safeHostname();
|
|
32
|
+
const user = safeUsername();
|
|
33
|
+
const macHash = hashPrimaryMac();
|
|
34
|
+
|
|
35
|
+
const combined = `${host}|${user}|${macHash}`;
|
|
36
|
+
cachedFingerprint = createHash("sha256").update(combined).digest("hex");
|
|
37
|
+
return cachedFingerprint;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Exposed for tests that need to reset the module-level cache. */
|
|
41
|
+
export function _resetFingerprintCache(): void {
|
|
42
|
+
cachedFingerprint = null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function safeHostname(): string {
|
|
46
|
+
try {
|
|
47
|
+
return hostname();
|
|
48
|
+
} catch {
|
|
49
|
+
return "unknown-host";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function safeUsername(): string {
|
|
54
|
+
try {
|
|
55
|
+
return userInfo().username;
|
|
56
|
+
} catch {
|
|
57
|
+
return "unknown-user";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hashPrimaryMac(): string {
|
|
62
|
+
try {
|
|
63
|
+
const interfaces = networkInterfaces();
|
|
64
|
+
for (const name of Object.keys(interfaces).sort()) {
|
|
65
|
+
const addrs = interfaces[name] ?? [];
|
|
66
|
+
for (const addr of addrs) {
|
|
67
|
+
if (addr.internal) continue;
|
|
68
|
+
if (!addr.mac || addr.mac === "00:00:00:00:00:00") continue;
|
|
69
|
+
return createHash("sha256").update(addr.mac).digest("hex");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// fall through to the default below
|
|
74
|
+
}
|
|
75
|
+
return "no-mac-detected";
|
|
76
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
2
|
+
import { join, resolve } from "path";
|
|
3
|
+
import type { GitOps } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Real git operations wrapper. All commands use execFileSync with argv arrays —
|
|
7
|
+
* no shell interpolation, ever. File is the literal "git"; user-provided values
|
|
8
|
+
* flow through the args array which git parses without shell involvement.
|
|
9
|
+
*
|
|
10
|
+
* The cwd parameter is normalized to an absolute path at factory creation time
|
|
11
|
+
* so getGitDir() honors its interface contract of returning an absolute path.
|
|
12
|
+
*/
|
|
13
|
+
export function createGitOps(cwd: string = process.cwd()): GitOps {
|
|
14
|
+
const absoluteCwd = resolve(cwd);
|
|
15
|
+
function run(args: string[]): string {
|
|
16
|
+
return execFileSync("git", args, {
|
|
17
|
+
cwd: absoluteCwd,
|
|
18
|
+
encoding: "utf-8",
|
|
19
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
20
|
+
}).trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
isGitRepo(): boolean {
|
|
25
|
+
try {
|
|
26
|
+
run(["rev-parse", "--is-inside-work-tree"]);
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
getGitDir(): string {
|
|
34
|
+
return join(absoluteCwd, ".git");
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
getCurrentBranch(): string | null {
|
|
38
|
+
try {
|
|
39
|
+
const branch = run(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
40
|
+
return branch === "HEAD" ? null : branch;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
branchExists(name: string): boolean {
|
|
47
|
+
try {
|
|
48
|
+
run(["rev-parse", "--verify", `refs/heads/${name}`]);
|
|
49
|
+
return true;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
createAndCheckoutBranch(name: string): void {
|
|
56
|
+
run(["checkout", "-b", name]);
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
setConfig(key: string, value: string): void {
|
|
60
|
+
run(["config", key, value]);
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
fetchOrigin(): void {
|
|
64
|
+
run(["fetch", "origin", "main"]);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
revParse(ref: string): string | null {
|
|
68
|
+
try {
|
|
69
|
+
return run(["rev-parse", ref]);
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
countCommitsAhead(from: string, to: string): number {
|
|
76
|
+
try {
|
|
77
|
+
const out = run(["rev-list", "--count", `${from}..${to}`]);
|
|
78
|
+
const n = parseInt(out, 10);
|
|
79
|
+
return Number.isFinite(n) ? n : 0;
|
|
80
|
+
} catch {
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Test helper: detect if execFileSync would find git on this system. */
|
|
88
|
+
export function isGitAvailable(): boolean {
|
|
89
|
+
try {
|
|
90
|
+
execFileSync("git", ["--version"], { stdio: "ignore" });
|
|
91
|
+
return true;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { getSettingSync, setSetting } from "@/lib/settings/helpers";
|
|
2
|
+
import type { InstanceConfig, Guardrails, UpgradeState } from "./types";
|
|
3
|
+
|
|
4
|
+
const INSTANCE_KEY = "instance";
|
|
5
|
+
const GUARDRAILS_KEY = "instance.guardrails";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_GUARDRAILS: Guardrails = {
|
|
8
|
+
prePushHookInstalled: false,
|
|
9
|
+
prePushHookVersion: "",
|
|
10
|
+
pushRemoteBlocked: [],
|
|
11
|
+
consentStatus: "not_yet",
|
|
12
|
+
firstBootCompletedAt: null,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function readJson<T>(key: string): T | null {
|
|
16
|
+
const raw = getSettingSync(key);
|
|
17
|
+
if (raw === null) return null;
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(raw) as T;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getInstanceConfig(): InstanceConfig | null {
|
|
26
|
+
return readJson<InstanceConfig>(INSTANCE_KEY);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function setInstanceConfig(config: InstanceConfig): Promise<void> {
|
|
30
|
+
await setSetting(INSTANCE_KEY, JSON.stringify(config));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getGuardrails(): Guardrails {
|
|
34
|
+
return readJson<Guardrails>(GUARDRAILS_KEY) ?? { ...DEFAULT_GUARDRAILS };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function setGuardrails(guardrails: Guardrails): Promise<void> {
|
|
38
|
+
await setSetting(GUARDRAILS_KEY, JSON.stringify(guardrails));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const UPGRADE_KEY = "instance.upgrade";
|
|
42
|
+
|
|
43
|
+
const DEFAULT_UPGRADE_STATE: UpgradeState = {
|
|
44
|
+
lastPolledAt: null,
|
|
45
|
+
lastUpstreamSha: null,
|
|
46
|
+
localMainSha: null,
|
|
47
|
+
upgradeAvailable: false,
|
|
48
|
+
commitsBehind: 0,
|
|
49
|
+
lastSuccessfulUpgradeAt: null,
|
|
50
|
+
lastUpgradeTaskId: null,
|
|
51
|
+
pollFailureCount: 0,
|
|
52
|
+
lastPollError: null,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function getUpgradeState(): UpgradeState {
|
|
56
|
+
return readJson<UpgradeState>(UPGRADE_KEY) ?? { ...DEFAULT_UPGRADE_STATE };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function setUpgradeState(state: UpgradeState): Promise<void> {
|
|
60
|
+
await setSetting(UPGRADE_KEY, JSON.stringify(state));
|
|
61
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instance bootstrap shared types.
|
|
3
|
+
* See features/instance-bootstrap.md for full design rationale.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface InstanceConfig {
|
|
7
|
+
instanceId: string;
|
|
8
|
+
branchName: string;
|
|
9
|
+
isPrivateInstance: boolean;
|
|
10
|
+
createdAt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ConsentStatus = "not_yet" | "enabled" | "declined_permanently";
|
|
14
|
+
|
|
15
|
+
export interface Guardrails {
|
|
16
|
+
prePushHookInstalled: boolean;
|
|
17
|
+
prePushHookVersion: string;
|
|
18
|
+
pushRemoteBlocked: string[];
|
|
19
|
+
consentStatus: ConsentStatus;
|
|
20
|
+
firstBootCompletedAt: number | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UpgradeState {
|
|
24
|
+
lastPolledAt: number | null;
|
|
25
|
+
lastUpstreamSha: string | null;
|
|
26
|
+
localMainSha: string | null;
|
|
27
|
+
upgradeAvailable: boolean;
|
|
28
|
+
commitsBehind: number;
|
|
29
|
+
lastSuccessfulUpgradeAt: number | null;
|
|
30
|
+
lastUpgradeTaskId: string | null;
|
|
31
|
+
pollFailureCount: number;
|
|
32
|
+
lastPollError: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type EnsureSkipReason =
|
|
36
|
+
| "dev_mode_env"
|
|
37
|
+
| "dev_mode_sentinel"
|
|
38
|
+
| "no_git";
|
|
39
|
+
|
|
40
|
+
export type EnsureStepStatus = "ok" | "skipped" | "failed";
|
|
41
|
+
|
|
42
|
+
export interface EnsureStepResult {
|
|
43
|
+
step: string;
|
|
44
|
+
status: EnsureStepStatus;
|
|
45
|
+
reason?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface EnsureResult {
|
|
49
|
+
skipped?: EnsureSkipReason;
|
|
50
|
+
steps: EnsureStepResult[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Injectable wrapper around git commands.
|
|
55
|
+
* Real implementation in git-ops.ts uses execFileSync.
|
|
56
|
+
* Tests provide a mock implementation.
|
|
57
|
+
*/
|
|
58
|
+
export interface GitOps {
|
|
59
|
+
/** Returns true if the current working directory is inside a git repo (not a worktree of the main repo). */
|
|
60
|
+
isGitRepo(): boolean;
|
|
61
|
+
/** Returns the absolute path to the .git directory for the current repo. */
|
|
62
|
+
getGitDir(): string;
|
|
63
|
+
/** Returns the currently checked-out branch name, or null if detached HEAD. */
|
|
64
|
+
getCurrentBranch(): string | null;
|
|
65
|
+
/** Returns true if a branch with the given name exists locally. */
|
|
66
|
+
branchExists(name: string): boolean;
|
|
67
|
+
/** Creates a new branch at the current HEAD and checks it out. */
|
|
68
|
+
createAndCheckoutBranch(name: string): void;
|
|
69
|
+
/** Sets a git config value. Throws on failure. */
|
|
70
|
+
setConfig(key: string, value: string): void;
|
|
71
|
+
/** Fetches from origin. Throws on failure. */
|
|
72
|
+
fetchOrigin(): void;
|
|
73
|
+
/** Returns the SHA for a given ref (branch name or remote ref like "origin/main"). Returns null if the ref is unknown. */
|
|
74
|
+
revParse(ref: string): string | null;
|
|
75
|
+
/** Returns the count of commits reachable from `to` but not from `from`. Returns 0 if either ref is unknown. */
|
|
76
|
+
countCommitsAhead(from: string, to: string): number;
|
|
77
|
+
}
|