@jlongo78/agent-spaces 0.7.3 → 0.7.5
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +7 -0
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +19 -19
- package/.next/standalone/.next/routes-manifest.json +42 -0
- package/.next/standalone/.next/server/app/(desktop)/admin/analytics/page.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/admin/analytics/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/admin/analytics/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/admin/users/page.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/admin/users/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/admin/users/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/analytics/page.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/analytics/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/analytics/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/cortex/page.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/cortex/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/cortex/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/network/page.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/network/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/network/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/page.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/projects/page.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/sessions/[id]/page.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/sessions/[id]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/sessions/page.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/sessions/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/sessions/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/settings/page.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/page.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/pane/[id]/page.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/pane/[id]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/pane/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/remote/[nodeId]/[workspaceId]/page.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/remote/[nodeId]/[workspaceId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/terminal/remote/[nodeId]/[workspaceId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/workspaces/page.js +1 -1
- package/.next/standalone/.next/server/app/(desktop)/workspaces/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/(desktop)/workspaces/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +2 -2
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/analytics.html +1 -1
- package/.next/standalone/.next/server/app/admin/analytics.rsc +17 -16
- package/.next/standalone/.next/server/app/admin/analytics.segments/!KGRlc2t0b3Ap/admin/analytics/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/analytics.segments/!KGRlc2t0b3Ap/admin/analytics.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/admin/analytics.segments/!KGRlc2t0b3Ap/admin.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/admin/analytics.segments/!KGRlc2t0b3Ap.segment.rsc +5 -4
- package/.next/standalone/.next/server/app/admin/analytics.segments/_full.segment.rsc +17 -16
- package/.next/standalone/.next/server/app/admin/analytics.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/admin/analytics.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/analytics.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/users.html +1 -1
- package/.next/standalone/.next/server/app/admin/users.rsc +17 -16
- package/.next/standalone/.next/server/app/admin/users.segments/!KGRlc2t0b3Ap/admin/users/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/users.segments/!KGRlc2t0b3Ap/admin/users.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/admin/users.segments/!KGRlc2t0b3Ap/admin.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/admin/users.segments/!KGRlc2t0b3Ap.segment.rsc +5 -4
- package/.next/standalone/.next/server/app/admin/users.segments/_full.segment.rsc +17 -16
- package/.next/standalone/.next/server/app/admin/users.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/admin/users.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/admin/users.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/analytics.html +1 -1
- package/.next/standalone/.next/server/app/analytics.rsc +17 -16
- package/.next/standalone/.next/server/app/analytics.segments/!KGRlc2t0b3Ap/analytics/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/analytics.segments/!KGRlc2t0b3Ap/analytics.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/analytics.segments/!KGRlc2t0b3Ap.segment.rsc +5 -4
- package/.next/standalone/.next/server/app/analytics.segments/_full.segment.rsc +17 -16
- package/.next/standalone/.next/server/app/analytics.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/analytics.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/analytics.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/analytics/overview/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bulk/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/chat/route.js +1 -1
- package/.next/standalone/.next/server/app/api/chat/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/context/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/curation/assess/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/assess/route/build-manifest.json +11 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/assess/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/assess/route.js +7 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/assess/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/assess/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/assess/route_client-reference-manifest.js +2 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/publish/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/publish/route/build-manifest.json +11 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/publish/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/publish/route.js +7 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/publish/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/publish/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/publish/route_client-reference-manifest.js +2 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/refine/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/refine/route/build-manifest.json +11 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/refine/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/refine/route.js +7 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/refine/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/refine/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/refine/route_client-reference-manifest.js +2 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/review/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/review/route/build-manifest.json +11 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/review/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/review/route.js +7 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/review/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/review/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/review/route_client-reference-manifest.js +2 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/seed/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/seed/route/build-manifest.json +11 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/seed/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/seed/route.js +7 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/seed/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/seed/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/cortex/curation/seed/route_client-reference-manifest.js +2 -0
- package/.next/standalone/.next/server/app/api/cortex/export/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/federation/pending/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/federation/resolve/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/federation/search/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/federation/teach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/graph/edges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/graph/entities/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/graph/entities/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/graph/populate/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/import/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/ingest/bootstrap/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/ingest/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/knowledge/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/knowledge/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/lobes/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/lobes/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/lobes/share/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/marketplace/browse/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/cortex/marketplace/browse/route/build-manifest.json +11 -0
- package/.next/standalone/.next/server/app/api/cortex/marketplace/browse/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/cortex/marketplace/browse/route.js +7 -0
- package/.next/standalone/.next/server/app/api/cortex/marketplace/browse/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/cortex/marketplace/browse/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/cortex/marketplace/browse/route_client-reference-manifest.js +2 -0
- package/.next/standalone/.next/server/app/api/cortex/marketplace/preview/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/cortex/marketplace/preview/route/build-manifest.json +11 -0
- package/.next/standalone/.next/server/app/api/cortex/marketplace/preview/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/cortex/marketplace/preview/route.js +7 -0
- package/.next/standalone/.next/server/app/api/cortex/marketplace/preview/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/cortex/marketplace/preview/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/cortex/marketplace/preview/route_client-reference-manifest.js +2 -0
- package/.next/standalone/.next/server/app/api/cortex/mcp/call/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/mcp/tools/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/search/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/timeline/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/usage/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/cortex/workspace/[id]/context/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/events/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/folders/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/handshake/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/projects/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/search/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/sessions/[id]/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/sessions/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/sessions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/workspaces/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/workspaces/route.js +1 -1
- package/.next/standalone/.next/server/app/api/network/workspaces/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/panes/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/panes/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/search/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/chat/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sync/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/tags/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/tier/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/context/[key]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/context/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/messages/[msgId]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/[id]/sessions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/workspaces/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/cortex.html +1 -1
- package/.next/standalone/.next/server/app/cortex.rsc +17 -16
- package/.next/standalone/.next/server/app/cortex.segments/!KGRlc2t0b3Ap/cortex/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/cortex.segments/!KGRlc2t0b3Ap/cortex.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/cortex.segments/!KGRlc2t0b3Ap.segment.rsc +5 -4
- package/.next/standalone/.next/server/app/cortex.segments/_full.segment.rsc +17 -16
- package/.next/standalone/.next/server/app/cortex.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/cortex.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/cortex.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/login.html +1 -1
- package/.next/standalone/.next/server/app/login.rsc +2 -2
- package/.next/standalone/.next/server/app/login.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/login.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/login.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/login.segments/login/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/login.segments/login.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/m/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/m/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/m/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/m/projects.html +1 -1
- package/.next/standalone/.next/server/app/m/projects.rsc +4 -4
- package/.next/standalone/.next/server/app/m/projects.segments/_full.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/m/projects.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/projects.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/projects.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/projects.segments/m/projects/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/projects.segments/m/projects.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/projects.segments/m.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/sessions/[id]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/m/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/m/sessions/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/m/sessions/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/m/sessions.html +1 -1
- package/.next/standalone/.next/server/app/m/sessions.rsc +4 -4
- package/.next/standalone/.next/server/app/m/sessions.segments/_full.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/m/sessions.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/sessions.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/sessions.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/sessions.segments/m/sessions/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/sessions.segments/m/sessions.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/sessions.segments/m.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/m/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/m/settings.html +1 -1
- package/.next/standalone/.next/server/app/m/settings.rsc +4 -4
- package/.next/standalone/.next/server/app/m/settings.segments/_full.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/m/settings.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/settings.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/settings.segments/m/settings/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/settings.segments/m/settings.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/settings.segments/m.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/terminal/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/m/terminal/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/m/terminal.html +1 -1
- package/.next/standalone/.next/server/app/m/terminal.rsc +4 -4
- package/.next/standalone/.next/server/app/m/terminal.segments/_full.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/m/terminal.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/terminal.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/terminal.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/terminal.segments/m/terminal/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m/terminal.segments/m/terminal.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m/terminal.segments/m.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m.html +1 -1
- package/.next/standalone/.next/server/app/m.rsc +4 -4
- package/.next/standalone/.next/server/app/m.segments/_full.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/m.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/m.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m.segments/m/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/m.segments/m.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/network.html +1 -1
- package/.next/standalone/.next/server/app/network.rsc +17 -16
- package/.next/standalone/.next/server/app/network.segments/!KGRlc2t0b3Ap/network/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/network.segments/!KGRlc2t0b3Ap/network.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/network.segments/!KGRlc2t0b3Ap.segment.rsc +5 -4
- package/.next/standalone/.next/server/app/network.segments/_full.segment.rsc +17 -16
- package/.next/standalone/.next/server/app/network.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/network.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/network.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/projects.html +1 -1
- package/.next/standalone/.next/server/app/projects.rsc +17 -16
- package/.next/standalone/.next/server/app/projects.segments/!KGRlc2t0b3Ap/projects/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/projects.segments/!KGRlc2t0b3Ap/projects.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/projects.segments/!KGRlc2t0b3Ap.segment.rsc +5 -4
- package/.next/standalone/.next/server/app/projects.segments/_full.segment.rsc +17 -16
- package/.next/standalone/.next/server/app/projects.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/projects.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/projects.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/sessions.html +1 -1
- package/.next/standalone/.next/server/app/sessions.rsc +17 -16
- package/.next/standalone/.next/server/app/sessions.segments/!KGRlc2t0b3Ap/sessions/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/sessions.segments/!KGRlc2t0b3Ap/sessions.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/sessions.segments/!KGRlc2t0b3Ap.segment.rsc +5 -4
- package/.next/standalone/.next/server/app/sessions.segments/_full.segment.rsc +17 -16
- package/.next/standalone/.next/server/app/sessions.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/sessions.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/sessions.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +17 -16
- package/.next/standalone/.next/server/app/settings.segments/!KGRlc2t0b3Ap/settings/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/!KGRlc2t0b3Ap/settings.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/!KGRlc2t0b3Ap.segment.rsc +5 -4
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +17 -16
- package/.next/standalone/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/terminal.html +1 -1
- package/.next/standalone/.next/server/app/terminal.rsc +17 -16
- package/.next/standalone/.next/server/app/terminal.segments/!KGRlc2t0b3Ap/terminal/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/terminal.segments/!KGRlc2t0b3Ap/terminal.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/terminal.segments/!KGRlc2t0b3Ap.segment.rsc +5 -4
- package/.next/standalone/.next/server/app/terminal.segments/_full.segment.rsc +17 -16
- package/.next/standalone/.next/server/app/terminal.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/terminal.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/terminal.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/workspaces.html +1 -1
- package/.next/standalone/.next/server/app/workspaces.rsc +17 -16
- package/.next/standalone/.next/server/app/workspaces.segments/!KGRlc2t0b3Ap/workspaces/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/workspaces.segments/!KGRlc2t0b3Ap/workspaces.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/workspaces.segments/!KGRlc2t0b3Ap.segment.rsc +5 -4
- package/.next/standalone/.next/server/app/workspaces.segments/_full.segment.rsc +17 -16
- package/.next/standalone/.next/server/app/workspaces.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/workspaces.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/workspaces.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app-paths-manifest.json +7 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0041efe4._.js +98 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__00bf0ace._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e71d908._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e9142f3._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__10e47926._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1665dc78._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__175cbabf._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adae357._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1d359752._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1e8fabeb._.js +3 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1f8deca0._.js +8 -8
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__253fdda1._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__28e6434f._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__2a386564._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__2c20fb38._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__309132cd._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__cf3c60c2._.js → [root-of-the-server]__33fec964._.js} +4 -4
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__3786d8ae._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__3ae92407._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__3beda9fe._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__4619e9bd._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__4a051043._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__508002e4._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__5086c373._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__5913e097._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__5b5f68d2._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__5c1f2459._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__5ec8c977._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__63cebc6c._.js +98 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__64d30d4d._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__6c54fc2e._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__6dc1fb7e._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__6e568102._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__6faa04c0._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__74a34dc3._.js +98 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__7e7250a4._.js +3 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__8309e0a4._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__86cc0e2b._.js +6 -6
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__89c2565a._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__8d178ad9._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__93ee06f3._.js +3 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__9e4c154a._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__a9d2e1d3._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__ae53d343._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__b3a04cef._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__b4270b77._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__b6b6ce60._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__b9545dd9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__c88b63f7._.js +98 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__eee4c5e8._.js → [root-of-the-server]__cba5f007._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__cbf4ceb0._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__cefdba2f._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__cf9e82bb._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__d2897392._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__d3b2d856._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__d73273ca._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__d8417eb6._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__dc2a55de._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__e0d4690b._.js +3 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__e678dd53._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__e9223f55._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__ea630076._.js +3 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__eb8acb65._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__f26ca49d._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__f33e1101._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__f515f865._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__fceb5d60._.js +98 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__fed41403._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__ff2e98c2._.js +2 -2
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_cortex_curation_assess_route_actions_3e8ef2a6.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_cortex_curation_publish_route_actions_49238be3.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_cortex_curation_refine_route_actions_05675688.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_cortex_curation_review_route_actions_5cbb5f6b.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_cortex_curation_seed_route_actions_21084bdb.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_cortex_marketplace_browse_route_actions_7ed0768c.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_cortex_marketplace_preview_route_actions_38d4cc17.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19afc53d._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__66aca5d4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__efef0a06._.js → [root-of-the-server]__ca5beb09._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_7dd2ac82._.js → _078dd64d._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_163d0838._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_a4eeff0d._.js → _2230ad2d._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_9303a965._.js → _701606d5._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_72b1de37._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_d39bcfda._.js → _93ef0f79._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_950142a4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_a22b5eb0._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_aeeff784._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_c1cfdd09._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_c2d3f6de._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_8167090e._.js → _db2fec84._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/src_02bae6e5._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/src_app_(desktop)_terminal_page_tsx_de5e8d85._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/src_components_terminal_terminal-pane_tsx_803c5e2c._.js +2 -2
- package/.next/standalone/.next/server/edge/chunks/_d73df637._.js +1 -1
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +2 -2
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/25b7a243a404a1a7.js +1 -0
- package/.next/standalone/.next/static/chunks/2b997e211a5d547b.js +1 -0
- package/.next/standalone/.next/static/chunks/5e08abb00653754a.js +1 -0
- package/.next/standalone/.next/static/chunks/61f2ed39b75b1efc.js +1 -0
- package/.next/standalone/.next/static/chunks/6c78a1dfa7ec2959.css +3 -0
- package/.next/standalone/.next/static/chunks/7246f1ee445f7024.js +1 -0
- package/.next/standalone/.next/static/chunks/7424664c6ffa94bd.js +1 -0
- package/.next/standalone/.next/static/chunks/7e0091ab6c5ee8bd.js +1 -0
- package/.next/standalone/.next/static/chunks/8b3f4572fec83caa.js +5 -0
- package/.next/standalone/.next/static/chunks/{a7b2795949a6c63e.js → 9899cf4c2bdbe61d.js} +2 -2
- package/.next/standalone/.next/static/chunks/ac339e970df82fa5.js +5 -0
- package/.next/standalone/.next/static/chunks/{0e61e67b7b8fc3f3.js → c418112e102673ce.js} +1 -1
- package/.next/standalone/.next/static/chunks/e2f0a2330dedff4b.js +85 -0
- package/.next/standalone/.next/static/chunks/f0c8b3f8cc1939ec.js +1 -0
- package/.next/standalone/.next/static/chunks/f9f2628207848ac2.js +1 -0
- package/.next/standalone/bin/cortex-hook.sh +62 -62
- package/.next/standalone/bin/cortex-mcp.js +60 -60
- package/.next/standalone/docs/superpowers/plans/2026-03-13-cortex-wiring.md +1387 -1387
- package/.next/standalone/docs/superpowers/plans/2026-03-14-cortex-v2-entity-graph.md +1923 -1923
- package/.next/standalone/docs/superpowers/plans/2026-03-14-cortex-v2-knowledge-evolution.md +1113 -1113
- package/.next/standalone/docs/superpowers/plans/2026-03-15-cortex-v2-boundary-engine.md +853 -853
- package/.next/standalone/docs/superpowers/plans/2026-03-15-cortex-v2-context-engine.md +1274 -1274
- package/.next/standalone/docs/superpowers/plans/2026-03-15-cortex-v2-signal-ingestion.md +933 -933
- package/.next/standalone/docs/superpowers/plans/2026-03-16-cortex-lobes.md +1080 -1080
- package/.next/standalone/docs/superpowers/plans/2026-03-16-cortex-v2-gravity-system.md +768 -768
- package/.next/standalone/docs/superpowers/plans/2026-03-16-cortex-v2-ui.md +1108 -1108
- package/.next/standalone/docs/superpowers/plans/2026-03-18-cortex-ui-integration.md +1846 -0
- package/.next/standalone/docs/superpowers/specs/2026-03-13-cortex-wiring-design.md +268 -268
- package/.next/standalone/docs/superpowers/specs/2026-03-14-cortex-v2-design.md +623 -623
- package/.next/standalone/docs/superpowers/specs/2026-03-16-cortex-lobes-design.md +263 -263
- package/.next/standalone/docs/superpowers/specs/2026-03-16-cortex-v2-ui-design.md +240 -240
- package/.next/standalone/docs/superpowers/specs/2026-03-18-cortex-ui-integration-design.md +341 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/README.md +46 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/glib-2.0/include/glibconfig.h +221 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/index.js +1 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.8.17.3 +0 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/package.json +42 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/README.md +46 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/glib-2.0/include/glibconfig.h +221 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +1 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +42 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +30 -0
- package/.next/standalone/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64.node +0 -0
- package/.next/standalone/node_modules/@img/{sharp-win32-x64 → sharp-linux-x64}/package.json +46 -39
- package/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
- package/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +46 -0
- package/.next/standalone/package.json +102 -102
- package/.next/standalone/server.js +1 -1
- package/.next/standalone/src/app/(desktop)/cortex/page.tsx +78 -75
- package/.next/standalone/src/app/api/cortex/context/route.ts +78 -78
- package/.next/standalone/src/app/api/cortex/curation/assess/route.ts +27 -0
- package/.next/standalone/src/app/api/cortex/curation/publish/route.ts +23 -0
- package/.next/standalone/src/app/api/cortex/curation/refine/route.ts +23 -0
- package/.next/standalone/src/app/api/cortex/curation/review/route.ts +29 -0
- package/.next/standalone/src/app/api/cortex/curation/seed/route.ts +23 -0
- package/.next/standalone/src/app/api/cortex/export/route.ts +40 -40
- package/.next/standalone/src/app/api/cortex/federation/pending/route.ts +20 -20
- package/.next/standalone/src/app/api/cortex/federation/resolve/route.ts +43 -43
- package/.next/standalone/src/app/api/cortex/federation/search/route.ts +35 -35
- package/.next/standalone/src/app/api/cortex/federation/teach/route.ts +76 -76
- package/.next/standalone/src/app/api/cortex/graph/edges/route.ts +112 -112
- package/.next/standalone/src/app/api/cortex/graph/entities/[id]/route.ts +73 -73
- package/.next/standalone/src/app/api/cortex/graph/entities/route.ts +75 -75
- package/.next/standalone/src/app/api/cortex/graph/populate/route.ts +203 -203
- package/.next/standalone/src/app/api/cortex/import/route.ts +75 -43
- package/.next/standalone/src/app/api/cortex/import/status/route.ts +15 -15
- package/.next/standalone/src/app/api/cortex/ingest/bootstrap/route.ts +29 -29
- package/.next/standalone/src/app/api/cortex/ingest/status/route.ts +15 -15
- package/.next/standalone/src/app/api/cortex/knowledge/[id]/route.ts +91 -91
- package/.next/standalone/src/app/api/cortex/knowledge/route.ts +93 -93
- package/.next/standalone/src/app/api/cortex/lobes/[id]/route.ts +67 -67
- package/.next/standalone/src/app/api/cortex/lobes/route.ts +22 -22
- package/.next/standalone/src/app/api/cortex/lobes/share/route.ts +80 -80
- package/.next/standalone/src/app/api/cortex/marketplace/browse/route.ts +43 -0
- package/.next/standalone/src/app/api/cortex/marketplace/preview/route.ts +46 -0
- package/.next/standalone/src/app/api/cortex/mcp/call/route.ts +11 -11
- package/.next/standalone/src/app/api/cortex/mcp/tools/route.ts +6 -6
- package/.next/standalone/src/app/api/cortex/search/route.ts +43 -43
- package/.next/standalone/src/app/api/cortex/settings/route.ts +33 -33
- package/.next/standalone/src/app/api/cortex/status/route.ts +169 -129
- package/.next/standalone/src/app/api/cortex/timeline/route.ts +42 -42
- package/.next/standalone/src/app/api/cortex/usage/route.ts +31 -31
- package/.next/standalone/src/app/api/cortex/workspace/[id]/context/route.ts +41 -41
- package/.next/standalone/src/components/cortex/constants.ts +29 -0
- package/.next/standalone/src/components/cortex/cortex-dashboard.tsx +304 -228
- package/.next/standalone/src/components/cortex/cortex-indicator.tsx +44 -44
- package/.next/standalone/src/components/cortex/cortex-panel.tsx +140 -140
- package/.next/standalone/src/components/cortex/cortex-settings.tsx +221 -221
- package/.next/standalone/src/components/cortex/curation-tab.tsx +810 -0
- package/.next/standalone/src/components/cortex/entity-detail.tsx +101 -101
- package/.next/standalone/src/components/cortex/entity-graph.tsx +382 -382
- package/.next/standalone/src/components/cortex/import-dialog.tsx +212 -0
- package/.next/standalone/src/components/cortex/injection-badge.tsx +72 -72
- package/.next/standalone/src/components/cortex/knowledge-card.tsx +109 -127
- package/.next/standalone/src/components/cortex/knowledge-tab.tsx +158 -56
- package/.next/standalone/src/components/cortex/lobe-settings.tsx +215 -199
- package/.next/standalone/src/components/cortex/marketplace-card.tsx +126 -0
- package/.next/standalone/src/components/cortex/marketplace-tab.tsx +113 -0
- package/.next/standalone/src/lib/cortex/config.ts +40 -40
- package/.next/standalone/src/lib/cortex/debug.ts +10 -10
- package/.next/standalone/src/lib/cortex/distillation/usage-store.ts +18 -18
- package/.next/standalone/src/lib/cortex/graph/resolver.ts +10 -10
- package/.next/standalone/src/lib/cortex/graph/types.ts +22 -22
- package/.next/standalone/src/lib/cortex/index.ts +56 -56
- package/.next/standalone/src/lib/cortex/ingestion/bootstrap.ts +14 -14
- package/.next/standalone/src/lib/cortex/knowledge/compat.ts +14 -14
- package/.next/standalone/src/lib/cortex/knowledge/contradiction.ts +10 -10
- package/.next/standalone/src/lib/cortex/knowledge/types.ts +67 -67
- package/.next/standalone/src/lib/cortex/lobes/config.ts +16 -16
- package/.next/standalone/src/lib/cortex/lobes/resolver.ts +8 -8
- package/.next/standalone/src/lib/cortex/lobes/shares.ts +14 -14
- package/.next/standalone/src/lib/cortex/mcp/server.ts +8 -8
- package/.next/standalone/src/lib/cortex/portability/exporter.ts +6 -6
- package/.next/standalone/src/lib/cortex/portability/importer.ts +10 -10
- package/.next/standalone/src/lib/cortex/retrieval/context-engine.ts +10 -10
- package/.next/standalone/src/lib/cortex/types.ts +39 -38
- package/.next/standalone/tsconfig.json +34 -34
- package/LICENSE +661 -661
- package/README.md +131 -131
- package/bin/cortex-hook.sh +62 -62
- package/bin/cortex-mcp.js +60 -60
- package/bin/fix-standalone-externals.js +79 -79
- package/bin/lib/auto-setup.js +110 -110
- package/bin/mdns-service.js +171 -171
- package/bin/postinstall.js +35 -35
- package/bin/setup-admin.js +195 -195
- package/bin/spaces-dev.js +208 -208
- package/bin/spaces-install.js +599 -599
- package/bin/spaces-reset-totp.js +50 -50
- package/bin/spaces-service.js +1020 -1020
- package/bin/spaces-setup.js +253 -253
- package/bin/spaces.js +776 -728
- package/bin/ssh-auth-keys.sh +68 -68
- package/bin/terminal-server.js +1649 -1646
- package/package.json +102 -102
- package/.next/standalone/.claude/settings.local.json +0 -55
- package/.next/standalone/.claude/spaces-env.json +0 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ecce08b._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_7e63066a._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_a012c43b._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_b4db3983._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_cd50a174._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_e7a9e2b0._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_ed6c285b._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_133a7c8a._.js +0 -3
- package/.next/standalone/.next/static/chunks/17d164c01fa1aaa9.js +0 -5
- package/.next/standalone/.next/static/chunks/19da759dde107f02.js +0 -1
- package/.next/standalone/.next/static/chunks/58d78765e5140dcb.js +0 -1
- package/.next/standalone/.next/static/chunks/596bd56e0ad09173.js +0 -5
- package/.next/standalone/.next/static/chunks/78dd908e70bf8c4b.js +0 -85
- package/.next/standalone/.next/static/chunks/79aacab676df80c4.js +0 -1
- package/.next/standalone/.next/static/chunks/846d6ef408f69390.js +0 -1
- package/.next/standalone/.next/static/chunks/8de5e432a2fc563a.js +0 -1
- package/.next/standalone/.next/static/chunks/9196173f0d081f2c.js +0 -1
- package/.next/standalone/.next/static/chunks/9bd8a119aaeafc9e.js +0 -1
- package/.next/standalone/.next/static/chunks/e35e863c09511a51.js +0 -1
- package/.next/standalone/.next/static/chunks/f644c11ace7a0d9d.css +0 -3
- package/.next/standalone/.spaces/cortex-context.md +0 -70
- package/.next/standalone/node_modules/@img/sharp-win32-x64/lib/sharp-win32-x64.node +0 -0
- package/.next/standalone/src/components/cortex/context-tab.tsx +0 -171
- /package/.next/standalone/.next/static/{B207XFa7QvXAYS7Sqq2_4 → 77VYbwIoyxFNr5xevTrCu}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{B207XFa7QvXAYS7Sqq2_4 → 77VYbwIoyxFNr5xevTrCu}/_clientMiddlewareManifest.json +0 -0
- /package/.next/standalone/.next/static/{B207XFa7QvXAYS7Sqq2_4 → 77VYbwIoyxFNr5xevTrCu}/_ssgManifest.js +0 -0
- /package/.next/standalone/node_modules/@img/{sharp-win32-x64 → sharp-libvips-linux-x64}/versions.json +0 -0
package/bin/spaces-service.js
CHANGED
|
@@ -1,1020 +1,1020 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
'use strict';
|
|
4
|
-
|
|
5
|
-
const { execFileSync, spawnSync } = require('child_process');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const os = require('os');
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const readline = require('readline');
|
|
10
|
-
|
|
11
|
-
let SPACES_DIR = path.join(os.homedir(), '.spaces');
|
|
12
|
-
let CONFIG_PATH = path.join(SPACES_DIR, 'server.json');
|
|
13
|
-
let LOGS_DIR = path.join(SPACES_DIR, 'logs');
|
|
14
|
-
const SERVICE_NAME = 'spaces';
|
|
15
|
-
const LABEL = 'com.agentspaces.spaces';
|
|
16
|
-
const TASK_NAME = 'Spaces';
|
|
17
|
-
|
|
18
|
-
// Override SPACES_DIR to point to a different user's home
|
|
19
|
-
function setTargetHome(homedir) {
|
|
20
|
-
SPACES_DIR = path.join(homedir, '.spaces');
|
|
21
|
-
CONFIG_PATH = path.join(SPACES_DIR, 'server.json');
|
|
22
|
-
LOGS_DIR = path.join(SPACES_DIR, 'logs');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ─── Helpers ──────────────────────────────────────────────────
|
|
26
|
-
function log(msg) { console.log(` ${msg}`); }
|
|
27
|
-
function logOk(msg) { console.log(` ✓ ${msg}`); }
|
|
28
|
-
function logErr(msg) { console.error(` ✗ ${msg}`); }
|
|
29
|
-
|
|
30
|
-
function resolveConfig() {
|
|
31
|
-
let config = {};
|
|
32
|
-
if (fs.existsSync(CONFIG_PATH)) {
|
|
33
|
-
try { config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch {}
|
|
34
|
-
}
|
|
35
|
-
return {
|
|
36
|
-
port: config.port || 3457,
|
|
37
|
-
tier: config.tier || 'community',
|
|
38
|
-
basePath: config.basePath || '',
|
|
39
|
-
allowedOrigins: config.allowedOrigins || '',
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function resolveSpacesPath() {
|
|
44
|
-
return path.join(__dirname, 'spaces.js');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function resolveProjectDir() {
|
|
48
|
-
return path.join(__dirname, '..');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function resolveNodePath() {
|
|
52
|
-
return process.execPath;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function promptLevel() {
|
|
56
|
-
return new Promise((resolve) => {
|
|
57
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
58
|
-
console.log('');
|
|
59
|
-
log('Install as:');
|
|
60
|
-
log(' 1. System service (starts on boot, all users)');
|
|
61
|
-
log(' 2. User service (starts on login, current user only)');
|
|
62
|
-
console.log('');
|
|
63
|
-
rl.question(' Choice [2]: ', (answer) => {
|
|
64
|
-
rl.close();
|
|
65
|
-
const choice = answer.trim() || '2';
|
|
66
|
-
resolve(choice === '1' ? 'system' : 'user');
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function ensureLogsDir() {
|
|
72
|
-
fs.mkdirSync(LOGS_DIR, { recursive: true });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Detect Windows user profiles that have .claude/ (Claude Code data)
|
|
76
|
-
function findClaudeUsers() {
|
|
77
|
-
if (process.platform !== 'win32') return [];
|
|
78
|
-
const usersDir = path.dirname(os.homedir());
|
|
79
|
-
const skip = new Set(['Public', 'Default', 'Default User', 'All Users']);
|
|
80
|
-
try {
|
|
81
|
-
return fs.readdirSync(usersDir)
|
|
82
|
-
.filter(name => !skip.has(name) && !name.startsWith('.'))
|
|
83
|
-
.filter(name => {
|
|
84
|
-
const claudeDir = path.join(usersDir, name, '.claude');
|
|
85
|
-
return fs.existsSync(claudeDir);
|
|
86
|
-
})
|
|
87
|
-
.map(name => ({ name, homedir: path.join(usersDir, name) }));
|
|
88
|
-
} catch { return []; }
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Prompt for which user's home directory to target (system service only).
|
|
92
|
-
// Looks for users with .claude/ (Claude Code data) since that's what gets synced.
|
|
93
|
-
function promptTargetUser() {
|
|
94
|
-
return new Promise((resolve) => {
|
|
95
|
-
const currentHome = os.homedir();
|
|
96
|
-
const users = findClaudeUsers();
|
|
97
|
-
|
|
98
|
-
// No Claude users found — fall back to current user
|
|
99
|
-
if (users.length === 0) {
|
|
100
|
-
resolve(currentHome);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
// Single Claude user — auto-select
|
|
104
|
-
if (users.length === 1) {
|
|
105
|
-
log(`Using home directory: ${users[0].homedir}`);
|
|
106
|
-
resolve(users[0].homedir);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Multiple Claude users — ask which one
|
|
111
|
-
console.log('');
|
|
112
|
-
log('Multiple users have Claude Code data:');
|
|
113
|
-
users.forEach((u, i) => log(` ${i + 1}. ${u.name} (${u.homedir})`));
|
|
114
|
-
console.log('');
|
|
115
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
116
|
-
rl.question(` Run as user [1]: `, (answer) => {
|
|
117
|
-
rl.close();
|
|
118
|
-
const idx = parseInt(answer.trim() || '1', 10) - 1;
|
|
119
|
-
const chosen = users[idx] || users[0];
|
|
120
|
-
resolve(chosen.homedir);
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// --- SSH AuthorizedKeysCommand setup (Linux/macOS) ---
|
|
127
|
-
// Configures sshd to dynamically authorize Spaces users via a lookup script,
|
|
128
|
-
// eliminating the need to manually manage ~/.ssh/authorized_keys per user.
|
|
129
|
-
function configureAuthorizedKeysCommand() {
|
|
130
|
-
if (process.platform === 'win32') return; // Windows uses a different approach
|
|
131
|
-
|
|
132
|
-
const scriptSrc = path.join(__dirname, 'ssh-auth-keys.sh');
|
|
133
|
-
const scriptDst = '/opt/spaces/bin/ssh-auth-keys.sh';
|
|
134
|
-
const sshdConfig = '/etc/ssh/sshd_config';
|
|
135
|
-
const spacesConfDrop = '/etc/ssh/sshd_config.d/spaces.conf';
|
|
136
|
-
|
|
137
|
-
// Install the script to a stable system path
|
|
138
|
-
try {
|
|
139
|
-
spawnSync('sudo', ['mkdir', '-p', '/opt/spaces/bin'], { stdio: 'pipe', timeout: 5000 });
|
|
140
|
-
spawnSync('sudo', ['cp', scriptSrc, scriptDst], { stdio: 'pipe', timeout: 5000 });
|
|
141
|
-
spawnSync('sudo', ['chmod', '755', scriptDst], { stdio: 'pipe', timeout: 5000 });
|
|
142
|
-
spawnSync('sudo', ['chown', 'root:root', scriptDst], { stdio: 'pipe', timeout: 5000 });
|
|
143
|
-
logOk('Installed ssh-auth-keys.sh to ' + scriptDst);
|
|
144
|
-
} catch (e) {
|
|
145
|
-
logErr('Failed to install ssh-auth-keys.sh: ' + e.message);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Prefer sshd_config.d drop-in if the Include directive exists
|
|
150
|
-
const sshdContent = fs.existsSync(sshdConfig)
|
|
151
|
-
? fs.readFileSync(sshdConfig, 'utf-8') : '';
|
|
152
|
-
|
|
153
|
-
const configLines = [
|
|
154
|
-
'# Spaces: dynamically authorize shell users via admin DB lookup',
|
|
155
|
-
'AuthorizedKeysCommand ' + scriptDst + ' %u',
|
|
156
|
-
'AuthorizedKeysCommandUser nobody',
|
|
157
|
-
].join('\n') + '\n';
|
|
158
|
-
|
|
159
|
-
// Already configured?
|
|
160
|
-
if (sshdContent.includes('ssh-auth-keys.sh')) {
|
|
161
|
-
logOk('AuthorizedKeysCommand already configured in sshd_config');
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Try drop-in directory first (cleaner, doesn't touch main config)
|
|
166
|
-
const hasIncludeDir = sshdContent.includes('Include /etc/ssh/sshd_config.d/');
|
|
167
|
-
if (hasIncludeDir) {
|
|
168
|
-
try {
|
|
169
|
-
spawnSync('sudo', ['mkdir', '-p', '/etc/ssh/sshd_config.d'], { stdio: 'pipe', timeout: 5000 });
|
|
170
|
-
const result = spawnSync('sudo', ['tee', spacesConfDrop], {
|
|
171
|
-
input: configLines, stdio: ['pipe', 'pipe', 'inherit'], timeout: 5000,
|
|
172
|
-
});
|
|
173
|
-
if (result.status === 0) {
|
|
174
|
-
logOk('Created ' + spacesConfDrop);
|
|
175
|
-
}
|
|
176
|
-
} catch (e) {
|
|
177
|
-
logErr('Failed to write drop-in config: ' + e.message);
|
|
178
|
-
}
|
|
179
|
-
} else {
|
|
180
|
-
// Append directly to sshd_config
|
|
181
|
-
try {
|
|
182
|
-
const appendContent = '\n# Spaces SSH authorization\n' + configLines;
|
|
183
|
-
const result = spawnSync('sudo', ['tee', '-a', sshdConfig], {
|
|
184
|
-
input: appendContent, stdio: ['pipe', 'pipe', 'inherit'], timeout: 5000,
|
|
185
|
-
});
|
|
186
|
-
if (result.status === 0) {
|
|
187
|
-
logOk('Added AuthorizedKeysCommand to ' + sshdConfig);
|
|
188
|
-
}
|
|
189
|
-
} catch (e) {
|
|
190
|
-
logErr('Failed to update sshd_config: ' + e.message);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Restart sshd to pick up the change
|
|
195
|
-
try {
|
|
196
|
-
spawnSync('sudo', ['systemctl', 'restart', 'sshd'], { stdio: 'pipe', timeout: 10000 });
|
|
197
|
-
logOk('Restarted sshd');
|
|
198
|
-
} catch {
|
|
199
|
-
try {
|
|
200
|
-
spawnSync('sudo', ['systemctl', 'restart', 'ssh'], { stdio: 'pipe', timeout: 10000 });
|
|
201
|
-
logOk('Restarted ssh');
|
|
202
|
-
} catch {
|
|
203
|
-
log('Warning: could not restart sshd — restart it manually');
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// --- SSH key provisioning (for multi-user system service) ---
|
|
209
|
-
function checkOpenSSHServer() {
|
|
210
|
-
if (process.platform !== 'win32') return true;
|
|
211
|
-
try {
|
|
212
|
-
const result = spawnSync('powershell', ['-NoProfile', '-Command',
|
|
213
|
-
'Get-Service sshd -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Status'
|
|
214
|
-
], { encoding: 'utf-8', timeout: 10000 });
|
|
215
|
-
const status = (result.stdout || '').trim();
|
|
216
|
-
return status === 'Running' || status === 'Stopped';
|
|
217
|
-
} catch { return false; }
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function ensureOpenSSHServer() {
|
|
221
|
-
if (process.platform !== 'win32') return;
|
|
222
|
-
if (checkOpenSSHServer()) {
|
|
223
|
-
try {
|
|
224
|
-
spawnSync('powershell', ['-NoProfile', '-Command',
|
|
225
|
-
'Start-Service sshd; Set-Service -Name sshd -StartupType Automatic'
|
|
226
|
-
], { stdio: 'pipe', timeout: 15000 });
|
|
227
|
-
logOk('OpenSSH Server started and set to automatic');
|
|
228
|
-
} catch {
|
|
229
|
-
log('Warning: could not start OpenSSH Server');
|
|
230
|
-
}
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
log('Installing OpenSSH Server (required for multi-user terminals)...');
|
|
234
|
-
try {
|
|
235
|
-
const result = spawnSync('powershell', ['-NoProfile', '-Command',
|
|
236
|
-
'Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0'
|
|
237
|
-
], { encoding: 'utf-8', stdio: 'pipe', timeout: 120000 });
|
|
238
|
-
if (result.status === 0) {
|
|
239
|
-
logOk('OpenSSH Server installed');
|
|
240
|
-
spawnSync('powershell', ['-NoProfile', '-Command',
|
|
241
|
-
'Start-Service sshd; Set-Service -Name sshd -StartupType Automatic'
|
|
242
|
-
], { stdio: 'pipe', timeout: 15000 });
|
|
243
|
-
logOk('OpenSSH Server started');
|
|
244
|
-
} else {
|
|
245
|
-
logErr('Failed to install OpenSSH Server: ' + (result.stderr || '').trim());
|
|
246
|
-
log('Multi-user terminals will not work. Install manually:');
|
|
247
|
-
log(' Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0');
|
|
248
|
-
}
|
|
249
|
-
} catch (e) {
|
|
250
|
-
logErr('Failed to install OpenSSH Server: ' + e.message);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function ensureServiceKey() {
|
|
255
|
-
const keyPath = path.join(SPACES_DIR, 'service_key');
|
|
256
|
-
if (fs.existsSync(keyPath)) return keyPath;
|
|
257
|
-
log('Generating SSH service key...');
|
|
258
|
-
fs.mkdirSync(SPACES_DIR, { recursive: true });
|
|
259
|
-
const result = spawnSync('ssh-keygen', [
|
|
260
|
-
'-t', 'ed25519',
|
|
261
|
-
'-f', keyPath,
|
|
262
|
-
'-N', '',
|
|
263
|
-
'-C', 'spaces-service-key',
|
|
264
|
-
], { stdio: 'pipe', timeout: 10000 });
|
|
265
|
-
if (result.status !== 0) {
|
|
266
|
-
logErr('Failed to generate SSH key');
|
|
267
|
-
return null;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// On Windows, restrict private key permissions so OpenSSH accepts it.
|
|
271
|
-
// OpenSSH requires: no inherited ACLs, only the file owner + SYSTEM may have access.
|
|
272
|
-
if (process.platform === 'win32') {
|
|
273
|
-
try {
|
|
274
|
-
const currentUser = os.userInfo().username;
|
|
275
|
-
// Remove inheritance and all default ACLs, then grant only owner + SYSTEM
|
|
276
|
-
spawnSync('icacls', [keyPath, '/inheritance:r',
|
|
277
|
-
'/remove', 'BUILTIN\\Administrators',
|
|
278
|
-
'/remove', 'BUILTIN\\Users',
|
|
279
|
-
'/remove', 'Everyone',
|
|
280
|
-
'/grant:r', currentUser + ':(F)',
|
|
281
|
-
'/grant', 'NT AUTHORITY\\SYSTEM:(F)'], { stdio: 'pipe', timeout: 5000 });
|
|
282
|
-
} catch {}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
logOk('SSH service key generated');
|
|
286
|
-
return keyPath;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function authorizeServiceKey(keyPath, targetUser) {
|
|
290
|
-
const pubKey = fs.readFileSync(keyPath + '.pub', 'utf-8').trim();
|
|
291
|
-
|
|
292
|
-
if (process.platform === 'win32') {
|
|
293
|
-
// Windows OpenSSH ignores ~/.ssh/authorized_keys for admin users.
|
|
294
|
-
// Must use C:\ProgramData\ssh\administrators_authorized_keys instead.
|
|
295
|
-
const adminAuthKeys = path.join(process.env.ProgramData || 'C:\\ProgramData', 'ssh', 'administrators_authorized_keys');
|
|
296
|
-
const userAuthKeys = path.join(path.dirname(os.homedir()), targetUser, '.ssh', 'authorized_keys');
|
|
297
|
-
|
|
298
|
-
// Check if user is an administrator (exact match, not substring)
|
|
299
|
-
let isAdmin = false;
|
|
300
|
-
try {
|
|
301
|
-
const result = spawnSync('net', ['localgroup', 'Administrators'], { encoding: 'utf-8', timeout: 5000 });
|
|
302
|
-
const lines = (result.stdout || '').split(/\r?\n/);
|
|
303
|
-
const sep = lines.findIndex(l => l.trim().startsWith('---'));
|
|
304
|
-
if (sep >= 0) {
|
|
305
|
-
const members = lines.slice(sep + 1).map(l => l.trim()).filter(Boolean);
|
|
306
|
-
isAdmin = members.some(m => m.toLowerCase() === targetUser.toLowerCase());
|
|
307
|
-
}
|
|
308
|
-
} catch {}
|
|
309
|
-
|
|
310
|
-
const authKeysPath = isAdmin ? adminAuthKeys : userAuthKeys;
|
|
311
|
-
const authDir = path.dirname(authKeysPath);
|
|
312
|
-
if (!fs.existsSync(authDir)) fs.mkdirSync(authDir, { recursive: true });
|
|
313
|
-
|
|
314
|
-
// Check if key already authorized
|
|
315
|
-
if (fs.existsSync(authKeysPath)) {
|
|
316
|
-
const existing = fs.readFileSync(authKeysPath, 'utf-8');
|
|
317
|
-
if (existing.includes(pubKey)) return;
|
|
318
|
-
}
|
|
319
|
-
fs.appendFileSync(authKeysPath, pubKey + String.fromCharCode(10));
|
|
320
|
-
|
|
321
|
-
// Fix permissions for administrators_authorized_keys
|
|
322
|
-
if (isAdmin) {
|
|
323
|
-
try {
|
|
324
|
-
spawnSync('icacls', [authKeysPath, '/inheritance:r', '/grant', 'SYSTEM:(R)', '/grant', 'Administrators:(R)'], { stdio: 'pipe', timeout: 5000 });
|
|
325
|
-
} catch {}
|
|
326
|
-
}
|
|
327
|
-
} else {
|
|
328
|
-
// Linux/macOS: use ~/.ssh/authorized_keys
|
|
329
|
-
let userHome;
|
|
330
|
-
try {
|
|
331
|
-
// Use getent to resolve the actual home directory (works with LDAP, NIS, etc.)
|
|
332
|
-
const result = spawnSync('getent', ['passwd', targetUser], { encoding: 'utf-8', timeout: 5000 });
|
|
333
|
-
const fields = (result.stdout || '').split(':');
|
|
334
|
-
userHome = fields[5] || (process.platform === 'darwin' ? `/Users/${targetUser}` : `/home/${targetUser}`);
|
|
335
|
-
} catch {
|
|
336
|
-
userHome = process.platform === 'darwin' ? `/Users/${targetUser}` : `/home/${targetUser}`;
|
|
337
|
-
}
|
|
338
|
-
const sshDir = path.join(userHome, '.ssh');
|
|
339
|
-
const authKeysPath = path.join(sshDir, 'authorized_keys');
|
|
340
|
-
if (!fs.existsSync(sshDir)) fs.mkdirSync(sshDir, { recursive: true, mode: 0o700 });
|
|
341
|
-
if (fs.existsSync(authKeysPath)) {
|
|
342
|
-
const existing = fs.readFileSync(authKeysPath, 'utf-8');
|
|
343
|
-
if (existing.includes(pubKey)) return;
|
|
344
|
-
}
|
|
345
|
-
fs.appendFileSync(authKeysPath, pubKey + String.fromCharCode(10));
|
|
346
|
-
}
|
|
347
|
-
logOk('SSH key authorized for ' + targetUser);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const LEVEL_PATH = path.join(SPACES_DIR, 'service-level');
|
|
351
|
-
|
|
352
|
-
function saveLevel(level) {
|
|
353
|
-
fs.mkdirSync(SPACES_DIR, { recursive: true });
|
|
354
|
-
fs.writeFileSync(LEVEL_PATH, level);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function loadLevel() {
|
|
358
|
-
try { return fs.readFileSync(LEVEL_PATH, 'utf-8').trim(); } catch { return null; }
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// ─── Platform detection ───────────────────────────────────────
|
|
362
|
-
function getPlatform() {
|
|
363
|
-
switch (process.platform) {
|
|
364
|
-
case 'linux': return 'linux';
|
|
365
|
-
case 'darwin': return 'darwin';
|
|
366
|
-
case 'win32': return 'win32';
|
|
367
|
-
default:
|
|
368
|
-
logErr(`Unsupported platform: ${process.platform}`);
|
|
369
|
-
process.exit(1);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// ─── Linux (systemd) ─────────────────────────────────────────
|
|
374
|
-
function linuxServicePath(level) {
|
|
375
|
-
if (level === 'system') {
|
|
376
|
-
return `/etc/systemd/system/${SERVICE_NAME}.service`;
|
|
377
|
-
}
|
|
378
|
-
const userDir = path.join(os.homedir(), '.config', 'systemd', 'user');
|
|
379
|
-
fs.mkdirSync(userDir, { recursive: true });
|
|
380
|
-
return path.join(userDir, `${SERVICE_NAME}.service`);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function linuxUnitFile(level) {
|
|
384
|
-
const config = resolveConfig();
|
|
385
|
-
const nodePath = resolveNodePath();
|
|
386
|
-
const spacesPath = resolveSpacesPath();
|
|
387
|
-
const projectDir = resolveProjectDir();
|
|
388
|
-
|
|
389
|
-
let envLines = [
|
|
390
|
-
`Environment=SPACES_SERVICE=1`,
|
|
391
|
-
`Environment=SPACES_PORT=${config.port}`,
|
|
392
|
-
];
|
|
393
|
-
// Don't hardcode SPACES_TIER — let spaces.js auto-detect from installed packages
|
|
394
|
-
if (config.tier && config.tier !== 'community') {
|
|
395
|
-
envLines.push(`Environment=SPACES_TIER=${config.tier}`);
|
|
396
|
-
}
|
|
397
|
-
if (config.basePath) {
|
|
398
|
-
envLines.push(`Environment=SPACES_BASE_PATH=${config.basePath}`);
|
|
399
|
-
}
|
|
400
|
-
if (config.allowedOrigins) {
|
|
401
|
-
envLines.push(`Environment=SPACES_ALLOWED_ORIGINS=${config.allowedOrigins}`);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Allow users to set API keys (OPENAI_API_KEY, etc.) in ~/.spaces/env
|
|
405
|
-
const envFilePath = path.join(SPACES_DIR, 'env');
|
|
406
|
-
|
|
407
|
-
let serviceSection = [
|
|
408
|
-
'Type=simple',
|
|
409
|
-
`ExecStart=${nodePath} ${spacesPath}`,
|
|
410
|
-
`WorkingDirectory=${projectDir}`,
|
|
411
|
-
`EnvironmentFile=-${envFilePath}`,
|
|
412
|
-
...envLines,
|
|
413
|
-
'Restart=on-failure',
|
|
414
|
-
'RestartSec=5',
|
|
415
|
-
];
|
|
416
|
-
|
|
417
|
-
if (level === 'system') {
|
|
418
|
-
const username = os.userInfo().username;
|
|
419
|
-
serviceSection.push(`User=${username}`);
|
|
420
|
-
serviceSection.push(`Group=${username}`);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const wantedBy = level === 'system' ? 'multi-user.target' : 'default.target';
|
|
424
|
-
|
|
425
|
-
return [
|
|
426
|
-
'[Unit]',
|
|
427
|
-
'Description=Spaces - Agent Workspace Manager',
|
|
428
|
-
'After=network.target',
|
|
429
|
-
'',
|
|
430
|
-
'[Service]',
|
|
431
|
-
...serviceSection,
|
|
432
|
-
'',
|
|
433
|
-
'[Install]',
|
|
434
|
-
`WantedBy=${wantedBy}`,
|
|
435
|
-
'',
|
|
436
|
-
].join('\n');
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function linuxSystemctl(level, ...args) {
|
|
440
|
-
if (level === 'system') {
|
|
441
|
-
execFileSync('sudo', ['systemctl', ...args], { stdio: 'inherit' });
|
|
442
|
-
} else {
|
|
443
|
-
// User-level systemctl needs XDG_RUNTIME_DIR to connect to the user bus.
|
|
444
|
-
// Always set it from the real UID — inherited env may have a stale value
|
|
445
|
-
// (e.g. from a prior session with a different UID mapping).
|
|
446
|
-
const uid = process.getuid();
|
|
447
|
-
const env = {
|
|
448
|
-
...process.env,
|
|
449
|
-
XDG_RUNTIME_DIR: `/run/user/${uid}`,
|
|
450
|
-
DBUS_SESSION_BUS_ADDRESS: `unix:path=/run/user/${uid}/bus`,
|
|
451
|
-
};
|
|
452
|
-
execFileSync('systemctl', ['--user', ...args], { stdio: 'inherit', env });
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
async function linuxInstall() {
|
|
457
|
-
const level = await promptLevel();
|
|
458
|
-
const unitContent = linuxUnitFile(level);
|
|
459
|
-
const servicePath = linuxServicePath(level);
|
|
460
|
-
|
|
461
|
-
log(`Writing unit file to ${servicePath}`);
|
|
462
|
-
if (level === 'system') {
|
|
463
|
-
const result = spawnSync('sudo', ['tee', servicePath], {
|
|
464
|
-
input: unitContent,
|
|
465
|
-
stdio: ['pipe', 'pipe', 'inherit'],
|
|
466
|
-
});
|
|
467
|
-
if (result.status !== 0) {
|
|
468
|
-
logErr('Failed to write unit file');
|
|
469
|
-
process.exit(1);
|
|
470
|
-
}
|
|
471
|
-
} else {
|
|
472
|
-
fs.writeFileSync(servicePath, unitContent);
|
|
473
|
-
}
|
|
474
|
-
logOk('Unit file written');
|
|
475
|
-
|
|
476
|
-
saveLevel(level);
|
|
477
|
-
|
|
478
|
-
linuxSystemctl(level, 'daemon-reload');
|
|
479
|
-
logOk('Reloaded systemd daemon');
|
|
480
|
-
|
|
481
|
-
linuxSystemctl(level, 'enable', `${SERVICE_NAME}.service`);
|
|
482
|
-
logOk('Service enabled');
|
|
483
|
-
|
|
484
|
-
linuxSystemctl(level, 'start', `${SERVICE_NAME}.service`);
|
|
485
|
-
logOk('Service started');
|
|
486
|
-
|
|
487
|
-
if (level === 'user') {
|
|
488
|
-
try {
|
|
489
|
-
const uid = process.getuid();
|
|
490
|
-
const env = {
|
|
491
|
-
...process.env,
|
|
492
|
-
XDG_RUNTIME_DIR: `/run/user/${uid}`,
|
|
493
|
-
DBUS_SESSION_BUS_ADDRESS: `unix:path=/run/user/${uid}/bus`,
|
|
494
|
-
};
|
|
495
|
-
execFileSync('loginctl', ['enable-linger', os.userInfo().username], { stdio: 'inherit', env });
|
|
496
|
-
logOk('Enabled login lingering for user service');
|
|
497
|
-
} catch {
|
|
498
|
-
log('Warning: could not enable-linger (user service may not start on boot)');
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Set up SSH AuthorizedKeysCommand for seamless multi-user terminal access
|
|
503
|
-
if (level === 'system') {
|
|
504
|
-
configureAuthorizedKeysCommand();
|
|
505
|
-
const keyPath = ensureServiceKey();
|
|
506
|
-
if (keyPath) {
|
|
507
|
-
authorizeServiceKey(keyPath, os.userInfo().username);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
logOk(`Spaces installed as ${level} service`);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
async function linuxUninstall() {
|
|
515
|
-
const level = loadLevel() || 'user';
|
|
516
|
-
const servicePath = linuxServicePath(level);
|
|
517
|
-
|
|
518
|
-
try {
|
|
519
|
-
linuxSystemctl(level, 'stop', `${SERVICE_NAME}.service`);
|
|
520
|
-
logOk('Service stopped');
|
|
521
|
-
} catch {
|
|
522
|
-
log('Service was not running');
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
try {
|
|
526
|
-
linuxSystemctl(level, 'disable', `${SERVICE_NAME}.service`);
|
|
527
|
-
logOk('Service disabled');
|
|
528
|
-
} catch {
|
|
529
|
-
log('Service was not enabled');
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
try {
|
|
533
|
-
if (level === 'system') {
|
|
534
|
-
execFileSync('sudo', ['rm', '-f', servicePath], { stdio: 'inherit' });
|
|
535
|
-
} else {
|
|
536
|
-
fs.unlinkSync(servicePath);
|
|
537
|
-
}
|
|
538
|
-
logOk('Unit file removed');
|
|
539
|
-
} catch {
|
|
540
|
-
log('Unit file was already removed');
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
try {
|
|
544
|
-
linuxSystemctl(level, 'daemon-reload');
|
|
545
|
-
logOk('Reloaded systemd daemon');
|
|
546
|
-
} catch {
|
|
547
|
-
log('Warning: daemon-reload failed');
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
try {
|
|
551
|
-
fs.unlinkSync(LEVEL_PATH);
|
|
552
|
-
} catch {}
|
|
553
|
-
|
|
554
|
-
logOk('Spaces service uninstalled');
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
async function linuxStart() {
|
|
558
|
-
const level = loadLevel() || 'user';
|
|
559
|
-
linuxSystemctl(level, 'start', `${SERVICE_NAME}.service`);
|
|
560
|
-
logOk('Service started');
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
async function linuxStop() {
|
|
564
|
-
const level = loadLevel() || 'user';
|
|
565
|
-
linuxSystemctl(level, 'stop', `${SERVICE_NAME}.service`);
|
|
566
|
-
logOk('Service stopped');
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
async function linuxStatus() {
|
|
570
|
-
const level = loadLevel() || 'user';
|
|
571
|
-
try {
|
|
572
|
-
linuxSystemctl(level, 'status', `${SERVICE_NAME}.service`);
|
|
573
|
-
} catch {
|
|
574
|
-
// systemctl status returns non-zero for stopped/failed services
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
async function linuxLogs() {
|
|
579
|
-
const level = loadLevel() || 'user';
|
|
580
|
-
if (level === 'user') {
|
|
581
|
-
const uid = process.getuid();
|
|
582
|
-
const env = {
|
|
583
|
-
...process.env,
|
|
584
|
-
XDG_RUNTIME_DIR: `/run/user/${uid}`,
|
|
585
|
-
DBUS_SESSION_BUS_ADDRESS: `unix:path=/run/user/${uid}/bus`,
|
|
586
|
-
};
|
|
587
|
-
spawnSync('journalctl', ['--user', '-u', SERVICE_NAME, '-f', '--no-pager'], { stdio: 'inherit', env });
|
|
588
|
-
} else {
|
|
589
|
-
spawnSync('sudo', ['journalctl', '-u', SERVICE_NAME, '-f', '--no-pager'], { stdio: 'inherit' });
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// ─── macOS (launchd) ─────────────────────────────────────────
|
|
594
|
-
function darwinPlistPath(level) {
|
|
595
|
-
if (level === 'system') {
|
|
596
|
-
return `/Library/LaunchDaemons/${LABEL}.plist`;
|
|
597
|
-
}
|
|
598
|
-
return path.join(os.homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function darwinPlistContent(level) {
|
|
602
|
-
ensureLogsDir();
|
|
603
|
-
const config = resolveConfig();
|
|
604
|
-
const nodePath = resolveNodePath();
|
|
605
|
-
const spacesPath = resolveSpacesPath();
|
|
606
|
-
const projectDir = resolveProjectDir();
|
|
607
|
-
const outLog = path.join(LOGS_DIR, 'spaces.out.log');
|
|
608
|
-
const errLog = path.join(LOGS_DIR, 'spaces.err.log');
|
|
609
|
-
|
|
610
|
-
let envEntries = [
|
|
611
|
-
` <key>SPACES_SERVICE</key>`,
|
|
612
|
-
` <string>1</string>`,
|
|
613
|
-
` <key>SPACES_PORT</key>`,
|
|
614
|
-
` <string>${config.port}</string>`,
|
|
615
|
-
];
|
|
616
|
-
// Don't hardcode SPACES_TIER — let spaces.js auto-detect from installed packages
|
|
617
|
-
if (config.tier && config.tier !== 'community') {
|
|
618
|
-
envEntries.push(` <key>SPACES_TIER</key>`);
|
|
619
|
-
envEntries.push(` <string>${config.tier}</string>`);
|
|
620
|
-
}
|
|
621
|
-
if (config.basePath) {
|
|
622
|
-
envEntries.push(` <key>SPACES_BASE_PATH</key>`);
|
|
623
|
-
envEntries.push(` <string>${config.basePath}</string>`);
|
|
624
|
-
}
|
|
625
|
-
if (config.allowedOrigins) {
|
|
626
|
-
envEntries.push(` <key>SPACES_ALLOWED_ORIGINS</key>`);
|
|
627
|
-
envEntries.push(` <string>${config.allowedOrigins}</string>`);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
let extraKeys = '';
|
|
631
|
-
if (level === 'system') {
|
|
632
|
-
extraKeys = ` <key>UserName</key>\n <string>${os.userInfo().username}</string>\n`;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
return [
|
|
636
|
-
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
637
|
-
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
638
|
-
'<plist version="1.0">',
|
|
639
|
-
'<dict>',
|
|
640
|
-
` <key>Label</key>`,
|
|
641
|
-
` <string>${LABEL}</string>`,
|
|
642
|
-
` <key>ProgramArguments</key>`,
|
|
643
|
-
` <array>`,
|
|
644
|
-
` <string>${nodePath}</string>`,
|
|
645
|
-
` <string>${spacesPath}</string>`,
|
|
646
|
-
` </array>`,
|
|
647
|
-
` <key>WorkingDirectory</key>`,
|
|
648
|
-
` <string>${projectDir}</string>`,
|
|
649
|
-
` <key>EnvironmentVariables</key>`,
|
|
650
|
-
` <dict>`,
|
|
651
|
-
...envEntries,
|
|
652
|
-
` </dict>`,
|
|
653
|
-
` <key>RunAtLoad</key>`,
|
|
654
|
-
` <true/>`,
|
|
655
|
-
` <key>KeepAlive</key>`,
|
|
656
|
-
` <true/>`,
|
|
657
|
-
` <key>StandardOutPath</key>`,
|
|
658
|
-
` <string>${outLog}</string>`,
|
|
659
|
-
` <key>StandardErrorPath</key>`,
|
|
660
|
-
` <string>${errLog}</string>`,
|
|
661
|
-
extraKeys ? extraKeys.trimEnd() : null,
|
|
662
|
-
'</dict>',
|
|
663
|
-
'</plist>',
|
|
664
|
-
'',
|
|
665
|
-
].filter((line) => line !== null).join('\n');
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
async function darwinInstall() {
|
|
669
|
-
const level = await promptLevel();
|
|
670
|
-
const plistPath = darwinPlistPath(level);
|
|
671
|
-
const plistContent = darwinPlistContent(level);
|
|
672
|
-
|
|
673
|
-
// Unload existing (ignore errors)
|
|
674
|
-
try {
|
|
675
|
-
execFileSync('launchctl', ['unload', '-w', plistPath], { stdio: 'pipe' });
|
|
676
|
-
} catch {}
|
|
677
|
-
|
|
678
|
-
log(`Writing plist to ${plistPath}`);
|
|
679
|
-
if (level === 'system') {
|
|
680
|
-
const result = spawnSync('sudo', ['tee', plistPath], {
|
|
681
|
-
input: plistContent,
|
|
682
|
-
stdio: ['pipe', 'pipe', 'inherit'],
|
|
683
|
-
});
|
|
684
|
-
if (result.status !== 0) {
|
|
685
|
-
logErr('Failed to write plist file');
|
|
686
|
-
process.exit(1);
|
|
687
|
-
}
|
|
688
|
-
execFileSync('sudo', ['chown', 'root:wheel', plistPath], { stdio: 'inherit' });
|
|
689
|
-
execFileSync('sudo', ['chmod', '644', plistPath], { stdio: 'inherit' });
|
|
690
|
-
} else {
|
|
691
|
-
const agentsDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
692
|
-
fs.mkdirSync(agentsDir, { recursive: true });
|
|
693
|
-
fs.writeFileSync(plistPath, plistContent);
|
|
694
|
-
}
|
|
695
|
-
logOk('Plist file written');
|
|
696
|
-
|
|
697
|
-
saveLevel(level);
|
|
698
|
-
|
|
699
|
-
if (level === 'system') {
|
|
700
|
-
execFileSync('sudo', ['launchctl', 'load', '-w', plistPath], { stdio: 'inherit' });
|
|
701
|
-
} else {
|
|
702
|
-
execFileSync('launchctl', ['load', '-w', plistPath], { stdio: 'inherit' });
|
|
703
|
-
}
|
|
704
|
-
logOk('Service loaded');
|
|
705
|
-
|
|
706
|
-
// Set up SSH AuthorizedKeysCommand for seamless multi-user terminal access
|
|
707
|
-
if (level === 'system') {
|
|
708
|
-
configureAuthorizedKeysCommand();
|
|
709
|
-
const keyPath = ensureServiceKey();
|
|
710
|
-
if (keyPath) {
|
|
711
|
-
authorizeServiceKey(keyPath, os.userInfo().username);
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
logOk(`Spaces installed as ${level} service`);
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
async function darwinUninstall() {
|
|
719
|
-
const level = loadLevel() || 'user';
|
|
720
|
-
const plistPath = darwinPlistPath(level);
|
|
721
|
-
|
|
722
|
-
try {
|
|
723
|
-
if (level === 'system') {
|
|
724
|
-
execFileSync('sudo', ['launchctl', 'unload', '-w', plistPath], { stdio: 'inherit' });
|
|
725
|
-
} else {
|
|
726
|
-
execFileSync('launchctl', ['unload', '-w', plistPath], { stdio: 'inherit' });
|
|
727
|
-
}
|
|
728
|
-
logOk('Service unloaded');
|
|
729
|
-
} catch {
|
|
730
|
-
log('Service was not loaded');
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
try {
|
|
734
|
-
if (level === 'system') {
|
|
735
|
-
execFileSync('sudo', ['rm', '-f', plistPath], { stdio: 'inherit' });
|
|
736
|
-
} else {
|
|
737
|
-
fs.unlinkSync(plistPath);
|
|
738
|
-
}
|
|
739
|
-
logOk('Plist file removed');
|
|
740
|
-
} catch {
|
|
741
|
-
log('Plist file was already removed');
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
try {
|
|
745
|
-
fs.unlinkSync(LEVEL_PATH);
|
|
746
|
-
} catch {}
|
|
747
|
-
|
|
748
|
-
logOk('Spaces service uninstalled');
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
async function darwinStart() {
|
|
752
|
-
const level = loadLevel() || 'user';
|
|
753
|
-
const plistPath = darwinPlistPath(level);
|
|
754
|
-
if (level === 'system') {
|
|
755
|
-
execFileSync('sudo', ['launchctl', 'load', '-w', plistPath], { stdio: 'inherit' });
|
|
756
|
-
} else {
|
|
757
|
-
execFileSync('launchctl', ['load', '-w', plistPath], { stdio: 'inherit' });
|
|
758
|
-
}
|
|
759
|
-
logOk('Service started');
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
async function darwinStop() {
|
|
763
|
-
const level = loadLevel() || 'user';
|
|
764
|
-
const plistPath = darwinPlistPath(level);
|
|
765
|
-
if (level === 'system') {
|
|
766
|
-
execFileSync('sudo', ['launchctl', 'unload', '-w', plistPath], { stdio: 'inherit' });
|
|
767
|
-
} else {
|
|
768
|
-
execFileSync('launchctl', ['unload', '-w', plistPath], { stdio: 'inherit' });
|
|
769
|
-
}
|
|
770
|
-
logOk('Service stopped');
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
async function darwinStatus() {
|
|
774
|
-
try {
|
|
775
|
-
const result = execFileSync('launchctl', ['list'], { encoding: 'utf-8' });
|
|
776
|
-
const lines = result.split('\n').filter((line) => line.includes(LABEL));
|
|
777
|
-
if (lines.length > 0) {
|
|
778
|
-
log('Spaces service status:');
|
|
779
|
-
lines.forEach((line) => log(line));
|
|
780
|
-
} else {
|
|
781
|
-
log('Spaces service is not loaded');
|
|
782
|
-
}
|
|
783
|
-
} catch {
|
|
784
|
-
log('Spaces service is not loaded');
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
async function darwinLogs() {
|
|
789
|
-
ensureLogsDir();
|
|
790
|
-
const outLogPath = path.join(LOGS_DIR, 'spaces.out.log');
|
|
791
|
-
if (!fs.existsSync(outLogPath)) {
|
|
792
|
-
logErr(`Log file not found: ${outLogPath}`);
|
|
793
|
-
log('Service may not have started yet');
|
|
794
|
-
process.exit(1);
|
|
795
|
-
}
|
|
796
|
-
spawnSync('tail', ['-f', outLogPath], { stdio: 'inherit' });
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
// ─── Windows (Task Scheduler) ────────────────────────────────
|
|
800
|
-
function win32WrapperScript(level) {
|
|
801
|
-
ensureLogsDir();
|
|
802
|
-
const config = resolveConfig();
|
|
803
|
-
const nodePath = resolveNodePath();
|
|
804
|
-
const spacesPath = resolveSpacesPath();
|
|
805
|
-
const outLog = path.join(LOGS_DIR, 'spaces.out.log');
|
|
806
|
-
const wrapperPath = path.join(SPACES_DIR, 'spaces-service.cmd');
|
|
807
|
-
|
|
808
|
-
const lines = [
|
|
809
|
-
'@echo off',
|
|
810
|
-
];
|
|
811
|
-
// When running as SYSTEM, override USERPROFILE so os.homedir() resolves
|
|
812
|
-
// to the target user's home directory (where .spaces/ config lives).
|
|
813
|
-
if (level === 'system') {
|
|
814
|
-
const homedir = path.dirname(SPACES_DIR); // use resolved target, not os.homedir()
|
|
815
|
-
const drive = path.parse(homedir).root.slice(0, -1);
|
|
816
|
-
const rest = homedir.slice(drive.length);
|
|
817
|
-
lines.push(`set USERPROFILE=${homedir}`);
|
|
818
|
-
lines.push(`set HOMEDRIVE=${drive}`);
|
|
819
|
-
lines.push(`set HOMEPATH=${rest}`);
|
|
820
|
-
}
|
|
821
|
-
lines.push('set SPACES_SERVICE=1');
|
|
822
|
-
lines.push(`set SPACES_PORT=${config.port}`);
|
|
823
|
-
// Don't hardcode SPACES_TIER — let spaces.js auto-detect from installed packages
|
|
824
|
-
if (config.tier && config.tier !== 'community') {
|
|
825
|
-
lines.push(`set SPACES_TIER=${config.tier}`);
|
|
826
|
-
}
|
|
827
|
-
if (config.basePath) {
|
|
828
|
-
lines.push(`set SPACES_BASE_PATH=${config.basePath}`);
|
|
829
|
-
}
|
|
830
|
-
if (config.allowedOrigins) {
|
|
831
|
-
lines.push(`set SPACES_ALLOWED_ORIGINS=${config.allowedOrigins}`);
|
|
832
|
-
}
|
|
833
|
-
lines.push(`"${nodePath}" "${spacesPath}" >> "${outLog}" 2>&1`);
|
|
834
|
-
lines.push('');
|
|
835
|
-
|
|
836
|
-
fs.writeFileSync(wrapperPath, lines.join('\r\n'));
|
|
837
|
-
return wrapperPath;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
async function win32Install() {
|
|
841
|
-
const level = await promptLevel();
|
|
842
|
-
|
|
843
|
-
// For system service, determine the target user's home directory
|
|
844
|
-
// (may differ from the admin account running the installer)
|
|
845
|
-
if (level === 'system' && process.platform === 'win32') {
|
|
846
|
-
const targetHome = await promptTargetUser();
|
|
847
|
-
setTargetHome(targetHome);
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
const wrapperPath = win32WrapperScript(level);
|
|
851
|
-
|
|
852
|
-
log(`Wrapper script written to ${wrapperPath}`);
|
|
853
|
-
|
|
854
|
-
// Delete existing task (ignore errors)
|
|
855
|
-
try {
|
|
856
|
-
execFileSync('schtasks', ['/Delete', '/TN', TASK_NAME, '/F'], { stdio: 'pipe' });
|
|
857
|
-
} catch {}
|
|
858
|
-
|
|
859
|
-
if (level === 'system') {
|
|
860
|
-
execFileSync('schtasks', ['/Create', '/TN', TASK_NAME, '/TR', `"${wrapperPath}"`, '/SC', 'ONSTART', '/RU', 'SYSTEM', '/F'], { stdio: 'inherit' });
|
|
861
|
-
} else {
|
|
862
|
-
execFileSync('schtasks', ['/Create', '/TN', TASK_NAME, '/TR', `"${wrapperPath}"`, '/SC', 'ONLOGON', '/RL', 'HIGHEST', '/F'], { stdio: 'inherit' });
|
|
863
|
-
}
|
|
864
|
-
logOk('Scheduled task created');
|
|
865
|
-
|
|
866
|
-
saveLevel(level);
|
|
867
|
-
|
|
868
|
-
// Set up SSH for multi-user support (system service only)
|
|
869
|
-
// Only ensure OpenSSH is available — key generation is handled at runtime
|
|
870
|
-
// by ensureServiceKeyAtRuntime() in terminal-server.js, which runs as SYSTEM
|
|
871
|
-
// so the key is owned by SYSTEM and OpenSSH accepts it.
|
|
872
|
-
if (level === 'system') {
|
|
873
|
-
ensureOpenSSHServer();
|
|
874
|
-
log('SSH service key will be generated on first run (as SYSTEM)');
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
execFileSync('schtasks', ['/Run', '/TN', TASK_NAME], { stdio: 'inherit' });
|
|
878
|
-
logOk('Task started');
|
|
879
|
-
|
|
880
|
-
logOk(`Spaces installed as ${level} service`);
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
async function win32Uninstall() {
|
|
884
|
-
try {
|
|
885
|
-
execFileSync('schtasks', ['/End', '/TN', TASK_NAME], { stdio: 'pipe' });
|
|
886
|
-
logOk('Task ended');
|
|
887
|
-
} catch {
|
|
888
|
-
log('Task was not running');
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
try {
|
|
892
|
-
execFileSync('schtasks', ['/Delete', '/TN', TASK_NAME, '/F'], { stdio: 'pipe' });
|
|
893
|
-
logOk('Scheduled task removed');
|
|
894
|
-
} catch {
|
|
895
|
-
log('Scheduled task was already removed');
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
try {
|
|
899
|
-
const wrapperPath = path.join(SPACES_DIR, 'spaces-service.cmd');
|
|
900
|
-
fs.unlinkSync(wrapperPath);
|
|
901
|
-
logOk('Wrapper script removed');
|
|
902
|
-
} catch {
|
|
903
|
-
log('Wrapper script was already removed');
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
try {
|
|
907
|
-
fs.unlinkSync(LEVEL_PATH);
|
|
908
|
-
} catch {}
|
|
909
|
-
|
|
910
|
-
logOk('Spaces service uninstalled');
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
async function win32Start() {
|
|
914
|
-
// Try schtasks /Run first (works when running as admin or for user-level tasks)
|
|
915
|
-
try {
|
|
916
|
-
execFileSync('schtasks', ['/Run', '/TN', TASK_NAME], { stdio: 'pipe' });
|
|
917
|
-
logOk('Task started');
|
|
918
|
-
return;
|
|
919
|
-
} catch {}
|
|
920
|
-
|
|
921
|
-
// Fallback: launch the wrapper script directly as a detached process.
|
|
922
|
-
// This works for non-admin shells when the task is registered as SYSTEM.
|
|
923
|
-
// The service will run as the current user, not SYSTEM, but functionally
|
|
924
|
-
// equivalent for local development.
|
|
925
|
-
const wrapperPath = path.join(SPACES_DIR, 'spaces-service.cmd');
|
|
926
|
-
if (!fs.existsSync(wrapperPath)) {
|
|
927
|
-
logErr('Wrapper script not found — run "spaces service install" first');
|
|
928
|
-
process.exit(1);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
const { spawn } = require('child_process');
|
|
932
|
-
const child = spawn('cmd.exe', ['/c', wrapperPath], {
|
|
933
|
-
detached: true,
|
|
934
|
-
stdio: 'ignore',
|
|
935
|
-
windowsHide: true,
|
|
936
|
-
});
|
|
937
|
-
child.unref();
|
|
938
|
-
logOk('Service started (direct launch)');
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
async function win32Stop() {
|
|
942
|
-
try {
|
|
943
|
-
execFileSync('schtasks', ['/End', '/TN', TASK_NAME], { stdio: 'pipe' });
|
|
944
|
-
} catch {}
|
|
945
|
-
|
|
946
|
-
// Kill the actual node processes on our ports (verify it's node before killing)
|
|
947
|
-
const config = resolveConfig();
|
|
948
|
-
const ports = [config.port || 3457, 3400];
|
|
949
|
-
let killed = 0;
|
|
950
|
-
for (const port of ports) {
|
|
951
|
-
try {
|
|
952
|
-
const output = execFileSync('netstat', ['-ano'], { encoding: 'utf-8' });
|
|
953
|
-
for (const line of output.split(String.fromCharCode(10))) {
|
|
954
|
-
if (line.includes(':' + port + ' ') && line.includes('LISTENING')) {
|
|
955
|
-
const parts = line.trim().split(/\s+/);
|
|
956
|
-
const pid = parseInt(parts[parts.length - 1], 10);
|
|
957
|
-
if (pid > 0) {
|
|
958
|
-
// Verify the process is node.exe before killing to avoid terminating unrelated processes
|
|
959
|
-
try {
|
|
960
|
-
const taskInfo = execFileSync('tasklist', ['/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH'], { encoding: 'utf-8' });
|
|
961
|
-
if (!taskInfo.toLowerCase().includes('node.exe')) continue;
|
|
962
|
-
process.kill(pid, 'SIGTERM'); killed++;
|
|
963
|
-
} catch {}
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
} catch {}
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
if (killed > 0) {
|
|
971
|
-
logOk('Stopped ' + killed + ' process(es)');
|
|
972
|
-
} else {
|
|
973
|
-
logOk('Task stopped');
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
async function win32Status() {
|
|
978
|
-
try {
|
|
979
|
-
execFileSync('schtasks', ['/Query', '/TN', TASK_NAME, '/V', '/FO', 'LIST'], { stdio: 'inherit' });
|
|
980
|
-
} catch {
|
|
981
|
-
log('Spaces service is not installed');
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
async function win32Logs() {
|
|
986
|
-
ensureLogsDir();
|
|
987
|
-
const outLogPath = path.join(LOGS_DIR, 'spaces.out.log');
|
|
988
|
-
if (!fs.existsSync(outLogPath)) {
|
|
989
|
-
logErr(`Log file not found: ${outLogPath}`);
|
|
990
|
-
log('Service may not have started yet');
|
|
991
|
-
process.exit(1);
|
|
992
|
-
}
|
|
993
|
-
spawnSync('powershell', ['-Command', `Get-Content "${outLogPath}" -Wait -Tail 50`], { stdio: 'inherit' });
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
// ─── Dispatch table ──────────────────────────────────────────
|
|
997
|
-
const platforms = {
|
|
998
|
-
linux: { install: linuxInstall, uninstall: linuxUninstall, start: linuxStart, stop: linuxStop, status: linuxStatus, logs: linuxLogs },
|
|
999
|
-
darwin: { install: darwinInstall, uninstall: darwinUninstall, start: darwinStart, stop: darwinStop, status: darwinStatus, logs: darwinLogs },
|
|
1000
|
-
win32: { install: win32Install, uninstall: win32Uninstall, start: win32Start, stop: win32Stop, status: win32Status, logs: win32Logs },
|
|
1001
|
-
};
|
|
1002
|
-
|
|
1003
|
-
// ─── CLI ──────────────────────────────────────────────────────
|
|
1004
|
-
async function main() {
|
|
1005
|
-
const action = process.argv[2];
|
|
1006
|
-
const platform = getPlatform();
|
|
1007
|
-
const dispatch = platforms[platform];
|
|
1008
|
-
|
|
1009
|
-
if (!action || !dispatch[action]) {
|
|
1010
|
-
log('Usage: spaces service <install|uninstall|start|stop|status|logs>');
|
|
1011
|
-
process.exit(action ? 1 : 0);
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
await dispatch[action]();
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
main().catch((err) => {
|
|
1018
|
-
logErr(err.message);
|
|
1019
|
-
process.exit(1);
|
|
1020
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const readline = require('readline');
|
|
10
|
+
|
|
11
|
+
let SPACES_DIR = path.join(os.homedir(), '.spaces');
|
|
12
|
+
let CONFIG_PATH = path.join(SPACES_DIR, 'server.json');
|
|
13
|
+
let LOGS_DIR = path.join(SPACES_DIR, 'logs');
|
|
14
|
+
const SERVICE_NAME = 'spaces';
|
|
15
|
+
const LABEL = 'com.agentspaces.spaces';
|
|
16
|
+
const TASK_NAME = 'Spaces';
|
|
17
|
+
|
|
18
|
+
// Override SPACES_DIR to point to a different user's home
|
|
19
|
+
function setTargetHome(homedir) {
|
|
20
|
+
SPACES_DIR = path.join(homedir, '.spaces');
|
|
21
|
+
CONFIG_PATH = path.join(SPACES_DIR, 'server.json');
|
|
22
|
+
LOGS_DIR = path.join(SPACES_DIR, 'logs');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ─── Helpers ──────────────────────────────────────────────────
|
|
26
|
+
function log(msg) { console.log(` ${msg}`); }
|
|
27
|
+
function logOk(msg) { console.log(` ✓ ${msg}`); }
|
|
28
|
+
function logErr(msg) { console.error(` ✗ ${msg}`); }
|
|
29
|
+
|
|
30
|
+
function resolveConfig() {
|
|
31
|
+
let config = {};
|
|
32
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
33
|
+
try { config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch {}
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
port: config.port || 3457,
|
|
37
|
+
tier: config.tier || 'community',
|
|
38
|
+
basePath: config.basePath || '',
|
|
39
|
+
allowedOrigins: config.allowedOrigins || '',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveSpacesPath() {
|
|
44
|
+
return path.join(__dirname, 'spaces.js');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveProjectDir() {
|
|
48
|
+
return path.join(__dirname, '..');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveNodePath() {
|
|
52
|
+
return process.execPath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function promptLevel() {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
58
|
+
console.log('');
|
|
59
|
+
log('Install as:');
|
|
60
|
+
log(' 1. System service (starts on boot, all users)');
|
|
61
|
+
log(' 2. User service (starts on login, current user only)');
|
|
62
|
+
console.log('');
|
|
63
|
+
rl.question(' Choice [2]: ', (answer) => {
|
|
64
|
+
rl.close();
|
|
65
|
+
const choice = answer.trim() || '2';
|
|
66
|
+
resolve(choice === '1' ? 'system' : 'user');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ensureLogsDir() {
|
|
72
|
+
fs.mkdirSync(LOGS_DIR, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Detect Windows user profiles that have .claude/ (Claude Code data)
|
|
76
|
+
function findClaudeUsers() {
|
|
77
|
+
if (process.platform !== 'win32') return [];
|
|
78
|
+
const usersDir = path.dirname(os.homedir());
|
|
79
|
+
const skip = new Set(['Public', 'Default', 'Default User', 'All Users']);
|
|
80
|
+
try {
|
|
81
|
+
return fs.readdirSync(usersDir)
|
|
82
|
+
.filter(name => !skip.has(name) && !name.startsWith('.'))
|
|
83
|
+
.filter(name => {
|
|
84
|
+
const claudeDir = path.join(usersDir, name, '.claude');
|
|
85
|
+
return fs.existsSync(claudeDir);
|
|
86
|
+
})
|
|
87
|
+
.map(name => ({ name, homedir: path.join(usersDir, name) }));
|
|
88
|
+
} catch { return []; }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Prompt for which user's home directory to target (system service only).
|
|
92
|
+
// Looks for users with .claude/ (Claude Code data) since that's what gets synced.
|
|
93
|
+
function promptTargetUser() {
|
|
94
|
+
return new Promise((resolve) => {
|
|
95
|
+
const currentHome = os.homedir();
|
|
96
|
+
const users = findClaudeUsers();
|
|
97
|
+
|
|
98
|
+
// No Claude users found — fall back to current user
|
|
99
|
+
if (users.length === 0) {
|
|
100
|
+
resolve(currentHome);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Single Claude user — auto-select
|
|
104
|
+
if (users.length === 1) {
|
|
105
|
+
log(`Using home directory: ${users[0].homedir}`);
|
|
106
|
+
resolve(users[0].homedir);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Multiple Claude users — ask which one
|
|
111
|
+
console.log('');
|
|
112
|
+
log('Multiple users have Claude Code data:');
|
|
113
|
+
users.forEach((u, i) => log(` ${i + 1}. ${u.name} (${u.homedir})`));
|
|
114
|
+
console.log('');
|
|
115
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
116
|
+
rl.question(` Run as user [1]: `, (answer) => {
|
|
117
|
+
rl.close();
|
|
118
|
+
const idx = parseInt(answer.trim() || '1', 10) - 1;
|
|
119
|
+
const chosen = users[idx] || users[0];
|
|
120
|
+
resolve(chosen.homedir);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
// --- SSH AuthorizedKeysCommand setup (Linux/macOS) ---
|
|
127
|
+
// Configures sshd to dynamically authorize Spaces users via a lookup script,
|
|
128
|
+
// eliminating the need to manually manage ~/.ssh/authorized_keys per user.
|
|
129
|
+
function configureAuthorizedKeysCommand() {
|
|
130
|
+
if (process.platform === 'win32') return; // Windows uses a different approach
|
|
131
|
+
|
|
132
|
+
const scriptSrc = path.join(__dirname, 'ssh-auth-keys.sh');
|
|
133
|
+
const scriptDst = '/opt/spaces/bin/ssh-auth-keys.sh';
|
|
134
|
+
const sshdConfig = '/etc/ssh/sshd_config';
|
|
135
|
+
const spacesConfDrop = '/etc/ssh/sshd_config.d/spaces.conf';
|
|
136
|
+
|
|
137
|
+
// Install the script to a stable system path
|
|
138
|
+
try {
|
|
139
|
+
spawnSync('sudo', ['mkdir', '-p', '/opt/spaces/bin'], { stdio: 'pipe', timeout: 5000 });
|
|
140
|
+
spawnSync('sudo', ['cp', scriptSrc, scriptDst], { stdio: 'pipe', timeout: 5000 });
|
|
141
|
+
spawnSync('sudo', ['chmod', '755', scriptDst], { stdio: 'pipe', timeout: 5000 });
|
|
142
|
+
spawnSync('sudo', ['chown', 'root:root', scriptDst], { stdio: 'pipe', timeout: 5000 });
|
|
143
|
+
logOk('Installed ssh-auth-keys.sh to ' + scriptDst);
|
|
144
|
+
} catch (e) {
|
|
145
|
+
logErr('Failed to install ssh-auth-keys.sh: ' + e.message);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Prefer sshd_config.d drop-in if the Include directive exists
|
|
150
|
+
const sshdContent = fs.existsSync(sshdConfig)
|
|
151
|
+
? fs.readFileSync(sshdConfig, 'utf-8') : '';
|
|
152
|
+
|
|
153
|
+
const configLines = [
|
|
154
|
+
'# Spaces: dynamically authorize shell users via admin DB lookup',
|
|
155
|
+
'AuthorizedKeysCommand ' + scriptDst + ' %u',
|
|
156
|
+
'AuthorizedKeysCommandUser nobody',
|
|
157
|
+
].join('\n') + '\n';
|
|
158
|
+
|
|
159
|
+
// Already configured?
|
|
160
|
+
if (sshdContent.includes('ssh-auth-keys.sh')) {
|
|
161
|
+
logOk('AuthorizedKeysCommand already configured in sshd_config');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Try drop-in directory first (cleaner, doesn't touch main config)
|
|
166
|
+
const hasIncludeDir = sshdContent.includes('Include /etc/ssh/sshd_config.d/');
|
|
167
|
+
if (hasIncludeDir) {
|
|
168
|
+
try {
|
|
169
|
+
spawnSync('sudo', ['mkdir', '-p', '/etc/ssh/sshd_config.d'], { stdio: 'pipe', timeout: 5000 });
|
|
170
|
+
const result = spawnSync('sudo', ['tee', spacesConfDrop], {
|
|
171
|
+
input: configLines, stdio: ['pipe', 'pipe', 'inherit'], timeout: 5000,
|
|
172
|
+
});
|
|
173
|
+
if (result.status === 0) {
|
|
174
|
+
logOk('Created ' + spacesConfDrop);
|
|
175
|
+
}
|
|
176
|
+
} catch (e) {
|
|
177
|
+
logErr('Failed to write drop-in config: ' + e.message);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
// Append directly to sshd_config
|
|
181
|
+
try {
|
|
182
|
+
const appendContent = '\n# Spaces SSH authorization\n' + configLines;
|
|
183
|
+
const result = spawnSync('sudo', ['tee', '-a', sshdConfig], {
|
|
184
|
+
input: appendContent, stdio: ['pipe', 'pipe', 'inherit'], timeout: 5000,
|
|
185
|
+
});
|
|
186
|
+
if (result.status === 0) {
|
|
187
|
+
logOk('Added AuthorizedKeysCommand to ' + sshdConfig);
|
|
188
|
+
}
|
|
189
|
+
} catch (e) {
|
|
190
|
+
logErr('Failed to update sshd_config: ' + e.message);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Restart sshd to pick up the change
|
|
195
|
+
try {
|
|
196
|
+
spawnSync('sudo', ['systemctl', 'restart', 'sshd'], { stdio: 'pipe', timeout: 10000 });
|
|
197
|
+
logOk('Restarted sshd');
|
|
198
|
+
} catch {
|
|
199
|
+
try {
|
|
200
|
+
spawnSync('sudo', ['systemctl', 'restart', 'ssh'], { stdio: 'pipe', timeout: 10000 });
|
|
201
|
+
logOk('Restarted ssh');
|
|
202
|
+
} catch {
|
|
203
|
+
log('Warning: could not restart sshd — restart it manually');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// --- SSH key provisioning (for multi-user system service) ---
|
|
209
|
+
function checkOpenSSHServer() {
|
|
210
|
+
if (process.platform !== 'win32') return true;
|
|
211
|
+
try {
|
|
212
|
+
const result = spawnSync('powershell', ['-NoProfile', '-Command',
|
|
213
|
+
'Get-Service sshd -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Status'
|
|
214
|
+
], { encoding: 'utf-8', timeout: 10000 });
|
|
215
|
+
const status = (result.stdout || '').trim();
|
|
216
|
+
return status === 'Running' || status === 'Stopped';
|
|
217
|
+
} catch { return false; }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function ensureOpenSSHServer() {
|
|
221
|
+
if (process.platform !== 'win32') return;
|
|
222
|
+
if (checkOpenSSHServer()) {
|
|
223
|
+
try {
|
|
224
|
+
spawnSync('powershell', ['-NoProfile', '-Command',
|
|
225
|
+
'Start-Service sshd; Set-Service -Name sshd -StartupType Automatic'
|
|
226
|
+
], { stdio: 'pipe', timeout: 15000 });
|
|
227
|
+
logOk('OpenSSH Server started and set to automatic');
|
|
228
|
+
} catch {
|
|
229
|
+
log('Warning: could not start OpenSSH Server');
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
log('Installing OpenSSH Server (required for multi-user terminals)...');
|
|
234
|
+
try {
|
|
235
|
+
const result = spawnSync('powershell', ['-NoProfile', '-Command',
|
|
236
|
+
'Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0'
|
|
237
|
+
], { encoding: 'utf-8', stdio: 'pipe', timeout: 120000 });
|
|
238
|
+
if (result.status === 0) {
|
|
239
|
+
logOk('OpenSSH Server installed');
|
|
240
|
+
spawnSync('powershell', ['-NoProfile', '-Command',
|
|
241
|
+
'Start-Service sshd; Set-Service -Name sshd -StartupType Automatic'
|
|
242
|
+
], { stdio: 'pipe', timeout: 15000 });
|
|
243
|
+
logOk('OpenSSH Server started');
|
|
244
|
+
} else {
|
|
245
|
+
logErr('Failed to install OpenSSH Server: ' + (result.stderr || '').trim());
|
|
246
|
+
log('Multi-user terminals will not work. Install manually:');
|
|
247
|
+
log(' Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0');
|
|
248
|
+
}
|
|
249
|
+
} catch (e) {
|
|
250
|
+
logErr('Failed to install OpenSSH Server: ' + e.message);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function ensureServiceKey() {
|
|
255
|
+
const keyPath = path.join(SPACES_DIR, 'service_key');
|
|
256
|
+
if (fs.existsSync(keyPath)) return keyPath;
|
|
257
|
+
log('Generating SSH service key...');
|
|
258
|
+
fs.mkdirSync(SPACES_DIR, { recursive: true });
|
|
259
|
+
const result = spawnSync('ssh-keygen', [
|
|
260
|
+
'-t', 'ed25519',
|
|
261
|
+
'-f', keyPath,
|
|
262
|
+
'-N', '',
|
|
263
|
+
'-C', 'spaces-service-key',
|
|
264
|
+
], { stdio: 'pipe', timeout: 10000 });
|
|
265
|
+
if (result.status !== 0) {
|
|
266
|
+
logErr('Failed to generate SSH key');
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// On Windows, restrict private key permissions so OpenSSH accepts it.
|
|
271
|
+
// OpenSSH requires: no inherited ACLs, only the file owner + SYSTEM may have access.
|
|
272
|
+
if (process.platform === 'win32') {
|
|
273
|
+
try {
|
|
274
|
+
const currentUser = os.userInfo().username;
|
|
275
|
+
// Remove inheritance and all default ACLs, then grant only owner + SYSTEM
|
|
276
|
+
spawnSync('icacls', [keyPath, '/inheritance:r',
|
|
277
|
+
'/remove', 'BUILTIN\\Administrators',
|
|
278
|
+
'/remove', 'BUILTIN\\Users',
|
|
279
|
+
'/remove', 'Everyone',
|
|
280
|
+
'/grant:r', currentUser + ':(F)',
|
|
281
|
+
'/grant', 'NT AUTHORITY\\SYSTEM:(F)'], { stdio: 'pipe', timeout: 5000 });
|
|
282
|
+
} catch {}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
logOk('SSH service key generated');
|
|
286
|
+
return keyPath;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function authorizeServiceKey(keyPath, targetUser) {
|
|
290
|
+
const pubKey = fs.readFileSync(keyPath + '.pub', 'utf-8').trim();
|
|
291
|
+
|
|
292
|
+
if (process.platform === 'win32') {
|
|
293
|
+
// Windows OpenSSH ignores ~/.ssh/authorized_keys for admin users.
|
|
294
|
+
// Must use C:\ProgramData\ssh\administrators_authorized_keys instead.
|
|
295
|
+
const adminAuthKeys = path.join(process.env.ProgramData || 'C:\\ProgramData', 'ssh', 'administrators_authorized_keys');
|
|
296
|
+
const userAuthKeys = path.join(path.dirname(os.homedir()), targetUser, '.ssh', 'authorized_keys');
|
|
297
|
+
|
|
298
|
+
// Check if user is an administrator (exact match, not substring)
|
|
299
|
+
let isAdmin = false;
|
|
300
|
+
try {
|
|
301
|
+
const result = spawnSync('net', ['localgroup', 'Administrators'], { encoding: 'utf-8', timeout: 5000 });
|
|
302
|
+
const lines = (result.stdout || '').split(/\r?\n/);
|
|
303
|
+
const sep = lines.findIndex(l => l.trim().startsWith('---'));
|
|
304
|
+
if (sep >= 0) {
|
|
305
|
+
const members = lines.slice(sep + 1).map(l => l.trim()).filter(Boolean);
|
|
306
|
+
isAdmin = members.some(m => m.toLowerCase() === targetUser.toLowerCase());
|
|
307
|
+
}
|
|
308
|
+
} catch {}
|
|
309
|
+
|
|
310
|
+
const authKeysPath = isAdmin ? adminAuthKeys : userAuthKeys;
|
|
311
|
+
const authDir = path.dirname(authKeysPath);
|
|
312
|
+
if (!fs.existsSync(authDir)) fs.mkdirSync(authDir, { recursive: true });
|
|
313
|
+
|
|
314
|
+
// Check if key already authorized
|
|
315
|
+
if (fs.existsSync(authKeysPath)) {
|
|
316
|
+
const existing = fs.readFileSync(authKeysPath, 'utf-8');
|
|
317
|
+
if (existing.includes(pubKey)) return;
|
|
318
|
+
}
|
|
319
|
+
fs.appendFileSync(authKeysPath, pubKey + String.fromCharCode(10));
|
|
320
|
+
|
|
321
|
+
// Fix permissions for administrators_authorized_keys
|
|
322
|
+
if (isAdmin) {
|
|
323
|
+
try {
|
|
324
|
+
spawnSync('icacls', [authKeysPath, '/inheritance:r', '/grant', 'SYSTEM:(R)', '/grant', 'Administrators:(R)'], { stdio: 'pipe', timeout: 5000 });
|
|
325
|
+
} catch {}
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
// Linux/macOS: use ~/.ssh/authorized_keys
|
|
329
|
+
let userHome;
|
|
330
|
+
try {
|
|
331
|
+
// Use getent to resolve the actual home directory (works with LDAP, NIS, etc.)
|
|
332
|
+
const result = spawnSync('getent', ['passwd', targetUser], { encoding: 'utf-8', timeout: 5000 });
|
|
333
|
+
const fields = (result.stdout || '').split(':');
|
|
334
|
+
userHome = fields[5] || (process.platform === 'darwin' ? `/Users/${targetUser}` : `/home/${targetUser}`);
|
|
335
|
+
} catch {
|
|
336
|
+
userHome = process.platform === 'darwin' ? `/Users/${targetUser}` : `/home/${targetUser}`;
|
|
337
|
+
}
|
|
338
|
+
const sshDir = path.join(userHome, '.ssh');
|
|
339
|
+
const authKeysPath = path.join(sshDir, 'authorized_keys');
|
|
340
|
+
if (!fs.existsSync(sshDir)) fs.mkdirSync(sshDir, { recursive: true, mode: 0o700 });
|
|
341
|
+
if (fs.existsSync(authKeysPath)) {
|
|
342
|
+
const existing = fs.readFileSync(authKeysPath, 'utf-8');
|
|
343
|
+
if (existing.includes(pubKey)) return;
|
|
344
|
+
}
|
|
345
|
+
fs.appendFileSync(authKeysPath, pubKey + String.fromCharCode(10));
|
|
346
|
+
}
|
|
347
|
+
logOk('SSH key authorized for ' + targetUser);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const LEVEL_PATH = path.join(SPACES_DIR, 'service-level');
|
|
351
|
+
|
|
352
|
+
function saveLevel(level) {
|
|
353
|
+
fs.mkdirSync(SPACES_DIR, { recursive: true });
|
|
354
|
+
fs.writeFileSync(LEVEL_PATH, level);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function loadLevel() {
|
|
358
|
+
try { return fs.readFileSync(LEVEL_PATH, 'utf-8').trim(); } catch { return null; }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ─── Platform detection ───────────────────────────────────────
|
|
362
|
+
function getPlatform() {
|
|
363
|
+
switch (process.platform) {
|
|
364
|
+
case 'linux': return 'linux';
|
|
365
|
+
case 'darwin': return 'darwin';
|
|
366
|
+
case 'win32': return 'win32';
|
|
367
|
+
default:
|
|
368
|
+
logErr(`Unsupported platform: ${process.platform}`);
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ─── Linux (systemd) ─────────────────────────────────────────
|
|
374
|
+
function linuxServicePath(level) {
|
|
375
|
+
if (level === 'system') {
|
|
376
|
+
return `/etc/systemd/system/${SERVICE_NAME}.service`;
|
|
377
|
+
}
|
|
378
|
+
const userDir = path.join(os.homedir(), '.config', 'systemd', 'user');
|
|
379
|
+
fs.mkdirSync(userDir, { recursive: true });
|
|
380
|
+
return path.join(userDir, `${SERVICE_NAME}.service`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function linuxUnitFile(level) {
|
|
384
|
+
const config = resolveConfig();
|
|
385
|
+
const nodePath = resolveNodePath();
|
|
386
|
+
const spacesPath = resolveSpacesPath();
|
|
387
|
+
const projectDir = resolveProjectDir();
|
|
388
|
+
|
|
389
|
+
let envLines = [
|
|
390
|
+
`Environment=SPACES_SERVICE=1`,
|
|
391
|
+
`Environment=SPACES_PORT=${config.port}`,
|
|
392
|
+
];
|
|
393
|
+
// Don't hardcode SPACES_TIER — let spaces.js auto-detect from installed packages
|
|
394
|
+
if (config.tier && config.tier !== 'community') {
|
|
395
|
+
envLines.push(`Environment=SPACES_TIER=${config.tier}`);
|
|
396
|
+
}
|
|
397
|
+
if (config.basePath) {
|
|
398
|
+
envLines.push(`Environment=SPACES_BASE_PATH=${config.basePath}`);
|
|
399
|
+
}
|
|
400
|
+
if (config.allowedOrigins) {
|
|
401
|
+
envLines.push(`Environment=SPACES_ALLOWED_ORIGINS=${config.allowedOrigins}`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Allow users to set API keys (OPENAI_API_KEY, etc.) in ~/.spaces/env
|
|
405
|
+
const envFilePath = path.join(SPACES_DIR, 'env');
|
|
406
|
+
|
|
407
|
+
let serviceSection = [
|
|
408
|
+
'Type=simple',
|
|
409
|
+
`ExecStart=${nodePath} ${spacesPath}`,
|
|
410
|
+
`WorkingDirectory=${projectDir}`,
|
|
411
|
+
`EnvironmentFile=-${envFilePath}`,
|
|
412
|
+
...envLines,
|
|
413
|
+
'Restart=on-failure',
|
|
414
|
+
'RestartSec=5',
|
|
415
|
+
];
|
|
416
|
+
|
|
417
|
+
if (level === 'system') {
|
|
418
|
+
const username = os.userInfo().username;
|
|
419
|
+
serviceSection.push(`User=${username}`);
|
|
420
|
+
serviceSection.push(`Group=${username}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const wantedBy = level === 'system' ? 'multi-user.target' : 'default.target';
|
|
424
|
+
|
|
425
|
+
return [
|
|
426
|
+
'[Unit]',
|
|
427
|
+
'Description=Spaces - Agent Workspace Manager',
|
|
428
|
+
'After=network.target',
|
|
429
|
+
'',
|
|
430
|
+
'[Service]',
|
|
431
|
+
...serviceSection,
|
|
432
|
+
'',
|
|
433
|
+
'[Install]',
|
|
434
|
+
`WantedBy=${wantedBy}`,
|
|
435
|
+
'',
|
|
436
|
+
].join('\n');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function linuxSystemctl(level, ...args) {
|
|
440
|
+
if (level === 'system') {
|
|
441
|
+
execFileSync('sudo', ['systemctl', ...args], { stdio: 'inherit' });
|
|
442
|
+
} else {
|
|
443
|
+
// User-level systemctl needs XDG_RUNTIME_DIR to connect to the user bus.
|
|
444
|
+
// Always set it from the real UID — inherited env may have a stale value
|
|
445
|
+
// (e.g. from a prior session with a different UID mapping).
|
|
446
|
+
const uid = process.getuid();
|
|
447
|
+
const env = {
|
|
448
|
+
...process.env,
|
|
449
|
+
XDG_RUNTIME_DIR: `/run/user/${uid}`,
|
|
450
|
+
DBUS_SESSION_BUS_ADDRESS: `unix:path=/run/user/${uid}/bus`,
|
|
451
|
+
};
|
|
452
|
+
execFileSync('systemctl', ['--user', ...args], { stdio: 'inherit', env });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function linuxInstall() {
|
|
457
|
+
const level = await promptLevel();
|
|
458
|
+
const unitContent = linuxUnitFile(level);
|
|
459
|
+
const servicePath = linuxServicePath(level);
|
|
460
|
+
|
|
461
|
+
log(`Writing unit file to ${servicePath}`);
|
|
462
|
+
if (level === 'system') {
|
|
463
|
+
const result = spawnSync('sudo', ['tee', servicePath], {
|
|
464
|
+
input: unitContent,
|
|
465
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
466
|
+
});
|
|
467
|
+
if (result.status !== 0) {
|
|
468
|
+
logErr('Failed to write unit file');
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
fs.writeFileSync(servicePath, unitContent);
|
|
473
|
+
}
|
|
474
|
+
logOk('Unit file written');
|
|
475
|
+
|
|
476
|
+
saveLevel(level);
|
|
477
|
+
|
|
478
|
+
linuxSystemctl(level, 'daemon-reload');
|
|
479
|
+
logOk('Reloaded systemd daemon');
|
|
480
|
+
|
|
481
|
+
linuxSystemctl(level, 'enable', `${SERVICE_NAME}.service`);
|
|
482
|
+
logOk('Service enabled');
|
|
483
|
+
|
|
484
|
+
linuxSystemctl(level, 'start', `${SERVICE_NAME}.service`);
|
|
485
|
+
logOk('Service started');
|
|
486
|
+
|
|
487
|
+
if (level === 'user') {
|
|
488
|
+
try {
|
|
489
|
+
const uid = process.getuid();
|
|
490
|
+
const env = {
|
|
491
|
+
...process.env,
|
|
492
|
+
XDG_RUNTIME_DIR: `/run/user/${uid}`,
|
|
493
|
+
DBUS_SESSION_BUS_ADDRESS: `unix:path=/run/user/${uid}/bus`,
|
|
494
|
+
};
|
|
495
|
+
execFileSync('loginctl', ['enable-linger', os.userInfo().username], { stdio: 'inherit', env });
|
|
496
|
+
logOk('Enabled login lingering for user service');
|
|
497
|
+
} catch {
|
|
498
|
+
log('Warning: could not enable-linger (user service may not start on boot)');
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Set up SSH AuthorizedKeysCommand for seamless multi-user terminal access
|
|
503
|
+
if (level === 'system') {
|
|
504
|
+
configureAuthorizedKeysCommand();
|
|
505
|
+
const keyPath = ensureServiceKey();
|
|
506
|
+
if (keyPath) {
|
|
507
|
+
authorizeServiceKey(keyPath, os.userInfo().username);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
logOk(`Spaces installed as ${level} service`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function linuxUninstall() {
|
|
515
|
+
const level = loadLevel() || 'user';
|
|
516
|
+
const servicePath = linuxServicePath(level);
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
linuxSystemctl(level, 'stop', `${SERVICE_NAME}.service`);
|
|
520
|
+
logOk('Service stopped');
|
|
521
|
+
} catch {
|
|
522
|
+
log('Service was not running');
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
linuxSystemctl(level, 'disable', `${SERVICE_NAME}.service`);
|
|
527
|
+
logOk('Service disabled');
|
|
528
|
+
} catch {
|
|
529
|
+
log('Service was not enabled');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
if (level === 'system') {
|
|
534
|
+
execFileSync('sudo', ['rm', '-f', servicePath], { stdio: 'inherit' });
|
|
535
|
+
} else {
|
|
536
|
+
fs.unlinkSync(servicePath);
|
|
537
|
+
}
|
|
538
|
+
logOk('Unit file removed');
|
|
539
|
+
} catch {
|
|
540
|
+
log('Unit file was already removed');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
try {
|
|
544
|
+
linuxSystemctl(level, 'daemon-reload');
|
|
545
|
+
logOk('Reloaded systemd daemon');
|
|
546
|
+
} catch {
|
|
547
|
+
log('Warning: daemon-reload failed');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
try {
|
|
551
|
+
fs.unlinkSync(LEVEL_PATH);
|
|
552
|
+
} catch {}
|
|
553
|
+
|
|
554
|
+
logOk('Spaces service uninstalled');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function linuxStart() {
|
|
558
|
+
const level = loadLevel() || 'user';
|
|
559
|
+
linuxSystemctl(level, 'start', `${SERVICE_NAME}.service`);
|
|
560
|
+
logOk('Service started');
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function linuxStop() {
|
|
564
|
+
const level = loadLevel() || 'user';
|
|
565
|
+
linuxSystemctl(level, 'stop', `${SERVICE_NAME}.service`);
|
|
566
|
+
logOk('Service stopped');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function linuxStatus() {
|
|
570
|
+
const level = loadLevel() || 'user';
|
|
571
|
+
try {
|
|
572
|
+
linuxSystemctl(level, 'status', `${SERVICE_NAME}.service`);
|
|
573
|
+
} catch {
|
|
574
|
+
// systemctl status returns non-zero for stopped/failed services
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async function linuxLogs() {
|
|
579
|
+
const level = loadLevel() || 'user';
|
|
580
|
+
if (level === 'user') {
|
|
581
|
+
const uid = process.getuid();
|
|
582
|
+
const env = {
|
|
583
|
+
...process.env,
|
|
584
|
+
XDG_RUNTIME_DIR: `/run/user/${uid}`,
|
|
585
|
+
DBUS_SESSION_BUS_ADDRESS: `unix:path=/run/user/${uid}/bus`,
|
|
586
|
+
};
|
|
587
|
+
spawnSync('journalctl', ['--user', '-u', SERVICE_NAME, '-f', '--no-pager'], { stdio: 'inherit', env });
|
|
588
|
+
} else {
|
|
589
|
+
spawnSync('sudo', ['journalctl', '-u', SERVICE_NAME, '-f', '--no-pager'], { stdio: 'inherit' });
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// ─── macOS (launchd) ─────────────────────────────────────────
|
|
594
|
+
function darwinPlistPath(level) {
|
|
595
|
+
if (level === 'system') {
|
|
596
|
+
return `/Library/LaunchDaemons/${LABEL}.plist`;
|
|
597
|
+
}
|
|
598
|
+
return path.join(os.homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function darwinPlistContent(level) {
|
|
602
|
+
ensureLogsDir();
|
|
603
|
+
const config = resolveConfig();
|
|
604
|
+
const nodePath = resolveNodePath();
|
|
605
|
+
const spacesPath = resolveSpacesPath();
|
|
606
|
+
const projectDir = resolveProjectDir();
|
|
607
|
+
const outLog = path.join(LOGS_DIR, 'spaces.out.log');
|
|
608
|
+
const errLog = path.join(LOGS_DIR, 'spaces.err.log');
|
|
609
|
+
|
|
610
|
+
let envEntries = [
|
|
611
|
+
` <key>SPACES_SERVICE</key>`,
|
|
612
|
+
` <string>1</string>`,
|
|
613
|
+
` <key>SPACES_PORT</key>`,
|
|
614
|
+
` <string>${config.port}</string>`,
|
|
615
|
+
];
|
|
616
|
+
// Don't hardcode SPACES_TIER — let spaces.js auto-detect from installed packages
|
|
617
|
+
if (config.tier && config.tier !== 'community') {
|
|
618
|
+
envEntries.push(` <key>SPACES_TIER</key>`);
|
|
619
|
+
envEntries.push(` <string>${config.tier}</string>`);
|
|
620
|
+
}
|
|
621
|
+
if (config.basePath) {
|
|
622
|
+
envEntries.push(` <key>SPACES_BASE_PATH</key>`);
|
|
623
|
+
envEntries.push(` <string>${config.basePath}</string>`);
|
|
624
|
+
}
|
|
625
|
+
if (config.allowedOrigins) {
|
|
626
|
+
envEntries.push(` <key>SPACES_ALLOWED_ORIGINS</key>`);
|
|
627
|
+
envEntries.push(` <string>${config.allowedOrigins}</string>`);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
let extraKeys = '';
|
|
631
|
+
if (level === 'system') {
|
|
632
|
+
extraKeys = ` <key>UserName</key>\n <string>${os.userInfo().username}</string>\n`;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return [
|
|
636
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
637
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
638
|
+
'<plist version="1.0">',
|
|
639
|
+
'<dict>',
|
|
640
|
+
` <key>Label</key>`,
|
|
641
|
+
` <string>${LABEL}</string>`,
|
|
642
|
+
` <key>ProgramArguments</key>`,
|
|
643
|
+
` <array>`,
|
|
644
|
+
` <string>${nodePath}</string>`,
|
|
645
|
+
` <string>${spacesPath}</string>`,
|
|
646
|
+
` </array>`,
|
|
647
|
+
` <key>WorkingDirectory</key>`,
|
|
648
|
+
` <string>${projectDir}</string>`,
|
|
649
|
+
` <key>EnvironmentVariables</key>`,
|
|
650
|
+
` <dict>`,
|
|
651
|
+
...envEntries,
|
|
652
|
+
` </dict>`,
|
|
653
|
+
` <key>RunAtLoad</key>`,
|
|
654
|
+
` <true/>`,
|
|
655
|
+
` <key>KeepAlive</key>`,
|
|
656
|
+
` <true/>`,
|
|
657
|
+
` <key>StandardOutPath</key>`,
|
|
658
|
+
` <string>${outLog}</string>`,
|
|
659
|
+
` <key>StandardErrorPath</key>`,
|
|
660
|
+
` <string>${errLog}</string>`,
|
|
661
|
+
extraKeys ? extraKeys.trimEnd() : null,
|
|
662
|
+
'</dict>',
|
|
663
|
+
'</plist>',
|
|
664
|
+
'',
|
|
665
|
+
].filter((line) => line !== null).join('\n');
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function darwinInstall() {
|
|
669
|
+
const level = await promptLevel();
|
|
670
|
+
const plistPath = darwinPlistPath(level);
|
|
671
|
+
const plistContent = darwinPlistContent(level);
|
|
672
|
+
|
|
673
|
+
// Unload existing (ignore errors)
|
|
674
|
+
try {
|
|
675
|
+
execFileSync('launchctl', ['unload', '-w', plistPath], { stdio: 'pipe' });
|
|
676
|
+
} catch {}
|
|
677
|
+
|
|
678
|
+
log(`Writing plist to ${plistPath}`);
|
|
679
|
+
if (level === 'system') {
|
|
680
|
+
const result = spawnSync('sudo', ['tee', plistPath], {
|
|
681
|
+
input: plistContent,
|
|
682
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
683
|
+
});
|
|
684
|
+
if (result.status !== 0) {
|
|
685
|
+
logErr('Failed to write plist file');
|
|
686
|
+
process.exit(1);
|
|
687
|
+
}
|
|
688
|
+
execFileSync('sudo', ['chown', 'root:wheel', plistPath], { stdio: 'inherit' });
|
|
689
|
+
execFileSync('sudo', ['chmod', '644', plistPath], { stdio: 'inherit' });
|
|
690
|
+
} else {
|
|
691
|
+
const agentsDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
692
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
693
|
+
fs.writeFileSync(plistPath, plistContent);
|
|
694
|
+
}
|
|
695
|
+
logOk('Plist file written');
|
|
696
|
+
|
|
697
|
+
saveLevel(level);
|
|
698
|
+
|
|
699
|
+
if (level === 'system') {
|
|
700
|
+
execFileSync('sudo', ['launchctl', 'load', '-w', plistPath], { stdio: 'inherit' });
|
|
701
|
+
} else {
|
|
702
|
+
execFileSync('launchctl', ['load', '-w', plistPath], { stdio: 'inherit' });
|
|
703
|
+
}
|
|
704
|
+
logOk('Service loaded');
|
|
705
|
+
|
|
706
|
+
// Set up SSH AuthorizedKeysCommand for seamless multi-user terminal access
|
|
707
|
+
if (level === 'system') {
|
|
708
|
+
configureAuthorizedKeysCommand();
|
|
709
|
+
const keyPath = ensureServiceKey();
|
|
710
|
+
if (keyPath) {
|
|
711
|
+
authorizeServiceKey(keyPath, os.userInfo().username);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
logOk(`Spaces installed as ${level} service`);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async function darwinUninstall() {
|
|
719
|
+
const level = loadLevel() || 'user';
|
|
720
|
+
const plistPath = darwinPlistPath(level);
|
|
721
|
+
|
|
722
|
+
try {
|
|
723
|
+
if (level === 'system') {
|
|
724
|
+
execFileSync('sudo', ['launchctl', 'unload', '-w', plistPath], { stdio: 'inherit' });
|
|
725
|
+
} else {
|
|
726
|
+
execFileSync('launchctl', ['unload', '-w', plistPath], { stdio: 'inherit' });
|
|
727
|
+
}
|
|
728
|
+
logOk('Service unloaded');
|
|
729
|
+
} catch {
|
|
730
|
+
log('Service was not loaded');
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
try {
|
|
734
|
+
if (level === 'system') {
|
|
735
|
+
execFileSync('sudo', ['rm', '-f', plistPath], { stdio: 'inherit' });
|
|
736
|
+
} else {
|
|
737
|
+
fs.unlinkSync(plistPath);
|
|
738
|
+
}
|
|
739
|
+
logOk('Plist file removed');
|
|
740
|
+
} catch {
|
|
741
|
+
log('Plist file was already removed');
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
try {
|
|
745
|
+
fs.unlinkSync(LEVEL_PATH);
|
|
746
|
+
} catch {}
|
|
747
|
+
|
|
748
|
+
logOk('Spaces service uninstalled');
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async function darwinStart() {
|
|
752
|
+
const level = loadLevel() || 'user';
|
|
753
|
+
const plistPath = darwinPlistPath(level);
|
|
754
|
+
if (level === 'system') {
|
|
755
|
+
execFileSync('sudo', ['launchctl', 'load', '-w', plistPath], { stdio: 'inherit' });
|
|
756
|
+
} else {
|
|
757
|
+
execFileSync('launchctl', ['load', '-w', plistPath], { stdio: 'inherit' });
|
|
758
|
+
}
|
|
759
|
+
logOk('Service started');
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function darwinStop() {
|
|
763
|
+
const level = loadLevel() || 'user';
|
|
764
|
+
const plistPath = darwinPlistPath(level);
|
|
765
|
+
if (level === 'system') {
|
|
766
|
+
execFileSync('sudo', ['launchctl', 'unload', '-w', plistPath], { stdio: 'inherit' });
|
|
767
|
+
} else {
|
|
768
|
+
execFileSync('launchctl', ['unload', '-w', plistPath], { stdio: 'inherit' });
|
|
769
|
+
}
|
|
770
|
+
logOk('Service stopped');
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async function darwinStatus() {
|
|
774
|
+
try {
|
|
775
|
+
const result = execFileSync('launchctl', ['list'], { encoding: 'utf-8' });
|
|
776
|
+
const lines = result.split('\n').filter((line) => line.includes(LABEL));
|
|
777
|
+
if (lines.length > 0) {
|
|
778
|
+
log('Spaces service status:');
|
|
779
|
+
lines.forEach((line) => log(line));
|
|
780
|
+
} else {
|
|
781
|
+
log('Spaces service is not loaded');
|
|
782
|
+
}
|
|
783
|
+
} catch {
|
|
784
|
+
log('Spaces service is not loaded');
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
async function darwinLogs() {
|
|
789
|
+
ensureLogsDir();
|
|
790
|
+
const outLogPath = path.join(LOGS_DIR, 'spaces.out.log');
|
|
791
|
+
if (!fs.existsSync(outLogPath)) {
|
|
792
|
+
logErr(`Log file not found: ${outLogPath}`);
|
|
793
|
+
log('Service may not have started yet');
|
|
794
|
+
process.exit(1);
|
|
795
|
+
}
|
|
796
|
+
spawnSync('tail', ['-f', outLogPath], { stdio: 'inherit' });
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// ─── Windows (Task Scheduler) ────────────────────────────────
|
|
800
|
+
function win32WrapperScript(level) {
|
|
801
|
+
ensureLogsDir();
|
|
802
|
+
const config = resolveConfig();
|
|
803
|
+
const nodePath = resolveNodePath();
|
|
804
|
+
const spacesPath = resolveSpacesPath();
|
|
805
|
+
const outLog = path.join(LOGS_DIR, 'spaces.out.log');
|
|
806
|
+
const wrapperPath = path.join(SPACES_DIR, 'spaces-service.cmd');
|
|
807
|
+
|
|
808
|
+
const lines = [
|
|
809
|
+
'@echo off',
|
|
810
|
+
];
|
|
811
|
+
// When running as SYSTEM, override USERPROFILE so os.homedir() resolves
|
|
812
|
+
// to the target user's home directory (where .spaces/ config lives).
|
|
813
|
+
if (level === 'system') {
|
|
814
|
+
const homedir = path.dirname(SPACES_DIR); // use resolved target, not os.homedir()
|
|
815
|
+
const drive = path.parse(homedir).root.slice(0, -1);
|
|
816
|
+
const rest = homedir.slice(drive.length);
|
|
817
|
+
lines.push(`set USERPROFILE=${homedir}`);
|
|
818
|
+
lines.push(`set HOMEDRIVE=${drive}`);
|
|
819
|
+
lines.push(`set HOMEPATH=${rest}`);
|
|
820
|
+
}
|
|
821
|
+
lines.push('set SPACES_SERVICE=1');
|
|
822
|
+
lines.push(`set SPACES_PORT=${config.port}`);
|
|
823
|
+
// Don't hardcode SPACES_TIER — let spaces.js auto-detect from installed packages
|
|
824
|
+
if (config.tier && config.tier !== 'community') {
|
|
825
|
+
lines.push(`set SPACES_TIER=${config.tier}`);
|
|
826
|
+
}
|
|
827
|
+
if (config.basePath) {
|
|
828
|
+
lines.push(`set SPACES_BASE_PATH=${config.basePath}`);
|
|
829
|
+
}
|
|
830
|
+
if (config.allowedOrigins) {
|
|
831
|
+
lines.push(`set SPACES_ALLOWED_ORIGINS=${config.allowedOrigins}`);
|
|
832
|
+
}
|
|
833
|
+
lines.push(`"${nodePath}" "${spacesPath}" >> "${outLog}" 2>&1`);
|
|
834
|
+
lines.push('');
|
|
835
|
+
|
|
836
|
+
fs.writeFileSync(wrapperPath, lines.join('\r\n'));
|
|
837
|
+
return wrapperPath;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
async function win32Install() {
|
|
841
|
+
const level = await promptLevel();
|
|
842
|
+
|
|
843
|
+
// For system service, determine the target user's home directory
|
|
844
|
+
// (may differ from the admin account running the installer)
|
|
845
|
+
if (level === 'system' && process.platform === 'win32') {
|
|
846
|
+
const targetHome = await promptTargetUser();
|
|
847
|
+
setTargetHome(targetHome);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const wrapperPath = win32WrapperScript(level);
|
|
851
|
+
|
|
852
|
+
log(`Wrapper script written to ${wrapperPath}`);
|
|
853
|
+
|
|
854
|
+
// Delete existing task (ignore errors)
|
|
855
|
+
try {
|
|
856
|
+
execFileSync('schtasks', ['/Delete', '/TN', TASK_NAME, '/F'], { stdio: 'pipe' });
|
|
857
|
+
} catch {}
|
|
858
|
+
|
|
859
|
+
if (level === 'system') {
|
|
860
|
+
execFileSync('schtasks', ['/Create', '/TN', TASK_NAME, '/TR', `"${wrapperPath}"`, '/SC', 'ONSTART', '/RU', 'SYSTEM', '/F'], { stdio: 'inherit' });
|
|
861
|
+
} else {
|
|
862
|
+
execFileSync('schtasks', ['/Create', '/TN', TASK_NAME, '/TR', `"${wrapperPath}"`, '/SC', 'ONLOGON', '/RL', 'HIGHEST', '/F'], { stdio: 'inherit' });
|
|
863
|
+
}
|
|
864
|
+
logOk('Scheduled task created');
|
|
865
|
+
|
|
866
|
+
saveLevel(level);
|
|
867
|
+
|
|
868
|
+
// Set up SSH for multi-user support (system service only)
|
|
869
|
+
// Only ensure OpenSSH is available — key generation is handled at runtime
|
|
870
|
+
// by ensureServiceKeyAtRuntime() in terminal-server.js, which runs as SYSTEM
|
|
871
|
+
// so the key is owned by SYSTEM and OpenSSH accepts it.
|
|
872
|
+
if (level === 'system') {
|
|
873
|
+
ensureOpenSSHServer();
|
|
874
|
+
log('SSH service key will be generated on first run (as SYSTEM)');
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
execFileSync('schtasks', ['/Run', '/TN', TASK_NAME], { stdio: 'inherit' });
|
|
878
|
+
logOk('Task started');
|
|
879
|
+
|
|
880
|
+
logOk(`Spaces installed as ${level} service`);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
async function win32Uninstall() {
|
|
884
|
+
try {
|
|
885
|
+
execFileSync('schtasks', ['/End', '/TN', TASK_NAME], { stdio: 'pipe' });
|
|
886
|
+
logOk('Task ended');
|
|
887
|
+
} catch {
|
|
888
|
+
log('Task was not running');
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
try {
|
|
892
|
+
execFileSync('schtasks', ['/Delete', '/TN', TASK_NAME, '/F'], { stdio: 'pipe' });
|
|
893
|
+
logOk('Scheduled task removed');
|
|
894
|
+
} catch {
|
|
895
|
+
log('Scheduled task was already removed');
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
try {
|
|
899
|
+
const wrapperPath = path.join(SPACES_DIR, 'spaces-service.cmd');
|
|
900
|
+
fs.unlinkSync(wrapperPath);
|
|
901
|
+
logOk('Wrapper script removed');
|
|
902
|
+
} catch {
|
|
903
|
+
log('Wrapper script was already removed');
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
try {
|
|
907
|
+
fs.unlinkSync(LEVEL_PATH);
|
|
908
|
+
} catch {}
|
|
909
|
+
|
|
910
|
+
logOk('Spaces service uninstalled');
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
async function win32Start() {
|
|
914
|
+
// Try schtasks /Run first (works when running as admin or for user-level tasks)
|
|
915
|
+
try {
|
|
916
|
+
execFileSync('schtasks', ['/Run', '/TN', TASK_NAME], { stdio: 'pipe' });
|
|
917
|
+
logOk('Task started');
|
|
918
|
+
return;
|
|
919
|
+
} catch {}
|
|
920
|
+
|
|
921
|
+
// Fallback: launch the wrapper script directly as a detached process.
|
|
922
|
+
// This works for non-admin shells when the task is registered as SYSTEM.
|
|
923
|
+
// The service will run as the current user, not SYSTEM, but functionally
|
|
924
|
+
// equivalent for local development.
|
|
925
|
+
const wrapperPath = path.join(SPACES_DIR, 'spaces-service.cmd');
|
|
926
|
+
if (!fs.existsSync(wrapperPath)) {
|
|
927
|
+
logErr('Wrapper script not found — run "spaces service install" first');
|
|
928
|
+
process.exit(1);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const { spawn } = require('child_process');
|
|
932
|
+
const child = spawn('cmd.exe', ['/c', wrapperPath], {
|
|
933
|
+
detached: true,
|
|
934
|
+
stdio: 'ignore',
|
|
935
|
+
windowsHide: true,
|
|
936
|
+
});
|
|
937
|
+
child.unref();
|
|
938
|
+
logOk('Service started (direct launch)');
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
async function win32Stop() {
|
|
942
|
+
try {
|
|
943
|
+
execFileSync('schtasks', ['/End', '/TN', TASK_NAME], { stdio: 'pipe' });
|
|
944
|
+
} catch {}
|
|
945
|
+
|
|
946
|
+
// Kill the actual node processes on our ports (verify it's node before killing)
|
|
947
|
+
const config = resolveConfig();
|
|
948
|
+
const ports = [config.port || 3457, 3400];
|
|
949
|
+
let killed = 0;
|
|
950
|
+
for (const port of ports) {
|
|
951
|
+
try {
|
|
952
|
+
const output = execFileSync('netstat', ['-ano'], { encoding: 'utf-8' });
|
|
953
|
+
for (const line of output.split(String.fromCharCode(10))) {
|
|
954
|
+
if (line.includes(':' + port + ' ') && line.includes('LISTENING')) {
|
|
955
|
+
const parts = line.trim().split(/\s+/);
|
|
956
|
+
const pid = parseInt(parts[parts.length - 1], 10);
|
|
957
|
+
if (pid > 0) {
|
|
958
|
+
// Verify the process is node.exe before killing to avoid terminating unrelated processes
|
|
959
|
+
try {
|
|
960
|
+
const taskInfo = execFileSync('tasklist', ['/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH'], { encoding: 'utf-8' });
|
|
961
|
+
if (!taskInfo.toLowerCase().includes('node.exe')) continue;
|
|
962
|
+
process.kill(pid, 'SIGTERM'); killed++;
|
|
963
|
+
} catch {}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
} catch {}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
if (killed > 0) {
|
|
971
|
+
logOk('Stopped ' + killed + ' process(es)');
|
|
972
|
+
} else {
|
|
973
|
+
logOk('Task stopped');
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
async function win32Status() {
|
|
978
|
+
try {
|
|
979
|
+
execFileSync('schtasks', ['/Query', '/TN', TASK_NAME, '/V', '/FO', 'LIST'], { stdio: 'inherit' });
|
|
980
|
+
} catch {
|
|
981
|
+
log('Spaces service is not installed');
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
async function win32Logs() {
|
|
986
|
+
ensureLogsDir();
|
|
987
|
+
const outLogPath = path.join(LOGS_DIR, 'spaces.out.log');
|
|
988
|
+
if (!fs.existsSync(outLogPath)) {
|
|
989
|
+
logErr(`Log file not found: ${outLogPath}`);
|
|
990
|
+
log('Service may not have started yet');
|
|
991
|
+
process.exit(1);
|
|
992
|
+
}
|
|
993
|
+
spawnSync('powershell', ['-Command', `Get-Content "${outLogPath}" -Wait -Tail 50`], { stdio: 'inherit' });
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// ─── Dispatch table ──────────────────────────────────────────
|
|
997
|
+
const platforms = {
|
|
998
|
+
linux: { install: linuxInstall, uninstall: linuxUninstall, start: linuxStart, stop: linuxStop, status: linuxStatus, logs: linuxLogs },
|
|
999
|
+
darwin: { install: darwinInstall, uninstall: darwinUninstall, start: darwinStart, stop: darwinStop, status: darwinStatus, logs: darwinLogs },
|
|
1000
|
+
win32: { install: win32Install, uninstall: win32Uninstall, start: win32Start, stop: win32Stop, status: win32Status, logs: win32Logs },
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
// ─── CLI ──────────────────────────────────────────────────────
|
|
1004
|
+
async function main() {
|
|
1005
|
+
const action = process.argv[2];
|
|
1006
|
+
const platform = getPlatform();
|
|
1007
|
+
const dispatch = platforms[platform];
|
|
1008
|
+
|
|
1009
|
+
if (!action || !dispatch[action]) {
|
|
1010
|
+
log('Usage: spaces service <install|uninstall|start|stop|status|logs>');
|
|
1011
|
+
process.exit(action ? 1 : 0);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
await dispatch[action]();
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
main().catch((err) => {
|
|
1018
|
+
logErr(err.message);
|
|
1019
|
+
process.exit(1);
|
|
1020
|
+
});
|