@geminilight/mindos 1.0.10 → 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.
Files changed (174) hide show
  1. package/bin/mindos-shim.cjs +318 -7
  2. package/dist/agent/prompts.d.ts +1 -1
  3. package/dist/agent/prompts.d.ts.map +1 -1
  4. package/dist/agent/prompts.js +14 -0
  5. package/dist/agent/prompts.js.map +1 -1
  6. package/dist/agent-runtime/claude-code-cli.d.ts +34 -0
  7. package/dist/agent-runtime/claude-code-cli.d.ts.map +1 -0
  8. package/dist/agent-runtime/claude-code-cli.js +258 -0
  9. package/dist/agent-runtime/claude-code-cli.js.map +1 -0
  10. package/dist/agent-runtime/claude-code-sdk.d.ts +24 -0
  11. package/dist/agent-runtime/claude-code-sdk.d.ts.map +1 -0
  12. package/dist/agent-runtime/claude-code-sdk.js +415 -0
  13. package/dist/agent-runtime/claude-code-sdk.js.map +1 -0
  14. package/dist/agent-runtime/codex-app-server.d.ts +151 -0
  15. package/dist/agent-runtime/codex-app-server.d.ts.map +1 -0
  16. package/dist/agent-runtime/codex-app-server.js +626 -0
  17. package/dist/agent-runtime/codex-app-server.js.map +1 -0
  18. package/dist/agent-runtime/index.d.ts +5 -0
  19. package/dist/agent-runtime/index.d.ts.map +1 -0
  20. package/dist/agent-runtime/index.js +5 -0
  21. package/dist/agent-runtime/index.js.map +1 -0
  22. package/dist/agent-runtime/run.d.ts +104 -0
  23. package/dist/agent-runtime/run.d.ts.map +1 -0
  24. package/dist/agent-runtime/run.js +494 -0
  25. package/dist/agent-runtime/run.js.map +1 -0
  26. package/dist/agent-runtime.d.ts +2 -0
  27. package/dist/agent-runtime.d.ts.map +1 -0
  28. package/dist/agent-runtime.js +2 -0
  29. package/dist/agent-runtime.js.map +1 -0
  30. package/dist/client.d.ts +8 -0
  31. package/dist/client.d.ts.map +1 -1
  32. package/dist/client.js.map +1 -1
  33. package/dist/foundation/config/schema.d.ts +30 -141
  34. package/dist/foundation/config/schema.d.ts.map +1 -1
  35. package/dist/foundation/config/schema.js +18 -4
  36. package/dist/foundation/config/schema.js.map +1 -1
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +1 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/knowledge/git/index.d.ts.map +1 -1
  42. package/dist/knowledge/git/index.js +43 -2
  43. package/dist/knowledge/git/index.js.map +1 -1
  44. package/dist/protocols/acp/agent-descriptors.d.ts.map +1 -1
  45. package/dist/protocols/acp/agent-descriptors.js +0 -3
  46. package/dist/protocols/acp/agent-descriptors.js.map +1 -1
  47. package/dist/protocols/acp/index.js +39 -28
  48. package/dist/protocols/acp/session.d.ts.map +1 -1
  49. package/dist/protocols/acp/session.js +11 -14
  50. package/dist/protocols/acp/session.js.map +1 -1
  51. package/dist/protocols/acp/subprocess.d.ts +4 -1
  52. package/dist/protocols/acp/subprocess.d.ts.map +1 -1
  53. package/dist/protocols/acp/subprocess.js +36 -9
  54. package/dist/protocols/acp/subprocess.js.map +1 -1
  55. package/dist/protocols/mcp-server/index.cjs +80 -68
  56. package/dist/server/channel-contract.d.ts +13 -0
  57. package/dist/server/channel-contract.d.ts.map +1 -0
  58. package/dist/server/channel-contract.js +115 -0
  59. package/dist/server/channel-contract.js.map +1 -0
  60. package/dist/server/contract.d.ts.map +1 -1
  61. package/dist/server/contract.js +10 -0
  62. package/dist/server/contract.js.map +1 -1
  63. package/dist/server/handlers/agent-runtime-codex.d.ts +26 -0
  64. package/dist/server/handlers/agent-runtime-codex.d.ts.map +1 -0
  65. package/dist/server/handlers/agent-runtime-codex.js +170 -0
  66. package/dist/server/handlers/agent-runtime-codex.js.map +1 -0
  67. package/dist/server/handlers/agent-runtimes.d.ts +126 -0
  68. package/dist/server/handlers/agent-runtimes.d.ts.map +1 -0
  69. package/dist/server/handlers/agent-runtimes.js +690 -0
  70. package/dist/server/handlers/agent-runtimes.js.map +1 -0
  71. package/dist/server/handlers/agents.d.ts +6 -0
  72. package/dist/server/handlers/agents.d.ts.map +1 -1
  73. package/dist/server/handlers/agents.js +41 -6
  74. package/dist/server/handlers/agents.js.map +1 -1
  75. package/dist/server/handlers/ask.d.ts +18 -0
  76. package/dist/server/handlers/ask.d.ts.map +1 -1
  77. package/dist/server/handlers/ask.js +70 -0
  78. package/dist/server/handlers/ask.js.map +1 -1
  79. package/dist/server/handlers/channels-verify.d.ts +1 -1
  80. package/dist/server/handlers/channels-verify.d.ts.map +1 -1
  81. package/dist/server/handlers/channels-verify.js +1 -63
  82. package/dist/server/handlers/channels-verify.js.map +1 -1
  83. package/dist/server/handlers/extract-docx.d.ts +42 -0
  84. package/dist/server/handlers/extract-docx.d.ts.map +1 -0
  85. package/dist/server/handlers/extract-docx.js +101 -0
  86. package/dist/server/handlers/extract-docx.js.map +1 -0
  87. package/dist/server/handlers/extract-pdf.d.ts +32 -0
  88. package/dist/server/handlers/extract-pdf.d.ts.map +1 -0
  89. package/dist/server/handlers/extract-pdf.js +116 -0
  90. package/dist/server/handlers/extract-pdf.js.map +1 -0
  91. package/dist/server/handlers/im-config.d.ts +1 -1
  92. package/dist/server/handlers/im-config.d.ts.map +1 -1
  93. package/dist/server/handlers/im-config.js +69 -59
  94. package/dist/server/handlers/im-config.js.map +1 -1
  95. package/dist/server/handlers/im-feishu-oauth.d.ts +55 -0
  96. package/dist/server/handlers/im-feishu-oauth.d.ts.map +1 -0
  97. package/dist/server/handlers/im-feishu-oauth.js +218 -0
  98. package/dist/server/handlers/im-feishu-oauth.js.map +1 -0
  99. package/dist/server/handlers/im-status.d.ts +15 -0
  100. package/dist/server/handlers/im-status.d.ts.map +1 -1
  101. package/dist/server/handlers/im-status.js +41 -24
  102. package/dist/server/handlers/im-status.js.map +1 -1
  103. package/dist/server/handlers/inbox-source.d.ts +18 -0
  104. package/dist/server/handlers/inbox-source.d.ts.map +1 -0
  105. package/dist/server/handlers/inbox-source.js +108 -0
  106. package/dist/server/handlers/inbox-source.js.map +1 -0
  107. package/dist/server/handlers/inbox.d.ts +2 -0
  108. package/dist/server/handlers/inbox.d.ts.map +1 -1
  109. package/dist/server/handlers/inbox.js +34 -2
  110. package/dist/server/handlers/inbox.js.map +1 -1
  111. package/dist/server/handlers/mcp-agents.d.ts +16 -1
  112. package/dist/server/handlers/mcp-agents.d.ts.map +1 -1
  113. package/dist/server/handlers/mcp-agents.js +174 -44
  114. package/dist/server/handlers/mcp-agents.js.map +1 -1
  115. package/dist/server/handlers/mcp-install.d.ts +5 -0
  116. package/dist/server/handlers/mcp-install.d.ts.map +1 -1
  117. package/dist/server/handlers/mcp-install.js +68 -20
  118. package/dist/server/handlers/mcp-install.js.map +1 -1
  119. package/dist/server/handlers/mcp-restart.js +1 -1
  120. package/dist/server/handlers/mcp-restart.js.map +1 -1
  121. package/dist/server/handlers/settings-list-models.d.ts +1 -1
  122. package/dist/server/handlers/settings-list-models.d.ts.map +1 -1
  123. package/dist/server/handlers/settings-list-models.js +6 -5
  124. package/dist/server/handlers/settings-list-models.js.map +1 -1
  125. package/dist/server/handlers/settings-test-key.d.ts.map +1 -1
  126. package/dist/server/handlers/settings-test-key.js +17 -7
  127. package/dist/server/handlers/settings-test-key.js.map +1 -1
  128. package/dist/server/handlers/settings.d.ts +2 -1
  129. package/dist/server/handlers/settings.d.ts.map +1 -1
  130. package/dist/server/handlers/settings.js +49 -5
  131. package/dist/server/handlers/settings.js.map +1 -1
  132. package/dist/server/handlers/skills.d.ts +1 -0
  133. package/dist/server/handlers/skills.d.ts.map +1 -1
  134. package/dist/server/handlers/skills.js +7 -5
  135. package/dist/server/handlers/skills.js.map +1 -1
  136. package/dist/server/handlers/sync.d.ts +15 -4
  137. package/dist/server/handlers/sync.d.ts.map +1 -1
  138. package/dist/server/handlers/sync.js +552 -81
  139. package/dist/server/handlers/sync.js.map +1 -1
  140. package/dist/server/handlers/uninstall.js +1 -0
  141. package/dist/server/handlers/uninstall.js.map +1 -1
  142. package/dist/server/handlers/update.js +1 -0
  143. package/dist/server/handlers/update.js.map +1 -1
  144. package/dist/server/http.d.ts +20 -0
  145. package/dist/server/http.d.ts.map +1 -1
  146. package/dist/server/http.js +223 -24
  147. package/dist/server/http.js.map +1 -1
  148. package/dist/server/index.d.ts +12 -4
  149. package/dist/server/index.d.ts.map +1 -1
  150. package/dist/server/index.js +9 -1
  151. package/dist/server/index.js.map +1 -1
  152. package/dist/server/mcp-agent-registry.d.ts +184 -0
  153. package/dist/server/mcp-agent-registry.d.ts.map +1 -1
  154. package/dist/server/mcp-agent-registry.js +100 -9
  155. package/dist/server/mcp-agent-registry.js.map +1 -1
  156. package/dist/server/provider-settings.d.ts +38 -0
  157. package/dist/server/provider-settings.d.ts.map +1 -0
  158. package/dist/server/provider-settings.js +286 -0
  159. package/dist/server/provider-settings.js.map +1 -0
  160. package/dist/server/route-ownership.d.ts.map +1 -1
  161. package/dist/server/route-ownership.js +15 -2
  162. package/dist/server/route-ownership.js.map +1 -1
  163. package/dist/session/index.d.ts +94 -15
  164. package/dist/session/index.d.ts.map +1 -1
  165. package/dist/session/index.js +115 -18
  166. package/dist/session/index.js.map +1 -1
  167. package/dist/session/pi-coding-agent-runtime.js +3 -3
  168. package/dist/session/pi-coding-agent-runtime.js.map +1 -1
  169. package/dist/setup/index.d.ts +1 -0
  170. package/dist/setup/index.d.ts.map +1 -1
  171. package/dist/setup/index.js +13 -0
  172. package/dist/setup/index.js.map +1 -1
  173. package/package.json +18 -12
  174. package/src/cli.js +1 -0
