evolcore 0.0.23 → 0.0.25

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 (245) hide show
  1. package/CHANGELOG.md +583 -516
  2. package/LICENSE +21 -21
  3. package/README.md +455 -453
  4. package/bin/codex-managed-acl.ps1 +160 -155
  5. package/bin/codex-managed-hook.mjs +209 -92
  6. package/bin/ec-safe-output.js +327 -245
  7. package/bin/ec.js +29 -29
  8. package/bin/install-codex-managed-hooks.mjs +555 -544
  9. package/dist/agents/baseagent-availability.js +4 -4
  10. package/dist/agents/baseagent.js +29 -1
  11. package/dist/agents/claude-runner.js +152 -14
  12. package/dist/agents/codex/app-server-client.js +18 -8
  13. package/dist/agents/codex/catalog.js +1 -1
  14. package/dist/agents/codex/diagnostics.js +215 -0
  15. package/dist/agents/codex-runner.js +445 -63
  16. package/dist/agents/ecagent-runner.js +31 -16
  17. package/dist/agents/gemini-runner.js +82 -4
  18. package/dist/agents/runner-types.js +1 -1
  19. package/dist/aun/aid/agentmd.js +56 -4
  20. package/dist/aun/aid/index.js +1 -0
  21. package/dist/aun/aid/managed-operation.js +80 -6
  22. package/dist/aun/aid/store.js +41 -0
  23. package/dist/aun/msg/managed-operation.js +6 -1
  24. package/dist/aun/outbox.js +56 -18
  25. package/dist/aun/rpc/client.js +4 -5
  26. package/dist/aun/storage/client.js +4 -1
  27. package/dist/aun/storage/index.js +1 -1
  28. package/dist/channels/aun.js +147 -76
  29. package/dist/channels/daemon.js +7 -1
  30. package/dist/channels/dingtalk.js +1 -0
  31. package/dist/channels/feishu.js +10 -4
  32. package/dist/channels/wecom.js +2 -1
  33. package/dist/cli/agent-command.js +214 -89
  34. package/dist/cli/agent-create.js +576 -0
  35. package/dist/cli/agent.js +129 -676
  36. package/dist/cli/aun-commands.js +419 -313
  37. package/dist/cli/bench.js +20 -20
  38. package/dist/cli/config.js +20 -20
  39. package/dist/cli/contact.js +10 -10
  40. package/dist/cli/ctl-command.js +22 -20
  41. package/dist/cli/daemon-commands.js +163 -32
  42. package/dist/cli/fs-command.js +67 -67
  43. package/dist/cli/handoff-command.js +7 -7
  44. package/dist/cli/index.js +79 -79
  45. package/dist/cli/link-rules.js +21 -21
  46. package/dist/cli/managed-command-guard.js +3 -0
  47. package/dist/cli/menu-client.js +11 -11
  48. package/dist/cli/net-check.js +22 -22
  49. package/dist/cli/queue-command.js +29 -29
  50. package/dist/cli/response.js +30 -30
  51. package/dist/cli/restart-monitor.js +462 -38
  52. package/dist/cli/stats.js +161 -51
  53. package/dist/cli/trigger-command.js +56 -56
  54. package/dist/config/builtin-roles.js +3 -0
  55. package/dist/config/config-manager.js +15 -0
  56. package/dist/config/config-operation-service.js +20 -3
  57. package/dist/config/resolved-config-op.js +39 -1
  58. package/dist/core/auth/auth-gateway.js +3 -2
  59. package/dist/core/auth/authorization-audit.js +72 -19
  60. package/dist/core/auth/operation-authorizer.js +125 -7
  61. package/dist/core/auth/operation-catalog.js +26 -2
  62. package/dist/core/auth/remote-access-store.js +3 -2
  63. package/dist/core/baseagent-loader.js +4 -4
  64. package/dist/core/bootstrap-messages.js +39 -18
  65. package/dist/core/bootstrap-service.js +32 -26
  66. package/dist/core/capability/capability-manager.js +8 -4
  67. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  68. package/dist/core/command/agent-control.js +60 -26
  69. package/dist/core/command/cli-intent-parser.js +18 -0
  70. package/dist/core/command/command-handler.js +173 -36
  71. package/dist/core/command/menu-catalog.js +4 -1
  72. package/dist/core/command/menu-handler.js +315 -75
  73. package/dist/core/command/menu-protocol.js +7 -2
  74. package/dist/core/command/slash-gate.js +1 -3
  75. package/dist/core/command/slash-handler.js +166 -57
  76. package/dist/core/data-migration.js +5 -15
  77. package/dist/core/event-catalog.js +8 -0
  78. package/dist/core/evolagent.js +3 -3
  79. package/dist/core/handoff/runtime.js +3 -1
  80. package/dist/core/handoff/store.js +7 -1
  81. package/dist/core/message/im-renderer.js +13 -6
  82. package/dist/core/message/message-bridge.js +5 -36
  83. package/dist/core/message/message-utils.js +6 -3
  84. package/dist/core/message/response-engine.js +85 -27
  85. package/dist/core/model/catalog-service.js +3 -1
  86. package/dist/core/model/model-catalog.js +10 -0
  87. package/dist/core/model/model-permission.js +4 -1
  88. package/dist/core/permission/approval-gateway.js +169 -33
  89. package/dist/core/permission/tool-policy.js +56 -10
  90. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  91. package/dist/core/session/session-manager.js +49 -13
  92. package/dist/core/session/session-renew.js +1 -0
  93. package/dist/index.js +327 -134
  94. package/dist/ipc.js +68 -21
  95. package/dist/paths.js +23 -1
  96. package/dist/stats/billing.js +18 -4
  97. package/dist/stats/db.js +159 -159
  98. package/dist/stats/eck-vars.js +9 -9
  99. package/dist/stats/query.js +139 -139
  100. package/dist/stats/role-budget.js +6 -6
  101. package/dist/stats/writer.js +51 -51
  102. package/dist/utils/atomic-write.js +26 -2
  103. package/dist/utils/avatar-upload.js +282 -23
  104. package/dist/utils/cross-platform.js +436 -1
  105. package/dist/utils/error-dict.json +160 -160
  106. package/dist/utils/error-utils.js +11 -4
  107. package/dist/utils/macos-autostart.js +22 -22
  108. package/dist/utils/model-prices.jsonl +23 -20
  109. package/dist/utils/npm-ops.js +24 -1
  110. package/dist/utils/welcome.js +22 -22
  111. package/dist/utils/windows-autostart.js +1 -1
  112. package/ecagent/LICENSE +21 -21
  113. package/ecagent/NOTICE +11 -11
  114. package/ecagent/README.md +61 -61
  115. package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
  116. package/ecagent/dist/harness/compaction/compaction.js +83 -83
  117. package/ecagent/dist/harness/messages.js +7 -7
  118. package/kits/docs/GUIDE.md +20 -20
  119. package/kits/docs/INDEX.md +66 -66
  120. package/kits/docs/aun/CHEATSHEET.md +19 -19
  121. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
  122. package/kits/docs/channels/aun.md +56 -56
  123. package/kits/docs/channels/feishu.md +56 -56
  124. package/kits/docs/context-assembly.md +365 -364
  125. package/kits/docs/eck_templates/GUIDE.template.md +22 -22
  126. package/kits/docs/eck_templates/INDEX.template.md +28 -28
  127. package/kits/docs/eck_templates/path-registry.template.md +33 -33
  128. package/kits/docs/eck_templates/runtime.template.md +19 -19
  129. package/kits/docs/evolcore/INDEX.md +71 -69
  130. package/kits/docs/evolcore/agent-create.md +170 -156
  131. package/kits/docs/evolcore/agent.md +83 -73
  132. package/kits/docs/evolcore/aid.md +64 -62
  133. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -0
  134. package/kits/docs/evolcore/config.md +195 -195
  135. package/kits/docs/evolcore/contact.md +68 -68
  136. package/kits/docs/evolcore/ctl.md +46 -46
  137. package/kits/docs/evolcore/event.md +216 -216
  138. package/kits/docs/evolcore/fs-architecture.md +1215 -1215
  139. package/kits/docs/evolcore/fs.md +110 -110
  140. package/kits/docs/evolcore/group-collaboration.md +243 -243
  141. package/kits/docs/evolcore/group-fs.md +17 -17
  142. package/kits/docs/evolcore/group-rules.md +251 -251
  143. package/kits/docs/evolcore/group.md +152 -152
  144. package/kits/docs/evolcore/menu-client.md +34 -34
  145. package/kits/docs/evolcore/model.md +50 -50
  146. package/kits/docs/evolcore/msg.md +165 -165
  147. package/kits/docs/evolcore/response.md +75 -75
  148. package/kits/docs/evolcore/rpc.md +37 -37
  149. package/kits/docs/evolcore/self-summary.md +29 -29
  150. package/kits/docs/evolcore/stats.md +83 -83
  151. package/kits/docs/evolcore/storage.md +50 -50
  152. package/kits/docs/evolcore/trigger.md +575 -575
  153. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
  154. package/kits/docs/identity/PATH_OPS.md +16 -16
  155. package/kits/docs/identity/ROLE_DETAIL.md +25 -25
  156. package/kits/docs/identity/identity-tools.md +26 -26
  157. package/kits/docs/path-registry.md +62 -62
  158. package/kits/docs/prompt-loading-architecture.md +270 -269
  159. package/kits/docs/venues/aun-group.md +45 -45
  160. package/kits/docs/venues/aun-private.md +10 -10
  161. package/kits/docs/venues/client-desktop.md +10 -10
  162. package/kits/docs/venues/client-mobile.md +10 -10
  163. package/kits/docs/venues/feishu-group.md +13 -13
  164. package/kits/docs/venues/feishu-private.md +9 -9
  165. package/kits/docs/venues/group.md +25 -25
  166. package/kits/docs/venues/private.md +10 -10
  167. package/kits/eck_manifest.auxiliary.json +43 -43
  168. package/kits/eck_manifest.json +195 -183
  169. package/kits/eck_message_manifest.json +74 -74
  170. package/kits/migrations/README-role-config-v4.md +32 -32
  171. package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
  172. package/kits/migrations/migrate-role-config-v4.mjs +614 -614
  173. package/kits/migrations/migrate-role-config-v5.mjs +365 -365
  174. package/kits/migrations/rename-config-file.mjs +99 -99
  175. package/kits/rules/01-overview.md +162 -162
  176. package/kits/rules/02-navigation.md +96 -96
  177. package/kits/rules/03-identity.md +39 -37
  178. package/kits/rules/04-relation.md +75 -75
  179. package/kits/rules/05-venue.md +60 -60
  180. package/kits/rules/06-channel.md +63 -63
  181. package/kits/schemas/_meta.json +53 -53
  182. package/kits/schemas/agent-config.schema.1.json +177 -177
  183. package/kits/schemas/agent-config.schema.10.json +395 -395
  184. package/kits/schemas/agent-config.schema.11.json +421 -421
  185. package/kits/schemas/agent-config.schema.12.json +427 -427
  186. package/kits/schemas/agent-config.schema.2.json +239 -239
  187. package/kits/schemas/agent-config.schema.3.json +119 -119
  188. package/kits/schemas/agent-config.schema.4.json +208 -208
  189. package/kits/schemas/agent-config.schema.5.json +326 -326
  190. package/kits/schemas/agent-config.schema.6.json +322 -322
  191. package/kits/schemas/agent-config.schema.7.json +303 -303
  192. package/kits/schemas/agent-config.schema.8.json +304 -304
  193. package/kits/schemas/agent-config.schema.9.json +364 -364
  194. package/kits/schemas/contact-book.schema.1.json +36 -36
  195. package/kits/schemas/contact-book.schema.2.json +43 -43
  196. package/kits/schemas/contact-book.schema.3.json +67 -67
  197. package/kits/schemas/daemon.schema.1.json +91 -91
  198. package/kits/schemas/daemon.schema.2.json +101 -101
  199. package/kits/schemas/daemon.schema.3.json +123 -123
  200. package/kits/schemas/daemon.schema.4.json +136 -136
  201. package/kits/schemas/daemon.schema.5.json +135 -135
  202. package/kits/schemas/daemon.schema.6.json +131 -131
  203. package/kits/schemas/defaults.schema.1.json +81 -81
  204. package/kits/schemas/defaults.schema.2.json +85 -85
  205. package/kits/schemas/defaults.schema.3.json +73 -73
  206. package/kits/schemas/defaults.schema.4.json +107 -107
  207. package/kits/schemas/defaults.schema.5.json +119 -119
  208. package/kits/schemas/menu-exec-schema-commands.md +208 -208
  209. package/kits/schemas/migrations/README.md +30 -30
  210. package/kits/schemas/relation-config.schema.1.json +158 -158
  211. package/kits/schemas/relation-config.schema.2.json +73 -73
  212. package/kits/schemas/relation-config.schema.3.json +50 -50
  213. package/kits/schemas/relation-config.schema.4.json +47 -47
  214. package/kits/schemas/relation-config.schema.5.json +47 -47
  215. package/kits/schemas/relation-config.schema.6.json +46 -46
  216. package/kits/schemas/relation-config.schema.7.json +59 -59
  217. package/kits/schemas/relation-config.schema.8.json +103 -103
  218. package/kits/schemas/role-config.schema.1.json +201 -201
  219. package/kits/schemas/role-registry.schema.1.json +35 -35
  220. package/kits/schemas/single-session.schema.1.json +31 -31
  221. package/kits/schemas/single-session.schema.2.json +57 -57
  222. package/kits/schemas/single-session.schema.3.json +32 -32
  223. package/kits/templates/bootstrap-welcome.md +17 -17
  224. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
  225. package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
  226. package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
  227. package/kits/templates/message-fragments/inject-default.md +2 -2
  228. package/kits/templates/message-fragments/item.md +2 -2
  229. package/kits/templates/roles/admin.json +96 -82
  230. package/kits/templates/roles/member.json +97 -96
  231. package/kits/templates/roles/owner.json +9 -9
  232. package/kits/templates/roles/visitor.json +86 -85
  233. package/kits/templates/system-fragments/baseagent.md +14 -14
  234. package/kits/templates/system-fragments/bootstrap.md +38 -29
  235. package/kits/templates/system-fragments/channel.md +64 -64
  236. package/kits/templates/system-fragments/commands.md +28 -28
  237. package/kits/templates/system-fragments/identity.md +13 -13
  238. package/kits/templates/system-fragments/relation.md +19 -19
  239. package/kits/templates/system-fragments/session.md +59 -59
  240. package/kits/templates/system-fragments/venue.md +31 -31
  241. package/package.json +98 -94
  242. package/AGENTS.md +0 -62
  243. package/dist/utils/codex-cli.js +0 -43
  244. package/skills/eclink/SKILL.md +0 -84
  245. package/skills/eclink/agents/openai.yaml +0 -4
