stagent 0.9.5 → 0.10.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 +5 -42
- package/dist/cli.js +42 -18
- package/docs/.coverage-gaps.json +13 -55
- package/docs/.last-generated +1 -1
- package/docs/features/provider-runtimes.md +4 -0
- package/docs/features/schedules.md +32 -4
- package/docs/features/settings.md +28 -5
- package/docs/features/tables.md +9 -2
- package/docs/features/workflows.md +10 -4
- package/docs/journeys/developer.md +15 -1
- package/docs/journeys/personal-use.md +21 -4
- 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/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/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/diagnostics/chat-streams/route.ts +65 -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/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 +0 -21
- 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/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 +6 -9
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
- package/src/components/chat/chat-command-popover.tsx +2 -2
- package/src/components/chat/chat-input.tsx +2 -3
- package/src/components/chat/chat-session-provider.tsx +720 -0
- package/src/components/chat/chat-shell.tsx +92 -401
- 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/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/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/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/app-sidebar.tsx +4 -3
- package/src/components/shared/command-palette.tsx +4 -5
- 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 +19 -0
- package/src/components/tasks/task-card.tsx +26 -3
- package/src/components/tasks/task-chip-bar.tsx +24 -0
- 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/instrumentation-node.ts +94 -0
- package/src/instrumentation.ts +4 -48
- package/src/lib/agents/__tests__/claude-agent.test.ts +199 -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/claude-agent.ts +155 -18
- package/src/lib/agents/execution-manager.ts +0 -35
- package/src/lib/agents/learned-context.ts +0 -12
- package/src/lib/agents/learning-session.ts +18 -5
- package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
- package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
- package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
- package/src/lib/agents/runtime/openai-codex.ts +29 -60
- package/src/lib/agents/runtime/types.ts +8 -0
- package/src/lib/book/chapter-mapping.ts +11 -0
- package/src/lib/book/content.ts +10 -0
- package/src/lib/chat/__tests__/active-streams.test.ts +49 -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__/stream-telemetry.test.ts +151 -0
- package/src/lib/chat/active-streams.ts +27 -0
- package/src/lib/chat/codex-engine.ts +16 -17
- package/src/lib/chat/context-builder.ts +5 -3
- package/src/lib/chat/engine.ts +50 -3
- package/src/lib/chat/reconcile.ts +117 -0
- package/src/lib/chat/stagent-tools.ts +1 -0
- package/src/lib/chat/stream-telemetry.ts +132 -0
- package/src/lib/chat/suggested-prompts.ts +28 -1
- package/src/lib/chat/system-prompt.ts +26 -1
- package/src/lib/chat/tool-catalog.ts +2 -1
- package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
- package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
- package/src/lib/chat/tools/document-tools.ts +29 -13
- package/src/lib/chat/tools/helpers.ts +39 -0
- package/src/lib/chat/tools/notification-tools.ts +9 -5
- 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/table-tools.ts +71 -0
- package/src/lib/chat/tools/task-tools.ts +84 -20
- package/src/lib/chat/tools/workflow-tools.ts +234 -32
- package/src/lib/constants/settings.ts +8 -18
- package/src/lib/data/__tests__/clear.test.ts +56 -2
- package/src/lib/data/clear.ts +20 -15
- 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 +45 -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 +68 -23
- package/src/lib/environment/workspace-context.ts +13 -1
- 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 +131 -0
- package/src/lib/instance/bootstrap.ts +270 -0
- package/src/lib/instance/detect.ts +49 -0
- package/src/lib/instance/fingerprint.ts +78 -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 +153 -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 +232 -13
- 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/instantiator.ts +22 -1
- package/src/lib/workflows/blueprints/types.ts +10 -2
- package/src/lib/workflows/delay.ts +106 -0
- package/src/lib/workflows/engine.ts +207 -4
- 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/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,125 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { InstanceSection } from "@/components/instance/instance-section";
|
|
5
|
+
|
|
6
|
+
const { push } = vi.hoisted(() => ({
|
|
7
|
+
push: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("next/navigation", () => ({
|
|
11
|
+
useRouter: () => ({ push }),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe("instance section", () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
vi.unstubAllGlobals();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("renders a single combined instance card for initialized instances", async () => {
|
|
24
|
+
vi.stubGlobal(
|
|
25
|
+
"fetch",
|
|
26
|
+
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
27
|
+
const url = String(input);
|
|
28
|
+
const method = init?.method ?? "GET";
|
|
29
|
+
|
|
30
|
+
if (url === "/api/instance/config" && method === "GET") {
|
|
31
|
+
return {
|
|
32
|
+
ok: true,
|
|
33
|
+
json: async () => ({
|
|
34
|
+
devMode: false,
|
|
35
|
+
config: {
|
|
36
|
+
instanceId: "instance_123456789",
|
|
37
|
+
branchName: "instance/demo",
|
|
38
|
+
isPrivateInstance: true,
|
|
39
|
+
createdAt: 1712700000,
|
|
40
|
+
},
|
|
41
|
+
guardrails: {
|
|
42
|
+
prePushHookInstalled: true,
|
|
43
|
+
prePushHookVersion: "1",
|
|
44
|
+
pushRemoteBlocked: ["main"],
|
|
45
|
+
consentStatus: "enabled",
|
|
46
|
+
firstBootCompletedAt: 1712700000,
|
|
47
|
+
},
|
|
48
|
+
upgrade: {
|
|
49
|
+
lastPolledAt: 1712700000,
|
|
50
|
+
upgradeAvailable: true,
|
|
51
|
+
commitsBehind: 3,
|
|
52
|
+
lastSuccessfulUpgradeAt: 1712600000,
|
|
53
|
+
pollFailureCount: 0,
|
|
54
|
+
lastPollError: null,
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (url === "/api/instance/upgrade/check" && method === "POST") {
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
json: async () => ({ ok: true }),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw new Error(`Unexpected fetch: ${method} ${url}`);
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
render(<InstanceSection />);
|
|
72
|
+
|
|
73
|
+
expect(await screen.findByRole("button", { name: "Check" })).toBeInTheDocument();
|
|
74
|
+
expect(screen.getByRole("button", { name: "Upgrade (3)" })).toBeInTheDocument();
|
|
75
|
+
expect(screen.getByRole("button", { name: "Repair setup" })).toBeInTheDocument();
|
|
76
|
+
expect(screen.queryByText("Upgrade instance")).not.toBeInTheDocument();
|
|
77
|
+
expect(screen.queryByText("Advanced: re-run instance setup")).not.toBeInTheDocument();
|
|
78
|
+
expect(screen.queryByText("Blocked branches")).not.toBeInTheDocument();
|
|
79
|
+
expect(screen.queryByText("Pre-push hook")).not.toBeInTheDocument();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("uses the shorter setup CTA when the instance is not initialized", async () => {
|
|
83
|
+
vi.stubGlobal(
|
|
84
|
+
"fetch",
|
|
85
|
+
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
86
|
+
const url = String(input);
|
|
87
|
+
const method = init?.method ?? "GET";
|
|
88
|
+
|
|
89
|
+
if (url === "/api/instance/config" && method === "GET") {
|
|
90
|
+
return {
|
|
91
|
+
ok: true,
|
|
92
|
+
json: async () => ({
|
|
93
|
+
devMode: false,
|
|
94
|
+
config: null,
|
|
95
|
+
guardrails: null,
|
|
96
|
+
upgrade: null,
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (url === "/api/instance/init" && method === "POST") {
|
|
102
|
+
return {
|
|
103
|
+
ok: true,
|
|
104
|
+
json: async () => ({ ok: true }),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
throw new Error(`Unexpected fetch: ${method} ${url}`);
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
render(<InstanceSection />);
|
|
113
|
+
|
|
114
|
+
expect(await screen.findByRole("button", { name: "Run setup" })).toBeInTheDocument();
|
|
115
|
+
expect(
|
|
116
|
+
screen.getByText("Instance setup incomplete. Run setup to initialize this workspace.")
|
|
117
|
+
).toBeInTheDocument();
|
|
118
|
+
|
|
119
|
+
fireEvent.click(screen.getByRole("button", { name: "Run setup" }));
|
|
120
|
+
|
|
121
|
+
await waitFor(() => {
|
|
122
|
+
expect(fetch).toHaveBeenCalledWith("/api/instance/init", { method: "POST" });
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Badge } from "@/components/ui/badge";
|
|
7
|
+
|
|
8
|
+
interface InstanceConfig {
|
|
9
|
+
instanceId: string;
|
|
10
|
+
branchName: string;
|
|
11
|
+
isPrivateInstance: boolean;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Guardrails {
|
|
16
|
+
prePushHookInstalled: boolean;
|
|
17
|
+
prePushHookVersion: string;
|
|
18
|
+
pushRemoteBlocked: string[];
|
|
19
|
+
consentStatus: "not_yet" | "enabled" | "declined_permanently";
|
|
20
|
+
firstBootCompletedAt: number | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface UpgradeState {
|
|
24
|
+
lastPolledAt: number | null;
|
|
25
|
+
upgradeAvailable: boolean;
|
|
26
|
+
commitsBehind: number;
|
|
27
|
+
lastSuccessfulUpgradeAt: number | null;
|
|
28
|
+
pollFailureCount: number;
|
|
29
|
+
lastPollError: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ConfigResponse {
|
|
33
|
+
devMode: boolean;
|
|
34
|
+
config: InstanceConfig | null;
|
|
35
|
+
guardrails: Guardrails | null;
|
|
36
|
+
upgrade: UpgradeState | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Settings → Instance section. Compact horizontal strip with title + actions
|
|
41
|
+
* in a top bar and metadata in a 4-column grid below. On the canonical dev
|
|
42
|
+
* repo (devMode=true) collapses to a single-row notice to avoid pretending
|
|
43
|
+
* the main branch is an instance.
|
|
44
|
+
*/
|
|
45
|
+
const STALE_THRESHOLD_MS = 5 * 60 * 1000;
|
|
46
|
+
|
|
47
|
+
export function InstanceSection() {
|
|
48
|
+
const router = useRouter();
|
|
49
|
+
const [state, setState] = useState<ConfigResponse | null>(null);
|
|
50
|
+
const [loading, setLoading] = useState(true);
|
|
51
|
+
const [busy, setBusy] = useState<"check" | "init" | "upgrade" | null>(null);
|
|
52
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
53
|
+
|
|
54
|
+
async function loadConfig() {
|
|
55
|
+
setLoading(true);
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch("/api/instance/config", { cache: "no-store" });
|
|
58
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
59
|
+
const data = (await res.json()) as ConfigResponse;
|
|
60
|
+
setState(data);
|
|
61
|
+
return data;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
setMessage(err instanceof Error ? err.message : String(err));
|
|
64
|
+
return null;
|
|
65
|
+
} finally {
|
|
66
|
+
setLoading(false);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Silent background refresh — used after auto-check on mount so we don't
|
|
71
|
+
// flicker the whole card back to its loading state.
|
|
72
|
+
async function refreshConfigSilent() {
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetch("/api/instance/config", { cache: "no-store" });
|
|
75
|
+
if (!res.ok) return;
|
|
76
|
+
const data = (await res.json()) as ConfigResponse;
|
|
77
|
+
setState(data);
|
|
78
|
+
} catch {
|
|
79
|
+
// Swallow — this is a best-effort refresh after auto-check.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
let cancelled = false;
|
|
85
|
+
(async () => {
|
|
86
|
+
const data = await loadConfig();
|
|
87
|
+
if (cancelled || !data || data.devMode || !data.config) return;
|
|
88
|
+
// If the cached upgrade state is older than 5 minutes, silently force
|
|
89
|
+
// a fresh check. This self-heals after manual `git pull` + merge in
|
|
90
|
+
// the terminal, so users don't see a stale "N updates pending" count.
|
|
91
|
+
const lastPolled = data.upgrade?.lastPolledAt ?? 0;
|
|
92
|
+
const ageMs = Date.now() - lastPolled * 1000;
|
|
93
|
+
if (ageMs > STALE_THRESHOLD_MS) {
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetch("/api/instance/upgrade/check", {
|
|
96
|
+
method: "POST",
|
|
97
|
+
});
|
|
98
|
+
if (res.ok && !cancelled) {
|
|
99
|
+
await refreshConfigSilent();
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// Silent — manual "Check for upgrades" button remains as fallback.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
})();
|
|
106
|
+
return () => {
|
|
107
|
+
cancelled = true;
|
|
108
|
+
};
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
async function checkNow() {
|
|
112
|
+
setBusy("check");
|
|
113
|
+
setMessage(null);
|
|
114
|
+
try {
|
|
115
|
+
const res = await fetch("/api/instance/upgrade/check", { method: "POST" });
|
|
116
|
+
if (res.status === 202) {
|
|
117
|
+
const body = await res.json();
|
|
118
|
+
setMessage(`Check skipped: ${body.skipped ?? body.error ?? "unknown"}`);
|
|
119
|
+
} else if (res.ok) {
|
|
120
|
+
setMessage("Check complete");
|
|
121
|
+
await loadConfig();
|
|
122
|
+
} else {
|
|
123
|
+
throw new Error(`HTTP ${res.status}`);
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
setMessage(err instanceof Error ? err.message : String(err));
|
|
127
|
+
} finally {
|
|
128
|
+
setBusy(null);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function startUpgrade() {
|
|
133
|
+
setBusy("upgrade");
|
|
134
|
+
setMessage(null);
|
|
135
|
+
try {
|
|
136
|
+
const res = await fetch("/api/instance/upgrade", { method: "POST" });
|
|
137
|
+
if (!res.ok) {
|
|
138
|
+
const body = await res.json().catch(() => ({}));
|
|
139
|
+
throw new Error(body.error ?? `HTTP ${res.status}`);
|
|
140
|
+
}
|
|
141
|
+
const data = (await res.json()) as { taskId: string };
|
|
142
|
+
router.push(`/tasks/${data.taskId}`);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
setMessage(err instanceof Error ? err.message : String(err));
|
|
145
|
+
setBusy(null);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function reinit() {
|
|
150
|
+
setBusy("init");
|
|
151
|
+
setMessage(null);
|
|
152
|
+
try {
|
|
153
|
+
const res = await fetch("/api/instance/init", { method: "POST" });
|
|
154
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
155
|
+
await loadConfig();
|
|
156
|
+
setMessage("Instance setup re-run complete");
|
|
157
|
+
} catch (err) {
|
|
158
|
+
setMessage(err instanceof Error ? err.message : String(err));
|
|
159
|
+
} finally {
|
|
160
|
+
setBusy(null);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (loading) {
|
|
165
|
+
return (
|
|
166
|
+
<section className="rounded-xl border bg-card px-5 py-4">
|
|
167
|
+
<h2 className="text-base font-semibold">Instance</h2>
|
|
168
|
+
<p className="mt-1 text-sm text-muted-foreground">Loading…</p>
|
|
169
|
+
</section>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Dev mode: main dev repo. Instance bootstrap is gated off. Show a slim
|
|
174
|
+
// single-row notice so the Settings page layout stays stable without
|
|
175
|
+
// misrepresenting the dev repo as an instance.
|
|
176
|
+
if (state?.devMode) {
|
|
177
|
+
return (
|
|
178
|
+
<section className="rounded-xl border bg-card px-5 py-3 flex items-center justify-between gap-4 flex-wrap">
|
|
179
|
+
<div className="flex items-center gap-3">
|
|
180
|
+
<h2 className="text-base font-semibold">Instance</h2>
|
|
181
|
+
<Badge variant="outline" className="text-xs font-normal">
|
|
182
|
+
Dev mode
|
|
183
|
+
</Badge>
|
|
184
|
+
</div>
|
|
185
|
+
<p className="text-xs text-muted-foreground">
|
|
186
|
+
Running on the main dev repo. Instance upgrade features are disabled.
|
|
187
|
+
Set{" "}
|
|
188
|
+
<code className="font-mono text-[11px] px-1 py-0.5 rounded bg-muted">
|
|
189
|
+
STAGENT_INSTANCE_MODE=true
|
|
190
|
+
</code>{" "}
|
|
191
|
+
to test.
|
|
192
|
+
</p>
|
|
193
|
+
</section>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const config = state?.config ?? null;
|
|
198
|
+
const guardrails = state?.guardrails ?? null;
|
|
199
|
+
const upgrade = state?.upgrade ?? null;
|
|
200
|
+
const hasConfig = config !== null;
|
|
201
|
+
|
|
202
|
+
// Not-initialized state
|
|
203
|
+
if (!hasConfig) {
|
|
204
|
+
return (
|
|
205
|
+
<section className="rounded-xl border bg-card px-5 py-4 space-y-3">
|
|
206
|
+
<div className="flex items-center justify-between gap-4 flex-wrap">
|
|
207
|
+
<h2 className="text-base font-semibold">Instance</h2>
|
|
208
|
+
<Button
|
|
209
|
+
variant="default"
|
|
210
|
+
size="sm"
|
|
211
|
+
onClick={reinit}
|
|
212
|
+
disabled={busy !== null}
|
|
213
|
+
>
|
|
214
|
+
{busy === "init" ? "Running…" : "Run setup"}
|
|
215
|
+
</Button>
|
|
216
|
+
</div>
|
|
217
|
+
<div className="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs">
|
|
218
|
+
Instance setup incomplete. Run setup to initialize this workspace.
|
|
219
|
+
</div>
|
|
220
|
+
{message && (
|
|
221
|
+
<div className="text-xs text-muted-foreground">{message}</div>
|
|
222
|
+
)}
|
|
223
|
+
</section>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const shortId = config!.instanceId.slice(0, 8) + "…";
|
|
228
|
+
const consentLabel = guardrails?.consentStatus ?? "unknown";
|
|
229
|
+
const hookLabel = guardrails?.prePushHookInstalled
|
|
230
|
+
? `v${guardrails.prePushHookVersion}`
|
|
231
|
+
: "not installed";
|
|
232
|
+
const blockedLabel = guardrails?.pushRemoteBlocked.length
|
|
233
|
+
? guardrails.pushRemoteBlocked.join(", ")
|
|
234
|
+
: "none";
|
|
235
|
+
const lastCheck = upgrade?.lastPolledAt
|
|
236
|
+
? new Date(upgrade.lastPolledAt * 1000).toLocaleString()
|
|
237
|
+
: "never";
|
|
238
|
+
const lastUpgrade = upgrade?.lastSuccessfulUpgradeAt
|
|
239
|
+
? new Date(upgrade.lastSuccessfulUpgradeAt * 1000).toLocaleString()
|
|
240
|
+
: "never";
|
|
241
|
+
const pollFailing = (upgrade?.pollFailureCount ?? 0) > 0;
|
|
242
|
+
|
|
243
|
+
const upgradeAvailable = upgrade?.upgradeAvailable ?? false;
|
|
244
|
+
const upgradeCount = upgrade?.commitsBehind ?? 0;
|
|
245
|
+
const startUpgradeDisabled = busy !== null || !upgradeAvailable;
|
|
246
|
+
const startUpgradeTitle = upgradeAvailable
|
|
247
|
+
? `Merge ${upgradeCount} upstream commit${upgradeCount === 1 ? "" : "s"} into ${config!.branchName}`
|
|
248
|
+
: "No upgrades available — click 'Check for upgrades' to refresh";
|
|
249
|
+
const statusMessage = pollFailing && upgrade?.lastPollError
|
|
250
|
+
? upgrade.lastPollError
|
|
251
|
+
: message;
|
|
252
|
+
const statusToneClass = pollFailing
|
|
253
|
+
? "text-amber-700 dark:text-amber-400"
|
|
254
|
+
: "text-muted-foreground";
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<section className="rounded-xl border bg-card">
|
|
258
|
+
<header className="flex items-start justify-between gap-4 px-5 py-3 border-b flex-wrap">
|
|
259
|
+
<div className="min-w-0 space-y-2">
|
|
260
|
+
<div className="flex items-center gap-3 min-w-0 flex-wrap">
|
|
261
|
+
<h2 className="text-base font-semibold">Instance</h2>
|
|
262
|
+
{upgradeAvailable && (
|
|
263
|
+
<Badge
|
|
264
|
+
variant="outline"
|
|
265
|
+
className="text-xs font-normal border-blue-500/40 bg-blue-500/10 text-blue-700 dark:text-blue-400"
|
|
266
|
+
>
|
|
267
|
+
{upgradeCount} update{upgradeCount === 1 ? "" : "s"} available
|
|
268
|
+
</Badge>
|
|
269
|
+
)}
|
|
270
|
+
{pollFailing && (
|
|
271
|
+
<Badge variant="destructive" className="text-xs font-normal">
|
|
272
|
+
Poll failing ({upgrade?.pollFailureCount})
|
|
273
|
+
</Badge>
|
|
274
|
+
)}
|
|
275
|
+
</div>
|
|
276
|
+
<p className="text-xs text-muted-foreground leading-relaxed max-w-prose">
|
|
277
|
+
Pull latest changes from{" "}
|
|
278
|
+
<code className="font-mono text-[11px] px-1 py-0.5 rounded bg-muted">
|
|
279
|
+
main
|
|
280
|
+
</code>{" "}
|
|
281
|
+
into{" "}
|
|
282
|
+
<code className="font-mono text-[11px] px-1 py-0.5 rounded bg-muted">
|
|
283
|
+
{config!.branchName}
|
|
284
|
+
</code>
|
|
285
|
+
. Nothing is pushed automatically.
|
|
286
|
+
</p>
|
|
287
|
+
</div>
|
|
288
|
+
<div className="flex items-center gap-2 shrink-0 flex-wrap">
|
|
289
|
+
<Button
|
|
290
|
+
variant="outline"
|
|
291
|
+
size="sm"
|
|
292
|
+
onClick={checkNow}
|
|
293
|
+
disabled={busy !== null}
|
|
294
|
+
>
|
|
295
|
+
{busy === "check" ? "Checking…" : "Check"}
|
|
296
|
+
</Button>
|
|
297
|
+
<Button
|
|
298
|
+
variant="default"
|
|
299
|
+
size="sm"
|
|
300
|
+
onClick={startUpgrade}
|
|
301
|
+
disabled={startUpgradeDisabled}
|
|
302
|
+
title={startUpgradeTitle}
|
|
303
|
+
>
|
|
304
|
+
{busy === "upgrade"
|
|
305
|
+
? "Starting…"
|
|
306
|
+
: upgradeAvailable
|
|
307
|
+
? `Upgrade (${upgradeCount})`
|
|
308
|
+
: "Upgrade"}
|
|
309
|
+
</Button>
|
|
310
|
+
<Button
|
|
311
|
+
variant="ghost"
|
|
312
|
+
size="sm"
|
|
313
|
+
onClick={reinit}
|
|
314
|
+
disabled={busy !== null}
|
|
315
|
+
>
|
|
316
|
+
{busy === "init" ? "Running…" : "Repair setup"}
|
|
317
|
+
</Button>
|
|
318
|
+
</div>
|
|
319
|
+
</header>
|
|
320
|
+
|
|
321
|
+
<dl className="grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-2 px-5 py-3 text-sm">
|
|
322
|
+
<Field label="Branch" mono>
|
|
323
|
+
{config!.branchName}
|
|
324
|
+
</Field>
|
|
325
|
+
<Field
|
|
326
|
+
label="Instance ID"
|
|
327
|
+
mono
|
|
328
|
+
title={config!.instanceId}
|
|
329
|
+
>
|
|
330
|
+
{shortId}
|
|
331
|
+
</Field>
|
|
332
|
+
<Field label="Last check">{lastCheck}</Field>
|
|
333
|
+
<Field label="Last upgrade">{lastUpgrade}</Field>
|
|
334
|
+
</dl>
|
|
335
|
+
|
|
336
|
+
<div className="flex items-start justify-between gap-3 border-t px-5 py-2.5 text-[11px]">
|
|
337
|
+
<p className={`leading-relaxed ${statusToneClass}`}>
|
|
338
|
+
{statusMessage ?? (
|
|
339
|
+
upgradeAvailable
|
|
340
|
+
? `Ready to merge ${upgradeCount} upstream update${upgradeCount === 1 ? "" : "s"}.`
|
|
341
|
+
: `Up to date. Last checked: ${lastCheck}.`
|
|
342
|
+
)}
|
|
343
|
+
</p>
|
|
344
|
+
<p className="shrink-0 text-muted-foreground">
|
|
345
|
+
Repairs local setup without changing data or commits.
|
|
346
|
+
</p>
|
|
347
|
+
</div>
|
|
348
|
+
</section>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function Field({
|
|
353
|
+
label,
|
|
354
|
+
children,
|
|
355
|
+
mono,
|
|
356
|
+
truncate,
|
|
357
|
+
title,
|
|
358
|
+
}: {
|
|
359
|
+
label: string;
|
|
360
|
+
children: ReactNode;
|
|
361
|
+
mono?: boolean;
|
|
362
|
+
truncate?: boolean;
|
|
363
|
+
title?: string;
|
|
364
|
+
}) {
|
|
365
|
+
return (
|
|
366
|
+
<div className="min-w-0">
|
|
367
|
+
<dt className="text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
368
|
+
{label}
|
|
369
|
+
</dt>
|
|
370
|
+
<dd
|
|
371
|
+
title={title}
|
|
372
|
+
className={
|
|
373
|
+
"mt-0.5 " +
|
|
374
|
+
(mono ? "font-mono text-xs " : "") +
|
|
375
|
+
(truncate ? "truncate" : "")
|
|
376
|
+
}
|
|
377
|
+
>
|
|
378
|
+
{children}
|
|
379
|
+
</dd>
|
|
380
|
+
</div>
|
|
381
|
+
);
|
|
382
|
+
}
|