@@ -1,21 +1,118 @@
1
1
  import { execFile, execFileSync } from 'node:child_process';
2
- import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
3
- import { homedir } from 'node:os';
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
- if (!syncConfig.enabled) {
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: state.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
- config.sync = { ...(config.sync ?? {}), enabled: true };
75
- writeConfig(config, services);
76
- return json({ ok: true, enabled: true });
177
+ return handleSyncToggle(mindRoot, config, services, true);
77
178
  case 'off':
78
- config.sync = { ...(config.sync ?? {}), enabled: false };
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
- delete config.sync;
101
- writeConfig(config, services);
102
- try {
103
- writeState({}, services);
104
- }
105
- catch { }
106
- return json({ ok: true, enabled: false });
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
- const args = ['sync', 'init', '--non-interactive', '--remote', remote, '--branch', branch];
120
- if (payload.token)
121
- args.push('--token', payload.token);
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
- return json({ content: '' });
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
- writeFileSync(resolveExistingSafe(mindRoot, '.gitignore'), payload.content, 'utf-8');
156
- return json({ ok: true });
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
- if (strategy === 'keep-remote' && existsSync(conflictPath)) {
180
- writeFileSync(originalPath, readFileSync(conflictPath, 'utf-8'), 'utf-8');
181
- }
182
- if (existsSync(conflictPath)) {
183
- unlinkSync(conflictPath);
184
- }
185
- const state = readState(services);
186
- if (state.conflicts) {
187
- state.conflicts = state.conflicts.filter((conflict) => conflict.file !== file);
188
- writeState(state, services);
189
- }
190
- return json({ ok: true });
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
- return json({
206
- local: existsSync(localPath) ? readFileSync(localPath, 'utf-8') : '',
207
- remote: existsSync(remotePath) ? readFileSync(remotePath, 'utf-8') : '',
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
- config.sync = config.sync ?? {};
223
- if (commitInterval !== undefined)
224
- config.sync.autoCommitInterval = commitInterval;
225
- if (pullInterval !== undefined)
226
- config.sync.autoPullInterval = pullInterval;
227
- writeConfig(config, services);
228
- return json({
229
- autoCommitInterval: config.sync.autoCommitInterval || 30,
230
- autoPullInterval: config.sync.autoPullInterval || 300,
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 readJsonFile(filePath) {
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
- return JSON.parse(readFileSync(filePath, 'utf-8'));
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
- return runGit(cwd, ['rev-list', '--count', '@{u}..HEAD']);
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
- return '?';
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