@@ -1,12 +1,197 @@
1
1
  import path from 'path';
2
- import { fileURLToPath } from 'url';
2
+ import { fileURLToPath, pathToFileURL } from 'url';
3
+ import { createRequire } from 'module';
3
4
  import { execFileSync, execFile, spawn, spawnSync } from 'child_process';
4
5
  import { promisify } from 'util';
5
6
  import fs from 'fs';
6
7
  import { getProcessStartTime, parseCimDate } from './process-introspect.js';
7
8
  import { decodeWindowsOutput } from './windows-output.js';
8
9
  const execFileAsync = promisify(execFile);
10
+ const CLI_PROBE_FAILURE_BACKOFF_MS = 5_000;
11
+ const CLI_PROBE_PACKAGES = {
12
+ codex: '@openai/codex',
13
+ claude: '@anthropic-ai/claude-code',
14
+ gemini: '@google/gemini-cli',
15
+ };
16
+ const cliPathCache = new Map();
17
+ const cliInstallationCache = new Map();
18
+ const cliProbeCache = new Map();
9
19
  export const isWindows = process.platform === 'win32';
20
+ /** Resolve a CLI executable without spawning where/which. */
21
+ export function resolveProbePath(command) {
22
+ const pathKey = `${command}\0${process.cwd()}\0${process.env.PATH ?? ''}\0${process.env.PATHEXT ?? ''}`;
23
+ const cachedPath = cliPathCache.get(pathKey);
24
+ if (cachedPath && fs.existsSync(cachedPath))
25
+ return cachedPath;
26
+ cliPathCache.delete(pathKey);
27
+ const directories = (process.env.PATH ?? '').split(path.delimiter);
28
+ if (isWindows)
29
+ directories.unshift(process.cwd());
30
+ const extensions = isWindows
31
+ ? (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(ext => /\.(com|exe|bat|cmd)$/i.test(ext))
32
+ : [''];
33
+ for (const directory of directories) {
34
+ for (const extension of extensions) {
35
+ const candidate = path.resolve(directory.replace(/^"|"$/g, ''), command + extension);
36
+ try {
37
+ if (!fs.statSync(candidate).isFile())
38
+ continue;
39
+ fs.accessSync(candidate, isWindows ? fs.constants.F_OK : fs.constants.X_OK);
40
+ cliPathCache.set(pathKey, candidate);
41
+ return candidate;
42
+ }
43
+ catch { /* Continue PATH lookup. */ }
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ function cliFileIdentity(file) {
49
+ try {
50
+ const real = fs.realpathSync(file);
51
+ const stat = fs.statSync(real);
52
+ return `${real}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
53
+ }
54
+ catch {
55
+ return `${file}:missing`;
56
+ }
57
+ }
58
+ /** Return an installation fingerprint so probes refresh after CLI upgrades. */
59
+ export function probeInstallationIdentity(command, executable) {
60
+ const cacheKey = `${command}\0${executable}`;
61
+ const cached = cliInstallationCache.get(cacheKey);
62
+ if (cached && cached.files.map(cliFileIdentity).join('|') === cached.identity)
63
+ return cached.identity;
64
+ const files = [executable];
65
+ const packageName = CLI_PROBE_PACKAGES[command];
66
+ if (packageName) {
67
+ files.push(path.join(path.dirname(executable), 'node_modules', packageName, 'package.json'));
68
+ try {
69
+ const real = fs.realpathSync(executable);
70
+ const require = createRequire(real);
71
+ const packageFile = require.resolve(`${packageName}/package.json`);
72
+ files.push(packageFile);
73
+ if (command === 'codex') {
74
+ const platformPackage = `@openai/codex-${process.platform}-${process.arch}`;
75
+ const platformFile = createRequire(packageFile).resolve(`${platformPackage}/package.json`);
76
+ files.push(platformFile);
77
+ const arch = process.arch === 'x64' ? 'x86_64' : 'aarch64';
78
+ const target = isWindows ? `${arch}-pc-windows-msvc`
79
+ : process.platform === 'darwin' ? `${arch}-apple-darwin` : `${arch}-unknown-linux-musl`;
80
+ for (const root of [path.dirname(platformFile), path.dirname(packageFile)]) {
81
+ files.push(path.join(root, 'vendor', target, 'bin', isWindows ? 'codex.exe' : 'codex'));
82
+ }
83
+ }
84
+ }
85
+ catch { /* Standalone executables have no npm package. */ }
86
+ }
87
+ const uniqueFiles = [...new Set(files)];
88
+ const identity = uniqueFiles.map(cliFileIdentity).join('|');
89
+ cliInstallationCache.set(cacheKey, { files: uniqueFiles, identity });
90
+ return identity;
91
+ }
92
+ /** Clear CLI probe caches after an in-process CLI installation. */
93
+ export function invalidateCliProbes() {
94
+ cliPathCache.clear();
95
+ cliInstallationCache.clear();
96
+ cliProbeCache.clear();
97
+ }
98
+ function cliProbeTimeouts(command, options) {
99
+ return options.timeouts?.length ? options.timeouts : command === 'codex' ? [3_000, 15_000] : [3_000];
100
+ }
101
+ function prepareCliProbe(command, args) {
102
+ const executable = resolveProbePath(command);
103
+ if (!executable)
104
+ throw Object.assign(new Error(`${command} CLI not found`), { code: 'ENOENT' });
105
+ const identity = `${probeInstallationIdentity(command, executable)}|${process.env.ComSpec ?? ''}`;
106
+ const key = JSON.stringify([command, args]);
107
+ let entry = cliProbeCache.get(key);
108
+ if (!entry || entry.identity !== identity || (entry.retryAt !== undefined && Date.now() >= entry.retryAt)) {
109
+ entry = { identity };
110
+ cliProbeCache.set(key, entry);
111
+ }
112
+ const shim = isWindows && /\.(cmd|bat)$/i.test(executable);
113
+ return { entry, command: shim ? process.env.ComSpec ?? 'cmd.exe' : executable,
114
+ args: shim ? ['/d', '/s', '/c', executable, ...args] : args };
115
+ }
116
+ function failCliProbe(entry, error) {
117
+ entry.error = error;
118
+ entry.retryAt = Date.now() + CLI_PROBE_FAILURE_BACKOFF_MS;
119
+ throw error;
120
+ }
121
+ export function probeCliSync(command, args, options = {}) {
122
+ const request = prepareCliProbe(command, args);
123
+ if (request.entry.output !== undefined)
124
+ return request.entry.output;
125
+ if (request.entry.error !== undefined)
126
+ throw request.entry.error;
127
+ for (const [index, timeout] of cliProbeTimeouts(command, options).entries()) {
128
+ const startedAt = Date.now();
129
+ try {
130
+ return request.entry.output = execFileSync(request.command, request.args, { encoding: 'utf8', timeout, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
131
+ }
132
+ catch (error) {
133
+ options.onFailure?.(args, startedAt, error, timeout);
134
+ if (error.code !== 'ETIMEDOUT' || index === cliProbeTimeouts(command, options).length - 1)
135
+ return failCliProbe(request.entry, error);
136
+ }
137
+ }
138
+ throw new Error('CLI probe exhausted');
139
+ }
140
+ export async function probeCli(command, args, options = {}) {
141
+ const request = prepareCliProbe(command, args);
142
+ if (request.entry.output !== undefined)
143
+ return request.entry.output;
144
+ if (request.entry.error !== undefined)
145
+ throw request.entry.error;
146
+ if (request.entry.pending)
147
+ return request.entry.pending;
148
+ const run = async () => {
149
+ const timeouts = cliProbeTimeouts(command, options);
150
+ for (const [index, timeout] of timeouts.entries()) {
151
+ const startedAt = Date.now();
152
+ try {
153
+ const output = await new Promise((resolve, reject) => {
154
+ const child = execFile(request.command, request.args, { encoding: 'utf8', timeout, windowsHide: true }, (error, stdout, stderr) => {
155
+ if (error) {
156
+ if (error.killed && error.signal === 'SIGTERM' && error.code == null)
157
+ error.code = 'ETIMEDOUT';
158
+ Object.assign(error, { stdout, stderr, path: request.command, spawnargs: request.args });
159
+ reject(error);
160
+ }
161
+ else
162
+ resolve(stdout);
163
+ });
164
+ child.stdin?.end();
165
+ });
166
+ return request.entry.output = output;
167
+ }
168
+ catch (error) {
169
+ options.onFailure?.(args, startedAt, error, timeout);
170
+ if (error.code !== 'ETIMEDOUT' || index === timeouts.length - 1)
171
+ return failCliProbe(request.entry, error);
172
+ }
173
+ }
174
+ throw new Error('CLI probe exhausted');
175
+ };
176
+ request.entry.pending = run();
177
+ try {
178
+ return await request.entry.pending;
179
+ }
180
+ finally {
181
+ request.entry.pending = undefined;
182
+ }
183
+ }
184
+ /** Non-blocking version lookup used by status menus. */
185
+ export function cachedCliVersion(command) {
186
+ try {
187
+ const { entry } = prepareCliProbe(command, ['--version']);
188
+ if (entry.output !== undefined)
189
+ return entry.output.match(/\b\d+\.\d+\.\d+(?:[-+][\w.-]+)?/)?.[0] ?? null;
190
+ void probeCli(command, ['--version']).catch(() => { });
191
+ }
192
+ catch { /* Missing CLI. */ }
193
+ return null;
194
+ }
10
195
  /**
11
196
  * Start a detached EvolCore Node process without creating a console window on
12
197
  * Windows. The child is unref'd here so callers cannot accidentally keep the
@@ -22,6 +207,256 @@ export function spawnDetachedNode(args, env = {}) {
22
207
  child.unref();
23
208
  return child;
24
209
  }
210
+ /**
211
+ * Spawn the restart monitor and wait until it proves it is running.
212
+ *
213
+ * Every caller stops (or kills) the daemon right after this returns, so a
214
+ * monitor that dies while loading leaves nothing running and nobody watching.
215
+ * In a checkout the monitor is executed through tsx, where a syntax error
216
+ * anywhere in its import graph is fatal at load time — and the usual
217
+ * `stdio: 'ignore'` would throw that diagnostic away. So stderr is captured,
218
+ * and success requires the child to still be alive after it had a chance to
219
+ * fail, not merely to have been spawned.
220
+ *
221
+ * `isAliveInRegistry` lets the caller define proof-of-life without dragging the
222
+ * instance registry into this module.
223
+ */
224
+ export async function launchMonitorAndConfirm(args, env, isAliveInRegistry, hasOtherLiveMonitor, timeoutMs = 15_000) {
225
+ const child = spawn(process.execPath, [...args], {
226
+ detached: true,
227
+ stdio: ['ignore', 'ignore', 'pipe'],
228
+ windowsHide: isWindows,
229
+ env: { ...process.env, ...env },
230
+ });
231
+ let stderrTail = '';
232
+ child.stderr?.on('data', (chunk) => {
233
+ stderrTail = (stderrTail + chunk.toString()).slice(-2000);
234
+ });
235
+ // Capturing stderr must not defeat detached-process semantics. A referenced
236
+ // pipe keeps the caller's event loop alive until the monitor exits, which
237
+ // makes restart commands wait for the monitor's full lifetime.
238
+ child.stderr?.unref?.();
239
+ let exitCode = null;
240
+ let spawnError = null;
241
+ child.on('exit', code => { exitCode = code; });
242
+ child.on('error', err => { spawnError = err.message; });
243
+ const childPid = child.pid;
244
+ child.unref();
245
+ if (!childPid)
246
+ return { ok: false, reason: 'spawn 未返回 PID' };
247
+ const deadline = Date.now() + timeoutMs;
248
+ while (Date.now() < deadline) {
249
+ await new Promise(resolve => setTimeout(resolve, 300));
250
+ if (spawnError)
251
+ return { ok: false, reason: spawnError, stderrTail };
252
+ if (exitCode !== null) {
253
+ // Exiting cleanly is legitimate when another monitor already owns this
254
+ // restart (single-instance election) — that one is alive and in charge.
255
+ if (exitCode === 0 && hasOtherLiveMonitor(childPid))
256
+ return { ok: true };
257
+ return { ok: false, reason: `进程已退出,exit=${exitCode}`, stderrTail };
258
+ }
259
+ // Registering means the module graph loaded and the monitor started work.
260
+ if (isAliveInRegistry(childPid))
261
+ return { ok: true };
262
+ }
263
+ // Still running but not yet registered: a slow start is not a failure.
264
+ if (isProcessRunning(childPid))
265
+ return { ok: true };
266
+ return { ok: false, reason: '进程未登记且已消失', stderrTail };
267
+ }
268
+ /**
269
+ * Copy the monitor out of the package directory so an upgrade cannot replace it
270
+ * mid-flight.
271
+ *
272
+ * This is the installed-package counterpart of running the monitor from `src/`
273
+ * in a checkout: `npm install -g` swaps the whole package directory, and the
274
+ * monitor has ~170 lazy `await import()` sites, so a module it has not loaded
275
+ * yet would come from the new version while the rest is still the old one.
276
+ * Staging `dist/` elsewhere keeps its module graph on one consistent version.
277
+ *
278
+ * Only `dist/` and `package.json` are copied (a few MB). `node_modules` is
279
+ * linked, because copying it is hundreds of MB. The link degrades in steps so
280
+ * that no platform is left without a path forward:
281
+ *
282
+ * 1. symlink — `junction` on Windows (works without elevation, unlike a
283
+ * `dir` symlink which needs admin or developer mode), plain dir symlink
284
+ * elsewhere. Junctions cannot span volumes, hence the fallbacks.
285
+ * 2. full copy — slow but always permitted.
286
+ * 3. skip staging — caller runs the monitor in place, exactly as before.
287
+ * Losing the upgrade isolation is much better than losing the restart.
288
+ */
289
+ export function prepareMonitorStage(packageRoot, stageParent) {
290
+ const distSource = path.join(packageRoot, 'dist');
291
+ const modulesSource = path.join(packageRoot, 'node_modules');
292
+ if (!fs.existsSync(distSource)) {
293
+ return { stageDir: null, linkMode: 'none', note: 'dist/ 不存在,跳过 stage' };
294
+ }
295
+ // Clear stages left behind by a monitor that was killed before self-cleanup.
296
+ try {
297
+ const cutoff = Date.now() - 60 * 60 * 1000;
298
+ for (const entry of fs.readdirSync(stageParent, { withFileTypes: true })) {
299
+ if (!entry.isDirectory())
300
+ continue;
301
+ const full = path.join(stageParent, entry.name);
302
+ if (fs.statSync(full).mtimeMs < cutoff)
303
+ fs.rmSync(full, { recursive: true, force: true });
304
+ }
305
+ }
306
+ catch { /* stageParent may not exist yet */ }
307
+ const stageDir = path.join(stageParent, `${process.pid}-${Date.now()}`);
308
+ try {
309
+ fs.mkdirSync(stageDir, { recursive: true });
310
+ fs.cpSync(distSource, path.join(stageDir, 'dist'), { recursive: true });
311
+ const pkgJson = path.join(packageRoot, 'package.json');
312
+ if (fs.existsSync(pkgJson))
313
+ fs.copyFileSync(pkgJson, path.join(stageDir, 'package.json'));
314
+ }
315
+ catch (error) {
316
+ try {
317
+ fs.rmSync(stageDir, { recursive: true, force: true });
318
+ }
319
+ catch { }
320
+ return { stageDir: null, linkMode: 'none', note: `复制 dist/ 失败:${error.message}` };
321
+ }
322
+ if (!fs.existsSync(modulesSource)) {
323
+ // Dependencies hoisted elsewhere; nothing to link, node resolves upward.
324
+ return { stageDir, linkMode: 'none' };
325
+ }
326
+ const stageModules = path.join(stageDir, 'node_modules');
327
+ try {
328
+ fs.symlinkSync(modulesSource, stageModules, isWindows ? 'junction' : 'dir');
329
+ // A junction to another volume is created but unusable, so prove it reads.
330
+ fs.readdirSync(stageModules);
331
+ return { stageDir, linkMode: 'symlink' };
332
+ }
333
+ catch (linkError) {
334
+ try {
335
+ fs.rmSync(stageModules, { recursive: true, force: true });
336
+ }
337
+ catch { }
338
+ try {
339
+ fs.cpSync(modulesSource, stageModules, { recursive: true });
340
+ return {
341
+ stageDir,
342
+ linkMode: 'copy',
343
+ note: `链接 node_modules 失败(${linkError.code ?? 'unknown'}),已改为完整复制`,
344
+ };
345
+ }
346
+ catch (copyError) {
347
+ try {
348
+ fs.rmSync(stageDir, { recursive: true, force: true });
349
+ }
350
+ catch { }
351
+ return {
352
+ stageDir: null,
353
+ linkMode: 'none',
354
+ note: `无法准备 stage(link: ${linkError.message}; copy: ${copyError.message})`,
355
+ };
356
+ }
357
+ }
358
+ }
359
+ /** Remove a staging directory created by prepareMonitorStage. */
360
+ export function cleanupMonitorStage(stageDir) {
361
+ try {
362
+ // Remove the link itself, never what it points at.
363
+ const modules = path.join(stageDir, 'node_modules');
364
+ const stat = fs.lstatSync(modules, { throwIfNoEntry: false });
365
+ if (stat?.isSymbolicLink())
366
+ fs.unlinkSync(modules);
367
+ }
368
+ catch { /* fall through to the recursive removal */ }
369
+ try {
370
+ fs.rmSync(stageDir, { recursive: true, force: true });
371
+ }
372
+ catch { }
373
+ }
374
+ /** True when the current process may write into `dir`. */
375
+ export function isDirWritable(dir) {
376
+ try {
377
+ fs.accessSync(dir, fs.constants.W_OK);
378
+ return true;
379
+ }
380
+ catch {
381
+ return false;
382
+ }
383
+ }
384
+ /**
385
+ * Resolve the launch argv for the restart monitor, staging it out of the
386
+ * package directory when that package can be replaced underneath it.
387
+ *
388
+ * Checkout: `resolveCliEntry` already points at `src/` via tsx, which no build
389
+ * can delete — nothing to stage.
390
+ *
391
+ * Installed: the monitor lives in the very directory `npm install -g` swaps,
392
+ * so it is copied to `stageParent` first and told where the real package is
393
+ * through `EVOLCORE_TARGET_PACKAGE_ROOT`. If staging cannot be done we fall
394
+ * back to launching in place: losing upgrade isolation is far better than
395
+ * losing the ability to restart.
396
+ */
397
+ export function resolveMonitorLaunch(packageRoot, stageParent) {
398
+ const resolved = resolveCliEntry(packageRoot);
399
+ if (resolved.usingSource) {
400
+ return { args: resolved.args, entry: resolved.entry, env: {}, stageDir: null };
401
+ }
402
+ const stage = prepareMonitorStage(packageRoot, stageParent);
403
+ if (!stage.stageDir) {
404
+ return {
405
+ args: resolved.args,
406
+ entry: resolved.entry,
407
+ env: {},
408
+ stageDir: null,
409
+ note: stage.note ? `${stage.note};本次未启用 stage 隔离` : undefined,
410
+ };
411
+ }
412
+ const stagedEntry = path.join(stage.stageDir, 'dist', 'cli', 'index.js');
413
+ if (!fs.existsSync(stagedEntry)) {
414
+ cleanupMonitorStage(stage.stageDir);
415
+ return { args: resolved.args, entry: resolved.entry, env: {}, stageDir: null, note: 'stage 缺少入口,已退回原地启动' };
416
+ }
417
+ return {
418
+ args: [stagedEntry],
419
+ entry: stagedEntry,
420
+ env: {
421
+ EVOLCORE_TARGET_PACKAGE_ROOT: packageRoot,
422
+ EVOLCORE_MONITOR_STAGE: stage.stageDir,
423
+ },
424
+ stageDir: stage.stageDir,
425
+ note: stage.note,
426
+ };
427
+ }
428
+ /**
429
+ * Resolve how to launch the EvolCore CLI (`restart`, `restart-monitor`, ...).
430
+ *
431
+ * In a development checkout the compiled `dist/` is deleted and rebuilt by
432
+ * `npm run build`, so anything spawned from `dist/cli/index.js` disappears
433
+ * exactly when a build is in flight. The restart monitor is the watchdog that
434
+ * has to outlive that window, so it must not live inside the artifact it
435
+ * supervises: when sources and tsx are both available we run the monitor
436
+ * straight from `src/`, mirroring what `bin/ec.js` already does for the CLI.
437
+ *
438
+ * An npm-installed package ships `dist/` without `src/` and without tsx (a
439
+ * devDependency), so both checks fail and the caller keeps the previous
440
+ * behaviour byte for byte.
441
+ */
442
+ export function resolveCliEntry(packageRoot) {
443
+ const sourceEntry = path.join(packageRoot, 'src', 'cli', 'index.ts');
444
+ const distEntry = path.join(packageRoot, 'dist', 'cli', 'index.js');
445
+ if (fs.existsSync(sourceEntry)) {
446
+ // tsx is a devDependency: present in a checkout, absent from the published
447
+ // package. Resolve it relative to the target root so `ec mode` switching
448
+ // between roots cannot borrow the wrong copy.
449
+ try {
450
+ const require = createRequire(path.join(packageRoot, 'package.json'));
451
+ const tsxUrl = pathToFileURL(require.resolve('tsx')).href;
452
+ return { args: ['--import', tsxUrl, sourceEntry], usingSource: true, entry: sourceEntry };
453
+ }
454
+ catch {
455
+ // tsx missing (e.g. `npm prune --production`): fall through to dist.
456
+ }
457
+ }
458
+ return { args: [distEntry], usingSource: false, entry: distEntry };
459
+ }
25
460
  const ENCODE_PATH_MAX = 200;
26
461
  function encodePathHash(s) {
27
462
  let h = 0;