@assistkick/create 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/scaffolder.d.ts +12 -1
- package/dist/src/scaffolder.js +40 -3
- package/dist/src/scaffolder.js.map +1 -1
- package/package.json +1 -1
- package/templates/assistkick-product-system/package.json +1 -1
- package/templates/assistkick-product-system/packages/backend/package.json +1 -0
- package/templates/assistkick-product-system/packages/backend/src/mcp/permission_mcp_server.ts +196 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +31 -7
- package/templates/assistkick-product-system/packages/backend/src/routes/auth.ts +15 -12
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.test.ts +95 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.ts +97 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_permission.ts +94 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_sessions.ts +189 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.test.ts +131 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.ts +94 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +12 -3
- package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +2 -2
- package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +390 -22
- package/templates/assistkick-product-system/packages/backend/src/routes/git_branches.test.ts +306 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/git_connect.test.ts +133 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +66 -9
- package/templates/assistkick-product-system/packages/backend/src/routes/preview.ts +204 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/projects.test.ts +205 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +37 -9
- package/templates/assistkick-product-system/packages/backend/src/routes/skills.test.ts +139 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/skills.ts +95 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +5 -4
- package/templates/assistkick-product-system/packages/backend/src/routes/users.ts +4 -4
- package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +8 -8
- package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +5 -5
- package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +6 -6
- package/templates/assistkick-product-system/packages/backend/src/server.ts +107 -27
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +105 -203
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +76 -266
- package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.test.ts +427 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.ts +345 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.test.ts +170 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.ts +106 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.test.ts +217 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.ts +188 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.test.ts +1243 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.ts +894 -0
- package/templates/assistkick-product-system/packages/backend/src/services/coherence-review.ts +3 -3
- package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.test.ts +85 -0
- package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.ts +54 -0
- package/templates/assistkick-product-system/packages/backend/src/services/email_service.ts +13 -10
- package/templates/assistkick-product-system/packages/backend/src/services/init.ts +11 -3
- package/templates/assistkick-product-system/packages/backend/src/services/invitation_service.ts +1 -1
- package/templates/assistkick-product-system/packages/backend/src/services/password_reset_service.ts +1 -1
- package/templates/assistkick-product-system/packages/backend/src/services/permission_service.test.ts +243 -0
- package/templates/assistkick-product-system/packages/backend/src/services/permission_service.ts +259 -0
- package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.test.ts +172 -0
- package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.ts +225 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +29 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +17 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +255 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +300 -25
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +44 -0
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +62 -7
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +77 -6
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +129 -8
- package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +2 -1
- package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.test.ts +45 -0
- package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.ts +157 -0
- package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +4 -3
- package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +3 -3
- package/templates/assistkick-product-system/packages/frontend/package.json +5 -0
- package/templates/assistkick-product-system/packages/frontend/src/App.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +336 -5
- package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +192 -12
- package/templates/assistkick-product-system/packages/frontend/src/components/AttachmentPreviewList.tsx +98 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/AutocompleteDropdown.tsx +65 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatAttachButton.tsx +56 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatDropZone.tsx +80 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageBubble.tsx +155 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageContent.tsx +182 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageInput.tsx +233 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatSessionSidebar.tsx +218 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatStopButton.tsx +32 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatTodoSidebar.tsx +113 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatView.tsx +842 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/CommitMessageModal.tsx +82 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/DiagramOverlay.tsx +160 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +5 -5
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +9 -10
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +5 -5
- package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +112 -41
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/components/HighlightedText.tsx +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ImageLightbox.tsx +192 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/components/MentionPill.tsx +33 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/MermaidBlock.tsx +148 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/PermissionDialog.tsx +91 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/PermissionModeSelector.tsx +229 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +249 -83
- package/templates/assistkick-product-system/packages/frontend/src/components/QueuedMessageBubble.tsx +38 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +212 -117
- package/templates/assistkick-product-system/packages/frontend/src/components/SystemPromptAccordion.tsx +48 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/TaskIcon.tsx +11 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +25 -9
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolDiffView.tsx +114 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolResultCard.tsx +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolUseCard.tsx +149 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +25 -8
- package/templates/assistkick-product-system/packages/frontend/src/components/UnifiedGitWidget.tsx +722 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +2 -1
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/ProgrammableNode.tsx +178 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +3 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +103 -9
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +26 -2
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +42 -1
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useDocumentTitle.ts +11 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +1 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/use_chat_stream.ts +826 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/use_file_tree_cache.ts +69 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/use_mention_autocomplete.ts +284 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.test.ts +183 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.ts +150 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.test.ts +305 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.ts +113 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.test.ts +157 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.ts +95 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.test.ts +65 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.ts +110 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/message_queue.ts +66 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.test.ts +124 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.ts +112 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/ChatRoute.tsx +8 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +0 -4
- package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/accept_invitation.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/forgot_password.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/login.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/register.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/reset_password.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useAttachmentStore.ts +66 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useChatSessionStore.ts +107 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useMessageQueueStore.ts +110 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/usePreviewStore.ts +78 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +7 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +6 -1
- package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +30 -357
- package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.test.ts +115 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.ts +91 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.test.ts +30 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.ts +3 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0015_magenta_jazinda.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0016_giant_xorn.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0017_sloppy_mentor.sql +6 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0018_vengeful_kabuki.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0019_careful_sentinels.sql +8 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0020_clever_spot.sql +27 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0021_graceful_hex.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0022_short_kingpin.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0023_ambiguous_sharon_carter.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0024_fat_unus.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0015_snapshot.json +1552 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0016_snapshot.json +1560 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0017_snapshot.json +1598 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0018_snapshot.json +1657 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0019_snapshot.json +1709 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0020_snapshot.json +1733 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0021_snapshot.json +1740 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0022_snapshot.json +1755 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0023_snapshot.json +1762 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0024_snapshot.json +1769 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +70 -0
- package/templates/assistkick-product-system/packages/shared/db/schema.ts +40 -1
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.test.ts +236 -0
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +46 -5
- package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +65 -39
- package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.test.ts +173 -0
- package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.ts +213 -0
- package/templates/assistkick-product-system/packages/shared/lib/validator.test.ts +70 -0
- package/templates/assistkick-product-system/packages/shared/lib/validator.ts +17 -1
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +803 -27
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +502 -68
- package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +4 -4
- package/templates/assistkick-product-system/packages/shared/package.json +2 -1
- package/templates/assistkick-product-system/packages/shared/test_fixtures/hanging_stream.mjs +46 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +44 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +7 -0
- package/templates/assistkick-product-system/packages/shared/tools/remove_node.ts +2 -1
- package/templates/assistkick-product-system/packages/shared/tools/resolve_question.ts +2 -1
- package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -1
- package/templates/assistkick-product-system/tests/message_queue.test.ts +178 -0
- package/templates/assistkick-product-system/tests/message_queue_per_session.test.ts +143 -0
- package/templates/skills/assistkick-bootstrap/SKILL.md +26 -26
- package/templates/skills/assistkick-code-reviewer/SKILL.md +45 -46
- package/templates/skills/assistkick-db-explorer/SKILL.md +13 -13
- package/templates/skills/assistkick-debugger/SKILL.md +23 -23
- package/templates/skills/assistkick-developer/SKILL.md +59 -63
- package/templates/skills/assistkick-interview/SKILL.md +26 -26
- package/templates/skills/assistkick-video-composition-agent/SKILL.md +231 -0
- package/templates/skills/assistkick-video-script-writer/SKILL.md +136 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preview server routes — REST API for managing dev preview servers
|
|
3
|
+
* and reverse proxy middleware for forwarding requests to dev servers.
|
|
4
|
+
*
|
|
5
|
+
* API routes:
|
|
6
|
+
* POST /api/preview/start — start a preview server
|
|
7
|
+
* POST /api/preview/stop — stop a preview server
|
|
8
|
+
* GET /api/preview/list — list running preview servers
|
|
9
|
+
* POST /api/preview/toggle-access — toggle public/private access
|
|
10
|
+
*
|
|
11
|
+
* Proxy route:
|
|
12
|
+
* /apps/<app-name>/* — reverse proxy to the dev server
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Router } from 'express';
|
|
16
|
+
import type { Request, Response } from 'express';
|
|
17
|
+
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
18
|
+
import type { PreviewServerManager } from '../services/preview_server_manager.js';
|
|
19
|
+
import type { AuthMiddleware } from '../middleware/auth_middleware.js';
|
|
20
|
+
import type { ProjectWorkspaceService } from '../services/project_workspace_service.js';
|
|
21
|
+
|
|
22
|
+
interface PreviewRoutesDeps {
|
|
23
|
+
previewManager: PreviewServerManager;
|
|
24
|
+
workspaceService: ProjectWorkspaceService;
|
|
25
|
+
projectService: { getById: (id: string) => Promise<{ previewCommand: string | null } | null> };
|
|
26
|
+
log: (tag: string, ...args: unknown[]) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const createPreviewApiRoutes = ({ previewManager, workspaceService, projectService, log }: PreviewRoutesDeps): Router => {
|
|
30
|
+
const router: Router = Router();
|
|
31
|
+
|
|
32
|
+
// POST /api/preview/start
|
|
33
|
+
router.post('/start', async (req: Request, res: Response) => {
|
|
34
|
+
const { appName, projectDir, projectId, command } = req.body;
|
|
35
|
+
|
|
36
|
+
if (!appName || typeof appName !== 'string') {
|
|
37
|
+
res.status(400).json({ error: 'appName is required' });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Resolve projectDir from projectId if not provided directly
|
|
42
|
+
const resolvedDir = projectDir || (projectId ? workspaceService.getWorkspacePath(projectId) : null);
|
|
43
|
+
if (!resolvedDir || typeof resolvedDir !== 'string') {
|
|
44
|
+
res.status(400).json({ error: 'projectDir or projectId is required' });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Validate appName: only alphanumeric, hyphens, underscores
|
|
49
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(appName)) {
|
|
50
|
+
res.status(400).json({ error: 'appName must contain only alphanumeric characters, hyphens, and underscores' });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Resolve command: explicit > project previewCommand > auto-detect (in manager)
|
|
55
|
+
let resolvedCommand = command?.trim() || undefined;
|
|
56
|
+
if (!resolvedCommand && projectId) {
|
|
57
|
+
const project = await projectService.getById(projectId);
|
|
58
|
+
if (project?.previewCommand) {
|
|
59
|
+
resolvedCommand = project.previewCommand;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const app = await previewManager.start(appName.trim(), resolvedDir.trim(), resolvedCommand);
|
|
65
|
+
log('PREVIEW', `Started preview app "${app.appName}" on port ${app.port}`);
|
|
66
|
+
res.status(201).json({ app });
|
|
67
|
+
} catch (err: any) {
|
|
68
|
+
log('PREVIEW', `Failed to start preview: ${err.message}`);
|
|
69
|
+
res.status(409).json({ error: err.message });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// POST /api/preview/stop
|
|
74
|
+
router.post('/stop', async (req: Request, res: Response) => {
|
|
75
|
+
const { appName } = req.body;
|
|
76
|
+
|
|
77
|
+
if (!appName || typeof appName !== 'string') {
|
|
78
|
+
res.status(400).json({ error: 'appName is required' });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const stopped = previewManager.stop(appName.trim());
|
|
83
|
+
if (!stopped) {
|
|
84
|
+
res.status(404).json({ error: `Preview app "${appName}" not found` });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
res.json({ ok: true });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// GET /api/preview/list
|
|
92
|
+
router.get('/list', async (_req: Request, res: Response) => {
|
|
93
|
+
const apps = previewManager.list();
|
|
94
|
+
res.json({ apps });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// POST /api/preview/toggle-access
|
|
98
|
+
router.post('/toggle-access', async (req: Request, res: Response) => {
|
|
99
|
+
const { appName, isPublic } = req.body;
|
|
100
|
+
|
|
101
|
+
if (!appName || typeof appName !== 'string') {
|
|
102
|
+
res.status(400).json({ error: 'appName is required' });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (typeof isPublic !== 'boolean') {
|
|
106
|
+
res.status(400).json({ error: 'isPublic (boolean) is required' });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const toggled = previewManager.toggleAccess(appName.trim(), isPublic);
|
|
111
|
+
if (!toggled) {
|
|
112
|
+
res.status(404).json({ error: `Preview app "${appName}" not found` });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
res.json({ ok: true, isPublic });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return router;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
interface PreviewProxyDeps {
|
|
123
|
+
previewManager: PreviewServerManager;
|
|
124
|
+
authMiddleware: AuthMiddleware;
|
|
125
|
+
log: (tag: string, ...args: unknown[]) => void;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Creates the /apps/:appName proxy middleware.
|
|
130
|
+
* - Checks if the app exists and is running
|
|
131
|
+
* - Enforces auth for private apps
|
|
132
|
+
* - Resets idle timer on each request
|
|
133
|
+
* - Proxies HTTP and WebSocket to the dev server
|
|
134
|
+
*/
|
|
135
|
+
export const createPreviewProxyMiddleware = ({ previewManager, authMiddleware, log }: PreviewProxyDeps): Router => {
|
|
136
|
+
const router: Router = Router();
|
|
137
|
+
|
|
138
|
+
// Dynamic proxy — resolves the target per-request based on the app name
|
|
139
|
+
const proxy = createProxyMiddleware({
|
|
140
|
+
router: (req) => {
|
|
141
|
+
const appName = (req as any).previewAppName as string;
|
|
142
|
+
const app = previewManager.get(appName);
|
|
143
|
+
if (!app) {
|
|
144
|
+
return 'http://127.0.0.1:1'; // Will fail, caught by error handler
|
|
145
|
+
}
|
|
146
|
+
return `http://127.0.0.1:${app.port}`;
|
|
147
|
+
},
|
|
148
|
+
pathRewrite: (path, req) => {
|
|
149
|
+
const appName = (req as any).previewAppName as string;
|
|
150
|
+
// Strip /apps/<appName> prefix from the path
|
|
151
|
+
const prefix = `/apps/${appName}`;
|
|
152
|
+
return path.startsWith(prefix) ? path.slice(prefix.length) || '/' : path;
|
|
153
|
+
},
|
|
154
|
+
changeOrigin: true,
|
|
155
|
+
ws: true,
|
|
156
|
+
on: {
|
|
157
|
+
error: (err, req, res) => {
|
|
158
|
+
log('PREVIEW', `Proxy error: ${err.message}`);
|
|
159
|
+
if ('writeHead' in res && typeof res.writeHead === 'function') {
|
|
160
|
+
(res as any).writeHead(502);
|
|
161
|
+
(res as any).end('Preview server unavailable');
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Middleware to extract app name, check access, reset idle timer
|
|
168
|
+
router.use('/apps/:appName', async (req: Request, res: Response, next) => {
|
|
169
|
+
const appName = req.params.appName as string;
|
|
170
|
+
const app = previewManager.get(appName);
|
|
171
|
+
|
|
172
|
+
if (!app) {
|
|
173
|
+
res.status(404).json({ error: `Preview app "${appName}" not found or not running` });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Enforce auth for private apps
|
|
178
|
+
if (!app.isPublic) {
|
|
179
|
+
// Use auth middleware inline — if it sends 401, we stop here
|
|
180
|
+
await new Promise<void>((resolve) => {
|
|
181
|
+
authMiddleware.requireAuth(req, res, (() => {
|
|
182
|
+
resolve();
|
|
183
|
+
}) as any);
|
|
184
|
+
});
|
|
185
|
+
// If response was already sent (401), don't proceed
|
|
186
|
+
if (res.headersSent) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Reset idle timer on access
|
|
192
|
+
previewManager.resetIdleTimer(appName);
|
|
193
|
+
|
|
194
|
+
// Attach app name for the proxy router function
|
|
195
|
+
(req as any).previewAppName = appName;
|
|
196
|
+
|
|
197
|
+
next();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Mount the proxy after the access-check middleware
|
|
201
|
+
router.use('/apps/:appName', proxy as any);
|
|
202
|
+
|
|
203
|
+
return router;
|
|
204
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createProjectRoutes } from './projects.ts';
|
|
4
|
+
import express from 'express';
|
|
5
|
+
import type { Server } from 'node:http';
|
|
6
|
+
|
|
7
|
+
const createMockProjectService = () => ({
|
|
8
|
+
listActive: mock.fn(async () => []),
|
|
9
|
+
create: mock.fn(async (name: string, type: string) => ({
|
|
10
|
+
id: 'proj_test1234',
|
|
11
|
+
name,
|
|
12
|
+
type,
|
|
13
|
+
isDefault: 0,
|
|
14
|
+
archivedAt: null,
|
|
15
|
+
repoUrl: null,
|
|
16
|
+
githubInstallationId: null,
|
|
17
|
+
githubRepoFullName: null,
|
|
18
|
+
baseBranch: null,
|
|
19
|
+
gitAuthMethod: null,
|
|
20
|
+
sshPrivateKeyEncrypted: null,
|
|
21
|
+
sshPublicKey: null,
|
|
22
|
+
previewCommand: null,
|
|
23
|
+
createdAt: '2026-03-11T00:00:00.000Z',
|
|
24
|
+
updatedAt: '2026-03-11T00:00:00.000Z',
|
|
25
|
+
})),
|
|
26
|
+
rename: mock.fn(),
|
|
27
|
+
updatePreviewCommand: mock.fn(async (id: string, cmd: string | null) => ({
|
|
28
|
+
id,
|
|
29
|
+
name: 'Test',
|
|
30
|
+
type: 'software',
|
|
31
|
+
isDefault: 0,
|
|
32
|
+
archivedAt: null,
|
|
33
|
+
previewCommand: cmd,
|
|
34
|
+
createdAt: '2026-03-11T00:00:00.000Z',
|
|
35
|
+
updatedAt: '2026-03-17T00:00:00.000Z',
|
|
36
|
+
})),
|
|
37
|
+
archive: mock.fn(),
|
|
38
|
+
restore: mock.fn(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const createMockWorkspaceService = (gitAvailable = true) => ({
|
|
42
|
+
verifyGitAvailable: mock.fn(async () => {
|
|
43
|
+
if (!gitAvailable) {
|
|
44
|
+
throw new Error('Git is required to create a project. Please install git and try again.');
|
|
45
|
+
}
|
|
46
|
+
}),
|
|
47
|
+
initWorkspace: mock.fn(async () => '/tmp/workspaces/proj_test1234'),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const createTestApp = (projectService: any, workspaceService: any) => {
|
|
51
|
+
const log = mock.fn();
|
|
52
|
+
const app = express();
|
|
53
|
+
app.use(express.json());
|
|
54
|
+
const routes = createProjectRoutes({ projectService, workspaceService, log });
|
|
55
|
+
app.use('/api/projects', routes);
|
|
56
|
+
return { app, log };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const request = (server: Server) => {
|
|
60
|
+
const addr = server.address() as { port: number };
|
|
61
|
+
const base = `http://127.0.0.1:${addr.port}`;
|
|
62
|
+
return {
|
|
63
|
+
post: async (path: string, body: any) => {
|
|
64
|
+
const res = await fetch(`${base}${path}`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'Content-Type': 'application/json' },
|
|
67
|
+
body: JSON.stringify(body),
|
|
68
|
+
});
|
|
69
|
+
return { status: res.status, body: await res.json() };
|
|
70
|
+
},
|
|
71
|
+
patch: async (path: string, body: any) => {
|
|
72
|
+
const res = await fetch(`${base}${path}`, {
|
|
73
|
+
method: 'PATCH',
|
|
74
|
+
headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
body: JSON.stringify(body),
|
|
76
|
+
});
|
|
77
|
+
return { status: res.status, body: await res.json() };
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
describe('Project Routes — POST /api/projects', () => {
|
|
83
|
+
let server: Server;
|
|
84
|
+
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
if (server) server.close();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('creates a project with git init when git is available', async () => {
|
|
90
|
+
const projectService = createMockProjectService();
|
|
91
|
+
const workspaceService = createMockWorkspaceService(true);
|
|
92
|
+
const { app } = createTestApp(projectService, workspaceService);
|
|
93
|
+
server = app.listen(0);
|
|
94
|
+
const req = request(server);
|
|
95
|
+
|
|
96
|
+
const res = await req.post('/api/projects', { name: 'My Project' });
|
|
97
|
+
|
|
98
|
+
assert.equal(res.status, 201);
|
|
99
|
+
assert.equal(res.body.project.name, 'My Project');
|
|
100
|
+
assert.equal(res.body.project.id, 'proj_test1234');
|
|
101
|
+
|
|
102
|
+
// Verify git was checked and workspace was initialized
|
|
103
|
+
assert.equal(workspaceService.verifyGitAvailable.mock.calls.length, 1);
|
|
104
|
+
assert.equal(workspaceService.initWorkspace.mock.calls.length, 1);
|
|
105
|
+
assert.equal(workspaceService.initWorkspace.mock.calls[0].arguments[0], 'proj_test1234');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('returns 400 when git is not available', async () => {
|
|
109
|
+
const projectService = createMockProjectService();
|
|
110
|
+
const workspaceService = createMockWorkspaceService(false);
|
|
111
|
+
const { app } = createTestApp(projectService, workspaceService);
|
|
112
|
+
server = app.listen(0);
|
|
113
|
+
const req = request(server);
|
|
114
|
+
|
|
115
|
+
const res = await req.post('/api/projects', { name: 'My Project' });
|
|
116
|
+
|
|
117
|
+
assert.equal(res.status, 400);
|
|
118
|
+
assert.equal(res.body.error, 'Git is required to create a project. Please install git and try again.');
|
|
119
|
+
|
|
120
|
+
// Project should NOT have been created
|
|
121
|
+
assert.equal(projectService.create.mock.calls.length, 0);
|
|
122
|
+
assert.equal(workspaceService.initWorkspace.mock.calls.length, 0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('returns 400 when project name is missing', async () => {
|
|
126
|
+
const projectService = createMockProjectService();
|
|
127
|
+
const workspaceService = createMockWorkspaceService(true);
|
|
128
|
+
const { app } = createTestApp(projectService, workspaceService);
|
|
129
|
+
server = app.listen(0);
|
|
130
|
+
const req = request(server);
|
|
131
|
+
|
|
132
|
+
const res = await req.post('/api/projects', {});
|
|
133
|
+
|
|
134
|
+
assert.equal(res.status, 400);
|
|
135
|
+
assert.equal(res.body.error, 'Project name is required');
|
|
136
|
+
|
|
137
|
+
// Neither git check nor creation should have been called
|
|
138
|
+
assert.equal(workspaceService.verifyGitAvailable.mock.calls.length, 0);
|
|
139
|
+
assert.equal(projectService.create.mock.calls.length, 0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('passes project type through to service', async () => {
|
|
143
|
+
const projectService = createMockProjectService();
|
|
144
|
+
const workspaceService = createMockWorkspaceService(true);
|
|
145
|
+
const { app } = createTestApp(projectService, workspaceService);
|
|
146
|
+
server = app.listen(0);
|
|
147
|
+
const req = request(server);
|
|
148
|
+
|
|
149
|
+
await req.post('/api/projects', { name: 'Video Proj', type: 'video' });
|
|
150
|
+
|
|
151
|
+
assert.equal(projectService.create.mock.calls[0].arguments[0], 'Video Proj');
|
|
152
|
+
assert.equal(projectService.create.mock.calls[0].arguments[1], 'video');
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('Project Routes — PATCH /api/projects/:id', () => {
|
|
157
|
+
let server: Server;
|
|
158
|
+
|
|
159
|
+
afterEach(() => {
|
|
160
|
+
if (server) server.close();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('updates preview command via PATCH', async () => {
|
|
164
|
+
const projectService = createMockProjectService();
|
|
165
|
+
const workspaceService = createMockWorkspaceService(true);
|
|
166
|
+
const { app } = createTestApp(projectService, workspaceService);
|
|
167
|
+
server = app.listen(0);
|
|
168
|
+
const req = request(server);
|
|
169
|
+
|
|
170
|
+
const res = await req.patch('/api/projects/proj_test1234', { previewCommand: 'npm run dev' });
|
|
171
|
+
|
|
172
|
+
assert.equal(res.status, 200);
|
|
173
|
+
assert.equal(res.body.project.previewCommand, 'npm run dev');
|
|
174
|
+
assert.equal(projectService.updatePreviewCommand.mock.calls.length, 1);
|
|
175
|
+
assert.equal(projectService.updatePreviewCommand.mock.calls[0].arguments[0], 'proj_test1234');
|
|
176
|
+
assert.equal(projectService.updatePreviewCommand.mock.calls[0].arguments[1], 'npm run dev');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('clears preview command when empty string is sent', async () => {
|
|
180
|
+
const projectService = createMockProjectService();
|
|
181
|
+
const workspaceService = createMockWorkspaceService(true);
|
|
182
|
+
const { app } = createTestApp(projectService, workspaceService);
|
|
183
|
+
server = app.listen(0);
|
|
184
|
+
const req = request(server);
|
|
185
|
+
|
|
186
|
+
const res = await req.patch('/api/projects/proj_test1234', { previewCommand: '' });
|
|
187
|
+
|
|
188
|
+
assert.equal(res.status, 200);
|
|
189
|
+
assert.equal(projectService.updatePreviewCommand.mock.calls.length, 1);
|
|
190
|
+
assert.equal(projectService.updatePreviewCommand.mock.calls[0].arguments[1], null);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('returns 400 when neither name nor previewCommand provided', async () => {
|
|
194
|
+
const projectService = createMockProjectService();
|
|
195
|
+
const workspaceService = createMockWorkspaceService(true);
|
|
196
|
+
const { app } = createTestApp(projectService, workspaceService);
|
|
197
|
+
server = app.listen(0);
|
|
198
|
+
const req = request(server);
|
|
199
|
+
|
|
200
|
+
const res = await req.patch('/api/projects/proj_test1234', {});
|
|
201
|
+
|
|
202
|
+
assert.equal(res.status, 400);
|
|
203
|
+
assert.equal(res.body.error, 'At least one of name or previewCommand is required');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -9,13 +9,15 @@
|
|
|
9
9
|
|
|
10
10
|
import { Router } from 'express';
|
|
11
11
|
import type { ProjectService } from '../services/project_service.js';
|
|
12
|
+
import type { ProjectWorkspaceService } from '../services/project_workspace_service.js';
|
|
12
13
|
|
|
13
14
|
interface ProjectRoutesDeps {
|
|
14
15
|
projectService: ProjectService;
|
|
16
|
+
workspaceService: ProjectWorkspaceService;
|
|
15
17
|
log: (tag: string, ...args: any[]) => void;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
export const createProjectRoutes = ({ projectService, log }: ProjectRoutesDeps): Router => {
|
|
20
|
+
export const createProjectRoutes = ({ projectService, workspaceService, log }: ProjectRoutesDeps): Router => {
|
|
19
21
|
const router: Router = Router();
|
|
20
22
|
|
|
21
23
|
// GET /api/projects — list active projects
|
|
@@ -44,8 +46,21 @@ export const createProjectRoutes = ({ projectService, log }: ProjectRoutesDeps):
|
|
|
44
46
|
const validTypes = ['software', 'video'];
|
|
45
47
|
const projectType = type && validTypes.includes(type) ? type : 'software';
|
|
46
48
|
|
|
49
|
+
try {
|
|
50
|
+
// Verify git is available before creating the project
|
|
51
|
+
await workspaceService.verifyGitAvailable();
|
|
52
|
+
} catch (err: any) {
|
|
53
|
+
log('PROJECTS', `Git verification failed: ${err.message}`);
|
|
54
|
+
res.status(400).json({ error: err.message });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
try {
|
|
48
59
|
const project = await projectService.create(name.trim(), projectType);
|
|
60
|
+
|
|
61
|
+
// Auto-initialize local git repo with an empty commit
|
|
62
|
+
await workspaceService.initWorkspace(project.id);
|
|
63
|
+
|
|
49
64
|
res.status(201).json({ project });
|
|
50
65
|
} catch (err: any) {
|
|
51
66
|
log('PROJECTS', `Create project failed: ${err.message}`);
|
|
@@ -53,27 +68,40 @@ export const createProjectRoutes = ({ projectService, log }: ProjectRoutesDeps):
|
|
|
53
68
|
}
|
|
54
69
|
});
|
|
55
70
|
|
|
56
|
-
// PATCH /api/projects/:id —
|
|
71
|
+
// PATCH /api/projects/:id — update a project (name and/or previewCommand)
|
|
57
72
|
router.patch('/:id', async (req, res) => {
|
|
58
73
|
const { id } = req.params;
|
|
59
|
-
const { name } = req.body;
|
|
60
|
-
log('PROJECTS', `PATCH /api/projects/${id} name="${name}"`);
|
|
74
|
+
const { name, previewCommand } = req.body;
|
|
75
|
+
log('PROJECTS', `PATCH /api/projects/${id} name="${name}" previewCommand="${previewCommand}"`);
|
|
61
76
|
|
|
62
|
-
|
|
63
|
-
|
|
77
|
+
// At least one field must be provided
|
|
78
|
+
const hasName = name && typeof name === 'string' && name.trim();
|
|
79
|
+
const hasPreviewCommand = previewCommand !== undefined;
|
|
80
|
+
|
|
81
|
+
if (!hasName && !hasPreviewCommand) {
|
|
82
|
+
res.status(400).json({ error: 'At least one of name or previewCommand is required' });
|
|
64
83
|
return;
|
|
65
84
|
}
|
|
66
85
|
|
|
67
86
|
try {
|
|
68
|
-
|
|
87
|
+
let project;
|
|
88
|
+
if (hasName) {
|
|
89
|
+
project = await projectService.rename(id, name.trim());
|
|
90
|
+
}
|
|
91
|
+
if (hasPreviewCommand) {
|
|
92
|
+
const cmd = typeof previewCommand === 'string' && previewCommand.trim()
|
|
93
|
+
? previewCommand.trim()
|
|
94
|
+
: null;
|
|
95
|
+
project = await projectService.updatePreviewCommand(id, cmd);
|
|
96
|
+
}
|
|
69
97
|
res.json({ project });
|
|
70
98
|
} catch (err: any) {
|
|
71
|
-
log('PROJECTS', `
|
|
99
|
+
log('PROJECTS', `Update project failed: ${err.message}`);
|
|
72
100
|
if (err.message === 'Project not found') {
|
|
73
101
|
res.status(404).json({ error: err.message });
|
|
74
102
|
return;
|
|
75
103
|
}
|
|
76
|
-
res.status(500).json({ error: 'Failed to
|
|
104
|
+
res.status(500).json({ error: 'Failed to update project' });
|
|
77
105
|
}
|
|
78
106
|
});
|
|
79
107
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, writeFile, mkdir, rm } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { createSkillRoutes, parseSkillMd } from './skills.ts';
|
|
7
|
+
import express from 'express';
|
|
8
|
+
import type { Server } from 'node:http';
|
|
9
|
+
|
|
10
|
+
/** Create a test app with skill routes mounted at /api/projects/:id/skills */
|
|
11
|
+
const createTestApp = (workspacePath: string) => {
|
|
12
|
+
const workspaceService = {
|
|
13
|
+
getWorkspacePath: (_projectId: string) => workspacePath,
|
|
14
|
+
} as any;
|
|
15
|
+
const log = mock.fn();
|
|
16
|
+
const app = express();
|
|
17
|
+
app.use(express.json());
|
|
18
|
+
const skillRoutes = createSkillRoutes({ workspaceService, log });
|
|
19
|
+
app.use('/api/projects/:id/skills', skillRoutes);
|
|
20
|
+
return { app, log };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Simple HTTP request helper using built-in fetch against an Express server */
|
|
24
|
+
const request = (server: Server) => {
|
|
25
|
+
const addr = server.address() as { port: number };
|
|
26
|
+
const base = `http://127.0.0.1:${addr.port}`;
|
|
27
|
+
return {
|
|
28
|
+
get: async (path: string) => {
|
|
29
|
+
const res = await fetch(`${base}${path}`);
|
|
30
|
+
return { status: res.status, body: await res.json() };
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe('Skills Routes', () => {
|
|
36
|
+
let tmpDir: string;
|
|
37
|
+
let server: Server;
|
|
38
|
+
let http: ReturnType<typeof request>;
|
|
39
|
+
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'skills-routes-test-'));
|
|
42
|
+
const { app } = createTestApp(tmpDir);
|
|
43
|
+
server = await new Promise<Server>((resolve) => {
|
|
44
|
+
const s = app.listen(0, '127.0.0.1', () => resolve(s));
|
|
45
|
+
});
|
|
46
|
+
http = request(server);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(async () => {
|
|
50
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
51
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('GET / — list skills', () => {
|
|
55
|
+
it('returns empty skills when .claude/skills does not exist', async () => {
|
|
56
|
+
const { status, body } = await http.get('/api/projects/proj_1/skills');
|
|
57
|
+
assert.equal(status, 200);
|
|
58
|
+
assert.deepEqual(body.skills, []);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns skill folders with parsed SKILL.md metadata', async () => {
|
|
62
|
+
const skillsDir = join(tmpDir, '.claude', 'skills', 'my-skill');
|
|
63
|
+
await mkdir(skillsDir, { recursive: true });
|
|
64
|
+
await writeFile(join(skillsDir, 'SKILL.md'), '# My Awesome Skill\nDoes amazing things.\n\n## Usage\n...');
|
|
65
|
+
|
|
66
|
+
const { status, body } = await http.get('/api/projects/proj_1/skills');
|
|
67
|
+
assert.equal(status, 200);
|
|
68
|
+
assert.equal(body.skills.length, 1);
|
|
69
|
+
assert.equal(body.skills[0].folderName, 'my-skill');
|
|
70
|
+
assert.equal(body.skills[0].name, 'My Awesome Skill');
|
|
71
|
+
assert.equal(body.skills[0].description, 'Does amazing things.');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('uses folder name as fallback when SKILL.md is missing', async () => {
|
|
75
|
+
const skillsDir = join(tmpDir, '.claude', 'skills', 'bare-skill');
|
|
76
|
+
await mkdir(skillsDir, { recursive: true });
|
|
77
|
+
|
|
78
|
+
const { status, body } = await http.get('/api/projects/proj_1/skills');
|
|
79
|
+
assert.equal(status, 200);
|
|
80
|
+
assert.equal(body.skills.length, 1);
|
|
81
|
+
assert.equal(body.skills[0].folderName, 'bare-skill');
|
|
82
|
+
assert.equal(body.skills[0].name, 'bare-skill');
|
|
83
|
+
assert.equal(body.skills[0].description, '');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('lists multiple skills sorted by discovery order', async () => {
|
|
87
|
+
const baseDir = join(tmpDir, '.claude', 'skills');
|
|
88
|
+
await mkdir(join(baseDir, 'alpha-skill'), { recursive: true });
|
|
89
|
+
await mkdir(join(baseDir, 'beta-skill'), { recursive: true });
|
|
90
|
+
await writeFile(join(baseDir, 'alpha-skill', 'SKILL.md'), '# Alpha\nFirst skill.');
|
|
91
|
+
await writeFile(join(baseDir, 'beta-skill', 'SKILL.md'), '# Beta\nSecond skill.');
|
|
92
|
+
|
|
93
|
+
const { status, body } = await http.get('/api/projects/proj_1/skills');
|
|
94
|
+
assert.equal(status, 200);
|
|
95
|
+
assert.equal(body.skills.length, 2);
|
|
96
|
+
const names = body.skills.map((s: any) => s.folderName);
|
|
97
|
+
assert.ok(names.includes('alpha-skill'));
|
|
98
|
+
assert.ok(names.includes('beta-skill'));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('ignores non-directory entries in skills folder', async () => {
|
|
102
|
+
const baseDir = join(tmpDir, '.claude', 'skills');
|
|
103
|
+
await mkdir(baseDir, { recursive: true });
|
|
104
|
+
await writeFile(join(baseDir, 'not-a-skill.txt'), 'just a file');
|
|
105
|
+
await mkdir(join(baseDir, 'real-skill'));
|
|
106
|
+
|
|
107
|
+
const { status, body } = await http.get('/api/projects/proj_1/skills');
|
|
108
|
+
assert.equal(status, 200);
|
|
109
|
+
assert.equal(body.skills.length, 1);
|
|
110
|
+
assert.equal(body.skills[0].folderName, 'real-skill');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('parseSkillMd', () => {
|
|
116
|
+
it('parses name and description from markdown', () => {
|
|
117
|
+
const result = parseSkillMd('# My Skill\nThis is the description.\n\n## Details\nMore info.');
|
|
118
|
+
assert.equal(result.name, 'My Skill');
|
|
119
|
+
assert.equal(result.description, 'This is the description.');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('returns empty strings for empty content', () => {
|
|
123
|
+
const result = parseSkillMd('');
|
|
124
|
+
assert.equal(result.name, '');
|
|
125
|
+
assert.equal(result.description, '');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('handles content with only a heading', () => {
|
|
129
|
+
const result = parseSkillMd('# Just A Heading');
|
|
130
|
+
assert.equal(result.name, 'Just A Heading');
|
|
131
|
+
assert.equal(result.description, '');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('skips sub-headings when looking for description', () => {
|
|
135
|
+
const result = parseSkillMd('# Name\n## Sub Heading\nActual description here.');
|
|
136
|
+
assert.equal(result.name, 'Name');
|
|
137
|
+
assert.equal(result.description, 'Actual description here.');
|
|
138
|
+
});
|
|
139
|
+
});
|