@assistkick/create 1.0.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/bin/create.d.ts +2 -0
- package/dist/bin/create.js +25 -0
- package/dist/bin/create.js.map +1 -0
- package/dist/src/scaffolder.d.ts +22 -0
- package/dist/src/scaffolder.js +120 -0
- package/dist/src/scaffolder.js.map +1 -0
- package/package.json +24 -0
- package/templates/product-system/.env.example +8 -0
- package/templates/product-system/CLAUDE.md +45 -0
- package/templates/product-system/package.json +32 -0
- package/templates/product-system/packages/backend/package.json +37 -0
- package/templates/product-system/packages/backend/src/middleware/auth_middleware.test.ts +86 -0
- package/templates/product-system/packages/backend/src/middleware/auth_middleware.ts +35 -0
- package/templates/product-system/packages/backend/src/routes/auth.ts +463 -0
- package/templates/product-system/packages/backend/src/routes/coherence.ts +187 -0
- package/templates/product-system/packages/backend/src/routes/graph.ts +67 -0
- package/templates/product-system/packages/backend/src/routes/kanban.ts +201 -0
- package/templates/product-system/packages/backend/src/routes/pipeline.ts +41 -0
- package/templates/product-system/packages/backend/src/routes/projects.ts +122 -0
- package/templates/product-system/packages/backend/src/routes/users.ts +97 -0
- package/templates/product-system/packages/backend/src/server.ts +159 -0
- package/templates/product-system/packages/backend/src/services/auth_service.test.ts +115 -0
- package/templates/product-system/packages/backend/src/services/auth_service.ts +82 -0
- package/templates/product-system/packages/backend/src/services/coherence-review.ts +339 -0
- package/templates/product-system/packages/backend/src/services/email_service.ts +75 -0
- package/templates/product-system/packages/backend/src/services/init.ts +80 -0
- package/templates/product-system/packages/backend/src/services/invitation_service.test.ts +235 -0
- package/templates/product-system/packages/backend/src/services/invitation_service.ts +193 -0
- package/templates/product-system/packages/backend/src/services/password_reset_service.test.ts +151 -0
- package/templates/product-system/packages/backend/src/services/password_reset_service.ts +135 -0
- package/templates/product-system/packages/backend/src/services/project_service.test.ts +215 -0
- package/templates/product-system/packages/backend/src/services/project_service.ts +171 -0
- package/templates/product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -0
- package/templates/product-system/packages/backend/src/services/pty_session_manager.ts +279 -0
- package/templates/product-system/packages/backend/src/services/terminal_ws_handler.ts +133 -0
- package/templates/product-system/packages/backend/src/services/user_management_service.test.ts +158 -0
- package/templates/product-system/packages/backend/src/services/user_management_service.ts +128 -0
- package/templates/product-system/packages/backend/tsconfig.json +22 -0
- package/templates/product-system/packages/frontend/index.html +13 -0
- package/templates/product-system/packages/frontend/package-lock.json +2666 -0
- package/templates/product-system/packages/frontend/package.json +30 -0
- package/templates/product-system/packages/frontend/public/favicon.svg +16 -0
- package/templates/product-system/packages/frontend/src/App.tsx +29 -0
- package/templates/product-system/packages/frontend/src/api/client.ts +386 -0
- package/templates/product-system/packages/frontend/src/api/client_projects.test.ts +104 -0
- package/templates/product-system/packages/frontend/src/api/client_refresh.test.ts +145 -0
- package/templates/product-system/packages/frontend/src/components/CoherenceView.tsx +414 -0
- package/templates/product-system/packages/frontend/src/components/GraphLegend.tsx +124 -0
- package/templates/product-system/packages/frontend/src/components/GraphSettings.tsx +112 -0
- package/templates/product-system/packages/frontend/src/components/GraphView.tsx +370 -0
- package/templates/product-system/packages/frontend/src/components/InviteUserDialog.tsx +85 -0
- package/templates/product-system/packages/frontend/src/components/KanbanView.tsx +470 -0
- package/templates/product-system/packages/frontend/src/components/LoginPage.tsx +116 -0
- package/templates/product-system/packages/frontend/src/components/ProjectSelector.tsx +187 -0
- package/templates/product-system/packages/frontend/src/components/QaIssueSheet.tsx +192 -0
- package/templates/product-system/packages/frontend/src/components/SidePanel.tsx +231 -0
- package/templates/product-system/packages/frontend/src/components/TerminalView.tsx +200 -0
- package/templates/product-system/packages/frontend/src/components/Toolbar.tsx +84 -0
- package/templates/product-system/packages/frontend/src/components/UsersView.tsx +249 -0
- package/templates/product-system/packages/frontend/src/constants/graph.ts +191 -0
- package/templates/product-system/packages/frontend/src/hooks/useAuth.tsx +54 -0
- package/templates/product-system/packages/frontend/src/hooks/useGraph.ts +27 -0
- package/templates/product-system/packages/frontend/src/hooks/useKanban.ts +21 -0
- package/templates/product-system/packages/frontend/src/hooks/useProjects.ts +86 -0
- package/templates/product-system/packages/frontend/src/hooks/useTheme.ts +26 -0
- package/templates/product-system/packages/frontend/src/hooks/useToast.tsx +62 -0
- package/templates/product-system/packages/frontend/src/hooks/use_projects_logic.test.ts +61 -0
- package/templates/product-system/packages/frontend/src/main.tsx +12 -0
- package/templates/product-system/packages/frontend/src/pages/accept_invitation_page.tsx +167 -0
- package/templates/product-system/packages/frontend/src/pages/forgot_password_page.tsx +100 -0
- package/templates/product-system/packages/frontend/src/pages/register_page.tsx +137 -0
- package/templates/product-system/packages/frontend/src/pages/reset_password_page.tsx +146 -0
- package/templates/product-system/packages/frontend/src/routes/ProtectedRoute.tsx +12 -0
- package/templates/product-system/packages/frontend/src/routes/accept_invitation.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/dashboard.tsx +221 -0
- package/templates/product-system/packages/frontend/src/routes/forgot_password.tsx +13 -0
- package/templates/product-system/packages/frontend/src/routes/login.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/register.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/reset_password.tsx +13 -0
- package/templates/product-system/packages/frontend/src/styles/index.css +3358 -0
- package/templates/product-system/packages/frontend/src/utils/auth_validation.test.ts +51 -0
- package/templates/product-system/packages/frontend/src/utils/auth_validation.ts +19 -0
- package/templates/product-system/packages/frontend/src/utils/login_validation.test.ts +61 -0
- package/templates/product-system/packages/frontend/src/utils/login_validation.ts +24 -0
- package/templates/product-system/packages/frontend/src/utils/logout.test.ts +63 -0
- package/templates/product-system/packages/frontend/src/utils/node_sizing.test.ts +62 -0
- package/templates/product-system/packages/frontend/src/utils/node_sizing.ts +24 -0
- package/templates/product-system/packages/frontend/src/utils/task_status.test.ts +53 -0
- package/templates/product-system/packages/frontend/src/utils/task_status.ts +14 -0
- package/templates/product-system/packages/frontend/tsconfig.json +21 -0
- package/templates/product-system/packages/frontend/vite.config.ts +20 -0
- package/templates/product-system/packages/shared/.env.example +3 -0
- package/templates/product-system/packages/shared/README.md +1 -0
- package/templates/product-system/packages/shared/db/migrate.ts +32 -0
- package/templates/product-system/packages/shared/db/migrations/0000_dashing_gorgon.sql +128 -0
- package/templates/product-system/packages/shared/db/migrations/meta/0000_snapshot.json +819 -0
- package/templates/product-system/packages/shared/db/migrations/meta/_journal.json +13 -0
- package/templates/product-system/packages/shared/db/schema.ts +137 -0
- package/templates/product-system/packages/shared/drizzle.config.js +14 -0
- package/templates/product-system/packages/shared/lib/claude-service.ts +215 -0
- package/templates/product-system/packages/shared/lib/coherence.ts +278 -0
- package/templates/product-system/packages/shared/lib/completeness.ts +30 -0
- package/templates/product-system/packages/shared/lib/constants.ts +327 -0
- package/templates/product-system/packages/shared/lib/db.ts +81 -0
- package/templates/product-system/packages/shared/lib/git_workflow.ts +110 -0
- package/templates/product-system/packages/shared/lib/graph.ts +186 -0
- package/templates/product-system/packages/shared/lib/kanban.ts +161 -0
- package/templates/product-system/packages/shared/lib/markdown.ts +205 -0
- package/templates/product-system/packages/shared/lib/pipeline-state-store.ts +124 -0
- package/templates/product-system/packages/shared/lib/pipeline.ts +489 -0
- package/templates/product-system/packages/shared/lib/prompt_builder.ts +170 -0
- package/templates/product-system/packages/shared/lib/relevance_search.ts +159 -0
- package/templates/product-system/packages/shared/lib/session.ts +152 -0
- package/templates/product-system/packages/shared/lib/validator.ts +117 -0
- package/templates/product-system/packages/shared/lib/work_summary_parser.ts +130 -0
- package/templates/product-system/packages/shared/package.json +30 -0
- package/templates/product-system/packages/shared/scripts/assign-project.ts +52 -0
- package/templates/product-system/packages/shared/tools/add_edge.ts +61 -0
- package/templates/product-system/packages/shared/tools/add_node.ts +101 -0
- package/templates/product-system/packages/shared/tools/end_session.ts +87 -0
- package/templates/product-system/packages/shared/tools/get_gaps.ts +87 -0
- package/templates/product-system/packages/shared/tools/get_kanban.ts +125 -0
- package/templates/product-system/packages/shared/tools/get_node.ts +78 -0
- package/templates/product-system/packages/shared/tools/get_status.ts +98 -0
- package/templates/product-system/packages/shared/tools/migrate_to_turso.ts +385 -0
- package/templates/product-system/packages/shared/tools/move_card.ts +143 -0
- package/templates/product-system/packages/shared/tools/rebuild_index.ts +77 -0
- package/templates/product-system/packages/shared/tools/remove_edge.ts +59 -0
- package/templates/product-system/packages/shared/tools/remove_node.ts +96 -0
- package/templates/product-system/packages/shared/tools/resolve_question.ts +75 -0
- package/templates/product-system/packages/shared/tools/search_nodes.ts +106 -0
- package/templates/product-system/packages/shared/tools/start_session.ts +144 -0
- package/templates/product-system/packages/shared/tools/update_node.ts +133 -0
- package/templates/product-system/packages/shared/tsconfig.json +24 -0
- package/templates/product-system/pnpm-workspace.yaml +2 -0
- package/templates/product-system/smoke_test.ts +219 -0
- package/templates/product-system/tests/coherence_review.test.ts +562 -0
- package/templates/product-system/tests/db_sqlite_fallback.test.ts +75 -0
- package/templates/product-system/tests/edge_type_color_coding.test.ts +147 -0
- package/templates/product-system/tests/emit-tool-use-events.test.ts +85 -0
- package/templates/product-system/tests/feature_kind.test.ts +139 -0
- package/templates/product-system/tests/gap_indicators.test.ts +199 -0
- package/templates/product-system/tests/graceful_init.test.ts +142 -0
- package/templates/product-system/tests/graph_legend.test.ts +314 -0
- package/templates/product-system/tests/graph_settings_sheet.test.ts +804 -0
- package/templates/product-system/tests/hide_defined_filter.test.ts +205 -0
- package/templates/product-system/tests/kanban.test.ts +529 -0
- package/templates/product-system/tests/neighborhood_focus.test.ts +132 -0
- package/templates/product-system/tests/node_search.test.ts +340 -0
- package/templates/product-system/tests/node_sizing.test.ts +170 -0
- package/templates/product-system/tests/node_type_toggle_filters.test.ts +285 -0
- package/templates/product-system/tests/node_type_visual_encoding.test.ts +103 -0
- package/templates/product-system/tests/pipeline-state-store.test.ts +268 -0
- package/templates/product-system/tests/pipeline-unit.test.ts +593 -0
- package/templates/product-system/tests/pipeline.test.ts +195 -0
- package/templates/product-system/tests/pipeline_stats_all_cards.test.ts +193 -0
- package/templates/product-system/tests/play_all.test.ts +296 -0
- package/templates/product-system/tests/qa_issue_sheet.test.ts +464 -0
- package/templates/product-system/tests/relevance_search.test.ts +186 -0
- package/templates/product-system/tests/search_reorder.test.ts +88 -0
- package/templates/product-system/tests/serve_ui.test.ts +281 -0
- package/templates/product-system/tests/serve_ui_drizzle.test.ts +114 -0
- package/templates/product-system/tests/session_context_recall.test.ts +135 -0
- package/templates/product-system/tests/side_panel.test.ts +345 -0
- package/templates/product-system/tests/spec_completeness_label.test.ts +69 -0
- package/templates/product-system/tests/url_routing_test.ts +122 -0
- package/templates/product-system/tests/user_login.test.ts +150 -0
- package/templates/product-system/tests/user_registration.test.ts +205 -0
- package/templates/product-system/tests/web_terminal.test.ts +572 -0
- package/templates/product-system/tests/work_summary.test.ts +211 -0
- package/templates/product-system/tests/zoom_pan.test.ts +43 -0
- package/templates/product-system/tsconfig.json +24 -0
- package/templates/skills/product-bootstrap/SKILL.md +312 -0
- package/templates/skills/product-code-reviewer/SKILL.md +147 -0
- package/templates/skills/product-debugger/SKILL.md +206 -0
- package/templates/skills/product-debugger/references/agent-browser.md +1156 -0
- package/templates/skills/product-developer/SKILL.md +182 -0
- package/templates/skills/product-interview/SKILL.md +220 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@interview-system/frontend",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"clean": "rm -rf dist"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
14
|
+
"@xterm/addon-web-links": "^0.12.0",
|
|
15
|
+
"@xterm/xterm": "^6.0.0",
|
|
16
|
+
"d3": "^7.9.0",
|
|
17
|
+
"marked": "^15.0.0",
|
|
18
|
+
"react": "^19.1.0",
|
|
19
|
+
"react-dom": "^19.1.0",
|
|
20
|
+
"react-router-dom": "^7.6.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/d3": "^7.4.3",
|
|
24
|
+
"@types/react": "^19.1.0",
|
|
25
|
+
"@types/react-dom": "^19.1.0",
|
|
26
|
+
"@vitejs/plugin-react": "^4.5.2",
|
|
27
|
+
"typescript": "^5.9.3",
|
|
28
|
+
"vite": "^7.3.1"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
|
+
<!-- Background rounded square -->
|
|
3
|
+
<rect width="32" height="32" rx="6" fill="#1a1b1e"/>
|
|
4
|
+
<!-- Edges connecting nodes -->
|
|
5
|
+
<line x1="10" y1="9" x2="22" y2="9" stroke="#373a40" stroke-width="1.5"/>
|
|
6
|
+
<line x1="10" y1="9" x2="16" y2="22" stroke="#373a40" stroke-width="1.5"/>
|
|
7
|
+
<line x1="22" y1="9" x2="16" y2="22" stroke="#373a40" stroke-width="1.5"/>
|
|
8
|
+
<line x1="22" y1="9" x2="26" y2="18" stroke="#373a40" stroke-width="1.5"/>
|
|
9
|
+
<line x1="16" y1="22" x2="6" y2="20" stroke="#373a40" stroke-width="1.5"/>
|
|
10
|
+
<!-- Nodes -->
|
|
11
|
+
<circle cx="10" cy="9" r="3.5" fill="#4dabf7"/>
|
|
12
|
+
<circle cx="22" cy="9" r="3" fill="#69db7c"/>
|
|
13
|
+
<circle cx="16" cy="22" r="3.5" fill="#4dabf7"/>
|
|
14
|
+
<circle cx="26" cy="18" r="2.5" fill="#909296"/>
|
|
15
|
+
<circle cx="6" cy="20" r="2.5" fill="#69db7c"/>
|
|
16
|
+
</svg>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Routes, Route, Navigate } from 'react-router-dom';
|
|
3
|
+
import { ProtectedRoute } from './routes/ProtectedRoute';
|
|
4
|
+
import { LoginRoute } from './routes/login';
|
|
5
|
+
import { RegisterRoute } from './routes/register';
|
|
6
|
+
import { ForgotPasswordRoute } from './routes/forgot_password';
|
|
7
|
+
import { ResetPasswordRoute } from './routes/reset_password';
|
|
8
|
+
import { AcceptInvitationRoute } from './routes/accept_invitation';
|
|
9
|
+
import { DashboardRoute } from './routes/dashboard';
|
|
10
|
+
|
|
11
|
+
export function App() {
|
|
12
|
+
return (
|
|
13
|
+
<Routes>
|
|
14
|
+
<Route path="/login" element={<LoginRoute />} />
|
|
15
|
+
<Route path="/register" element={<RegisterRoute />} />
|
|
16
|
+
<Route path="/forgot-password" element={<ForgotPasswordRoute />} />
|
|
17
|
+
<Route path="/reset-password" element={<ResetPasswordRoute />} />
|
|
18
|
+
<Route path="/accept-invitation" element={<AcceptInvitationRoute />} />
|
|
19
|
+
<Route element={<ProtectedRoute />}>
|
|
20
|
+
<Route path="/" element={<Navigate to="/graph" replace />} />
|
|
21
|
+
<Route path="/graph" element={<DashboardRoute />} />
|
|
22
|
+
<Route path="/kanban" element={<DashboardRoute />} />
|
|
23
|
+
<Route path="/coherence" element={<DashboardRoute />} />
|
|
24
|
+
<Route path="/users" element={<DashboardRoute />} />
|
|
25
|
+
<Route path="/terminal" element={<DashboardRoute />} />
|
|
26
|
+
</Route>
|
|
27
|
+
</Routes>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API client for fetching graph data and node content.
|
|
3
|
+
* All HTTP calls to the server go through this class.
|
|
4
|
+
*/
|
|
5
|
+
export class ApiClient {
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
private refreshPromise: Promise<boolean> | null = null;
|
|
8
|
+
|
|
9
|
+
constructor(baseUrl = '') {
|
|
10
|
+
this.baseUrl = baseUrl;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private attemptRefresh = async (): Promise<boolean> => {
|
|
14
|
+
try {
|
|
15
|
+
const resp = await fetch(`${this.baseUrl}/api/auth/refresh`, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
credentials: 'same-origin',
|
|
18
|
+
});
|
|
19
|
+
return resp.ok;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
private fetchWithRefresh = async (url: string, options: RequestInit = {}): Promise<Response> => {
|
|
26
|
+
const opts = { ...options, credentials: 'same-origin' as RequestCredentials };
|
|
27
|
+
const resp = await fetch(url, opts);
|
|
28
|
+
|
|
29
|
+
if (resp.status !== 401) return resp;
|
|
30
|
+
|
|
31
|
+
// Deduplicate concurrent refresh attempts
|
|
32
|
+
if (!this.refreshPromise) {
|
|
33
|
+
this.refreshPromise = this.attemptRefresh().finally(() => {
|
|
34
|
+
this.refreshPromise = null;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const refreshed = await this.refreshPromise;
|
|
39
|
+
if (!refreshed) return resp;
|
|
40
|
+
|
|
41
|
+
// Retry the original request with the new access token
|
|
42
|
+
return fetch(url, opts);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
fetchGraph = async (projectId?: string) => {
|
|
46
|
+
const url = projectId
|
|
47
|
+
? `${this.baseUrl}/api/graph?project_id=${encodeURIComponent(projectId)}`
|
|
48
|
+
: `${this.baseUrl}/api/graph`;
|
|
49
|
+
const resp = await this.fetchWithRefresh(url);
|
|
50
|
+
if (!resp.ok) throw new Error(`Failed to fetch graph: ${resp.status}`);
|
|
51
|
+
return resp.json();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
fetchNode = async (nodeId: string) => {
|
|
55
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/node/${nodeId}`);
|
|
56
|
+
if (!resp.ok) throw new Error(`Failed to fetch node ${nodeId}: ${resp.status}`);
|
|
57
|
+
return resp.json();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
fetchKanban = async (projectId?: string) => {
|
|
61
|
+
const url = projectId
|
|
62
|
+
? `${this.baseUrl}/api/kanban?project_id=${encodeURIComponent(projectId)}`
|
|
63
|
+
: `${this.baseUrl}/api/kanban`;
|
|
64
|
+
const resp = await this.fetchWithRefresh(url);
|
|
65
|
+
if (!resp.ok) throw new Error(`Failed to fetch kanban: ${resp.status}`);
|
|
66
|
+
return resp.json();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
moveCard = async (featureId: string, column: string) => {
|
|
70
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/kanban/${featureId}/move`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
body: JSON.stringify({ column }),
|
|
74
|
+
});
|
|
75
|
+
if (!resp.ok) {
|
|
76
|
+
const err = await resp.json();
|
|
77
|
+
throw new Error(err.error || `Failed to move card: ${resp.status}`);
|
|
78
|
+
}
|
|
79
|
+
return resp.json();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
createNote = async (featureId: string, text: string) => {
|
|
83
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/kanban/${featureId}/notes`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
body: JSON.stringify({ text }),
|
|
87
|
+
});
|
|
88
|
+
if (!resp.ok) throw new Error(`Failed to create note: ${resp.status}`);
|
|
89
|
+
return resp.json();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
updateNote = async (featureId: string, noteId: string, text: string) => {
|
|
93
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/kanban/${featureId}/notes/${noteId}`, {
|
|
94
|
+
method: 'PUT',
|
|
95
|
+
headers: { 'Content-Type': 'application/json' },
|
|
96
|
+
body: JSON.stringify({ text }),
|
|
97
|
+
});
|
|
98
|
+
if (!resp.ok) throw new Error(`Failed to update note: ${resp.status}`);
|
|
99
|
+
return resp.json();
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
deleteNote = async (featureId: string, noteId: string) => {
|
|
103
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/kanban/${featureId}/notes/${noteId}`, {
|
|
104
|
+
method: 'DELETE',
|
|
105
|
+
});
|
|
106
|
+
if (!resp.ok) throw new Error(`Failed to delete note: ${resp.status}`);
|
|
107
|
+
return resp.json();
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
startPipeline = async (featureId: string) => {
|
|
111
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/kanban/${featureId}/develop`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: { 'Content-Type': 'application/json' },
|
|
114
|
+
});
|
|
115
|
+
if (!resp.ok) {
|
|
116
|
+
let msg = `Failed to start pipeline: ${resp.status}`;
|
|
117
|
+
try { const err = await resp.json(); msg = err.error || msg; } catch { /* non-JSON response */ }
|
|
118
|
+
throw new Error(msg);
|
|
119
|
+
}
|
|
120
|
+
return resp.json();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
getPipelineStatus = async (featureId: string) => {
|
|
124
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/kanban/${featureId}/pipeline`);
|
|
125
|
+
if (!resp.ok) throw new Error(`Failed to get pipeline status: ${resp.status}`);
|
|
126
|
+
return resp.json();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
fetchCoherence = async (projectId?: string) => {
|
|
130
|
+
const qs = projectId ? `?project_id=${encodeURIComponent(projectId)}` : '';
|
|
131
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/coherence${qs}`);
|
|
132
|
+
if (!resp.ok) throw new Error(`Failed to fetch coherence: ${resp.status}`);
|
|
133
|
+
return resp.json();
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
runCoherenceReview = async (projectId?: string) => {
|
|
137
|
+
const qs = projectId ? `?project_id=${encodeURIComponent(projectId)}` : '';
|
|
138
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/coherence/run${qs}`, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: { 'Content-Type': 'application/json' },
|
|
141
|
+
});
|
|
142
|
+
if (!resp.ok) {
|
|
143
|
+
let msg = `Failed to start coherence review: ${resp.status}`;
|
|
144
|
+
try { const err = await resp.json(); msg = err.error || msg; } catch { /* non-JSON */ }
|
|
145
|
+
throw new Error(msg);
|
|
146
|
+
}
|
|
147
|
+
return resp.json();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
approveProposal = async (proposalId: string, projectId?: string) => {
|
|
151
|
+
const qs = projectId ? `?project_id=${encodeURIComponent(projectId)}` : '';
|
|
152
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/coherence/${proposalId}/approve${qs}`, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: { 'Content-Type': 'application/json' },
|
|
155
|
+
});
|
|
156
|
+
if (!resp.ok) {
|
|
157
|
+
let msg = `Failed to approve proposal: ${resp.status}`;
|
|
158
|
+
try { const err = await resp.json(); msg = err.error || msg; } catch { /* non-JSON */ }
|
|
159
|
+
throw new Error(msg);
|
|
160
|
+
}
|
|
161
|
+
return resp.json();
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
dismissProposal = async (proposalId: string, projectId?: string) => {
|
|
165
|
+
const qs = projectId ? `?project_id=${encodeURIComponent(projectId)}` : '';
|
|
166
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/coherence/${proposalId}/dismiss${qs}`, {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: { 'Content-Type': 'application/json' },
|
|
169
|
+
});
|
|
170
|
+
if (!resp.ok) {
|
|
171
|
+
let msg = `Failed to dismiss proposal: ${resp.status}`;
|
|
172
|
+
try { const err = await resp.json(); msg = err.error || msg; } catch { /* non-JSON */ }
|
|
173
|
+
throw new Error(msg);
|
|
174
|
+
}
|
|
175
|
+
return resp.json();
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
batchApprove = async (batchId: string, dismissIds: string[] = [], projectId?: string) => {
|
|
179
|
+
const qs = projectId ? `?project_id=${encodeURIComponent(projectId)}` : '';
|
|
180
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/coherence/batch/approve${qs}`, {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
headers: { 'Content-Type': 'application/json' },
|
|
183
|
+
body: JSON.stringify({ batch_id: batchId, dismiss_ids: dismissIds }),
|
|
184
|
+
});
|
|
185
|
+
if (!resp.ok) {
|
|
186
|
+
let msg = `Failed to batch approve: ${resp.status}`;
|
|
187
|
+
try { const err = await resp.json(); msg = err.error || msg; } catch { /* non-JSON */ }
|
|
188
|
+
throw new Error(msg);
|
|
189
|
+
}
|
|
190
|
+
return resp.json();
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
unblockCard = async (featureId: string) => {
|
|
194
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/kanban/${featureId}/unblock`, {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
headers: { 'Content-Type': 'application/json' },
|
|
197
|
+
});
|
|
198
|
+
if (!resp.ok) {
|
|
199
|
+
let msg = `Failed to unblock card: ${resp.status}`;
|
|
200
|
+
try { const err = await resp.json(); msg = err.error || msg; } catch { /* non-JSON response */ }
|
|
201
|
+
throw new Error(msg);
|
|
202
|
+
}
|
|
203
|
+
return resp.json();
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
logout = async () => {
|
|
207
|
+
const resp = await fetch(`${this.baseUrl}/api/auth/logout`, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
credentials: 'same-origin',
|
|
210
|
+
});
|
|
211
|
+
if (!resp.ok) throw new Error(`Logout failed: ${resp.status}`);
|
|
212
|
+
return resp.json();
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
forgotPassword = async (email: string) => {
|
|
216
|
+
const resp = await fetch(`${this.baseUrl}/api/auth/forgot-password`, {
|
|
217
|
+
method: 'POST',
|
|
218
|
+
headers: { 'Content-Type': 'application/json' },
|
|
219
|
+
body: JSON.stringify({ email }),
|
|
220
|
+
});
|
|
221
|
+
const data = await resp.json();
|
|
222
|
+
if (!resp.ok) {
|
|
223
|
+
throw new Error(data.error || `Request failed: ${resp.status}`);
|
|
224
|
+
}
|
|
225
|
+
return data;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
resetPassword = async (token: string, password: string) => {
|
|
229
|
+
const resp = await fetch(`${this.baseUrl}/api/auth/reset-password`, {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: { 'Content-Type': 'application/json' },
|
|
232
|
+
body: JSON.stringify({ token, password }),
|
|
233
|
+
});
|
|
234
|
+
const data = await resp.json();
|
|
235
|
+
if (!resp.ok) {
|
|
236
|
+
throw new Error(data.error || `Reset failed: ${resp.status}`);
|
|
237
|
+
}
|
|
238
|
+
return data;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
register = async (email: string, password: string) => {
|
|
242
|
+
const resp = await fetch(`${this.baseUrl}/api/auth/register`, {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers: { 'Content-Type': 'application/json' },
|
|
245
|
+
credentials: 'same-origin',
|
|
246
|
+
body: JSON.stringify({ email, password }),
|
|
247
|
+
});
|
|
248
|
+
const data = await resp.json();
|
|
249
|
+
if (!resp.ok) {
|
|
250
|
+
throw new Error(data.error || `Registration failed: ${resp.status}`);
|
|
251
|
+
}
|
|
252
|
+
return data;
|
|
253
|
+
};
|
|
254
|
+
sendInvitation = async (email: string) => {
|
|
255
|
+
const resp = await fetch(`${this.baseUrl}/api/auth/invite`, {
|
|
256
|
+
method: 'POST',
|
|
257
|
+
headers: { 'Content-Type': 'application/json' },
|
|
258
|
+
credentials: 'same-origin',
|
|
259
|
+
body: JSON.stringify({ email }),
|
|
260
|
+
});
|
|
261
|
+
const data = await resp.json();
|
|
262
|
+
if (!resp.ok) {
|
|
263
|
+
throw new Error(data.error || `Invitation failed: ${resp.status}`);
|
|
264
|
+
}
|
|
265
|
+
return data;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
validateInvitation = async (token: string, email: string) => {
|
|
269
|
+
const resp = await fetch(
|
|
270
|
+
`${this.baseUrl}/api/auth/invitation?token=${encodeURIComponent(token)}&email=${encodeURIComponent(email)}`,
|
|
271
|
+
{ credentials: 'same-origin' },
|
|
272
|
+
);
|
|
273
|
+
const data = await resp.json();
|
|
274
|
+
if (!resp.ok) {
|
|
275
|
+
throw new Error(data.error || `Validation failed: ${resp.status}`);
|
|
276
|
+
}
|
|
277
|
+
return data;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
listUsers = async () => {
|
|
281
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/users`);
|
|
282
|
+
if (!resp.ok) throw new Error(`Failed to list users: ${resp.status}`);
|
|
283
|
+
return resp.json();
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
deleteUser = async (userId: string) => {
|
|
287
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/users/${userId}`, {
|
|
288
|
+
method: 'DELETE',
|
|
289
|
+
});
|
|
290
|
+
if (!resp.ok) {
|
|
291
|
+
const data = await resp.json();
|
|
292
|
+
throw new Error(data.error || `Failed to delete user: ${resp.status}`);
|
|
293
|
+
}
|
|
294
|
+
return resp.json();
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
listInvitations = async () => {
|
|
298
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/users/invitations`);
|
|
299
|
+
if (!resp.ok) throw new Error(`Failed to list invitations: ${resp.status}`);
|
|
300
|
+
return resp.json();
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
deleteInvitation = async (invitationId: string) => {
|
|
304
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/users/invitations/${invitationId}`, {
|
|
305
|
+
method: 'DELETE',
|
|
306
|
+
});
|
|
307
|
+
if (!resp.ok) {
|
|
308
|
+
const data = await resp.json();
|
|
309
|
+
throw new Error(data.error || `Failed to delete invitation: ${resp.status}`);
|
|
310
|
+
}
|
|
311
|
+
return resp.json();
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Project management
|
|
315
|
+
fetchProjects = async () => {
|
|
316
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/projects`);
|
|
317
|
+
if (!resp.ok) throw new Error(`Failed to fetch projects: ${resp.status}`);
|
|
318
|
+
return resp.json();
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
createProject = async (name: string) => {
|
|
322
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/projects`, {
|
|
323
|
+
method: 'POST',
|
|
324
|
+
headers: { 'Content-Type': 'application/json' },
|
|
325
|
+
body: JSON.stringify({ name }),
|
|
326
|
+
});
|
|
327
|
+
if (!resp.ok) {
|
|
328
|
+
const data = await resp.json();
|
|
329
|
+
throw new Error(data.error || `Failed to create project: ${resp.status}`);
|
|
330
|
+
}
|
|
331
|
+
return resp.json();
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
renameProject = async (id: string, name: string) => {
|
|
335
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/projects/${id}`, {
|
|
336
|
+
method: 'PATCH',
|
|
337
|
+
headers: { 'Content-Type': 'application/json' },
|
|
338
|
+
body: JSON.stringify({ name }),
|
|
339
|
+
});
|
|
340
|
+
if (!resp.ok) {
|
|
341
|
+
const data = await resp.json();
|
|
342
|
+
throw new Error(data.error || `Failed to rename project: ${resp.status}`);
|
|
343
|
+
}
|
|
344
|
+
return resp.json();
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
archiveProject = async (id: string) => {
|
|
348
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/projects/${id}/archive`, {
|
|
349
|
+
method: 'POST',
|
|
350
|
+
headers: { 'Content-Type': 'application/json' },
|
|
351
|
+
});
|
|
352
|
+
if (!resp.ok) {
|
|
353
|
+
const data = await resp.json();
|
|
354
|
+
throw new Error(data.error || `Failed to archive project: ${resp.status}`);
|
|
355
|
+
}
|
|
356
|
+
return resp.json();
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
restoreProject = async (id: string) => {
|
|
360
|
+
const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/projects/${id}/restore`, {
|
|
361
|
+
method: 'POST',
|
|
362
|
+
headers: { 'Content-Type': 'application/json' },
|
|
363
|
+
});
|
|
364
|
+
if (!resp.ok) {
|
|
365
|
+
const data = await resp.json();
|
|
366
|
+
throw new Error(data.error || `Failed to restore project: ${resp.status}`);
|
|
367
|
+
}
|
|
368
|
+
return resp.json();
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
acceptInvitation = async (token: string, email: string, password: string) => {
|
|
372
|
+
const resp = await fetch(`${this.baseUrl}/api/auth/accept-invitation`, {
|
|
373
|
+
method: 'POST',
|
|
374
|
+
headers: { 'Content-Type': 'application/json' },
|
|
375
|
+
credentials: 'same-origin',
|
|
376
|
+
body: JSON.stringify({ token, email, password }),
|
|
377
|
+
});
|
|
378
|
+
const data = await resp.json();
|
|
379
|
+
if (!resp.ok) {
|
|
380
|
+
throw new Error(data.error || `Accept invitation failed: ${resp.status}`);
|
|
381
|
+
}
|
|
382
|
+
return data;
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export const apiClient = new ApiClient();
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, mock, beforeEach, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { ApiClient } from './client.ts';
|
|
4
|
+
|
|
5
|
+
describe('ApiClient project methods', () => {
|
|
6
|
+
let originalFetch: typeof globalThis.fetch;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
originalFetch = globalThis.fetch;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
globalThis.fetch = originalFetch;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('fetchProjects returns list of projects', async () => {
|
|
17
|
+
const projects = [{ id: 'proj_001', name: 'Test', isDefault: 1 }];
|
|
18
|
+
globalThis.fetch = mock.fn(async () => ({
|
|
19
|
+
ok: true,
|
|
20
|
+
status: 200,
|
|
21
|
+
json: async () => ({ projects }),
|
|
22
|
+
})) as any;
|
|
23
|
+
|
|
24
|
+
const client = new ApiClient();
|
|
25
|
+
const result = await client.fetchProjects();
|
|
26
|
+
assert.deepEqual(result.projects, projects);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('createProject sends name and returns created project', async () => {
|
|
30
|
+
const project = { id: 'proj_002', name: 'New Project' };
|
|
31
|
+
let capturedBody: string | undefined;
|
|
32
|
+
|
|
33
|
+
globalThis.fetch = mock.fn(async (_url: any, init?: any) => {
|
|
34
|
+
capturedBody = init?.body;
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
status: 201,
|
|
38
|
+
json: async () => ({ project }),
|
|
39
|
+
};
|
|
40
|
+
}) as any;
|
|
41
|
+
|
|
42
|
+
const client = new ApiClient();
|
|
43
|
+
const result = await client.createProject('New Project');
|
|
44
|
+
assert.deepEqual(result.project, project);
|
|
45
|
+
assert.equal(JSON.parse(capturedBody!).name, 'New Project');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('renameProject sends PATCH with new name', async () => {
|
|
49
|
+
let capturedUrl: string | undefined;
|
|
50
|
+
let capturedMethod: string | undefined;
|
|
51
|
+
let capturedBody: string | undefined;
|
|
52
|
+
|
|
53
|
+
globalThis.fetch = mock.fn(async (url: any, init?: any) => {
|
|
54
|
+
capturedUrl = url;
|
|
55
|
+
capturedMethod = init?.method;
|
|
56
|
+
capturedBody = init?.body;
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
status: 200,
|
|
60
|
+
json: async () => ({ project: { id: 'proj_001', name: 'Renamed' } }),
|
|
61
|
+
};
|
|
62
|
+
}) as any;
|
|
63
|
+
|
|
64
|
+
const client = new ApiClient();
|
|
65
|
+
await client.renameProject('proj_001', 'Renamed');
|
|
66
|
+
assert.ok(capturedUrl!.includes('/api/projects/proj_001'));
|
|
67
|
+
assert.equal(capturedMethod, 'PATCH');
|
|
68
|
+
assert.equal(JSON.parse(capturedBody!).name, 'Renamed');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('archiveProject sends POST to archive endpoint', async () => {
|
|
72
|
+
let capturedUrl: string | undefined;
|
|
73
|
+
let capturedMethod: string | undefined;
|
|
74
|
+
|
|
75
|
+
globalThis.fetch = mock.fn(async (url: any, init?: any) => {
|
|
76
|
+
capturedUrl = url;
|
|
77
|
+
capturedMethod = init?.method;
|
|
78
|
+
return {
|
|
79
|
+
ok: true,
|
|
80
|
+
status: 200,
|
|
81
|
+
json: async () => ({ project: { id: 'proj_001', archivedAt: '2026-01-01' } }),
|
|
82
|
+
};
|
|
83
|
+
}) as any;
|
|
84
|
+
|
|
85
|
+
const client = new ApiClient();
|
|
86
|
+
await client.archiveProject('proj_001');
|
|
87
|
+
assert.ok(capturedUrl!.includes('/api/projects/proj_001/archive'));
|
|
88
|
+
assert.equal(capturedMethod, 'POST');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('throws on failed project creation', async () => {
|
|
92
|
+
globalThis.fetch = mock.fn(async () => ({
|
|
93
|
+
ok: false,
|
|
94
|
+
status: 400,
|
|
95
|
+
json: async () => ({ error: 'Project name is required' }),
|
|
96
|
+
})) as any;
|
|
97
|
+
|
|
98
|
+
const client = new ApiClient();
|
|
99
|
+
await assert.rejects(
|
|
100
|
+
() => client.createProject(''),
|
|
101
|
+
{ message: 'Project name is required' },
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
});
|