@geminilight/mindos 1.0.9 → 1.0.29
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/bin/mindos-shim.cjs +318 -7
- package/dist/agent/prompts.d.ts +1 -1
- package/dist/agent/prompts.d.ts.map +1 -1
- package/dist/agent/prompts.js +14 -0
- package/dist/agent/prompts.js.map +1 -1
- package/dist/agent-runtime/claude-code-cli.d.ts +34 -0
- package/dist/agent-runtime/claude-code-cli.d.ts.map +1 -0
- package/dist/agent-runtime/claude-code-cli.js +258 -0
- package/dist/agent-runtime/claude-code-cli.js.map +1 -0
- package/dist/agent-runtime/claude-code-sdk.d.ts +24 -0
- package/dist/agent-runtime/claude-code-sdk.d.ts.map +1 -0
- package/dist/agent-runtime/claude-code-sdk.js +415 -0
- package/dist/agent-runtime/claude-code-sdk.js.map +1 -0
- package/dist/agent-runtime/codex-app-server.d.ts +151 -0
- package/dist/agent-runtime/codex-app-server.d.ts.map +1 -0
- package/dist/agent-runtime/codex-app-server.js +626 -0
- package/dist/agent-runtime/codex-app-server.js.map +1 -0
- package/dist/agent-runtime/index.d.ts +5 -0
- package/dist/agent-runtime/index.d.ts.map +1 -0
- package/dist/agent-runtime/index.js +5 -0
- package/dist/agent-runtime/index.js.map +1 -0
- package/dist/agent-runtime/run.d.ts +104 -0
- package/dist/agent-runtime/run.d.ts.map +1 -0
- package/dist/agent-runtime/run.js +494 -0
- package/dist/agent-runtime/run.js.map +1 -0
- package/dist/agent-runtime.d.ts +2 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +2 -0
- package/dist/agent-runtime.js.map +1 -0
- package/dist/client.d.ts +8 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/foundation/config/schema.d.ts +30 -141
- package/dist/foundation/config/schema.d.ts.map +1 -1
- package/dist/foundation/config/schema.js +18 -4
- package/dist/foundation/config/schema.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/knowledge/git/index.d.ts.map +1 -1
- package/dist/knowledge/git/index.js +43 -2
- package/dist/knowledge/git/index.js.map +1 -1
- package/dist/protocols/acp/agent-descriptors.d.ts.map +1 -1
- package/dist/protocols/acp/agent-descriptors.js +0 -3
- package/dist/protocols/acp/agent-descriptors.js.map +1 -1
- package/dist/protocols/acp/index.js +39 -28
- package/dist/protocols/acp/session.d.ts.map +1 -1
- package/dist/protocols/acp/session.js +11 -14
- package/dist/protocols/acp/session.js.map +1 -1
- package/dist/protocols/acp/subprocess.d.ts +4 -1
- package/dist/protocols/acp/subprocess.d.ts.map +1 -1
- package/dist/protocols/acp/subprocess.js +36 -9
- package/dist/protocols/acp/subprocess.js.map +1 -1
- package/dist/protocols/mcp-server/index.cjs +80 -68
- package/dist/server/channel-contract.d.ts +13 -0
- package/dist/server/channel-contract.d.ts.map +1 -0
- package/dist/server/channel-contract.js +115 -0
- package/dist/server/channel-contract.js.map +1 -0
- package/dist/server/contract.d.ts.map +1 -1
- package/dist/server/contract.js +10 -0
- package/dist/server/contract.js.map +1 -1
- package/dist/server/handlers/agent-runtime-codex.d.ts +26 -0
- package/dist/server/handlers/agent-runtime-codex.d.ts.map +1 -0
- package/dist/server/handlers/agent-runtime-codex.js +170 -0
- package/dist/server/handlers/agent-runtime-codex.js.map +1 -0
- package/dist/server/handlers/agent-runtimes.d.ts +126 -0
- package/dist/server/handlers/agent-runtimes.d.ts.map +1 -0
- package/dist/server/handlers/agent-runtimes.js +690 -0
- package/dist/server/handlers/agent-runtimes.js.map +1 -0
- package/dist/server/handlers/agents.d.ts +6 -0
- package/dist/server/handlers/agents.d.ts.map +1 -1
- package/dist/server/handlers/agents.js +41 -6
- package/dist/server/handlers/agents.js.map +1 -1
- package/dist/server/handlers/ask.d.ts +18 -0
- package/dist/server/handlers/ask.d.ts.map +1 -1
- package/dist/server/handlers/ask.js +70 -0
- package/dist/server/handlers/ask.js.map +1 -1
- package/dist/server/handlers/channels-verify.d.ts +1 -1
- package/dist/server/handlers/channels-verify.d.ts.map +1 -1
- package/dist/server/handlers/channels-verify.js +1 -63
- package/dist/server/handlers/channels-verify.js.map +1 -1
- package/dist/server/handlers/extract-docx.d.ts +42 -0
- package/dist/server/handlers/extract-docx.d.ts.map +1 -0
- package/dist/server/handlers/extract-docx.js +101 -0
- package/dist/server/handlers/extract-docx.js.map +1 -0
- package/dist/server/handlers/extract-pdf.d.ts +32 -0
- package/dist/server/handlers/extract-pdf.d.ts.map +1 -0
- package/dist/server/handlers/extract-pdf.js +116 -0
- package/dist/server/handlers/extract-pdf.js.map +1 -0
- package/dist/server/handlers/im-config.d.ts +1 -1
- package/dist/server/handlers/im-config.d.ts.map +1 -1
- package/dist/server/handlers/im-config.js +69 -59
- package/dist/server/handlers/im-config.js.map +1 -1
- package/dist/server/handlers/im-feishu-oauth.d.ts +55 -0
- package/dist/server/handlers/im-feishu-oauth.d.ts.map +1 -0
- package/dist/server/handlers/im-feishu-oauth.js +218 -0
- package/dist/server/handlers/im-feishu-oauth.js.map +1 -0
- package/dist/server/handlers/im-status.d.ts +15 -0
- package/dist/server/handlers/im-status.d.ts.map +1 -1
- package/dist/server/handlers/im-status.js +41 -24
- package/dist/server/handlers/im-status.js.map +1 -1
- package/dist/server/handlers/inbox-source.d.ts +18 -0
- package/dist/server/handlers/inbox-source.d.ts.map +1 -0
- package/dist/server/handlers/inbox-source.js +108 -0
- package/dist/server/handlers/inbox-source.js.map +1 -0
- package/dist/server/handlers/inbox.d.ts +2 -0
- package/dist/server/handlers/inbox.d.ts.map +1 -1
- package/dist/server/handlers/inbox.js +34 -2
- package/dist/server/handlers/inbox.js.map +1 -1
- package/dist/server/handlers/mcp-agents.d.ts +16 -1
- package/dist/server/handlers/mcp-agents.d.ts.map +1 -1
- package/dist/server/handlers/mcp-agents.js +174 -44
- package/dist/server/handlers/mcp-agents.js.map +1 -1
- package/dist/server/handlers/mcp-install.d.ts +5 -0
- package/dist/server/handlers/mcp-install.d.ts.map +1 -1
- package/dist/server/handlers/mcp-install.js +68 -20
- package/dist/server/handlers/mcp-install.js.map +1 -1
- package/dist/server/handlers/mcp-restart.js +1 -1
- package/dist/server/handlers/mcp-restart.js.map +1 -1
- package/dist/server/handlers/settings-list-models.d.ts +1 -1
- package/dist/server/handlers/settings-list-models.d.ts.map +1 -1
- package/dist/server/handlers/settings-list-models.js +6 -5
- package/dist/server/handlers/settings-list-models.js.map +1 -1
- package/dist/server/handlers/settings-test-key.d.ts.map +1 -1
- package/dist/server/handlers/settings-test-key.js +17 -7
- package/dist/server/handlers/settings-test-key.js.map +1 -1
- package/dist/server/handlers/settings.d.ts +2 -1
- package/dist/server/handlers/settings.d.ts.map +1 -1
- package/dist/server/handlers/settings.js +49 -5
- package/dist/server/handlers/settings.js.map +1 -1
- package/dist/server/handlers/skills.d.ts +1 -0
- package/dist/server/handlers/skills.d.ts.map +1 -1
- package/dist/server/handlers/skills.js +7 -5
- package/dist/server/handlers/skills.js.map +1 -1
- package/dist/server/handlers/sync.d.ts +15 -4
- package/dist/server/handlers/sync.d.ts.map +1 -1
- package/dist/server/handlers/sync.js +552 -81
- package/dist/server/handlers/sync.js.map +1 -1
- package/dist/server/handlers/uninstall.js +1 -0
- package/dist/server/handlers/uninstall.js.map +1 -1
- package/dist/server/handlers/update.js +1 -0
- package/dist/server/handlers/update.js.map +1 -1
- package/dist/server/http.d.ts +20 -0
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +223 -24
- package/dist/server/http.js.map +1 -1
- package/dist/server/index.d.ts +12 -4
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +9 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp-agent-registry.d.ts +184 -0
- package/dist/server/mcp-agent-registry.d.ts.map +1 -1
- package/dist/server/mcp-agent-registry.js +100 -9
- package/dist/server/mcp-agent-registry.js.map +1 -1
- package/dist/server/provider-settings.d.ts +38 -0
- package/dist/server/provider-settings.d.ts.map +1 -0
- package/dist/server/provider-settings.js +286 -0
- package/dist/server/provider-settings.js.map +1 -0
- package/dist/server/route-ownership.d.ts.map +1 -1
- package/dist/server/route-ownership.js +15 -2
- package/dist/server/route-ownership.js.map +1 -1
- package/dist/session/index.d.ts +95 -15
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js +116 -18
- package/dist/session/index.js.map +1 -1
- package/dist/session/pi-coding-agent-runtime.js +3 -3
- package/dist/session/pi-coding-agent-runtime.js.map +1 -1
- package/dist/setup/index.d.ts +1 -0
- package/dist/setup/index.d.ts.map +1 -1
- package/dist/setup/index.js +13 -0
- package/dist/setup/index.js.map +1 -1
- package/package.json +18 -12
- package/src/cli.js +1 -0
|
@@ -1,21 +1,118 @@
|
|
|
1
1
|
import { execFile, execFileSync } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { homedir, hostname } from 'node:os';
|
|
4
5
|
import { dirname, join, resolve } from 'node:path';
|
|
5
6
|
import { resolveExistingSafe } from '../../foundation/security/index.js';
|
|
6
7
|
import { json } from '../response.js';
|
|
7
8
|
const DEFAULT_MINDOS_DIR = join(homedir(), '.mindos');
|
|
8
9
|
const DEFAULT_CONFIG_PATH = join(DEFAULT_MINDOS_DIR, 'config.json');
|
|
9
10
|
const DEFAULT_SYNC_STATE_PATH = join(DEFAULT_MINDOS_DIR, 'sync-state.json');
|
|
11
|
+
const SYNC_LOCK_OWNER_STALE_MS = 5 * 60 * 1000;
|
|
12
|
+
const SYNC_LOCK_ALIVE_HARD_STALE_MS = 30 * 60 * 1000;
|
|
13
|
+
const PROTECTED_GITIGNORE_ENTRIES = ['*.sync-conflict', 'INSTRUCTION.md'];
|
|
14
|
+
class SyncLockedError extends Error {
|
|
15
|
+
owner;
|
|
16
|
+
code = 'SYNC_LOCKED';
|
|
17
|
+
constructor(owner) {
|
|
18
|
+
super(formatSyncLockedMessage(owner));
|
|
19
|
+
this.owner = owner;
|
|
20
|
+
this.name = 'SyncLockedError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function ensureProtectedGitignoreEntries(content) {
|
|
24
|
+
const normalizedLines = content.split(/\r?\n/).map(line => line.trim());
|
|
25
|
+
const missing = PROTECTED_GITIGNORE_ENTRIES.filter(entry => !normalizedLines.includes(entry));
|
|
26
|
+
if (missing.length === 0)
|
|
27
|
+
return content;
|
|
28
|
+
const base = content.trimEnd();
|
|
29
|
+
const appendix = [
|
|
30
|
+
'# MindOS protected sync files',
|
|
31
|
+
...missing,
|
|
32
|
+
'',
|
|
33
|
+
].join('\n');
|
|
34
|
+
return base ? `${base}\n\n${appendix}` : `${appendix}`;
|
|
35
|
+
}
|
|
10
36
|
export async function handleSyncGet(services = {}) {
|
|
11
37
|
try {
|
|
12
38
|
const config = readConfig(services);
|
|
13
|
-
const syncConfig = config.sync
|
|
39
|
+
const syncConfig = config.sync;
|
|
14
40
|
const state = readState(services);
|
|
15
41
|
const mindRoot = config.mindRoot;
|
|
16
|
-
|
|
42
|
+
const conflicts = normalizeConflicts(state.conflicts);
|
|
43
|
+
const configReadError = getConfigReadError(config);
|
|
44
|
+
if (configReadError) {
|
|
45
|
+
return json({
|
|
46
|
+
enabled: false,
|
|
47
|
+
configured: true,
|
|
48
|
+
needsSetup: true,
|
|
49
|
+
provider: 'git',
|
|
50
|
+
remote: '(not configured)',
|
|
51
|
+
branch: 'main',
|
|
52
|
+
lastSync: state.lastSync || null,
|
|
53
|
+
lastPull: state.lastPull || null,
|
|
54
|
+
unpushed: '?',
|
|
55
|
+
conflicts,
|
|
56
|
+
lastError: configReadError,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (!syncConfig) {
|
|
17
60
|
return json({ enabled: false });
|
|
18
61
|
}
|
|
62
|
+
if (!syncConfig.enabled) {
|
|
63
|
+
if (!mindRoot) {
|
|
64
|
+
return json({
|
|
65
|
+
enabled: false,
|
|
66
|
+
configured: true,
|
|
67
|
+
needsSetup: true,
|
|
68
|
+
provider: syncConfig.provider || 'git',
|
|
69
|
+
remote: '(not configured)',
|
|
70
|
+
branch: 'main',
|
|
71
|
+
lastSync: state.lastSync || null,
|
|
72
|
+
lastPull: state.lastPull || null,
|
|
73
|
+
unpushed: '?',
|
|
74
|
+
conflicts,
|
|
75
|
+
lastError: state.lastError || 'Knowledge base directory is not configured. Please re-configure sync.',
|
|
76
|
+
autoCommitInterval: syncConfig.autoCommitInterval || 30,
|
|
77
|
+
autoPullInterval: syncConfig.autoPullInterval || 300,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const hasRepo = callIsGitRepo(services, mindRoot);
|
|
81
|
+
const remote = hasRepo ? callGetRemoteUrl(services, mindRoot) : null;
|
|
82
|
+
if (!hasRepo || !remote) {
|
|
83
|
+
return json({
|
|
84
|
+
enabled: false,
|
|
85
|
+
configured: true,
|
|
86
|
+
needsSetup: true,
|
|
87
|
+
provider: syncConfig.provider || 'git',
|
|
88
|
+
remote: redactGitRemote(remote) || '(not configured)',
|
|
89
|
+
branch: hasRepo ? (callGetBranch(services, mindRoot) || 'main') : 'main',
|
|
90
|
+
lastSync: state.lastSync || null,
|
|
91
|
+
lastPull: state.lastPull || null,
|
|
92
|
+
unpushed: '?',
|
|
93
|
+
conflicts,
|
|
94
|
+
lastError: state.lastError || (!hasRepo
|
|
95
|
+
? 'Git repository not found in knowledge base directory. Please re-configure sync.'
|
|
96
|
+
: 'Remote not configured. Please re-configure sync.'),
|
|
97
|
+
autoCommitInterval: syncConfig.autoCommitInterval || 30,
|
|
98
|
+
autoPullInterval: syncConfig.autoPullInterval || 300,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return json({
|
|
102
|
+
enabled: false,
|
|
103
|
+
configured: true,
|
|
104
|
+
provider: syncConfig.provider || 'git',
|
|
105
|
+
remote: redactGitRemote(remote),
|
|
106
|
+
branch: callGetBranch(services, mindRoot) || 'main',
|
|
107
|
+
lastSync: state.lastSync || null,
|
|
108
|
+
lastPull: state.lastPull || null,
|
|
109
|
+
unpushed: callGetUnpushedCount(services, mindRoot),
|
|
110
|
+
conflicts,
|
|
111
|
+
lastError: state.lastError || null,
|
|
112
|
+
autoCommitInterval: syncConfig.autoCommitInterval || 30,
|
|
113
|
+
autoPullInterval: syncConfig.autoPullInterval || 300,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
19
116
|
const hasRepo = !!mindRoot && callIsGitRepo(services, mindRoot);
|
|
20
117
|
const remote = hasRepo && mindRoot ? callGetRemoteUrl(services, mindRoot) : null;
|
|
21
118
|
if (!hasRepo || !remote) {
|
|
@@ -23,15 +120,15 @@ export async function handleSyncGet(services = {}) {
|
|
|
23
120
|
enabled: true,
|
|
24
121
|
needsSetup: true,
|
|
25
122
|
provider: syncConfig.provider || 'git',
|
|
26
|
-
remote: remote || '(not configured)',
|
|
27
|
-
branch: 'main',
|
|
28
|
-
lastSync: null,
|
|
29
|
-
lastPull: null,
|
|
123
|
+
remote: redactGitRemote(remote) || '(not configured)',
|
|
124
|
+
branch: hasRepo && mindRoot ? (callGetBranch(services, mindRoot) || 'main') : 'main',
|
|
125
|
+
lastSync: state.lastSync || null,
|
|
126
|
+
lastPull: state.lastPull || null,
|
|
30
127
|
unpushed: '?',
|
|
31
|
-
conflicts
|
|
32
|
-
lastError: !hasRepo
|
|
128
|
+
conflicts,
|
|
129
|
+
lastError: state.lastError || (!hasRepo
|
|
33
130
|
? 'Git repository not found in knowledge base directory. Please re-configure sync.'
|
|
34
|
-
: 'Remote not configured. Please re-configure sync.',
|
|
131
|
+
: 'Remote not configured. Please re-configure sync.'),
|
|
35
132
|
autoCommitInterval: syncConfig.autoCommitInterval || 30,
|
|
36
133
|
autoPullInterval: syncConfig.autoPullInterval || 300,
|
|
37
134
|
});
|
|
@@ -39,18 +136,20 @@ export async function handleSyncGet(services = {}) {
|
|
|
39
136
|
return json({
|
|
40
137
|
enabled: true,
|
|
41
138
|
provider: syncConfig.provider || 'git',
|
|
42
|
-
remote,
|
|
139
|
+
remote: redactGitRemote(remote),
|
|
43
140
|
branch: callGetBranch(services, mindRoot) || 'main',
|
|
44
141
|
lastSync: state.lastSync || null,
|
|
45
142
|
lastPull: state.lastPull || null,
|
|
46
143
|
unpushed: callGetUnpushedCount(services, mindRoot),
|
|
47
|
-
conflicts
|
|
144
|
+
conflicts,
|
|
48
145
|
lastError: state.lastError || null,
|
|
49
146
|
autoCommitInterval: syncConfig.autoCommitInterval || 30,
|
|
50
147
|
autoPullInterval: syncConfig.autoPullInterval || 300,
|
|
51
148
|
});
|
|
52
149
|
}
|
|
53
150
|
catch (error) {
|
|
151
|
+
if (isSyncLockedError(error))
|
|
152
|
+
return syncLockedResponse(error);
|
|
54
153
|
return json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 });
|
|
55
154
|
}
|
|
56
155
|
}
|
|
@@ -58,6 +157,10 @@ export async function handleSyncPost(body, services = {}) {
|
|
|
58
157
|
try {
|
|
59
158
|
const payload = body && typeof body === 'object' ? body : {};
|
|
60
159
|
const config = readConfig(services);
|
|
160
|
+
const configReadError = getConfigReadError(config);
|
|
161
|
+
if (configReadError && payload.action !== 'reset') {
|
|
162
|
+
return json({ error: configReadError }, { status: 500 });
|
|
163
|
+
}
|
|
61
164
|
const mindRoot = config.mindRoot;
|
|
62
165
|
if (payload.action === 'reset') {
|
|
63
166
|
return handleSyncReset(config, services);
|
|
@@ -67,63 +170,88 @@ export async function handleSyncPost(body, services = {}) {
|
|
|
67
170
|
}
|
|
68
171
|
switch (payload.action) {
|
|
69
172
|
case 'init':
|
|
70
|
-
return await handleSyncInit(payload, services);
|
|
173
|
+
return await handleSyncInit(payload, config, services);
|
|
71
174
|
case 'now':
|
|
72
175
|
return await handleSyncNow(mindRoot, services);
|
|
73
176
|
case 'on':
|
|
74
|
-
|
|
75
|
-
writeConfig(config, services);
|
|
76
|
-
return json({ ok: true, enabled: true });
|
|
177
|
+
return handleSyncToggle(mindRoot, config, services, true);
|
|
77
178
|
case 'off':
|
|
78
|
-
|
|
79
|
-
writeConfig(config, services);
|
|
80
|
-
return json({ ok: true, enabled: false });
|
|
179
|
+
return handleSyncToggle(mindRoot, config, services, false);
|
|
81
180
|
case 'gitignore-get':
|
|
82
181
|
return handleGitignoreGet(mindRoot);
|
|
83
182
|
case 'gitignore-save':
|
|
84
|
-
return handleGitignoreSave(mindRoot, payload);
|
|
183
|
+
return handleGitignoreSave(mindRoot, payload, services);
|
|
85
184
|
case 'resolve-conflict':
|
|
86
185
|
return handleResolveConflict(mindRoot, payload, services);
|
|
87
186
|
case 'conflict-preview':
|
|
88
|
-
return handleConflictPreview(mindRoot, payload);
|
|
187
|
+
return handleConflictPreview(mindRoot, payload, services);
|
|
89
188
|
case 'update-intervals':
|
|
90
|
-
return handleUpdateIntervals(payload, config, services);
|
|
189
|
+
return handleUpdateIntervals(mindRoot, payload, config, services);
|
|
91
190
|
default:
|
|
92
191
|
return json({ error: `Unknown action: ${payload.action}` }, { status: 400 });
|
|
93
192
|
}
|
|
94
193
|
}
|
|
95
194
|
catch (error) {
|
|
195
|
+
if (isSyncLockedError(error))
|
|
196
|
+
return syncLockedResponse(error);
|
|
96
197
|
return json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 });
|
|
97
198
|
}
|
|
98
199
|
}
|
|
200
|
+
function handleSyncToggle(mindRoot, config, services, enabled) {
|
|
201
|
+
return withServerSyncLock(mindRoot, enabled ? 'sync-on' : 'sync-off', services, () => {
|
|
202
|
+
config.sync = { ...(config.sync ?? {}), enabled };
|
|
203
|
+
writeConfig(config, services);
|
|
204
|
+
if (enabled)
|
|
205
|
+
notifySyncDaemon(services, 'restart', mindRoot);
|
|
206
|
+
else
|
|
207
|
+
notifySyncDaemon(services, 'stop');
|
|
208
|
+
return json({ ok: true, enabled });
|
|
209
|
+
});
|
|
210
|
+
}
|
|
99
211
|
function handleSyncReset(config, services) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
212
|
+
return withServerSyncLock(config.mindRoot ?? null, 'reset', services, () => {
|
|
213
|
+
if (getConfigReadError(config)) {
|
|
214
|
+
backupUnreadableConfig(services);
|
|
215
|
+
writeConfig({}, services);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
delete config.sync;
|
|
219
|
+
writeConfig(stripInternalConfigFields(config), services);
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
writeState({}, services);
|
|
223
|
+
}
|
|
224
|
+
catch { }
|
|
225
|
+
notifySyncDaemon(services, 'stop');
|
|
226
|
+
return json({ ok: true, enabled: false });
|
|
227
|
+
});
|
|
107
228
|
}
|
|
108
|
-
async function handleSyncInit(payload, services) {
|
|
229
|
+
async function handleSyncInit(payload, config, services) {
|
|
109
230
|
const remote = payload.remote?.trim();
|
|
110
231
|
if (!remote) {
|
|
111
232
|
return json({ error: 'Remote URL is required' }, { status: 400 });
|
|
112
233
|
}
|
|
113
234
|
const isHttps = remote.startsWith('https://');
|
|
114
|
-
const isSsh = /^git@[\w.-]+:.+/.test(remote);
|
|
235
|
+
const isSsh = /^git@[\w.-]+:.+/.test(remote) || /^ssh:\/\/git@[^/]+\/.+/.test(remote);
|
|
115
236
|
if (!isHttps && !isSsh) {
|
|
116
237
|
return json({ error: 'Invalid remote URL — must be HTTPS or SSH format' }, { status: 400 });
|
|
117
238
|
}
|
|
118
239
|
const branch = payload.branch?.trim() || 'main';
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
240
|
+
if (!isValidGitBranchName(branch)) {
|
|
241
|
+
return json({ error: 'Invalid branch name' }, { status: 400 });
|
|
242
|
+
}
|
|
243
|
+
const normalizedRemote = normalizeHttpsRemoteCredentials(remote, payload.token?.trim());
|
|
244
|
+
const args = ['sync', 'init', '--non-interactive', '--remote', normalizedRemote.remote, '--branch', branch];
|
|
245
|
+
const envOverrides = normalizedRemote.token ? { MINDOS_SYNC_TOKEN: normalizedRemote.token } : undefined;
|
|
122
246
|
try {
|
|
123
|
-
await runCli(args, 120000, services);
|
|
247
|
+
await runCli(args, 120000, services, envOverrides);
|
|
248
|
+
if (config.mindRoot)
|
|
249
|
+
notifySyncDaemon(services, 'restart', config.mindRoot);
|
|
124
250
|
return json({ success: true, message: 'Sync initialized' });
|
|
125
251
|
}
|
|
126
252
|
catch (error) {
|
|
253
|
+
if (isSyncLockedError(error))
|
|
254
|
+
return syncLockedResponse(error);
|
|
127
255
|
return json({ error: error instanceof Error ? error.message : String(error) }, { status: 400 });
|
|
128
256
|
}
|
|
129
257
|
}
|
|
@@ -136,6 +264,8 @@ async function handleSyncNow(mindRoot, services) {
|
|
|
136
264
|
return json({ ok: true });
|
|
137
265
|
}
|
|
138
266
|
catch (error) {
|
|
267
|
+
if (isSyncLockedError(error))
|
|
268
|
+
return syncLockedResponse(error);
|
|
139
269
|
return json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 });
|
|
140
270
|
}
|
|
141
271
|
}
|
|
@@ -143,19 +273,29 @@ function handleGitignoreGet(mindRoot) {
|
|
|
143
273
|
try {
|
|
144
274
|
return json({ content: readFileSync(resolveExistingSafe(mindRoot, '.gitignore'), 'utf-8') });
|
|
145
275
|
}
|
|
146
|
-
catch {
|
|
147
|
-
|
|
276
|
+
catch (error) {
|
|
277
|
+
if (isFsErrorCode(error, 'ENOENT'))
|
|
278
|
+
return json({ content: '' });
|
|
279
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
280
|
+
if (/access denied|outside root|absolute paths/i.test(message))
|
|
281
|
+
return json({ error: 'Access denied' }, { status: 403 });
|
|
282
|
+
return json({ error: message }, { status: 500 });
|
|
148
283
|
}
|
|
149
284
|
}
|
|
150
|
-
function handleGitignoreSave(mindRoot, payload) {
|
|
285
|
+
function handleGitignoreSave(mindRoot, payload, services) {
|
|
151
286
|
if (typeof payload.content !== 'string') {
|
|
152
287
|
return json({ error: 'Missing content' }, { status: 400 });
|
|
153
288
|
}
|
|
289
|
+
const content = ensureProtectedGitignoreEntries(payload.content);
|
|
154
290
|
try {
|
|
155
|
-
|
|
156
|
-
|
|
291
|
+
return withServerSyncLock(mindRoot, 'gitignore-save', services, () => {
|
|
292
|
+
writeFileSync(resolveExistingSafe(mindRoot, '.gitignore'), content, 'utf-8');
|
|
293
|
+
return json({ ok: true, content });
|
|
294
|
+
});
|
|
157
295
|
}
|
|
158
296
|
catch (error) {
|
|
297
|
+
if (isSyncLockedError(error))
|
|
298
|
+
return syncLockedResponse(error);
|
|
159
299
|
const message = error instanceof Error ? error.message : String(error);
|
|
160
300
|
if (/access denied|outside root|absolute paths/i.test(message))
|
|
161
301
|
return json({ error: 'Access denied' }, { status: 403 });
|
|
@@ -163,11 +303,14 @@ function handleGitignoreSave(mindRoot, payload) {
|
|
|
163
303
|
}
|
|
164
304
|
}
|
|
165
305
|
function handleResolveConflict(mindRoot, payload, services) {
|
|
166
|
-
const file = payload.remote;
|
|
167
|
-
const strategy = payload.branch ?? 'keep-local';
|
|
306
|
+
const file = payload.file ?? payload.remote;
|
|
307
|
+
const strategy = payload.strategy ?? payload.branch ?? 'keep-local';
|
|
168
308
|
if (!file || typeof file !== 'string') {
|
|
169
309
|
return json({ error: 'Missing file path' }, { status: 400 });
|
|
170
310
|
}
|
|
311
|
+
if (strategy !== 'keep-local' && strategy !== 'keep-remote') {
|
|
312
|
+
return json({ error: 'Invalid conflict resolution strategy' }, { status: 400 });
|
|
313
|
+
}
|
|
171
314
|
if (!isPathWithinMindRoot(mindRoot, file)) {
|
|
172
315
|
return json({ error: 'Invalid file path' }, { status: 400 });
|
|
173
316
|
}
|
|
@@ -176,21 +319,33 @@ function handleResolveConflict(mindRoot, payload, services) {
|
|
|
176
319
|
if (!conflictPath || !originalPath) {
|
|
177
320
|
return json({ error: 'Invalid file path' }, { status: 400 });
|
|
178
321
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
322
|
+
return withServerSyncLock(mindRoot, 'resolve-conflict', services, () => {
|
|
323
|
+
const state = readState(services);
|
|
324
|
+
const conflict = findConflict(state, file);
|
|
325
|
+
if (strategy === 'keep-remote') {
|
|
326
|
+
if (conflict?.remoteExists === false) {
|
|
327
|
+
rmSync(originalPath, { force: true });
|
|
328
|
+
}
|
|
329
|
+
else if (existsSync(conflictPath)) {
|
|
330
|
+
writeFileSync(originalPath, readFileSync(conflictPath, 'utf-8'), 'utf-8');
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
return json({ error: 'Remote conflict backup is missing' }, { status: 409 });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else if (conflict?.localExists === false) {
|
|
337
|
+
rmSync(originalPath, { force: true });
|
|
338
|
+
}
|
|
339
|
+
if (existsSync(conflictPath)) {
|
|
340
|
+
unlinkSync(conflictPath);
|
|
341
|
+
}
|
|
342
|
+
const nextConflicts = normalizeConflicts(state.conflicts).filter((conflict) => conflict.file !== file);
|
|
343
|
+
writeState({ ...state, conflicts: nextConflicts }, services);
|
|
344
|
+
return json({ ok: true });
|
|
345
|
+
});
|
|
191
346
|
}
|
|
192
|
-
function handleConflictPreview(mindRoot, payload) {
|
|
193
|
-
const file = payload.remote;
|
|
347
|
+
function handleConflictPreview(mindRoot, payload, services) {
|
|
348
|
+
const file = payload.file ?? payload.remote;
|
|
194
349
|
if (!file || typeof file !== 'string') {
|
|
195
350
|
return json({ error: 'Missing file path' }, { status: 400 });
|
|
196
351
|
}
|
|
@@ -202,12 +357,33 @@ function handleConflictPreview(mindRoot, payload) {
|
|
|
202
357
|
if (!localPath || !remotePath) {
|
|
203
358
|
return json({ error: 'Invalid file path' }, { status: 400 });
|
|
204
359
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
360
|
+
try {
|
|
361
|
+
const conflict = findConflict(readState(services), file);
|
|
362
|
+
if (!existsSync(remotePath)) {
|
|
363
|
+
if (conflict?.remoteExists === false) {
|
|
364
|
+
return json({
|
|
365
|
+
local: conflict?.localExists === false || !existsSync(localPath) ? '' : readFileSync(localPath, 'utf-8'),
|
|
366
|
+
remote: '',
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
return json({ error: 'Remote conflict backup is missing' }, { status: 409 });
|
|
370
|
+
}
|
|
371
|
+
return json({
|
|
372
|
+
local: conflict?.localExists === false || !existsSync(localPath) ? '' : readFileSync(localPath, 'utf-8'),
|
|
373
|
+
remote: readFileSync(remotePath, 'utf-8'),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
if (isFsErrorCode(error, 'ENOENT')) {
|
|
378
|
+
return json({ error: 'Remote conflict backup is missing' }, { status: 409 });
|
|
379
|
+
}
|
|
380
|
+
if (isFsErrorCode(error, 'EACCES') || isFsErrorCode(error, 'EPERM')) {
|
|
381
|
+
return json({ error: 'Access denied' }, { status: 403 });
|
|
382
|
+
}
|
|
383
|
+
return json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 });
|
|
384
|
+
}
|
|
209
385
|
}
|
|
210
|
-
function handleUpdateIntervals(payload, config, services) {
|
|
386
|
+
function handleUpdateIntervals(mindRoot, payload, config, services) {
|
|
211
387
|
const commitInterval = typeof payload.autoCommitInterval === 'number' ? payload.autoCommitInterval : undefined;
|
|
212
388
|
const pullInterval = typeof payload.autoPullInterval === 'number' ? payload.autoPullInterval : undefined;
|
|
213
389
|
if (commitInterval === undefined && pullInterval === undefined) {
|
|
@@ -219,21 +395,98 @@ function handleUpdateIntervals(payload, config, services) {
|
|
|
219
395
|
if (pullInterval !== undefined && (!Number.isInteger(pullInterval) || pullInterval < 60 || pullInterval > 3600)) {
|
|
220
396
|
return json({ error: 'autoPullInterval must be an integer between 60 and 3600 seconds' }, { status: 400 });
|
|
221
397
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
398
|
+
return withServerSyncLock(mindRoot, 'update-intervals', services, () => {
|
|
399
|
+
config.sync = config.sync ?? {};
|
|
400
|
+
if (commitInterval !== undefined)
|
|
401
|
+
config.sync.autoCommitInterval = commitInterval;
|
|
402
|
+
if (pullInterval !== undefined)
|
|
403
|
+
config.sync.autoPullInterval = pullInterval;
|
|
404
|
+
writeConfig(config, services);
|
|
405
|
+
if (config.sync.enabled)
|
|
406
|
+
notifySyncDaemon(services, 'reconfigure', mindRoot);
|
|
407
|
+
return json({
|
|
408
|
+
autoCommitInterval: config.sync.autoCommitInterval || 30,
|
|
409
|
+
autoPullInterval: config.sync.autoPullInterval || 300,
|
|
410
|
+
});
|
|
231
411
|
});
|
|
232
412
|
}
|
|
413
|
+
function isValidGitBranchName(input) {
|
|
414
|
+
const value = input.trim();
|
|
415
|
+
if (!value || value === '@')
|
|
416
|
+
return false;
|
|
417
|
+
if (value.startsWith('-') || value.startsWith('/') || value.endsWith('/'))
|
|
418
|
+
return false;
|
|
419
|
+
if (value.endsWith('.') || value.includes('..') || value.includes('//') || value.includes('@{'))
|
|
420
|
+
return false;
|
|
421
|
+
if (/[\s~^:?*\[\\\]]/.test(value))
|
|
422
|
+
return false;
|
|
423
|
+
return value.split('/').every(part => part && !part.startsWith('.') && !part.endsWith('.lock'));
|
|
424
|
+
}
|
|
425
|
+
function normalizeConflicts(value) {
|
|
426
|
+
const items = Array.isArray(value) ? value : (value && typeof value === 'object' ? [value] : []);
|
|
427
|
+
const conflicts = [];
|
|
428
|
+
for (const item of items) {
|
|
429
|
+
if (typeof item === 'string') {
|
|
430
|
+
const file = item.trim();
|
|
431
|
+
if (file)
|
|
432
|
+
conflicts.push({ file });
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (!item || typeof item !== 'object')
|
|
436
|
+
continue;
|
|
437
|
+
const record = item;
|
|
438
|
+
const file = typeof record.file === 'string' ? record.file.trim() : '';
|
|
439
|
+
if (!file)
|
|
440
|
+
continue;
|
|
441
|
+
conflicts.push({
|
|
442
|
+
file,
|
|
443
|
+
...(typeof record.time === 'string' && record.time ? { time: record.time } : {}),
|
|
444
|
+
...(record.noBackup === true ? { noBackup: true } : {}),
|
|
445
|
+
...(typeof record.localExists === 'boolean' ? { localExists: record.localExists } : {}),
|
|
446
|
+
...(typeof record.remoteExists === 'boolean' ? { remoteExists: record.remoteExists } : {}),
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return conflicts;
|
|
450
|
+
}
|
|
451
|
+
function findConflict(state, file) {
|
|
452
|
+
return normalizeConflicts(state.conflicts).find((conflict) => conflict.file === file) ?? null;
|
|
453
|
+
}
|
|
454
|
+
function notifySyncDaemon(services, action, mindRoot) {
|
|
455
|
+
try {
|
|
456
|
+
if (action === 'stop') {
|
|
457
|
+
services.syncDaemon?.stop?.();
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (!mindRoot)
|
|
461
|
+
return;
|
|
462
|
+
if (action === 'restart') {
|
|
463
|
+
if (services.syncDaemon?.restart) {
|
|
464
|
+
services.syncDaemon.restart(mindRoot);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
services.syncDaemon?.stop?.();
|
|
468
|
+
services.syncDaemon?.start?.(mindRoot);
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (action === 'reconfigure') {
|
|
473
|
+
if (services.syncDaemon?.reconfigure)
|
|
474
|
+
services.syncDaemon.reconfigure(mindRoot);
|
|
475
|
+
else if (services.syncDaemon?.restart)
|
|
476
|
+
services.syncDaemon.restart(mindRoot);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
services.syncDaemon?.start?.(mindRoot);
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
// Sync config/state has already been persisted. Runtime daemon refresh is
|
|
483
|
+
// best-effort and will also be corrected by the daemon config poller.
|
|
484
|
+
}
|
|
485
|
+
}
|
|
233
486
|
function readConfig(services) {
|
|
234
487
|
if (services.readConfig)
|
|
235
488
|
return services.readConfig();
|
|
236
|
-
return readJsonFile(services.configPath ?? DEFAULT_CONFIG_PATH);
|
|
489
|
+
return readJsonFile(services.configPath ?? DEFAULT_CONFIG_PATH, { reportParseError: true });
|
|
237
490
|
}
|
|
238
491
|
function writeConfig(config, services) {
|
|
239
492
|
if (services.writeConfig) {
|
|
@@ -254,13 +507,180 @@ function writeState(state, services) {
|
|
|
254
507
|
}
|
|
255
508
|
atomicWriteJson(services.statePath ?? DEFAULT_SYNC_STATE_PATH, state);
|
|
256
509
|
}
|
|
257
|
-
function
|
|
510
|
+
function withServerSyncLock(mindRoot, operation, services, callback) {
|
|
511
|
+
if (!mindRoot)
|
|
512
|
+
return callback();
|
|
513
|
+
const lock = acquireServerSyncLock(mindRoot, operation, services);
|
|
514
|
+
try {
|
|
515
|
+
return callback();
|
|
516
|
+
}
|
|
517
|
+
finally {
|
|
518
|
+
releaseServerSyncLock(lock);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
export function getServerSyncLockPath(mindRoot, services = {}) {
|
|
522
|
+
const normalized = resolve(mindRoot || '.');
|
|
523
|
+
const hash = createHash('sha256').update(normalized).digest('hex').slice(0, 16);
|
|
524
|
+
return join(services.syncLockDir ?? join(DEFAULT_MINDOS_DIR, 'sync-locks'), `${hash}.lock`);
|
|
525
|
+
}
|
|
526
|
+
function acquireServerSyncLock(mindRoot, operation, services) {
|
|
527
|
+
const lockPath = getServerSyncLockPath(mindRoot, services);
|
|
528
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
529
|
+
const token = randomUUID();
|
|
530
|
+
while (true) {
|
|
531
|
+
try {
|
|
532
|
+
mkdirSync(lockPath);
|
|
533
|
+
try {
|
|
534
|
+
writeFileSync(join(lockPath, 'owner.json'), `${JSON.stringify({
|
|
535
|
+
pid: process.pid,
|
|
536
|
+
hostname: hostname(),
|
|
537
|
+
operation,
|
|
538
|
+
mindRoot: resolve(mindRoot),
|
|
539
|
+
startedAt: new Date().toISOString(),
|
|
540
|
+
token,
|
|
541
|
+
}, null, 2)}\n`, 'utf-8');
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
return { lockPath, token };
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
if (!isFsErrorCode(error, 'EEXIST'))
|
|
551
|
+
throw error;
|
|
552
|
+
if (!isServerSyncLockStale(lockPath)) {
|
|
553
|
+
throw new SyncLockedError(readServerSyncLockOwner(lockPath));
|
|
554
|
+
}
|
|
555
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function releaseServerSyncLock(lock) {
|
|
560
|
+
const owner = readServerSyncLockOwner(lock.lockPath);
|
|
561
|
+
if (owner?.token === lock.token) {
|
|
562
|
+
rmSync(lock.lockPath, { recursive: true, force: true });
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function readServerSyncLockOwner(lockPath) {
|
|
566
|
+
try {
|
|
567
|
+
return JSON.parse(readFileSync(join(lockPath, 'owner.json'), 'utf-8'));
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function isServerSyncLockStale(lockPath) {
|
|
574
|
+
const owner = readServerSyncLockOwner(lockPath);
|
|
575
|
+
const ageMs = getServerSyncLockAgeMs(lockPath, owner);
|
|
576
|
+
if (!owner || typeof owner !== 'object')
|
|
577
|
+
return ageMs > SYNC_LOCK_OWNER_STALE_MS;
|
|
578
|
+
if (owner.hostname && owner.hostname !== hostname()) {
|
|
579
|
+
return ageMs > SYNC_LOCK_ALIVE_HARD_STALE_MS;
|
|
580
|
+
}
|
|
581
|
+
if (owner.pid && !isProcessAlive(owner.pid))
|
|
582
|
+
return true;
|
|
583
|
+
if (owner.pid && isProcessAlive(owner.pid))
|
|
584
|
+
return false;
|
|
585
|
+
return ageMs > SYNC_LOCK_OWNER_STALE_MS;
|
|
586
|
+
}
|
|
587
|
+
function getServerSyncLockAgeMs(lockPath, owner) {
|
|
588
|
+
const startedAt = owner?.startedAt ? new Date(owner.startedAt).getTime() : NaN;
|
|
589
|
+
if (Number.isFinite(startedAt))
|
|
590
|
+
return Math.max(0, Date.now() - startedAt);
|
|
591
|
+
try {
|
|
592
|
+
return Math.max(0, Date.now() - statSync(lockPath).mtimeMs);
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
return Number.POSITIVE_INFINITY;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function isProcessAlive(pid) {
|
|
599
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
600
|
+
return false;
|
|
601
|
+
try {
|
|
602
|
+
process.kill(pid, 0);
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
return isFsErrorCode(error, 'EPERM');
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function formatSyncLockedMessage(owner) {
|
|
610
|
+
const parts = [];
|
|
611
|
+
if (owner?.operation)
|
|
612
|
+
parts.push(`owner=${owner.operation}`);
|
|
613
|
+
if (owner?.pid)
|
|
614
|
+
parts.push(`pid=${owner.pid}`);
|
|
615
|
+
if (owner?.startedAt)
|
|
616
|
+
parts.push(`startedAt=${owner.startedAt}`);
|
|
617
|
+
const suffix = parts.length ? ` (${parts.join(', ')})` : '';
|
|
618
|
+
return `SYNC_LOCKED: Sync is already running${suffix}`;
|
|
619
|
+
}
|
|
620
|
+
function isSyncLockedError(error) {
|
|
621
|
+
const code = typeof error === 'object' && error !== null && 'code' in error ? String(error.code) : '';
|
|
622
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
623
|
+
return code === 'SYNC_LOCKED' || /SYNC_LOCKED/i.test(message);
|
|
624
|
+
}
|
|
625
|
+
function syncLockedResponse(error) {
|
|
626
|
+
const message = error instanceof SyncLockedError
|
|
627
|
+
? error.message
|
|
628
|
+
: normalizeSyncLockedMessage(error instanceof Error ? error.message : String(error));
|
|
629
|
+
return json({ error: message }, { status: 423 });
|
|
630
|
+
}
|
|
631
|
+
function normalizeSyncLockedMessage(message) {
|
|
632
|
+
const trimmed = stripAnsi(message).trim();
|
|
633
|
+
if (/^SYNC_LOCKED:/i.test(trimmed))
|
|
634
|
+
return trimmed;
|
|
635
|
+
const match = trimmed.match(/SYNC_LOCKED:.*$/ims);
|
|
636
|
+
return match ? match[0].trim() : 'SYNC_LOCKED: Sync is already running';
|
|
637
|
+
}
|
|
638
|
+
function stripAnsi(value) {
|
|
639
|
+
return value.replace(/\x1B\[[0-9;]*m/g, '');
|
|
640
|
+
}
|
|
641
|
+
function isFsErrorCode(error, code) {
|
|
642
|
+
return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
|
|
643
|
+
}
|
|
644
|
+
function readJsonFile(filePath, opts = {}) {
|
|
645
|
+
let raw = '';
|
|
258
646
|
try {
|
|
259
|
-
|
|
647
|
+
raw = readFileSync(filePath, 'utf-8');
|
|
260
648
|
}
|
|
261
649
|
catch {
|
|
262
650
|
return {};
|
|
263
651
|
}
|
|
652
|
+
try {
|
|
653
|
+
return JSON.parse(stripBom(raw));
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
if (!opts.reportParseError)
|
|
657
|
+
return {};
|
|
658
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
659
|
+
return {
|
|
660
|
+
__readError: `MindOS config file could not be read: ${detail}`,
|
|
661
|
+
__readErrorPath: filePath,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
function stripBom(value) {
|
|
666
|
+
return value.charCodeAt(0) === 0xFEFF ? value.slice(1) : value;
|
|
667
|
+
}
|
|
668
|
+
function getConfigReadError(config) {
|
|
669
|
+
return typeof config.__readError === 'string' && config.__readError ? config.__readError : null;
|
|
670
|
+
}
|
|
671
|
+
function stripInternalConfigFields(config) {
|
|
672
|
+
const { __readError, __readErrorPath, ...rest } = config;
|
|
673
|
+
return rest;
|
|
674
|
+
}
|
|
675
|
+
function backupUnreadableConfig(services) {
|
|
676
|
+
const configPath = services.configPath ?? DEFAULT_CONFIG_PATH;
|
|
677
|
+
if (services.readConfig || services.writeConfig || !existsSync(configPath))
|
|
678
|
+
return;
|
|
679
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
680
|
+
try {
|
|
681
|
+
renameSync(configPath, `${configPath}.broken-${stamp}`);
|
|
682
|
+
}
|
|
683
|
+
catch { }
|
|
264
684
|
}
|
|
265
685
|
function atomicWriteJson(filePath, data) {
|
|
266
686
|
const dir = dirname(filePath);
|
|
@@ -298,12 +718,27 @@ function callGetBranch(services, cwd) {
|
|
|
298
718
|
function callGetUnpushedCount(services, cwd) {
|
|
299
719
|
if (services.getUnpushedCount)
|
|
300
720
|
return services.getUnpushedCount(cwd);
|
|
721
|
+
let unpushedCommits = null;
|
|
722
|
+
let dirtyFiles = null;
|
|
301
723
|
try {
|
|
302
|
-
|
|
724
|
+
const raw = runGit(cwd, ['rev-list', '--count', '@{u}..HEAD']);
|
|
725
|
+
const parsed = Number.parseInt(raw, 10);
|
|
726
|
+
if (Number.isFinite(parsed))
|
|
727
|
+
unpushedCommits = parsed;
|
|
303
728
|
}
|
|
304
|
-
catch {
|
|
305
|
-
|
|
729
|
+
catch { }
|
|
730
|
+
try {
|
|
731
|
+
const status = runGit(cwd, ['status', '--porcelain=v1']);
|
|
732
|
+
dirtyFiles = status
|
|
733
|
+
? status.split('\n').filter(line => line.trim() && !line.slice(3).endsWith('.sync-conflict')).length
|
|
734
|
+
: 0;
|
|
306
735
|
}
|
|
736
|
+
catch { }
|
|
737
|
+
if (unpushedCommits === null && dirtyFiles === null)
|
|
738
|
+
return '?';
|
|
739
|
+
if (unpushedCommits === null && dirtyFiles === 0)
|
|
740
|
+
return '?';
|
|
741
|
+
return String((unpushedCommits || 0) + (dirtyFiles || 0));
|
|
307
742
|
}
|
|
308
743
|
function runGit(cwd, args) {
|
|
309
744
|
return execFileSync('git', args, {
|
|
@@ -312,6 +747,42 @@ function runGit(cwd, args) {
|
|
|
312
747
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
313
748
|
}).trim();
|
|
314
749
|
}
|
|
750
|
+
export function redactGitRemote(remote) {
|
|
751
|
+
if (!remote)
|
|
752
|
+
return null;
|
|
753
|
+
if (!/^https?:\/\//i.test(remote))
|
|
754
|
+
return remote;
|
|
755
|
+
try {
|
|
756
|
+
const parsed = new URL(remote);
|
|
757
|
+
parsed.username = '';
|
|
758
|
+
parsed.password = '';
|
|
759
|
+
return parsed.toString();
|
|
760
|
+
}
|
|
761
|
+
catch {
|
|
762
|
+
return remote.replace(/^(https?:\/\/)[^/@]+@/i, '$1');
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
function looksLikeAccessToken(value) {
|
|
766
|
+
return /^(gh[pousr]_|github_pat_|glpat-|pat_)/i.test(value);
|
|
767
|
+
}
|
|
768
|
+
function normalizeHttpsRemoteCredentials(remote, token) {
|
|
769
|
+
if (!/^https?:\/\//i.test(remote))
|
|
770
|
+
return { remote, token };
|
|
771
|
+
try {
|
|
772
|
+
const parsed = new URL(remote);
|
|
773
|
+
const embeddedPassword = parsed.password ? decodeURIComponent(parsed.password) : '';
|
|
774
|
+
const embeddedUsername = parsed.username ? decodeURIComponent(parsed.username) : '';
|
|
775
|
+
parsed.username = '';
|
|
776
|
+
parsed.password = '';
|
|
777
|
+
return {
|
|
778
|
+
remote: parsed.toString(),
|
|
779
|
+
token: token || embeddedPassword || (looksLikeAccessToken(embeddedUsername) ? embeddedUsername : undefined),
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
catch {
|
|
783
|
+
return { remote, token };
|
|
784
|
+
}
|
|
785
|
+
}
|
|
315
786
|
function isPathWithinMindRoot(mindRoot, filePath) {
|
|
316
787
|
return resolveMindRootPath(mindRoot, filePath) !== null;
|
|
317
788
|
}
|
|
@@ -323,12 +794,12 @@ function resolveMindRootPath(mindRoot, filePath) {
|
|
|
323
794
|
return null;
|
|
324
795
|
}
|
|
325
796
|
}
|
|
326
|
-
async function runCli(args, timeoutMs, services) {
|
|
797
|
+
async function runCli(args, timeoutMs, services, envOverrides) {
|
|
327
798
|
if (services.runCli) {
|
|
328
|
-
await services.runCli(args, timeoutMs);
|
|
799
|
+
await services.runCli(args, timeoutMs, envOverrides);
|
|
329
800
|
return;
|
|
330
801
|
}
|
|
331
|
-
const env = services.env ?? process.env;
|
|
802
|
+
const env = { ...(services.env ?? process.env), ...(envOverrides ?? {}) };
|
|
332
803
|
const nodeBin = services.nodeBin ?? env.MINDOS_NODE_BIN ?? process.execPath;
|
|
333
804
|
const cliPath = services.cliPath ?? resolveMindosCliPath({
|
|
334
805
|
env,
|
|
@@ -336,7 +807,7 @@ async function runCli(args, timeoutMs, services) {
|
|
|
336
807
|
projectRoot: services.projectRoot,
|
|
337
808
|
});
|
|
338
809
|
await new Promise((resolveDone, rejectDone) => {
|
|
339
|
-
execFile(nodeBin, [cliPath, ...args], { timeout: timeoutMs, encoding: 'utf-8' }, (error, stdout, stderr) => {
|
|
810
|
+
execFile(nodeBin, [cliPath, ...args], { timeout: timeoutMs, encoding: 'utf-8', env }, (error, stdout, stderr) => {
|
|
340
811
|
if (error)
|
|
341
812
|
rejectDone(new Error(formatProcessError(error, stdout, stderr)));
|
|
342
813
|
else
|