@cloudcli-ai/cloudcli 1.30.0 → 1.31.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/assets/{index-DI22MZMb.css → index-DF8TTVHl.css} +1 -1
- package/dist/assets/index-DbZAcc9z.js +1262 -0
- package/dist/index.html +2 -2
- package/dist-server/server/index.js +115 -955
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/middleware/auth.js +1 -1
- package/dist-server/server/middleware/auth.js.map +1 -1
- package/dist-server/server/modules/database/connection.js +125 -0
- package/dist-server/server/modules/database/connection.js.map +1 -0
- package/dist-server/server/modules/database/index.js +13 -0
- package/dist-server/server/modules/database/index.js.map +1 -0
- package/dist-server/server/modules/database/init-db.js +18 -0
- package/dist-server/server/modules/database/init-db.js.map +1 -0
- package/dist-server/server/modules/database/migrations.js +370 -0
- package/dist-server/server/modules/database/migrations.js.map +1 -0
- package/dist-server/server/modules/database/repositories/api-keys.js +72 -0
- package/dist-server/server/modules/database/repositories/api-keys.js.map +1 -0
- package/dist-server/server/modules/database/repositories/app-config.js +47 -0
- package/dist-server/server/modules/database/repositories/app-config.js.map +1 -0
- package/dist-server/server/modules/database/repositories/credentials.js +68 -0
- package/dist-server/server/modules/database/repositories/credentials.js.map +1 -0
- package/dist-server/server/modules/database/repositories/github-tokens.js +54 -0
- package/dist-server/server/modules/database/repositories/github-tokens.js.map +1 -0
- package/dist-server/server/modules/database/repositories/notification-preferences.js +72 -0
- package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -0
- package/dist-server/server/modules/database/repositories/projects.db.integration.test.js +64 -0
- package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/database/repositories/projects.db.js +160 -0
- package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/push-subscriptions.js +49 -0
- package/dist-server/server/modules/database/repositories/push-subscriptions.js.map +1 -0
- package/dist-server/server/modules/database/repositories/scan-state.db.js +31 -0
- package/dist-server/server/modules/database/repositories/scan-state.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js +109 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/users.js +88 -0
- package/dist-server/server/modules/database/repositories/users.js.map +1 -0
- package/dist-server/server/modules/database/repositories/vapid-keys.js +38 -0
- package/dist-server/server/modules/database/repositories/vapid-keys.js.map +1 -0
- package/dist-server/server/modules/database/schema.js +143 -0
- package/dist-server/server/modules/database/schema.js.map +1 -0
- package/dist-server/server/modules/projects/index.js +4 -0
- package/dist-server/server/modules/projects/index.js.map +1 -0
- package/dist-server/server/modules/projects/projects.routes.js +188 -0
- package/dist-server/server/modules/projects/projects.routes.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-clone.service.js +219 -0
- package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-delete.service.js +65 -0
- package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-management.service.js +93 -0
- package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-star.service.js +60 -0
- package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js +167 -0
- package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +169 -0
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -0
- package/dist-server/server/modules/projects/tests/project-clone.service.test.js +126 -0
- package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -0
- package/dist-server/server/modules/projects/tests/project-management.service.test.js +85 -0
- package/dist-server/server/modules/projects/tests/project-management.service.test.js.map +1 -0
- package/dist-server/server/modules/projects/tests/project-star.service.test.js +95 -0
- package/dist-server/server/modules/projects/tests/project-star.service.test.js.map +1 -0
- package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js +87 -0
- package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js.map +1 -0
- package/dist-server/server/modules/providers/index.js +4 -0
- package/dist-server/server/modules/providers/index.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +63 -0
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +144 -7
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +71 -0
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +225 -13
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +127 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +296 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +277 -10
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +2 -0
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +172 -0
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +868 -0
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js +56 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +234 -0
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/sessions.service.js +78 -3
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
- package/dist-server/server/modules/websocket/index.js +3 -0
- package/dist-server/server/modules/websocket/index.js.map +1 -0
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +188 -0
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +52 -0
- package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +334 -0
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/websocket-auth.service.js +32 -0
- package/dist-server/server/modules/websocket/services/websocket-auth.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/websocket-server.service.js +36 -0
- package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/websocket-state.service.js +14 -0
- package/dist-server/server/modules/websocket/services/websocket-state.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/websocket-writer.service.js +32 -0
- package/dist-server/server/modules/websocket/services/websocket-writer.service.js.map +1 -0
- package/dist-server/server/routes/agent.js +10 -17
- package/dist-server/server/routes/agent.js.map +1 -1
- package/dist-server/server/routes/auth.js +3 -1
- package/dist-server/server/routes/auth.js.map +1 -1
- package/dist-server/server/routes/commands.js +1 -1
- package/dist-server/server/routes/commands.js.map +1 -1
- package/dist-server/server/routes/gemini.js +2 -2
- package/dist-server/server/routes/gemini.js.map +1 -1
- package/dist-server/server/routes/git.js +33 -29
- package/dist-server/server/routes/git.js.map +1 -1
- package/dist-server/server/routes/settings.js +1 -1
- package/dist-server/server/routes/settings.js.map +1 -1
- package/dist-server/server/routes/taskmaster.js +97 -110
- package/dist-server/server/routes/taskmaster.js.map +1 -1
- package/dist-server/server/routes/user.js +1 -1
- package/dist-server/server/routes/user.js.map +1 -1
- package/dist-server/server/services/notification-orchestrator.js +2 -2
- package/dist-server/server/services/notification-orchestrator.js.map +1 -1
- package/dist-server/server/services/vapid-keys.js +2 -1
- package/dist-server/server/services/vapid-keys.js.map +1 -1
- package/dist-server/server/shared/types.js +0 -1
- package/dist-server/server/shared/types.js.map +1 -1
- package/dist-server/server/shared/utils.js +408 -7
- package/dist-server/server/shared/utils.js.map +1 -1
- package/dist-server/server/utils/taskmaster-websocket.js +18 -12
- package/dist-server/server/utils/taskmaster-websocket.js.map +1 -1
- package/dist-server/shared/modelConstants.js +2 -1
- package/dist-server/shared/modelConstants.js.map +1 -1
- package/package.json +3 -1
- package/server/index.js +146 -1012
- package/server/middleware/auth.js +1 -1
- package/server/modules/database/connection.ts +143 -0
- package/server/modules/database/index.ts +12 -0
- package/server/modules/database/init-db.ts +17 -0
- package/server/modules/database/migrations.ts +442 -0
- package/server/modules/database/repositories/api-keys.ts +119 -0
- package/server/modules/database/repositories/app-config.ts +53 -0
- package/server/modules/database/repositories/credentials.ts +106 -0
- package/server/modules/database/repositories/github-tokens.ts +100 -0
- package/server/modules/database/repositories/notification-preferences.ts +103 -0
- package/server/modules/database/repositories/projects.db.integration.test.ts +72 -0
- package/server/modules/database/repositories/projects.db.ts +183 -0
- package/server/modules/database/repositories/push-subscriptions.ts +80 -0
- package/server/modules/database/repositories/scan-state.db.ts +42 -0
- package/server/modules/database/repositories/sessions.db.ts +174 -0
- package/server/modules/database/repositories/users.ts +140 -0
- package/server/modules/database/repositories/vapid-keys.ts +57 -0
- package/server/modules/database/schema.ts +152 -0
- package/server/modules/projects/index.ts +6 -0
- package/server/modules/projects/projects.routes.ts +247 -0
- package/server/modules/projects/services/project-clone.service.ts +321 -0
- package/server/modules/projects/services/project-delete.service.ts +75 -0
- package/server/modules/projects/services/project-management.service.ts +150 -0
- package/server/modules/projects/services/project-star.service.ts +78 -0
- package/server/modules/projects/services/projects-has-taskmaster.service.ts +248 -0
- package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +285 -0
- package/server/modules/projects/tests/project-clone.service.test.ts +183 -0
- package/server/modules/projects/tests/project-management.service.test.ts +117 -0
- package/server/modules/projects/tests/project-star.service.test.ts +123 -0
- package/server/modules/projects/tests/projects-has-taskmaster.service.test.ts +105 -0
- package/server/modules/providers/index.ts +4 -0
- package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +110 -0
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +182 -13
- package/server/modules/providers/list/claude/claude.provider.ts +3 -1
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +119 -0
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +262 -17
- package/server/modules/providers/list/codex/codex.provider.ts +3 -1
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +176 -0
- package/server/modules/providers/list/cursor/cursor.provider.ts +3 -1
- package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +401 -0
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +327 -13
- package/server/modules/providers/list/gemini/gemini.provider.ts +3 -1
- package/server/modules/providers/provider.routes.ts +220 -12
- package/server/modules/providers/services/session-conversations-search.service.ts +1167 -0
- package/server/modules/providers/services/session-synchronizer.service.ts +74 -0
- package/server/modules/providers/services/sessions-watcher.service.ts +283 -0
- package/server/modules/providers/services/sessions.service.ts +89 -4
- package/server/modules/providers/shared/base/abstract.provider.ts +8 -1
- package/server/modules/websocket/README.md +267 -0
- package/server/modules/websocket/index.ts +2 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +271 -0
- package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +65 -0
- package/server/modules/websocket/services/shell-websocket.service.ts +453 -0
- package/server/modules/websocket/services/websocket-auth.service.ts +54 -0
- package/server/modules/websocket/services/websocket-server.service.ts +58 -0
- package/server/modules/websocket/services/websocket-state.service.ts +16 -0
- package/server/modules/websocket/services/websocket-writer.service.ts +38 -0
- package/server/routes/agent.js +11 -16
- package/server/routes/auth.js +4 -2
- package/server/routes/commands.js +1 -1
- package/server/routes/gemini.js +3 -2
- package/server/routes/git.js +33 -28
- package/server/routes/settings.js +1 -1
- package/server/routes/taskmaster.js +108 -111
- package/server/routes/user.js +1 -1
- package/server/services/notification-orchestrator.js +3 -2
- package/server/services/vapid-keys.js +2 -1
- package/server/shared/interfaces.ts +39 -1
- package/server/shared/types.ts +185 -30
- package/server/shared/utils.ts +485 -9
- package/server/utils/taskmaster-websocket.js +19 -13
- package/shared/modelConstants.js +2 -1
- package/dist/assets/index-RX-W_g_Q.js +0 -1262
- package/dist-server/server/database/db.js +0 -535
- package/dist-server/server/database/db.js.map +0 -1
- package/dist-server/server/database/schema.js +0 -97
- package/dist-server/server/database/schema.js.map +0 -1
- package/dist-server/server/projects.js +0 -2325
- package/dist-server/server/projects.js.map +0 -1
- package/dist-server/server/routes/codex.js +0 -18
- package/dist-server/server/routes/codex.js.map +0 -1
- package/dist-server/server/routes/messages.js +0 -56
- package/dist-server/server/routes/messages.js.map +0 -1
- package/dist-server/server/routes/projects.js +0 -505
- package/dist-server/server/routes/projects.js.map +0 -1
- package/server/database/db.js +0 -593
- package/server/database/schema.js +0 -102
- package/server/projects.js +0 -2555
- package/server/routes/codex.js +0 -19
- package/server/routes/messages.js +0 -61
- package/server/routes/projects.js +0 -548
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { access, mkdir, rm } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { githubTokensDb } from '@/modules/database/index.js';
|
|
6
|
+
import { createProject } from '@/modules/projects/services/project-management.service.js';
|
|
7
|
+
import type { WorkspacePathValidationResult } from '@/shared/types.js';
|
|
8
|
+
import { AppError, validateWorkspacePath } from '@/shared/utils.js';
|
|
9
|
+
|
|
10
|
+
type CloneProjectInput = {
|
|
11
|
+
workspacePath: string;
|
|
12
|
+
githubUrl: string;
|
|
13
|
+
githubTokenId?: number | null;
|
|
14
|
+
newGithubToken?: string | null;
|
|
15
|
+
userId: number | string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type CloneCompletePayload = {
|
|
19
|
+
project: Record<string, unknown>;
|
|
20
|
+
message: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type CloneProjectEventHandlers = {
|
|
24
|
+
onProgress: (message: string) => void;
|
|
25
|
+
onComplete: (payload: CloneCompletePayload) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type GitCloneProcess = {
|
|
29
|
+
stdout: NodeJS.ReadableStream | null;
|
|
30
|
+
stderr: NodeJS.ReadableStream | null;
|
|
31
|
+
on(event: 'close', listener: (code: number | null) => void): void;
|
|
32
|
+
on(event: 'error', listener: (error: NodeJS.ErrnoException) => void): void;
|
|
33
|
+
kill(): void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type CloneProjectDependencies = {
|
|
37
|
+
validatePath: (requestedPath: string) => Promise<WorkspacePathValidationResult>;
|
|
38
|
+
ensureDirectory: (directoryPath: string) => Promise<void>;
|
|
39
|
+
pathExists: (targetPath: string) => Promise<boolean>;
|
|
40
|
+
removePath: (targetPath: string) => Promise<void>;
|
|
41
|
+
getGithubTokenById: (
|
|
42
|
+
tokenId: number,
|
|
43
|
+
userId: number,
|
|
44
|
+
) => Promise<{ github_token: string } | null>;
|
|
45
|
+
spawnGitClone: (cloneUrl: string, clonePath: string) => GitCloneProcess;
|
|
46
|
+
registerProject: (projectPath: string, customName: string) => Promise<{ project: Record<string, unknown> }>;
|
|
47
|
+
logError: (message: string, error: unknown) => void;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type CloneProjectOperation = {
|
|
51
|
+
waitForCompletion: Promise<void>;
|
|
52
|
+
cancel: () => void;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
async function defaultPathExists(targetPath: string): Promise<boolean> {
|
|
56
|
+
try {
|
|
57
|
+
await access(targetPath);
|
|
58
|
+
return true;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sanitizeGitError(message: string, token: string | null): string {
|
|
69
|
+
if (!message || !token) {
|
|
70
|
+
return message;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const escapedToken = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
74
|
+
return message.replace(new RegExp(escapedToken, 'g'), '***');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveCloneFailureMessage(lastError: string, sanitizedError: string): string {
|
|
78
|
+
if (lastError.includes('Authentication failed') || lastError.includes('could not read Username')) {
|
|
79
|
+
return 'Authentication failed. Please check your credentials.';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (lastError.includes('Repository not found')) {
|
|
83
|
+
return 'Repository not found. Please check the URL and ensure you have access.';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (lastError.includes('already exists')) {
|
|
87
|
+
return 'Directory already exists';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (sanitizedError) {
|
|
91
|
+
return sanitizedError;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return 'Git clone failed';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveErrorMessage(error: unknown): string {
|
|
98
|
+
if (error instanceof AppError) {
|
|
99
|
+
return error.message;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (error instanceof Error && error.message) {
|
|
103
|
+
return error.message;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return 'Unexpected error';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const defaultDependencies: CloneProjectDependencies = {
|
|
110
|
+
validatePath: validateWorkspacePath,
|
|
111
|
+
ensureDirectory: async (directoryPath: string): Promise<void> => {
|
|
112
|
+
await mkdir(directoryPath, { recursive: true });
|
|
113
|
+
},
|
|
114
|
+
pathExists: defaultPathExists,
|
|
115
|
+
removePath: async (targetPath: string): Promise<void> => {
|
|
116
|
+
await rm(targetPath, { recursive: true, force: true });
|
|
117
|
+
},
|
|
118
|
+
getGithubTokenById: async (
|
|
119
|
+
tokenId: number,
|
|
120
|
+
userId: number,
|
|
121
|
+
): Promise<{ github_token: string } | null> => {
|
|
122
|
+
const tokenRow = githubTokensDb.getGithubTokenById(userId, tokenId) as
|
|
123
|
+
| { github_token: string }
|
|
124
|
+
| null;
|
|
125
|
+
return tokenRow;
|
|
126
|
+
},
|
|
127
|
+
spawnGitClone: (cloneUrl: string, clonePath: string): GitCloneProcess =>
|
|
128
|
+
spawn('git', ['clone', '--progress', '--', cloneUrl, clonePath], {
|
|
129
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
130
|
+
env: {
|
|
131
|
+
...process.env,
|
|
132
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
133
|
+
},
|
|
134
|
+
}) as unknown as GitCloneProcess,
|
|
135
|
+
registerProject: async (
|
|
136
|
+
projectPath: string,
|
|
137
|
+
customName: string,
|
|
138
|
+
): Promise<{ project: Record<string, unknown> }> =>
|
|
139
|
+
createProject({
|
|
140
|
+
projectPath,
|
|
141
|
+
customName,
|
|
142
|
+
}) as Promise<{ project: Record<string, unknown> }>,
|
|
143
|
+
logError: (message: string, error: unknown): void => {
|
|
144
|
+
console.error(message, error);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export async function startCloneProject(
|
|
149
|
+
input: CloneProjectInput,
|
|
150
|
+
handlers: CloneProjectEventHandlers,
|
|
151
|
+
dependencies: CloneProjectDependencies = defaultDependencies,
|
|
152
|
+
): Promise<CloneProjectOperation> {
|
|
153
|
+
const normalizedWorkspacePath = input.workspacePath.trim();
|
|
154
|
+
const normalizedGithubUrl = input.githubUrl.trim();
|
|
155
|
+
|
|
156
|
+
if (!normalizedWorkspacePath) {
|
|
157
|
+
throw new AppError('workspacePath and githubUrl are required', {
|
|
158
|
+
code: 'WORKSPACE_PATH_REQUIRED',
|
|
159
|
+
statusCode: 400,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!normalizedGithubUrl) {
|
|
164
|
+
throw new AppError('workspacePath and githubUrl are required', {
|
|
165
|
+
code: 'GITHUB_URL_REQUIRED',
|
|
166
|
+
statusCode: 400,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (normalizedGithubUrl.startsWith('-')) {
|
|
171
|
+
throw new AppError('Invalid githubUrl', {
|
|
172
|
+
code: 'INVALID_GITHUB_URL',
|
|
173
|
+
statusCode: 400,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const pathValidation = await dependencies.validatePath(normalizedWorkspacePath);
|
|
178
|
+
if (!pathValidation.valid || !pathValidation.resolvedPath) {
|
|
179
|
+
throw new AppError(pathValidation.error || 'Invalid workspace path', {
|
|
180
|
+
code: 'INVALID_PROJECT_PATH',
|
|
181
|
+
statusCode: 400,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const absolutePath = pathValidation.resolvedPath;
|
|
186
|
+
await dependencies.ensureDirectory(absolutePath);
|
|
187
|
+
|
|
188
|
+
let githubToken: string | null = null;
|
|
189
|
+
if (typeof input.githubTokenId === 'number') {
|
|
190
|
+
const numericUserId =
|
|
191
|
+
typeof input.userId === 'number' ? input.userId : Number.parseInt(String(input.userId), 10);
|
|
192
|
+
if (Number.isNaN(numericUserId)) {
|
|
193
|
+
throw new AppError('Authenticated user is required', {
|
|
194
|
+
code: 'AUTHENTICATION_REQUIRED',
|
|
195
|
+
statusCode: 401,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const token = await dependencies.getGithubTokenById(input.githubTokenId, numericUserId);
|
|
200
|
+
if (!token) {
|
|
201
|
+
throw new AppError('GitHub token not found', {
|
|
202
|
+
code: 'GITHUB_TOKEN_NOT_FOUND',
|
|
203
|
+
statusCode: 404,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
githubToken = token.github_token;
|
|
208
|
+
} else if (input.newGithubToken && input.newGithubToken.trim().length > 0) {
|
|
209
|
+
githubToken = input.newGithubToken.trim();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const sanitizedGithubUrl = normalizedGithubUrl.replace(/\/+$/, '').replace(/\.git$/, '');
|
|
213
|
+
const repoName = sanitizedGithubUrl.split('/').pop() || 'repository';
|
|
214
|
+
const clonePath = path.join(absolutePath, repoName);
|
|
215
|
+
|
|
216
|
+
if (await dependencies.pathExists(clonePath)) {
|
|
217
|
+
throw new AppError(
|
|
218
|
+
`Directory "${repoName}" already exists. Please choose a different location or remove the existing directory.`,
|
|
219
|
+
{
|
|
220
|
+
code: 'CLONE_TARGET_ALREADY_EXISTS',
|
|
221
|
+
statusCode: 409,
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let cloneUrl = normalizedGithubUrl;
|
|
227
|
+
if (githubToken) {
|
|
228
|
+
try {
|
|
229
|
+
const url = new URL(normalizedGithubUrl);
|
|
230
|
+
url.username = githubToken;
|
|
231
|
+
url.password = '';
|
|
232
|
+
cloneUrl = url.toString();
|
|
233
|
+
} catch {
|
|
234
|
+
// SSH URLs cannot be represented by URL constructor and are used as-is.
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
handlers.onProgress(`Cloning into '${repoName}'...`);
|
|
239
|
+
const gitProcess = dependencies.spawnGitClone(cloneUrl, clonePath);
|
|
240
|
+
let lastError = '';
|
|
241
|
+
|
|
242
|
+
gitProcess.stdout?.on('data', (data: Buffer | string) => {
|
|
243
|
+
const message = data.toString().trim();
|
|
244
|
+
if (message) {
|
|
245
|
+
handlers.onProgress(message);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
gitProcess.stderr?.on('data', (data: Buffer | string) => {
|
|
250
|
+
const message = data.toString().trim();
|
|
251
|
+
lastError = message;
|
|
252
|
+
if (message) {
|
|
253
|
+
handlers.onProgress(message);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const waitForCompletion = new Promise<void>((resolve, reject) => {
|
|
258
|
+
gitProcess.on('close', async (code) => {
|
|
259
|
+
if (code === 0) {
|
|
260
|
+
try {
|
|
261
|
+
const createdProject = await dependencies.registerProject(clonePath, repoName);
|
|
262
|
+
handlers.onComplete({
|
|
263
|
+
project: createdProject.project,
|
|
264
|
+
message: 'Repository cloned successfully',
|
|
265
|
+
});
|
|
266
|
+
resolve();
|
|
267
|
+
} catch (error) {
|
|
268
|
+
reject(
|
|
269
|
+
new AppError(`Clone succeeded but failed to add project: ${resolveErrorMessage(error)}`, {
|
|
270
|
+
code: 'CLONE_PROJECT_REGISTRATION_FAILED',
|
|
271
|
+
statusCode: 500,
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const sanitizedError = sanitizeGitError(lastError, githubToken);
|
|
279
|
+
const errorMessage = resolveCloneFailureMessage(lastError, sanitizedError);
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
await dependencies.removePath(clonePath);
|
|
283
|
+
} catch (cleanupError) {
|
|
284
|
+
dependencies.logError('Failed to clean up after clone failure:', cleanupError);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
reject(
|
|
288
|
+
new AppError(errorMessage, {
|
|
289
|
+
code: 'GIT_CLONE_FAILED',
|
|
290
|
+
statusCode: 500,
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
gitProcess.on('error', (error) => {
|
|
296
|
+
if (error.code === 'ENOENT') {
|
|
297
|
+
reject(
|
|
298
|
+
new AppError('Git is not installed or not in PATH', {
|
|
299
|
+
code: 'GIT_NOT_FOUND',
|
|
300
|
+
statusCode: 500,
|
|
301
|
+
}),
|
|
302
|
+
);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
reject(
|
|
307
|
+
new AppError(error.message, {
|
|
308
|
+
code: 'GIT_EXECUTION_FAILED',
|
|
309
|
+
statusCode: 500,
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
waitForCompletion,
|
|
317
|
+
cancel: () => {
|
|
318
|
+
gitProcess.kill();
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { projectsDb, sessionsDb } from '@/modules/database/index.js';
|
|
5
|
+
import { AppError } from '@/shared/utils.js';
|
|
6
|
+
|
|
7
|
+
function uniqueJsonlPathsFromSessions(
|
|
8
|
+
sessions: Array<{ jsonl_path: string | null }>,
|
|
9
|
+
): string[] {
|
|
10
|
+
const seen = new Set<string>();
|
|
11
|
+
const result: string[] = [];
|
|
12
|
+
|
|
13
|
+
for (const row of sessions) {
|
|
14
|
+
const raw = row.jsonl_path?.trim();
|
|
15
|
+
if (!raw) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const absolute = path.isAbsolute(raw) ? path.normalize(raw) : path.resolve(raw);
|
|
19
|
+
if (seen.has(absolute)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
seen.add(absolute);
|
|
23
|
+
result.push(absolute);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function unlinkJsonlIfExists(filePath: string): Promise<void> {
|
|
30
|
+
try {
|
|
31
|
+
await fs.unlink(filePath);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
34
|
+
if (code === 'ENOENT') {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
console.warn(`[project-delete] Failed to remove ${filePath}:`, (error as Error).message);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Loads all session rows for the project path and removes each distinct `jsonl_path` file on disk.
|
|
43
|
+
*/
|
|
44
|
+
export async function deleteSessionJsonlFilesForProjectPath(projectPath: string): Promise<void> {
|
|
45
|
+
const sessions = sessionsDb.getSessionsByProjectPath(projectPath);
|
|
46
|
+
const paths = uniqueJsonlPathsFromSessions(sessions);
|
|
47
|
+
|
|
48
|
+
for (const filePath of paths) {
|
|
49
|
+
await unlinkJsonlIfExists(filePath);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* - **Soft delete** (`force` false): set `isArchived` on the `projects` row (hide from the active list; DB only).
|
|
55
|
+
* - **Force** (`force` true): for each session row for that `project_path`, delete the file at `jsonl_path`
|
|
56
|
+
* (when set), then remove session rows and the `projects` row.
|
|
57
|
+
*/
|
|
58
|
+
export async function deleteOrArchiveProject(projectId: string, force: boolean): Promise<void> {
|
|
59
|
+
const row = projectsDb.getProjectById(projectId);
|
|
60
|
+
if (!row) {
|
|
61
|
+
throw new AppError(`Unknown projectId: ${projectId}`, {
|
|
62
|
+
code: 'PROJECT_NOT_FOUND',
|
|
63
|
+
statusCode: 404,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!force) {
|
|
68
|
+
projectsDb.updateProjectIsArchivedById(projectId, true);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await deleteSessionJsonlFilesForProjectPath(row.project_path);
|
|
73
|
+
sessionsDb.deleteSessionsByProjectPath(row.project_path);
|
|
74
|
+
projectsDb.deleteProjectById(projectId);
|
|
75
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { projectsDb } from '@/modules/database/index.js';
|
|
5
|
+
import type {
|
|
6
|
+
CreateProjectPathResult,
|
|
7
|
+
ProjectRepositoryRow,
|
|
8
|
+
WorkspacePathValidationResult,
|
|
9
|
+
} from '@/shared/types.js';
|
|
10
|
+
import { AppError, normalizeProjectPath, validateWorkspacePath } from '@/shared/utils.js';
|
|
11
|
+
|
|
12
|
+
type CreateProjectInput = {
|
|
13
|
+
projectPath: string;
|
|
14
|
+
customName?: string | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type CreateProjectDependencies = {
|
|
18
|
+
validatePath: (projectPath: string) => Promise<WorkspacePathValidationResult>;
|
|
19
|
+
ensureWorkspaceDirectory: (projectPath: string) => Promise<void>;
|
|
20
|
+
persistProjectPath: (projectPath: string, customName: string | null) => CreateProjectPathResult;
|
|
21
|
+
getProjectByPath: (projectPath: string) => ProjectRepositoryRow | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ProjectApiView = {
|
|
25
|
+
projectId: string;
|
|
26
|
+
path: string;
|
|
27
|
+
fullPath: string;
|
|
28
|
+
displayName: string;
|
|
29
|
+
customName: string | null;
|
|
30
|
+
isArchived: boolean;
|
|
31
|
+
isStarred: boolean;
|
|
32
|
+
sessions: [];
|
|
33
|
+
cursorSessions: [];
|
|
34
|
+
codexSessions: [];
|
|
35
|
+
geminiSessions: [];
|
|
36
|
+
sessionMeta: {
|
|
37
|
+
hasMore: false;
|
|
38
|
+
total: 0;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type CreateProjectServiceResult = {
|
|
43
|
+
outcome: 'created' | 'reactivated_archived';
|
|
44
|
+
project: ProjectApiView;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const defaultDependencies: CreateProjectDependencies = {
|
|
48
|
+
validatePath: validateWorkspacePath,
|
|
49
|
+
ensureWorkspaceDirectory: async (projectPath: string): Promise<void> => {
|
|
50
|
+
await fs.mkdir(projectPath, { recursive: true });
|
|
51
|
+
const directoryStats = await fs.stat(projectPath);
|
|
52
|
+
if (!directoryStats.isDirectory()) {
|
|
53
|
+
throw new AppError('Path exists but is not a directory', {
|
|
54
|
+
code: 'PROJECT_PATH_NOT_DIRECTORY',
|
|
55
|
+
statusCode: 400,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
persistProjectPath: (projectPath: string, customName: string | null): CreateProjectPathResult =>
|
|
60
|
+
projectsDb.createProjectPath(projectPath, customName),
|
|
61
|
+
getProjectByPath: (projectPath: string): ProjectRepositoryRow | null =>
|
|
62
|
+
projectsDb.getProjectPath(projectPath),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function resolveDisplayName(customName: string | null | undefined, projectPath: string): string {
|
|
66
|
+
const trimmedCustomName = typeof customName === 'string' ? customName.trim() : '';
|
|
67
|
+
if (trimmedCustomName.length > 0) {
|
|
68
|
+
return trimmedCustomName;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return path.basename(projectPath) || projectPath;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function mapProjectRowToApiView(projectRow: ProjectRepositoryRow): ProjectApiView {
|
|
75
|
+
return {
|
|
76
|
+
projectId: projectRow.project_id,
|
|
77
|
+
path: projectRow.project_path,
|
|
78
|
+
fullPath: projectRow.project_path,
|
|
79
|
+
displayName: resolveDisplayName(projectRow.custom_project_name, projectRow.project_path),
|
|
80
|
+
customName: projectRow.custom_project_name,
|
|
81
|
+
isArchived: Boolean(projectRow.isArchived),
|
|
82
|
+
isStarred: Boolean(projectRow.isStarred),
|
|
83
|
+
sessions: [],
|
|
84
|
+
cursorSessions: [],
|
|
85
|
+
codexSessions: [],
|
|
86
|
+
geminiSessions: [],
|
|
87
|
+
sessionMeta: {
|
|
88
|
+
hasMore: false,
|
|
89
|
+
total: 0,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function createProject(
|
|
95
|
+
input: CreateProjectInput,
|
|
96
|
+
dependencies: CreateProjectDependencies = defaultDependencies,
|
|
97
|
+
): Promise<CreateProjectServiceResult> {
|
|
98
|
+
const normalizedPath = normalizeProjectPath(input.projectPath || '');
|
|
99
|
+
if (!normalizedPath) {
|
|
100
|
+
throw new AppError('path is required', {
|
|
101
|
+
code: 'PROJECT_PATH_REQUIRED',
|
|
102
|
+
statusCode: 400,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const pathValidation = await dependencies.validatePath(normalizedPath);
|
|
107
|
+
if (!pathValidation.valid || !pathValidation.resolvedPath) {
|
|
108
|
+
throw new AppError('Invalid project path', {
|
|
109
|
+
code: 'INVALID_PROJECT_PATH',
|
|
110
|
+
statusCode: 400,
|
|
111
|
+
details: pathValidation.error ?? 'Path validation failed',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const resolvedProjectPath = normalizeProjectPath(pathValidation.resolvedPath);
|
|
116
|
+
await dependencies.ensureWorkspaceDirectory(resolvedProjectPath);
|
|
117
|
+
|
|
118
|
+
const normalizedCustomName = resolveDisplayName(input.customName ?? null, resolvedProjectPath);
|
|
119
|
+
const persistedProject = dependencies.persistProjectPath(resolvedProjectPath, normalizedCustomName);
|
|
120
|
+
|
|
121
|
+
if (persistedProject.outcome === 'active_conflict') {
|
|
122
|
+
throw new AppError('Project path already exists and is active', {
|
|
123
|
+
code: 'PROJECT_ALREADY_EXISTS',
|
|
124
|
+
statusCode: 409,
|
|
125
|
+
details: `Project path already exists: ${resolvedProjectPath}`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const projectRow = persistedProject.project ?? dependencies.getProjectByPath(resolvedProjectPath);
|
|
130
|
+
if (!projectRow) {
|
|
131
|
+
throw new AppError('Failed to resolve project after creation', {
|
|
132
|
+
code: 'PROJECT_CREATE_FAILED',
|
|
133
|
+
statusCode: 500,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Archived rows intentionally remain archived when reused, as requested.
|
|
138
|
+
return {
|
|
139
|
+
outcome: persistedProject.outcome,
|
|
140
|
+
project: mapProjectRowToApiView(projectRow),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Sets `projects.custom_project_name` for the given `projectId` (or clears it when empty).
|
|
146
|
+
*/
|
|
147
|
+
export function updateProjectDisplayName(projectId: string, newDisplayName: unknown): void {
|
|
148
|
+
const trimmed = typeof newDisplayName === 'string' ? newDisplayName.trim() : '';
|
|
149
|
+
projectsDb.updateCustomProjectNameById(projectId, trimmed.length > 0 ? trimmed : null);
|
|
150
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { projectsDb } from '@/modules/database/index.js';
|
|
2
|
+
import { AppError } from '@/shared/utils.js';
|
|
3
|
+
|
|
4
|
+
type ToggleProjectStarResult = {
|
|
5
|
+
isStarred: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type ApplyLegacyStarredProjectIdsResult = {
|
|
9
|
+
updated: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function normalizeProjectId(projectId: string): string {
|
|
13
|
+
return projectId.trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function uniqueProjectIds(projectIds: string[]): string[] {
|
|
17
|
+
const uniqueIds = new Set<string>();
|
|
18
|
+
for (const projectId of projectIds) {
|
|
19
|
+
const normalizedProjectId = normalizeProjectId(projectId);
|
|
20
|
+
if (!normalizedProjectId) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
uniqueIds.add(normalizedProjectId);
|
|
24
|
+
}
|
|
25
|
+
return [...uniqueIds];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Applies legacy `localStorage` stars keyed by DB `projectId` onto `projects.isStarred`.
|
|
30
|
+
*
|
|
31
|
+
* The operation is idempotent: already-starred projects are ignored, unknown ids are skipped.
|
|
32
|
+
*/
|
|
33
|
+
export function applyLegacyStarredProjectIds(projectIds: string[]): ApplyLegacyStarredProjectIdsResult {
|
|
34
|
+
const normalizedProjectIds = uniqueProjectIds(projectIds);
|
|
35
|
+
let updated = 0;
|
|
36
|
+
|
|
37
|
+
for (const projectId of normalizedProjectIds) {
|
|
38
|
+
const project = projectsDb.getProjectById(projectId);
|
|
39
|
+
if (!project) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (Boolean(project.isStarred)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
projectsDb.updateProjectIsStarredById(projectId, true);
|
|
48
|
+
updated += 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { updated };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Flips `projects.isStarred` for one project and returns the new state.
|
|
56
|
+
*/
|
|
57
|
+
export function toggleProjectStar(projectId: string): ToggleProjectStarResult {
|
|
58
|
+
const normalizedProjectId = normalizeProjectId(projectId);
|
|
59
|
+
if (!normalizedProjectId) {
|
|
60
|
+
throw new AppError('projectId is required', {
|
|
61
|
+
code: 'PROJECT_ID_REQUIRED',
|
|
62
|
+
statusCode: 400,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const project = projectsDb.getProjectById(normalizedProjectId);
|
|
67
|
+
if (!project) {
|
|
68
|
+
throw new AppError('Project not found', {
|
|
69
|
+
code: 'PROJECT_NOT_FOUND',
|
|
70
|
+
statusCode: 404,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const nextStarredState = !Boolean(project.isStarred);
|
|
75
|
+
projectsDb.updateProjectIsStarredById(normalizedProjectId, nextStarredState);
|
|
76
|
+
|
|
77
|
+
return { isStarred: nextStarredState };
|
|
78
|
+
}
|