@myrialabs/clopen 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -1
- package/backend/index.ts +25 -1
- package/backend/lib/auth/auth-service.ts +484 -0
- package/backend/lib/auth/index.ts +4 -0
- package/backend/lib/auth/permissions.ts +63 -0
- package/backend/lib/auth/rate-limiter.ts +145 -0
- package/backend/lib/auth/tokens.ts +53 -0
- package/backend/lib/chat/stream-manager.ts +4 -1
- package/backend/lib/database/migrations/024_create_users_table.ts +29 -0
- package/backend/lib/database/migrations/025_create_auth_sessions_table.ts +38 -0
- package/backend/lib/database/migrations/026_create_invite_tokens_table.ts +31 -0
- package/backend/lib/database/migrations/index.ts +21 -0
- package/backend/lib/database/queries/auth-queries.ts +201 -0
- package/backend/lib/database/queries/index.ts +2 -1
- package/backend/lib/database/queries/session-queries.ts +13 -0
- package/backend/lib/database/queries/snapshot-queries.ts +1 -1
- package/backend/lib/engine/adapters/opencode/server.ts +9 -1
- package/backend/lib/engine/adapters/opencode/stream.ts +175 -1
- package/backend/lib/mcp/config.ts +13 -18
- package/backend/lib/mcp/index.ts +9 -0
- package/backend/lib/mcp/remote-server.ts +132 -0
- package/backend/lib/mcp/servers/helper.ts +49 -3
- package/backend/lib/mcp/servers/index.ts +3 -2
- package/backend/lib/preview/browser/browser-audio-capture.ts +20 -3
- package/backend/lib/preview/browser/browser-navigation-tracker.ts +3 -0
- package/backend/lib/preview/browser/browser-pool.ts +73 -176
- package/backend/lib/preview/browser/browser-preview-service.ts +3 -2
- package/backend/lib/preview/browser/browser-tab-manager.ts +261 -23
- package/backend/lib/preview/browser/browser-video-capture.ts +36 -1
- package/backend/lib/snapshot/helpers.ts +22 -49
- package/backend/lib/snapshot/snapshot-service.ts +148 -83
- package/backend/lib/utils/ws.ts +65 -1
- package/backend/ws/auth/index.ts +17 -0
- package/backend/ws/auth/invites.ts +84 -0
- package/backend/ws/auth/login.ts +269 -0
- package/backend/ws/auth/status.ts +41 -0
- package/backend/ws/auth/users.ts +32 -0
- package/backend/ws/chat/stream.ts +13 -0
- package/backend/ws/engine/claude/accounts.ts +3 -1
- package/backend/ws/engine/utils.ts +38 -6
- package/backend/ws/index.ts +4 -4
- package/backend/ws/preview/browser/interact.ts +27 -5
- package/backend/ws/snapshot/restore.ts +111 -12
- package/backend/ws/snapshot/timeline.ts +56 -29
- package/bin/clopen.ts +56 -1
- package/bun.lock +113 -51
- package/frontend/App.svelte +47 -29
- package/frontend/lib/components/auth/InvitePage.svelte +215 -0
- package/frontend/lib/components/auth/LoginPage.svelte +129 -0
- package/frontend/lib/components/auth/SetupPage.svelte +1022 -0
- package/frontend/lib/components/chat/input/ChatInput.svelte +1 -2
- package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +2 -2
- package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +4 -4
- package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +11 -19
- package/frontend/lib/components/checkpoint/TimelineModal.svelte +15 -3
- package/frontend/lib/components/checkpoint/timeline/TimelineNode.svelte +30 -19
- package/frontend/lib/components/checkpoint/timeline/types.ts +4 -0
- package/frontend/lib/components/common/FolderBrowser.svelte +9 -9
- package/frontend/lib/components/common/UpdateBanner.svelte +2 -2
- package/frontend/lib/components/git/CommitForm.svelte +6 -4
- package/frontend/lib/components/history/HistoryModal.svelte +1 -1
- package/frontend/lib/components/history/HistoryView.svelte +1 -1
- package/frontend/lib/components/preview/browser/BrowserPreview.svelte +1 -1
- package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +12 -4
- package/frontend/lib/components/settings/SettingsModal.svelte +50 -15
- package/frontend/lib/components/settings/SettingsView.svelte +21 -7
- package/frontend/lib/components/settings/account/AccountSettings.svelte +5 -0
- package/frontend/lib/components/settings/admin/InviteManagement.svelte +239 -0
- package/frontend/lib/components/settings/admin/UserManagement.svelte +127 -0
- package/frontend/lib/components/settings/general/AdvancedSettings.svelte +10 -4
- package/frontend/lib/components/settings/general/AuthModeSettings.svelte +229 -0
- package/frontend/lib/components/settings/general/GeneralSettings.svelte +6 -1
- package/frontend/lib/components/settings/general/UpdateSettings.svelte +5 -5
- package/frontend/lib/components/settings/security/SecuritySettings.svelte +10 -0
- package/frontend/lib/components/settings/system/SystemSettings.svelte +10 -0
- package/frontend/lib/components/settings/user/UserSettings.svelte +147 -74
- package/frontend/lib/components/workspace/PanelHeader.svelte +1 -1
- package/frontend/lib/components/workspace/WorkspaceLayout.svelte +5 -10
- package/frontend/lib/components/workspace/panels/ChatPanel.svelte +3 -2
- package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +31 -8
- package/frontend/lib/stores/core/sessions.svelte.ts +15 -1
- package/frontend/lib/stores/features/auth.svelte.ts +296 -0
- package/frontend/lib/stores/features/settings.svelte.ts +53 -9
- package/frontend/lib/stores/features/user.svelte.ts +26 -68
- package/frontend/lib/stores/ui/settings-modal.svelte.ts +42 -21
- package/frontend/lib/stores/ui/update.svelte.ts +2 -14
- package/frontend/lib/stores/ui/workspace.svelte.ts +4 -4
- package/package.json +8 -6
- package/shared/types/stores/settings.ts +16 -2
- package/shared/utils/logger.ts +1 -0
- package/shared/utils/ws-client.ts +30 -13
- package/shared/utils/ws-server.ts +42 -4
- package/backend/lib/mcp/stdio-server.ts +0 -103
- package/backend/ws/mcp/index.ts +0 -61
|
@@ -14,7 +14,9 @@ import { debug } from '$shared/utils/logger';
|
|
|
14
14
|
import {
|
|
15
15
|
buildCheckpointTree,
|
|
16
16
|
getCheckpointPathToRoot,
|
|
17
|
-
|
|
17
|
+
findCheckpointForHead,
|
|
18
|
+
findSessionEnd,
|
|
19
|
+
INITIAL_NODE_ID
|
|
18
20
|
} from '../../lib/snapshot/helpers';
|
|
19
21
|
import { ws } from '$backend/lib/utils/ws';
|
|
20
22
|
|
|
@@ -53,7 +55,31 @@ export const restoreHandler = createRouter()
|
|
|
53
55
|
if (project) projectPath = project.path;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
// Build checkpoint path for branch-aware conflict detection
|
|
59
|
+
let targetPath: string[] | undefined;
|
|
60
|
+
let resolvedMessageId: string | null = messageId === INITIAL_NODE_ID ? null : messageId;
|
|
61
|
+
|
|
62
|
+
if (messageId !== INITIAL_NODE_ID) {
|
|
63
|
+
const allMessages = messageQueries.getAllBySessionId(sessionId);
|
|
64
|
+
const { checkpoints, parentMap } = buildCheckpointTree(allMessages);
|
|
65
|
+
const checkpointIdSet = new Set(checkpoints.map(c => c.id));
|
|
66
|
+
|
|
67
|
+
const resolvedId = checkpointIdSet.has(messageId)
|
|
68
|
+
? messageId
|
|
69
|
+
: findCheckpointForHead(messageId, allMessages, checkpointIdSet);
|
|
70
|
+
|
|
71
|
+
if (resolvedId) {
|
|
72
|
+
resolvedMessageId = resolvedId;
|
|
73
|
+
targetPath = getCheckpointPathToRoot(resolvedId, parentMap);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = await snapshotService.checkRestoreConflicts(
|
|
78
|
+
sessionId,
|
|
79
|
+
resolvedMessageId,
|
|
80
|
+
projectPath,
|
|
81
|
+
targetPath
|
|
82
|
+
);
|
|
57
83
|
|
|
58
84
|
debug.log('snapshot', `Conflict check: ${result.conflicts.length} conflicts, ${result.checkpointsToUndo.length} checkpoints to undo`);
|
|
59
85
|
|
|
@@ -82,12 +108,67 @@ export const restoreHandler = createRouter()
|
|
|
82
108
|
})
|
|
83
109
|
}, async ({ data }) => {
|
|
84
110
|
const { messageId, sessionId, conflictResolutions } = data;
|
|
111
|
+
const isInitialRestore = messageId === INITIAL_NODE_ID;
|
|
85
112
|
|
|
86
|
-
debug.log('snapshot',
|
|
87
|
-
debug.log('snapshot', `Target
|
|
113
|
+
debug.log('snapshot', `RESTORE - ${isInitialRestore ? 'Restoring to initial state' : 'Moving HEAD to checkpoint'}`);
|
|
114
|
+
debug.log('snapshot', `Target: ${messageId}`);
|
|
88
115
|
debug.log('snapshot', `Session: ${sessionId}`);
|
|
89
116
|
|
|
90
|
-
//
|
|
117
|
+
// Handle restore to initial state (before any messages)
|
|
118
|
+
if (isInitialRestore) {
|
|
119
|
+
// Clear HEAD (no messages active)
|
|
120
|
+
sessionQueries.clearHead(sessionId);
|
|
121
|
+
debug.log('snapshot', 'HEAD cleared (initial state)');
|
|
122
|
+
|
|
123
|
+
// Clear latest_sdk_session_id so next chat starts fresh
|
|
124
|
+
const db = (await import('../../lib/database')).getDatabase();
|
|
125
|
+
db.prepare(`UPDATE chat_sessions SET latest_sdk_session_id = NULL WHERE id = ?`).run(sessionId);
|
|
126
|
+
|
|
127
|
+
// Clear checkpoint_tree_state
|
|
128
|
+
checkpointQueries.deleteForSession(sessionId);
|
|
129
|
+
|
|
130
|
+
// Restore file system: revert ALL session changes
|
|
131
|
+
let filesRestored = 0;
|
|
132
|
+
let filesSkipped = 0;
|
|
133
|
+
|
|
134
|
+
const session = sessionQueries.getById(sessionId);
|
|
135
|
+
if (session) {
|
|
136
|
+
const project = projectQueries.getById(session.project_id);
|
|
137
|
+
if (project) {
|
|
138
|
+
const result = await snapshotService.restoreSessionScoped(
|
|
139
|
+
project.path,
|
|
140
|
+
sessionId,
|
|
141
|
+
null, // null = restore to initial (before all snapshots)
|
|
142
|
+
conflictResolutions
|
|
143
|
+
);
|
|
144
|
+
filesRestored = result.restoredFiles;
|
|
145
|
+
filesSkipped = result.skippedFiles;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Broadcast messages-changed
|
|
150
|
+
try {
|
|
151
|
+
ws.emit.chatSession(sessionId, 'chat:messages-changed', {
|
|
152
|
+
sessionId,
|
|
153
|
+
reason: 'restore',
|
|
154
|
+
timestamp: new Date().toISOString()
|
|
155
|
+
});
|
|
156
|
+
} catch (err) {
|
|
157
|
+
debug.error('snapshot', 'Failed to broadcast messages-changed:', err);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
restoredTo: {
|
|
162
|
+
messageId: INITIAL_NODE_ID,
|
|
163
|
+
timestamp: new Date().toISOString()
|
|
164
|
+
},
|
|
165
|
+
filesRestored,
|
|
166
|
+
filesSkipped
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Regular checkpoint restore
|
|
171
|
+
// 1. Get the target message
|
|
91
172
|
const checkpointMessage = messageQueries.getById(messageId);
|
|
92
173
|
if (!checkpointMessage) {
|
|
93
174
|
throw new Error('Checkpoint message not found');
|
|
@@ -99,9 +180,19 @@ export const restoreHandler = createRouter()
|
|
|
99
180
|
|
|
100
181
|
// 3. Get all messages and build checkpoint tree
|
|
101
182
|
const allMessages = messageQueries.getAllBySessionId(sessionId);
|
|
102
|
-
const { parentMap } = buildCheckpointTree(allMessages);
|
|
183
|
+
const { checkpoints, parentMap } = buildCheckpointTree(allMessages);
|
|
184
|
+
|
|
185
|
+
// 3b. Resolve the correct checkpoint for snapshot/tree operations
|
|
186
|
+
// The target message may be a non-checkpoint (e.g., assistant response)
|
|
187
|
+
// when called from edit mode. Walk back to find the nearest ancestor checkpoint.
|
|
188
|
+
const checkpointIdSet = new Set(checkpoints.map(c => c.id));
|
|
189
|
+
const resolvedCheckpointId = checkpointIdSet.has(messageId)
|
|
190
|
+
? messageId
|
|
191
|
+
: findCheckpointForHead(messageId, allMessages, checkpointIdSet);
|
|
103
192
|
|
|
104
|
-
|
|
193
|
+
debug.log('snapshot', `Resolved checkpoint: ${resolvedCheckpointId} (target was ${messageId})`);
|
|
194
|
+
|
|
195
|
+
// 4. Find session end (last message of target's session)
|
|
105
196
|
const sessionEnd = findSessionEnd(checkpointMessage, allMessages);
|
|
106
197
|
debug.log('snapshot', `Session end: ${sessionEnd.id}`);
|
|
107
198
|
|
|
@@ -137,12 +228,19 @@ export const restoreHandler = createRouter()
|
|
|
137
228
|
}
|
|
138
229
|
|
|
139
230
|
// 6. Update checkpoint_tree_state for ancestors
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
231
|
+
// Use resolved checkpoint ID (not raw messageId which may be a non-checkpoint)
|
|
232
|
+
// Also compute checkpointPath for branch-aware file restore
|
|
233
|
+
let checkpointPath: string[] = [];
|
|
234
|
+
if (resolvedCheckpointId) {
|
|
235
|
+
checkpointPath = getCheckpointPathToRoot(resolvedCheckpointId, parentMap);
|
|
236
|
+
if (checkpointPath.length > 1) {
|
|
237
|
+
checkpointQueries.updateActiveChildrenAlongPath(sessionId, checkpointPath);
|
|
238
|
+
}
|
|
143
239
|
}
|
|
144
240
|
|
|
145
241
|
// 7. Restore file system state using session-scoped restore
|
|
242
|
+
// Use resolved checkpoint ID so the snapshot lookup matches correctly
|
|
243
|
+
// (snapshots are keyed by checkpoint user message IDs, not assistant messages)
|
|
146
244
|
let filesRestored = 0;
|
|
147
245
|
let filesSkipped = 0;
|
|
148
246
|
|
|
@@ -153,8 +251,9 @@ export const restoreHandler = createRouter()
|
|
|
153
251
|
const result = await snapshotService.restoreSessionScoped(
|
|
154
252
|
project.path,
|
|
155
253
|
sessionId,
|
|
156
|
-
|
|
157
|
-
conflictResolutions
|
|
254
|
+
resolvedCheckpointId,
|
|
255
|
+
conflictResolutions,
|
|
256
|
+
checkpointPath.length > 0 ? checkpointPath : undefined
|
|
158
257
|
);
|
|
159
258
|
filesRestored = result.restoredFiles;
|
|
160
259
|
filesSkipped = result.skippedFiles;
|
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
getCheckpointPathToRoot,
|
|
16
16
|
findCheckpointForHead,
|
|
17
17
|
isDescendant,
|
|
18
|
-
getCheckpointFileStats
|
|
18
|
+
getCheckpointFileStats,
|
|
19
|
+
INITIAL_NODE_ID
|
|
19
20
|
} from '../../lib/snapshot/helpers';
|
|
20
21
|
import type { CheckpointNode, TimelineResponse } from '../../lib/snapshot/helpers';
|
|
21
22
|
import type { SDKMessage } from '$shared/types/messaging';
|
|
@@ -36,11 +37,8 @@ export const timelineHandler = createRouter()
|
|
|
36
37
|
|
|
37
38
|
// 1. Get current HEAD
|
|
38
39
|
const currentHead = sessionQueries.getHead(sessionId);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (!currentHead) {
|
|
42
|
-
return { nodes: [], currentHeadId: null };
|
|
43
|
-
}
|
|
40
|
+
const isAtInitialState = !currentHead;
|
|
41
|
+
debug.log('snapshot', `Current HEAD: ${currentHead || 'null (initial state)'}`);
|
|
44
42
|
|
|
45
43
|
// 2. Get all messages
|
|
46
44
|
const allMessages = messageQueries.getAllBySessionId(sessionId);
|
|
@@ -61,8 +59,10 @@ export const timelineHandler = createRouter()
|
|
|
61
59
|
const checkpointIdSet = new Set(checkpoints.map(c => c.id));
|
|
62
60
|
|
|
63
61
|
// 4. Find which checkpoint HEAD belongs to
|
|
64
|
-
const activeCheckpointId =
|
|
65
|
-
|
|
62
|
+
const activeCheckpointId = isAtInitialState
|
|
63
|
+
? null
|
|
64
|
+
: findCheckpointForHead(currentHead, allMessages, checkpointIdSet);
|
|
65
|
+
debug.log('snapshot', `Active checkpoint: ${activeCheckpointId || '(initial)'}`);
|
|
66
66
|
|
|
67
67
|
// 5. Build active path (from root to active checkpoint)
|
|
68
68
|
const activePathIds = new Set<string>();
|
|
@@ -76,22 +76,47 @@ export const timelineHandler = createRouter()
|
|
|
76
76
|
// 6. Get active children map from database
|
|
77
77
|
const activeChildrenMap = checkpointQueries.getAllActiveChildren(sessionId);
|
|
78
78
|
|
|
79
|
-
// 7.
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
79
|
+
// 7. Build response nodes
|
|
80
|
+
const nodes: CheckpointNode[] = [];
|
|
81
|
+
|
|
82
|
+
// Find root checkpoints (those with no parent)
|
|
83
|
+
const rootCheckpointIds = checkpoints
|
|
84
|
+
.filter(cp => !parentMap.has(cp.id))
|
|
85
|
+
.map(cp => cp.id);
|
|
86
|
+
|
|
87
|
+
// Get session started_at for the initial node timestamp
|
|
88
|
+
const session = sessionQueries.getById(sessionId);
|
|
89
|
+
const sessionStartedAt = session?.started_at || new Date().toISOString();
|
|
90
|
+
|
|
91
|
+
// Add the "Initial State" node at the beginning
|
|
92
|
+
// Its activeChildId points to the first root checkpoint on the active path,
|
|
93
|
+
// or the first root checkpoint if we're at initial state
|
|
94
|
+
let initialActiveChildId: string | null = null;
|
|
95
|
+
if (isAtInitialState) {
|
|
96
|
+
// At initial state: the first root checkpoint (by timestamp) is the active child
|
|
97
|
+
initialActiveChildId = rootCheckpointIds[0] || null;
|
|
98
|
+
} else {
|
|
99
|
+
// Find root checkpoint on active path
|
|
100
|
+
initialActiveChildId = rootCheckpointIds.find(id => activePathIds.has(id)) || rootCheckpointIds[0] || null;
|
|
91
101
|
}
|
|
92
102
|
|
|
93
|
-
|
|
94
|
-
|
|
103
|
+
nodes.push({
|
|
104
|
+
id: INITIAL_NODE_ID,
|
|
105
|
+
messageId: INITIAL_NODE_ID,
|
|
106
|
+
parentId: null,
|
|
107
|
+
activeChildId: initialActiveChildId,
|
|
108
|
+
timestamp: sessionStartedAt,
|
|
109
|
+
messageText: 'Session Start',
|
|
110
|
+
isOnActivePath: isAtInitialState || activePathIds.size > 0,
|
|
111
|
+
isOrphaned: false,
|
|
112
|
+
isCurrent: isAtInitialState,
|
|
113
|
+
hasSnapshot: false,
|
|
114
|
+
isInitial: true,
|
|
115
|
+
senderName: null,
|
|
116
|
+
filesChanged: 0,
|
|
117
|
+
insertions: 0,
|
|
118
|
+
deletions: 0
|
|
119
|
+
});
|
|
95
120
|
|
|
96
121
|
for (const cp of checkpoints) {
|
|
97
122
|
const sdk = JSON.parse(cp.sdk_message) as SDKMessage;
|
|
@@ -107,16 +132,16 @@ export const timelineHandler = createRouter()
|
|
|
107
132
|
isOrphaned = isDescendant(cp.id, activeCheckpointId, childrenMap);
|
|
108
133
|
}
|
|
109
134
|
|
|
110
|
-
// File stats
|
|
111
|
-
const
|
|
112
|
-
const stats = getCheckpointFileStats(cp, allMessages, nextTimestamp);
|
|
135
|
+
// File stats from checkpoint's own snapshot
|
|
136
|
+
const stats = getCheckpointFileStats(cp);
|
|
113
137
|
|
|
114
138
|
const snapshot = snapshotQueries.getByMessageId(cp.id);
|
|
115
139
|
|
|
116
140
|
nodes.push({
|
|
117
141
|
id: cp.id,
|
|
118
142
|
messageId: cp.id,
|
|
119
|
-
|
|
143
|
+
// Root checkpoints have initial node as parent
|
|
144
|
+
parentId: parentCpId || INITIAL_NODE_ID,
|
|
120
145
|
activeChildId,
|
|
121
146
|
timestamp: cp.timestamp,
|
|
122
147
|
messageText,
|
|
@@ -131,11 +156,13 @@ export const timelineHandler = createRouter()
|
|
|
131
156
|
});
|
|
132
157
|
}
|
|
133
158
|
|
|
134
|
-
|
|
135
|
-
|
|
159
|
+
const currentHeadId = isAtInitialState ? INITIAL_NODE_ID : activeCheckpointId;
|
|
160
|
+
|
|
161
|
+
debug.log('snapshot', `Timeline nodes: ${nodes.length} (including initial)`);
|
|
162
|
+
debug.log('snapshot', `Active path: ${activePathIds.size} nodes, current: ${currentHeadId}`);
|
|
136
163
|
|
|
137
164
|
return {
|
|
138
165
|
nodes,
|
|
139
|
-
currentHeadId
|
|
166
|
+
currentHeadId
|
|
140
167
|
};
|
|
141
168
|
});
|
package/bin/clopen.ts
CHANGED
|
@@ -31,6 +31,7 @@ interface CLIOptions {
|
|
|
31
31
|
help?: boolean;
|
|
32
32
|
version?: boolean;
|
|
33
33
|
update?: boolean;
|
|
34
|
+
resetPat?: boolean;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
// Get version from package.json
|
|
@@ -95,6 +96,7 @@ USAGE:
|
|
|
95
96
|
|
|
96
97
|
COMMANDS:
|
|
97
98
|
update Update clopen to the latest version
|
|
99
|
+
reset-pat Regenerate admin Personal Access Token
|
|
98
100
|
|
|
99
101
|
OPTIONS:
|
|
100
102
|
-p, --port <number> Port to run the server on (default: ${DEFAULT_PORT})
|
|
@@ -107,6 +109,7 @@ EXAMPLES:
|
|
|
107
109
|
clopen --port 9150 # Start on port 9150
|
|
108
110
|
clopen --host 0.0.0.0 # Bind to all network interfaces
|
|
109
111
|
clopen update # Update to the latest version
|
|
112
|
+
clopen reset-pat # Regenerate admin login token
|
|
110
113
|
clopen --version # Show version
|
|
111
114
|
|
|
112
115
|
For more information, visit: https://github.com/myrialabs/clopen
|
|
@@ -167,6 +170,10 @@ function parseArguments(): CLIOptions {
|
|
|
167
170
|
options.update = true;
|
|
168
171
|
break;
|
|
169
172
|
|
|
173
|
+
case 'reset-pat':
|
|
174
|
+
options.resetPat = true;
|
|
175
|
+
break;
|
|
176
|
+
|
|
170
177
|
default:
|
|
171
178
|
console.error(`❌ Error: Unknown option "${arg}"`);
|
|
172
179
|
console.log('Run "clopen --help" for usage information');
|
|
@@ -250,6 +257,31 @@ async function runUpdate() {
|
|
|
250
257
|
console.log('\n Restart clopen to apply the update.');
|
|
251
258
|
}
|
|
252
259
|
|
|
260
|
+
async function recoverAdminToken() {
|
|
261
|
+
const version = getVersion();
|
|
262
|
+
console.log(`\x1b[36mClopen\x1b[0m v${version} — Admin Token Recovery\n`);
|
|
263
|
+
|
|
264
|
+
// Initialize database (import dynamically to avoid loading full backend)
|
|
265
|
+
const { initializeDatabase } = await import('../backend/lib/database/index');
|
|
266
|
+
const { listUsers, regeneratePAT } = await import('../backend/lib/auth/auth-service');
|
|
267
|
+
|
|
268
|
+
await initializeDatabase();
|
|
269
|
+
|
|
270
|
+
const users = listUsers();
|
|
271
|
+
const admin = users.find(u => u.role === 'admin');
|
|
272
|
+
|
|
273
|
+
if (!admin) {
|
|
274
|
+
console.error('❌ No admin user found. Start clopen first to complete setup.');
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const newPAT = regeneratePAT(admin.id);
|
|
279
|
+
|
|
280
|
+
console.log(` Admin : ${admin.name}`);
|
|
281
|
+
console.log(` New PAT: \x1b[32m${newPAT}\x1b[0m`);
|
|
282
|
+
console.log(`\n Use this token to log in. Keep it safe — it won't be shown again.`);
|
|
283
|
+
}
|
|
284
|
+
|
|
253
285
|
async function setupEnvironment() {
|
|
254
286
|
// Check if .env exists, if not copy from .env.example
|
|
255
287
|
if (!existsSync(ENV_FILE)) {
|
|
@@ -368,7 +400,23 @@ async function main() {
|
|
|
368
400
|
|
|
369
401
|
// Show version if requested
|
|
370
402
|
if (options.version) {
|
|
371
|
-
|
|
403
|
+
const currentVersion = getVersion();
|
|
404
|
+
console.log(`v${currentVersion}`);
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
const response = await fetch('https://registry.npmjs.org/@myrialabs/clopen/latest');
|
|
408
|
+
if (response.ok) {
|
|
409
|
+
const data = await response.json() as { version: string };
|
|
410
|
+
if (isNewerVersion(currentVersion, data.version)) {
|
|
411
|
+
console.log(`\x1b[33mUpdate available: v${data.version}\x1b[0m — run \x1b[36mclopen update\x1b[0m to update`);
|
|
412
|
+
} else {
|
|
413
|
+
console.log('\x1b[32m(latest)\x1b[0m');
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
} catch {
|
|
417
|
+
// Silent fail — network unavailable
|
|
418
|
+
}
|
|
419
|
+
|
|
372
420
|
process.exit(0);
|
|
373
421
|
}
|
|
374
422
|
|
|
@@ -384,6 +432,13 @@ async function main() {
|
|
|
384
432
|
process.exit(0);
|
|
385
433
|
}
|
|
386
434
|
|
|
435
|
+
// Recover admin token if requested
|
|
436
|
+
if (options.resetPat) {
|
|
437
|
+
await setupEnvironment();
|
|
438
|
+
await recoverAdminToken();
|
|
439
|
+
process.exit(0);
|
|
440
|
+
}
|
|
441
|
+
|
|
387
442
|
// 1. Setup environment variables
|
|
388
443
|
await setupEnvironment();
|
|
389
444
|
|