evolcore 0.0.7 → 0.0.9
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/CHANGELOG.md +43 -0
- package/README.md +38 -4
- package/dist/agents/baseagent.js +117 -24
- package/dist/agents/claude-runner.js +31 -2
- package/dist/agents/codex-app-server-client.js +37 -2
- package/dist/agents/codex-runner.js +11 -3
- package/dist/aun/aid/control-aid.js +40 -27
- package/dist/aun/aid/domain.js +23 -0
- package/dist/aun/msg/group.js +9 -9
- package/dist/aun/msg/p2p.js +11 -6
- package/dist/aun/msg/upload.js +43 -29
- package/dist/aun/outbox.js +48 -5
- package/dist/channels/aun.js +225 -71
- package/dist/cli/aun-commands.js +10 -7
- package/dist/cli/bench.js +4 -3
- package/dist/cli/daemon-commands.js +208 -68
- package/dist/cli/data-command.js +62 -35
- package/dist/cli/index.js +11 -1
- package/dist/cli/init-channel.js +1 -1
- package/dist/cli/init.js +112 -17
- package/dist/cli/model.js +5 -5
- package/dist/cli/restart-monitor.js +102 -22
- package/dist/config/builtin-role-templates.js +4 -3
- package/dist/config/config-field-policy.js +6 -2
- package/dist/config/config-manager.js +79 -11
- package/dist/config/gateway-config.js +80 -35
- package/dist/config/role-schema.js +18 -3
- package/dist/config/role-service.js +3 -3
- package/dist/config/role-store.js +4 -3
- package/dist/config-store.js +19 -3
- package/dist/core/auth/operation-authorizer.js +3 -1
- package/dist/core/baseagent-loader.js +5 -3
- package/dist/core/bootstrap-service.js +0 -12
- package/dist/core/capability/providers/codex-capability-provider.js +2 -2
- package/dist/core/channel-loader.js +15 -2
- package/dist/core/command/command-handler.js +1 -1
- package/dist/core/command/evol-menu-version-gate.js +3 -2
- package/dist/core/command/menu-handler.js +50 -28
- package/dist/core/command/menu-protocol.js +7 -4
- package/dist/core/command/slash-handler.js +4 -4
- package/dist/core/data-migration.js +517 -24
- package/dist/core/inference/text-inference.js +18 -5
- package/dist/core/message/message-bridge.js +17 -6
- package/dist/core/message/response-engine.js +22 -3
- package/dist/core/model/config-scope.js +3 -3
- package/dist/core/permission/tool-policy.js +18 -0
- package/dist/core/protected-paths.js +12 -1
- package/dist/index.js +794 -725
- package/dist/ipc.js +3 -1
- package/dist/product.js +1 -0
- package/dist/utils/autostart.js +27 -0
- package/dist/utils/codex-cli.js +39 -0
- package/dist/utils/cross-platform.js +147 -18
- package/dist/utils/ecweb-utils.js +15 -2
- package/dist/utils/instance-registry.js +43 -6
- package/dist/utils/macos-autostart.js +179 -0
- package/dist/utils/process-introspect.js +7 -3
- package/dist/utils/windows-autostart.js +130 -0
- package/ecagent/dist/agent-loop.d.ts +1 -1
- package/ecagent/dist/agent-loop.js +1 -1
- package/ecagent/dist/agent.d.ts +1 -1
- package/ecagent/dist/agent.js +1 -1
- package/ecagent/dist/harness/agent-harness.d.ts +1 -1
- package/ecagent/dist/harness/compaction/branch-summarization.d.ts +1 -1
- package/ecagent/dist/harness/compaction/compaction.d.ts +1 -1
- package/ecagent/dist/harness/compaction/utils.d.ts +1 -1
- package/ecagent/dist/harness/messages.d.ts +1 -1
- package/ecagent/dist/harness/session/session.d.ts +1 -1
- package/ecagent/dist/harness/types.d.ts +1 -1
- package/ecagent/dist/proxy.d.ts +1 -1
- package/ecagent/dist/proxy.js +1 -1
- package/ecagent/dist/types.d.ts +1 -1
- package/kits/migrations/migrate-role-config-v5.mjs +27 -2
- package/kits/rules/01-overview.md +3 -2
- package/kits/schemas/_meta.json +2 -1
- package/kits/schemas/agent-config.schema.10.json +19 -1
- package/kits/schemas/daemon.schema.4.json +136 -0
- package/kits/schemas/defaults.schema.4.json +15 -1
- package/kits/schemas/relation-config.schema.8.json +22 -1
- package/kits/schemas/role-config.schema.1.json +2 -2
- package/package.json +2 -4
- package/ecagent/dist/agent-loop.d.ts.map +0 -1
- package/ecagent/dist/agent-loop.js.map +0 -1
- package/ecagent/dist/agent.d.ts.map +0 -1
- package/ecagent/dist/agent.js.map +0 -1
- package/ecagent/dist/harness/agent-harness.d.ts.map +0 -1
- package/ecagent/dist/harness/agent-harness.js.map +0 -1
- package/ecagent/dist/harness/compaction/branch-summarization.d.ts.map +0 -1
- package/ecagent/dist/harness/compaction/branch-summarization.js.map +0 -1
- package/ecagent/dist/harness/compaction/compaction.d.ts.map +0 -1
- package/ecagent/dist/harness/compaction/compaction.js.map +0 -1
- package/ecagent/dist/harness/compaction/utils.d.ts.map +0 -1
- package/ecagent/dist/harness/compaction/utils.js.map +0 -1
- package/ecagent/dist/harness/env/nodejs.d.ts.map +0 -1
- package/ecagent/dist/harness/env/nodejs.js.map +0 -1
- package/ecagent/dist/harness/messages.d.ts.map +0 -1
- package/ecagent/dist/harness/messages.js.map +0 -1
- package/ecagent/dist/harness/prompt-templates.d.ts.map +0 -1
- package/ecagent/dist/harness/prompt-templates.js.map +0 -1
- package/ecagent/dist/harness/session/jsonl-repo.d.ts.map +0 -1
- package/ecagent/dist/harness/session/jsonl-repo.js.map +0 -1
- package/ecagent/dist/harness/session/jsonl-storage.d.ts.map +0 -1
- package/ecagent/dist/harness/session/jsonl-storage.js.map +0 -1
- package/ecagent/dist/harness/session/memory-repo.d.ts.map +0 -1
- package/ecagent/dist/harness/session/memory-repo.js.map +0 -1
- package/ecagent/dist/harness/session/memory-storage.d.ts.map +0 -1
- package/ecagent/dist/harness/session/memory-storage.js.map +0 -1
- package/ecagent/dist/harness/session/repo-utils.d.ts.map +0 -1
- package/ecagent/dist/harness/session/repo-utils.js.map +0 -1
- package/ecagent/dist/harness/session/session.d.ts.map +0 -1
- package/ecagent/dist/harness/session/session.js.map +0 -1
- package/ecagent/dist/harness/session/uuid.d.ts.map +0 -1
- package/ecagent/dist/harness/session/uuid.js.map +0 -1
- package/ecagent/dist/harness/skills.d.ts.map +0 -1
- package/ecagent/dist/harness/skills.js.map +0 -1
- package/ecagent/dist/harness/system-prompt.d.ts.map +0 -1
- package/ecagent/dist/harness/system-prompt.js.map +0 -1
- package/ecagent/dist/harness/types.d.ts.map +0 -1
- package/ecagent/dist/harness/types.js.map +0 -1
- package/ecagent/dist/harness/utils/shell-output.d.ts.map +0 -1
- package/ecagent/dist/harness/utils/shell-output.js.map +0 -1
- package/ecagent/dist/harness/utils/truncate.d.ts.map +0 -1
- package/ecagent/dist/harness/utils/truncate.js.map +0 -1
- package/ecagent/dist/index.d.ts.map +0 -1
- package/ecagent/dist/index.js.map +0 -1
- package/ecagent/dist/node.d.ts.map +0 -1
- package/ecagent/dist/node.js.map +0 -1
- package/ecagent/dist/proxy.d.ts.map +0 -1
- package/ecagent/dist/proxy.js.map +0 -1
- package/ecagent/dist/runtime/api/lazy.d.ts.map +0 -1
- package/ecagent/dist/runtime/api/lazy.js.map +0 -1
- package/ecagent/dist/runtime/api/pi-messages.d.ts.map +0 -1
- package/ecagent/dist/runtime/api/pi-messages.js.map +0 -1
- package/ecagent/dist/runtime/api/pi-messages.lazy.d.ts.map +0 -1
- package/ecagent/dist/runtime/api/pi-messages.lazy.js.map +0 -1
- package/ecagent/dist/runtime/auth/context.d.ts.map +0 -1
- package/ecagent/dist/runtime/auth/context.js.map +0 -1
- package/ecagent/dist/runtime/auth/credential-store.d.ts.map +0 -1
- package/ecagent/dist/runtime/auth/credential-store.js.map +0 -1
- package/ecagent/dist/runtime/auth/helpers.d.ts.map +0 -1
- package/ecagent/dist/runtime/auth/helpers.js.map +0 -1
- package/ecagent/dist/runtime/auth/resolve.d.ts.map +0 -1
- package/ecagent/dist/runtime/auth/resolve.js.map +0 -1
- package/ecagent/dist/runtime/auth/types.d.ts.map +0 -1
- package/ecagent/dist/runtime/auth/types.js.map +0 -1
- package/ecagent/dist/runtime/compat.d.ts.map +0 -1
- package/ecagent/dist/runtime/compat.js.map +0 -1
- package/ecagent/dist/runtime/index.d.ts.map +0 -1
- package/ecagent/dist/runtime/index.js.map +0 -1
- package/ecagent/dist/runtime/models.d.ts.map +0 -1
- package/ecagent/dist/runtime/models.js.map +0 -1
- package/ecagent/dist/runtime/oauth.d.ts.map +0 -1
- package/ecagent/dist/runtime/oauth.js.map +0 -1
- package/ecagent/dist/runtime/providers/faux.d.ts.map +0 -1
- package/ecagent/dist/runtime/providers/faux.js.map +0 -1
- package/ecagent/dist/runtime/types.d.ts.map +0 -1
- package/ecagent/dist/runtime/types.js.map +0 -1
- package/ecagent/dist/runtime/utils/diagnostics.d.ts.map +0 -1
- package/ecagent/dist/runtime/utils/diagnostics.js.map +0 -1
- package/ecagent/dist/runtime/utils/event-stream.d.ts.map +0 -1
- package/ecagent/dist/runtime/utils/event-stream.js.map +0 -1
- package/ecagent/dist/runtime/utils/headers.d.ts.map +0 -1
- package/ecagent/dist/runtime/utils/headers.js.map +0 -1
- package/ecagent/dist/runtime/utils/json-parse.d.ts.map +0 -1
- package/ecagent/dist/runtime/utils/json-parse.js.map +0 -1
- package/ecagent/dist/runtime/utils/oauth/types.d.ts.map +0 -1
- package/ecagent/dist/runtime/utils/oauth/types.js.map +0 -1
- package/ecagent/dist/runtime/utils/overflow.d.ts.map +0 -1
- package/ecagent/dist/runtime/utils/overflow.js.map +0 -1
- package/ecagent/dist/runtime/utils/provider-env.d.ts.map +0 -1
- package/ecagent/dist/runtime/utils/provider-env.js.map +0 -1
- package/ecagent/dist/runtime/utils/retry.d.ts.map +0 -1
- package/ecagent/dist/runtime/utils/retry.js.map +0 -1
- package/ecagent/dist/runtime/utils/typebox-helpers.d.ts.map +0 -1
- package/ecagent/dist/runtime/utils/typebox-helpers.js.map +0 -1
- package/ecagent/dist/runtime/utils/validation.d.ts.map +0 -1
- package/ecagent/dist/runtime/utils/validation.js.map +0 -1
- package/ecagent/dist/types.d.ts.map +0 -1
- package/ecagent/dist/types.js.map +0 -1
- package/ecagent/package.json +0 -93
- package/ecagent/src/agent-loop.ts +0 -792
- package/ecagent/src/agent.ts +0 -575
- package/ecagent/src/harness/agent-harness.ts +0 -1029
- package/ecagent/src/harness/compaction/branch-summarization.ts +0 -261
- package/ecagent/src/harness/compaction/compaction.ts +0 -753
- package/ecagent/src/harness/compaction/utils.ts +0 -144
- package/ecagent/src/harness/env/nodejs.ts +0 -569
- package/ecagent/src/harness/messages.ts +0 -164
- package/ecagent/src/harness/prompt-templates.ts +0 -267
- package/ecagent/src/harness/session/jsonl-repo.ts +0 -179
- package/ecagent/src/harness/session/jsonl-storage.ts +0 -314
- package/ecagent/src/harness/session/memory-repo.ts +0 -50
- package/ecagent/src/harness/session/memory-storage.ts +0 -133
- package/ecagent/src/harness/session/repo-utils.ts +0 -51
- package/ecagent/src/harness/session/session.ts +0 -338
- package/ecagent/src/harness/session/uuid.ts +0 -54
- package/ecagent/src/harness/skills.ts +0 -375
- package/ecagent/src/harness/system-prompt.ts +0 -34
- package/ecagent/src/harness/types.ts +0 -838
- package/ecagent/src/harness/utils/shell-output.ts +0 -135
- package/ecagent/src/harness/utils/truncate.ts +0 -344
- package/ecagent/src/index.ts +0 -46
- package/ecagent/src/node.ts +0 -2
- package/ecagent/src/proxy.ts +0 -367
- package/ecagent/src/runtime/api/lazy.ts +0 -70
- package/ecagent/src/runtime/api/pi-messages.lazy.ts +0 -4
- package/ecagent/src/runtime/api/pi-messages.ts +0 -436
- package/ecagent/src/runtime/auth/context.ts +0 -45
- package/ecagent/src/runtime/auth/credential-store.ts +0 -47
- package/ecagent/src/runtime/auth/helpers.ts +0 -46
- package/ecagent/src/runtime/auth/resolve.ts +0 -141
- package/ecagent/src/runtime/auth/types.ts +0 -182
- package/ecagent/src/runtime/compat.ts +0 -158
- package/ecagent/src/runtime/index.ts +0 -31
- package/ecagent/src/runtime/models.ts +0 -452
- package/ecagent/src/runtime/oauth.ts +0 -1
- package/ecagent/src/runtime/providers/faux.ts +0 -538
- package/ecagent/src/runtime/types.ts +0 -718
- package/ecagent/src/runtime/utils/diagnostics.ts +0 -45
- package/ecagent/src/runtime/utils/event-stream.ts +0 -88
- package/ecagent/src/runtime/utils/headers.ts +0 -18
- package/ecagent/src/runtime/utils/json-parse.ts +0 -124
- package/ecagent/src/runtime/utils/oauth/types.ts +0 -79
- package/ecagent/src/runtime/utils/overflow.ts +0 -165
- package/ecagent/src/runtime/utils/provider-env.ts +0 -52
- package/ecagent/src/runtime/utils/retry.ts +0 -101
- package/ecagent/src/runtime/utils/typebox-helpers.ts +0 -24
- package/ecagent/src/runtime/utils/validation.ts +0 -310
- package/ecagent/src/types.ts +0 -430
- package/ecagent/tsconfig.build.json +0 -32
package/dist/ipc.js
CHANGED
|
@@ -247,6 +247,8 @@ export class IpcServer {
|
|
|
247
247
|
return roots;
|
|
248
248
|
}
|
|
249
249
|
start() {
|
|
250
|
+
if (this.server)
|
|
251
|
+
return Promise.resolve();
|
|
250
252
|
// Remove stale socket file (Unix only — named pipes auto-cleanup on process exit)
|
|
251
253
|
if (!isNamedPipe(this.socketPath)) {
|
|
252
254
|
try {
|
|
@@ -517,7 +519,7 @@ export class IpcServer {
|
|
|
517
519
|
mentions: Array.isArray(cmd.mentions)
|
|
518
520
|
? cmd.mentions.filter((entry) => !!entry && typeof entry === 'object' && !Array.isArray(entry))
|
|
519
521
|
: undefined,
|
|
520
|
-
encrypt: cmd.encrypt ===
|
|
522
|
+
encrypt: typeof cmd.encrypt === 'boolean' ? cmd.encrypt : undefined,
|
|
521
523
|
thread: typeof cmd.thread === 'string' ? cmd.thread : undefined,
|
|
522
524
|
returnPolicy: cmd.returnPolicy === 'required' || cmd.returnPolicy === 'none' ? cmd.returnPolicy : undefined,
|
|
523
525
|
originSessionId: typeof cmd.originSessionId === 'string' ? cmd.originSessionId : undefined,
|
package/dist/product.js
CHANGED
|
@@ -10,6 +10,7 @@ export const MAIN_CLI_BIN = 'ec';
|
|
|
10
10
|
export const WEB_PACKAGE_NAME = 'ec-web';
|
|
11
11
|
export const WEB_CLI_BIN = 'ec-web';
|
|
12
12
|
export const WEB_PACKAGE_LATEST = `${WEB_PACKAGE_NAME}@latest`;
|
|
13
|
+
export const MIN_ECWEB_VERSION = '0.0.8';
|
|
13
14
|
export const DAEMON_CONFIG_FILENAME = 'daemon.json';
|
|
14
15
|
export const DAEMON_SCHEMA_NAME = 'daemon';
|
|
15
16
|
export const RUNTIME_HOME_ENV = 'EVOLCORE_HOME';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { configureMacosAutostart, macosAutostartInstalled } from './macos-autostart.js';
|
|
2
|
+
import { configureWindowsAutostart, windowsAutostartInstalled } from './windows-autostart.js';
|
|
3
|
+
export function autostartInstalled() {
|
|
4
|
+
if (process.platform === 'win32')
|
|
5
|
+
return windowsAutostartInstalled();
|
|
6
|
+
if (process.platform === 'darwin')
|
|
7
|
+
return macosAutostartInstalled();
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
export function configureAutostart(enabled, runtimeRoot) {
|
|
11
|
+
if (process.platform === 'win32')
|
|
12
|
+
return configureWindowsAutostart(enabled, runtimeRoot);
|
|
13
|
+
if (process.platform === 'darwin')
|
|
14
|
+
return configureMacosAutostart(enabled, runtimeRoot);
|
|
15
|
+
return {
|
|
16
|
+
ok: !enabled,
|
|
17
|
+
enabled: false,
|
|
18
|
+
...(!enabled ? {} : { error: '登录自启仅支持 Windows 和 macOS 主机' }),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function autostartPlatformLabel() {
|
|
22
|
+
if (process.platform === 'win32')
|
|
23
|
+
return 'Windows';
|
|
24
|
+
if (process.platform === 'darwin')
|
|
25
|
+
return 'macOS';
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { isWindows, resolveCommandPath } from './cross-platform.js';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the installed Codex CLI entrypoint. npm creates a `.cmd` shim on
|
|
5
|
+
* Windows, which is not the same executable as the extensionless Unix shim.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveCodexCliPath() {
|
|
8
|
+
return resolveCommandPath('codex');
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Build a child-process invocation that works for npm's Windows command shim.
|
|
12
|
+
* The options are injectable so platform-specific behavior can be tested on
|
|
13
|
+
* non-Windows CI hosts.
|
|
14
|
+
*/
|
|
15
|
+
export function resolveCodexLaunchCommand(args, options = {}) {
|
|
16
|
+
const windows = options.isWindows ?? isWindows;
|
|
17
|
+
const commandPath = options.commandPath !== undefined
|
|
18
|
+
? options.commandPath
|
|
19
|
+
: resolveCodexCliPath();
|
|
20
|
+
if (!commandPath)
|
|
21
|
+
return null;
|
|
22
|
+
if (windows && /\.(cmd|bat)$/i.test(commandPath)) {
|
|
23
|
+
return {
|
|
24
|
+
command: options.comSpec ?? process.env.ComSpec ?? 'cmd.exe',
|
|
25
|
+
args: ['/d', '/s', '/c', commandPath, ...args],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return { command: commandPath, args };
|
|
29
|
+
}
|
|
30
|
+
/** Execute a Codex CLI command using the resolved npm shim/path. */
|
|
31
|
+
export function execCodexCliSync(args, options) {
|
|
32
|
+
const commandPath = resolveCodexCliPath();
|
|
33
|
+
if (!commandPath)
|
|
34
|
+
throw new Error('Codex CLI not found');
|
|
35
|
+
const launchCommand = resolveCodexLaunchCommand(args, { commandPath });
|
|
36
|
+
if (!launchCommand)
|
|
37
|
+
throw new Error('Codex CLI not found');
|
|
38
|
+
return execFileSync(launchCommand.command, launchCommand.args, options);
|
|
39
|
+
}
|
|
@@ -3,7 +3,7 @@ import { fileURLToPath } from 'url';
|
|
|
3
3
|
import { execFileSync, execFile, spawn, spawnSync } from 'child_process';
|
|
4
4
|
import { promisify } from 'util';
|
|
5
5
|
import fs from 'fs';
|
|
6
|
-
import { getProcessStartTime } from './process-introspect.js';
|
|
6
|
+
import { getProcessStartTime, parseCimDate } from './process-introspect.js';
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
8
|
export const isWindows = process.platform === 'win32';
|
|
9
9
|
const ENCODE_PATH_MAX = 200;
|
|
@@ -38,6 +38,10 @@ export function encodePath(projectPath) {
|
|
|
38
38
|
* Cross-platform process liveness check.
|
|
39
39
|
*/
|
|
40
40
|
export function isProcessRunning(pid) {
|
|
41
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
42
|
+
return false;
|
|
43
|
+
if (isWindows)
|
|
44
|
+
return isWindowsProcessRunning(pid);
|
|
41
45
|
try {
|
|
42
46
|
process.kill(pid, 0);
|
|
43
47
|
return true;
|
|
@@ -47,23 +51,65 @@ export function isProcessRunning(pid) {
|
|
|
47
51
|
return e.code === 'EPERM';
|
|
48
52
|
}
|
|
49
53
|
}
|
|
54
|
+
function isWindowsProcessRunning(pid) {
|
|
55
|
+
try {
|
|
56
|
+
const result = spawnSync('tasklist', ['/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH'], { encoding: 'utf-8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
57
|
+
if (result.status === 0 && new RegExp(`,"${pid}",`, 'i').test(result.stdout || ''))
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch { }
|
|
61
|
+
// Keep a conservative fallback for restricted Windows environments where
|
|
62
|
+
// tasklist is unavailable. A failed probe must never be treated as proof of
|
|
63
|
+
// process exit by lifecycle code.
|
|
64
|
+
try {
|
|
65
|
+
process.kill(pid, 0);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
return e?.code === 'EPERM';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
50
72
|
/**
|
|
51
73
|
* Cross-platform process termination.
|
|
52
74
|
*/
|
|
53
75
|
export function killProcess(pid, force = false) {
|
|
54
|
-
if (
|
|
76
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
77
|
+
return false;
|
|
78
|
+
if (isWindows) {
|
|
55
79
|
try {
|
|
56
|
-
|
|
80
|
+
const args = ['/PID', String(pid), '/T'];
|
|
81
|
+
if (force)
|
|
82
|
+
args.push('/F');
|
|
83
|
+
const result = spawnSync('taskkill', args, {
|
|
84
|
+
encoding: 'utf-8',
|
|
85
|
+
timeout: 10_000,
|
|
86
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
87
|
+
windowsHide: true,
|
|
88
|
+
});
|
|
89
|
+
return result.status === 0 || !isProcessRunning(pid);
|
|
57
90
|
}
|
|
58
|
-
catch {
|
|
59
|
-
|
|
60
|
-
else {
|
|
61
|
-
try {
|
|
62
|
-
process.kill(pid, force ? 'SIGKILL' : 'SIGTERM');
|
|
91
|
+
catch {
|
|
92
|
+
return !isProcessRunning(pid);
|
|
63
93
|
}
|
|
64
|
-
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
process.kill(pid, force ? 'SIGKILL' : 'SIGTERM');
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return !isProcessRunning(pid);
|
|
65
101
|
}
|
|
66
102
|
}
|
|
103
|
+
/** Wait until a process has really disappeared from the host process table. */
|
|
104
|
+
export async function waitForProcessExit(pid, timeoutMs = 10_000, intervalMs = 250) {
|
|
105
|
+
const deadline = Date.now() + timeoutMs;
|
|
106
|
+
while (Date.now() < deadline) {
|
|
107
|
+
if (!isProcessRunning(pid))
|
|
108
|
+
return true;
|
|
109
|
+
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
110
|
+
}
|
|
111
|
+
return !isProcessRunning(pid);
|
|
112
|
+
}
|
|
67
113
|
/**
|
|
68
114
|
* Cross-platform process search by command line pattern.
|
|
69
115
|
* Returns list of matching PIDs.
|
|
@@ -71,11 +117,37 @@ export function killProcess(pid, force = false) {
|
|
|
71
117
|
export function findProcesses(pattern) {
|
|
72
118
|
try {
|
|
73
119
|
if (isWindows) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
120
|
+
// WMI LIKE is wildcard matching, not JavaScript regex matching. Query a
|
|
121
|
+
// stable literal anchor first, then apply the requested regex locally.
|
|
122
|
+
const anchor = windowsProcessSearchAnchor(pattern);
|
|
123
|
+
const escapedAnchor = anchor.replace(/'/g, "''");
|
|
124
|
+
const script = `$a='${escapedAnchor}'; Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -and $_.CommandLine -like ('*' + $a + '*') } | Select-Object ProcessId,CommandLine | ConvertTo-Json -Compress`;
|
|
125
|
+
const result = spawnSync('powershell', ['-NoProfile', '-Command', script], { encoding: null, timeout: 8000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
126
|
+
const raw = decodeWindowsOutput(result.stdout).trim();
|
|
127
|
+
if (raw) {
|
|
128
|
+
const parsed = JSON.parse(raw);
|
|
129
|
+
const rows = !parsed ? [] : Array.isArray(parsed) ? parsed : [parsed];
|
|
130
|
+
const matcherCandidates = [pattern, pattern.replace(/\\\\/g, '\\')]
|
|
131
|
+
.filter((value, index, values) => values.indexOf(value) === index)
|
|
132
|
+
.map((value) => {
|
|
133
|
+
try {
|
|
134
|
+
return new RegExp(value, 'i');
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
.filter((value) => value !== null);
|
|
141
|
+
return rows
|
|
142
|
+
.filter(row => Number.isInteger(row.ProcessId) && row.ProcessId !== process.pid)
|
|
143
|
+
.filter(row => {
|
|
144
|
+
const commandLine = row.CommandLine || '';
|
|
145
|
+
const normalized = commandLine.replace(/\\/g, '/');
|
|
146
|
+
return matcherCandidates.some(matcher => matcher.test(commandLine) || matcher.test(normalized));
|
|
147
|
+
})
|
|
148
|
+
.map(row => row.ProcessId);
|
|
149
|
+
}
|
|
150
|
+
return [];
|
|
79
151
|
}
|
|
80
152
|
else {
|
|
81
153
|
const output = execFileSync('pgrep', ['-f', pattern], { encoding: 'utf-8' }).trim();
|
|
@@ -86,6 +158,10 @@ export function findProcesses(pattern) {
|
|
|
86
158
|
return [];
|
|
87
159
|
}
|
|
88
160
|
}
|
|
161
|
+
function windowsProcessSearchAnchor(pattern) {
|
|
162
|
+
const tokens = pattern.match(/[A-Za-z0-9][A-Za-z0-9._-]{2,}/g) || [];
|
|
163
|
+
return tokens.sort((a, b) => b.length - a.length)[0] || pattern;
|
|
164
|
+
}
|
|
89
165
|
/**
|
|
90
166
|
* Cross-platform: find PIDs listening on a TCP port.
|
|
91
167
|
* Used to clean up stale/orphaned listeners (e.g. manually-spawned ecweb
|
|
@@ -124,13 +200,35 @@ export function findProcessByPort(port) {
|
|
|
124
200
|
export function getProcessInfo(pid) {
|
|
125
201
|
try {
|
|
126
202
|
if (isWindows) {
|
|
203
|
+
// WMIC was removed from recent Windows installations. Prefer the
|
|
204
|
+
// supported CIM API and keep WMIC as a compatibility fallback.
|
|
205
|
+
const psScript = `(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}' | Select-Object WorkingSetSize,CreationDate | ConvertTo-Json -Compress)`;
|
|
206
|
+
const ps = spawnSync('powershell', ['-NoProfile', '-Command', psScript], {
|
|
207
|
+
encoding: null, timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
|
|
208
|
+
});
|
|
209
|
+
const raw = decodeWindowsOutput(ps.stdout).trim();
|
|
210
|
+
if (raw) {
|
|
211
|
+
const processInfo = JSON.parse(raw);
|
|
212
|
+
const memory = Number(processInfo.WorkingSetSize);
|
|
213
|
+
const createdAt = processInfo.CreationDate
|
|
214
|
+
? parseCimDate(processInfo.CreationDate) ?? parseDateString(processInfo.CreationDate)
|
|
215
|
+
: null;
|
|
216
|
+
return {
|
|
217
|
+
...(Number.isFinite(memory) && memory > 0 ? { memory: String(Math.round(memory / 1024)) } : {}),
|
|
218
|
+
...(createdAt !== null ? { uptime: formatUptime(Math.max(0, Math.floor((Date.now() - createdAt) / 1000))) } : {}),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
127
221
|
const result = spawnSync('wmic', ['process', 'where', `ProcessId=${pid}`, 'get', 'WorkingSetSize,CreationDate'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
128
222
|
const output = result.stdout || '';
|
|
129
223
|
const lines = output.trim().split('\n').filter(l => l.trim());
|
|
130
224
|
if (lines.length >= 2) {
|
|
131
225
|
const parts = lines[1].trim().split(/\s+/);
|
|
132
226
|
const memKB = parts[1] ? Math.round(parseInt(parts[1], 10) / 1024) : undefined;
|
|
133
|
-
|
|
227
|
+
const createdAt = parts[0] ? parseCimDate(parts[0]) ?? parseDateString(parts[0]) : null;
|
|
228
|
+
return {
|
|
229
|
+
...(memKB ? { memory: `${memKB}` } : {}),
|
|
230
|
+
...(createdAt !== null ? { uptime: formatUptime(Math.max(0, Math.floor((Date.now() - createdAt) / 1000))) } : {}),
|
|
231
|
+
};
|
|
134
232
|
}
|
|
135
233
|
}
|
|
136
234
|
else {
|
|
@@ -167,6 +265,37 @@ function formatUptime(totalSeconds) {
|
|
|
167
265
|
parts.push(`${seconds}s`);
|
|
168
266
|
return parts.join(' ');
|
|
169
267
|
}
|
|
268
|
+
function parseDateString(value) {
|
|
269
|
+
const microsoftJsonDate = value.match(/^\\?\/Date\(([-+]?\d+)\)\\?\/$/);
|
|
270
|
+
if (microsoftJsonDate)
|
|
271
|
+
return Number(microsoftJsonDate[1]);
|
|
272
|
+
const parsed = Date.parse(value);
|
|
273
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
274
|
+
}
|
|
275
|
+
/** Decode PowerShell output consistently across Windows 5.1 and pwsh. */
|
|
276
|
+
function decodeWindowsOutput(value) {
|
|
277
|
+
if (!value)
|
|
278
|
+
return '';
|
|
279
|
+
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
280
|
+
if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {
|
|
281
|
+
return bytes.subarray(2).toString('utf16le');
|
|
282
|
+
}
|
|
283
|
+
if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
|
|
284
|
+
const swapped = Buffer.allocUnsafe(bytes.length - 2);
|
|
285
|
+
for (let i = 2; i + 1 < bytes.length; i += 2) {
|
|
286
|
+
swapped[i - 2] = bytes[i + 1];
|
|
287
|
+
swapped[i - 1] = bytes[i];
|
|
288
|
+
}
|
|
289
|
+
return swapped.toString('utf16le');
|
|
290
|
+
}
|
|
291
|
+
const utf8 = bytes.toString('utf8').replace(/^\uFEFF/, '');
|
|
292
|
+
if (utf8.includes('\u0000')) {
|
|
293
|
+
const utf16 = bytes.toString('utf16le').replace(/^\uFEFF/, '');
|
|
294
|
+
if (utf16.includes('{') || utf16.includes('[') || utf16.includes('CommandLine'))
|
|
295
|
+
return utf16;
|
|
296
|
+
}
|
|
297
|
+
return utf8;
|
|
298
|
+
}
|
|
170
299
|
/**
|
|
171
300
|
* Cross-platform command existence check.
|
|
172
301
|
*/
|
|
@@ -178,7 +307,7 @@ export function commandExists(cmd) {
|
|
|
178
307
|
let exists = false;
|
|
179
308
|
try {
|
|
180
309
|
if (isWindows) {
|
|
181
|
-
const r = spawnSync('where', [cmd], { encoding: 'utf-8', stdio: 'pipe', windowsHide: true });
|
|
310
|
+
const r = spawnSync('where.exe', [cmd], { encoding: 'utf-8', stdio: 'pipe', windowsHide: true });
|
|
182
311
|
exists = r.status === 0;
|
|
183
312
|
}
|
|
184
313
|
else {
|
|
@@ -194,7 +323,7 @@ export function commandExists(cmd) {
|
|
|
194
323
|
}
|
|
195
324
|
/**
|
|
196
325
|
* 解析命令的真实可执行文件绝对路径。
|
|
197
|
-
* Windows: `where` 会列出全部同名文件(npm 全局 bin 同时生成无后缀 sh 包装、.cmd、.ps1),
|
|
326
|
+
* Windows: `where.exe` 会列出全部同名文件(npm 全局 bin 同时生成无后缀 sh 包装、.cmd、.ps1),
|
|
198
327
|
* 其中无后缀的那个是 Unix sh 脚本,Windows 无法直接 spawn(ENOENT)。
|
|
199
328
|
* 因此优先选 PATHEXT 可执行后缀(.cmd/.exe/.bat/.com),都没有才退回首行。
|
|
200
329
|
* 失败返回 null。不缓存——刚安装的命令需要重新探测。
|
|
@@ -202,7 +331,7 @@ export function commandExists(cmd) {
|
|
|
202
331
|
export function resolveCommandPath(cmd) {
|
|
203
332
|
try {
|
|
204
333
|
if (isWindows) {
|
|
205
|
-
const r = spawnSync('where', [cmd], { encoding: 'utf-8', stdio: 'pipe', windowsHide: true });
|
|
334
|
+
const r = spawnSync('where.exe', [cmd], { encoding: 'utf-8', stdio: 'pipe', windowsHide: true });
|
|
206
335
|
if (r.status !== 0 || !r.stdout)
|
|
207
336
|
return null;
|
|
208
337
|
const candidates = r.stdout.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { resolveGlobalPkg } from './npm-ops.js';
|
|
3
|
+
import { compareVersions, resolveGlobalPkg } from './npm-ops.js';
|
|
4
4
|
import { isWindows as platformIsWindows, resolveCommandPath } from './cross-platform.js';
|
|
5
|
-
import { WEB_CLI_BIN, WEB_PACKAGE_NAME } from '../product.js';
|
|
5
|
+
import { MIN_ECWEB_VERSION, WEB_CLI_BIN, WEB_PACKAGE_NAME } from '../product.js';
|
|
6
|
+
/** EC Web owns the session-log reader, so older builds cannot safely serve a newer daemon. */
|
|
7
|
+
export function isEcwebVersionSupported(version) {
|
|
8
|
+
const normalized = version?.trim();
|
|
9
|
+
if (!normalized || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(normalized))
|
|
10
|
+
return false;
|
|
11
|
+
return compareVersions(normalized, MIN_ECWEB_VERSION) >= 0;
|
|
12
|
+
}
|
|
13
|
+
export function ecwebVersionRequirementError(version) {
|
|
14
|
+
if (isEcwebVersionSupported(version))
|
|
15
|
+
return null;
|
|
16
|
+
const detected = version?.trim() || 'unknown';
|
|
17
|
+
return `EC Web ${detected} 低于最低要求 ${MIN_ECWEB_VERSION},拒绝启动。请运行: npm install -g ${WEB_PACKAGE_NAME}@latest`;
|
|
18
|
+
}
|
|
6
19
|
function readEcwebPackageEntry(pkgJsonPath) {
|
|
7
20
|
try {
|
|
8
21
|
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
@@ -350,12 +350,20 @@ export function findOrphanProcesses() {
|
|
|
350
350
|
// 1. 已登记 PID(自己 HOME 下的 main + 自己进程)
|
|
351
351
|
const known = new Set([process.pid]);
|
|
352
352
|
const status = scanInstances();
|
|
353
|
-
for (const m of status.mains)
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
353
|
+
for (const m of status.mains) {
|
|
354
|
+
if (m.alive)
|
|
355
|
+
known.add(m.record.pid);
|
|
356
|
+
}
|
|
357
|
+
for (const m of status.restartMonitors) {
|
|
358
|
+
if (m.alive)
|
|
359
|
+
known.add(m.record.pid);
|
|
360
|
+
}
|
|
357
361
|
// 2. 系统中所有跑 dist/index.js 的 node 进程
|
|
358
|
-
|
|
362
|
+
// Use a stable filename anchor for the Windows CIM query. The full path
|
|
363
|
+
// regex contains escaped separators and is applied after command lines have
|
|
364
|
+
// been fetched; embedding it in the CIM pre-filter can silently miss a
|
|
365
|
+
// backslash-delimited `dist\\index.js` command on some PowerShell builds.
|
|
366
|
+
const candidates = findProcesses('index\\.js');
|
|
359
367
|
const orphans = [];
|
|
360
368
|
for (const pid of candidates) {
|
|
361
369
|
if (known.has(pid))
|
|
@@ -395,11 +403,21 @@ export function killOrphans(orphans) {
|
|
|
395
403
|
}
|
|
396
404
|
function readCmdline(pid) {
|
|
397
405
|
if (isWindows) {
|
|
406
|
+
// WMIC is removed from recent Windows installations. Keep it as the
|
|
407
|
+
// first probe for older hosts, then use the supported CIM API when it is
|
|
408
|
+
// unavailable or returns no row.
|
|
398
409
|
try {
|
|
399
410
|
const result = spawnSync('wmic', ['process', 'where', `ProcessId=${pid}`, 'get', 'CommandLine', '/value'], { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
400
411
|
const out = result.stdout || '';
|
|
401
412
|
const m = out.match(/CommandLine=([^\r\n]+)/);
|
|
402
|
-
|
|
413
|
+
if (m?.[1]?.trim())
|
|
414
|
+
return m[1].trim();
|
|
415
|
+
}
|
|
416
|
+
catch { }
|
|
417
|
+
try {
|
|
418
|
+
const script = `(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}').CommandLine`;
|
|
419
|
+
const result = spawnSync('powershell', ['-NoProfile', '-Command', script], { encoding: null, timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
420
|
+
return decodeWindowsOutput(result.stdout).trim();
|
|
403
421
|
}
|
|
404
422
|
catch {
|
|
405
423
|
return '';
|
|
@@ -422,6 +440,25 @@ function readCmdline(pid) {
|
|
|
422
440
|
}
|
|
423
441
|
}
|
|
424
442
|
}
|
|
443
|
+
function decodeWindowsOutput(value) {
|
|
444
|
+
if (!value)
|
|
445
|
+
return '';
|
|
446
|
+
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
447
|
+
if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe)
|
|
448
|
+
return bytes.subarray(2).toString('utf16le');
|
|
449
|
+
if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
|
|
450
|
+
const swapped = Buffer.allocUnsafe(bytes.length - 2);
|
|
451
|
+
for (let i = 2; i + 1 < bytes.length; i += 2) {
|
|
452
|
+
swapped[i - 2] = bytes[i + 1];
|
|
453
|
+
swapped[i - 1] = bytes[i];
|
|
454
|
+
}
|
|
455
|
+
return swapped.toString('utf16le');
|
|
456
|
+
}
|
|
457
|
+
const utf8 = bytes.toString('utf8').replace(/^\uFEFF/, '');
|
|
458
|
+
if (utf8.includes('\u0000'))
|
|
459
|
+
return bytes.toString('utf16le').replace(/^\uFEFF/, '');
|
|
460
|
+
return utf8;
|
|
461
|
+
}
|
|
425
462
|
function readProcessEnvironment(pid) {
|
|
426
463
|
// Linux: /proc/<pid>/environ
|
|
427
464
|
if (!isWindows && process.platform !== 'darwin') {
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
import { getPackageRoot } from '../paths.js';
|
|
6
|
+
export const MACOS_AUTOSTART_LABEL = 'cn.evolai.evolcore';
|
|
7
|
+
function isMacos() {
|
|
8
|
+
return process.platform === 'darwin';
|
|
9
|
+
}
|
|
10
|
+
function plistPath() {
|
|
11
|
+
return path.join(os.homedir(), 'Library', 'LaunchAgents', `${MACOS_AUTOSTART_LABEL}.plist`);
|
|
12
|
+
}
|
|
13
|
+
function launchctl(args) {
|
|
14
|
+
return spawnSync('/bin/launchctl', args, {
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
timeout: 15_000,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function commandError(result, fallback) {
|
|
20
|
+
return String(result.error?.message || result.stderr || result.stdout || fallback).trim();
|
|
21
|
+
}
|
|
22
|
+
function guiDomain() {
|
|
23
|
+
const uid = process.getuid?.();
|
|
24
|
+
return uid === undefined ? undefined : `gui/${uid}`;
|
|
25
|
+
}
|
|
26
|
+
function xmlLiteral(value) {
|
|
27
|
+
return value
|
|
28
|
+
.replace(/&/g, '&')
|
|
29
|
+
.replace(/</g, '<')
|
|
30
|
+
.replace(/>/g, '>')
|
|
31
|
+
.replace(/"/g, '"')
|
|
32
|
+
.replace(/'/g, ''');
|
|
33
|
+
}
|
|
34
|
+
function createPlist(runtimeRoot) {
|
|
35
|
+
const absoluteRoot = path.resolve(runtimeRoot);
|
|
36
|
+
const cliEntry = path.join(getPackageRoot(), 'dist', 'cli', 'index.js');
|
|
37
|
+
if (!fs.existsSync(cliEntry)) {
|
|
38
|
+
throw new Error(`找不到 EvolCore CLI 入口: ${cliEntry}`);
|
|
39
|
+
}
|
|
40
|
+
const environment = [
|
|
41
|
+
['EVOLCORE_HOME', absoluteRoot],
|
|
42
|
+
['HOME', os.homedir()],
|
|
43
|
+
['PATH', Array.from(new Set([
|
|
44
|
+
path.dirname(process.execPath),
|
|
45
|
+
...(process.env.PATH ?? '').split(path.delimiter).filter(Boolean),
|
|
46
|
+
])).join(path.delimiter)],
|
|
47
|
+
];
|
|
48
|
+
const environmentXml = environment
|
|
49
|
+
.map(([key, value]) => ` <key>${xmlLiteral(key)}</key>\n <string>${xmlLiteral(value)}</string>`)
|
|
50
|
+
.join('\n');
|
|
51
|
+
const args = [process.execPath, '--no-warnings=ExperimentalWarning', cliEntry, 'start']
|
|
52
|
+
.map(value => ` <string>${xmlLiteral(value)}</string>`)
|
|
53
|
+
.join('\n');
|
|
54
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
55
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
56
|
+
<plist version="1.0">
|
|
57
|
+
<dict>
|
|
58
|
+
<key>Label</key>
|
|
59
|
+
<string>${xmlLiteral(MACOS_AUTOSTART_LABEL)}</string>
|
|
60
|
+
<key>ProgramArguments</key>
|
|
61
|
+
<array>
|
|
62
|
+
${args}
|
|
63
|
+
</array>
|
|
64
|
+
<key>EnvironmentVariables</key>
|
|
65
|
+
<dict>
|
|
66
|
+
${environmentXml}
|
|
67
|
+
</dict>
|
|
68
|
+
<key>RunAtLoad</key>
|
|
69
|
+
<true/>
|
|
70
|
+
<key>StandardOutPath</key>
|
|
71
|
+
<string>${xmlLiteral(path.join(absoluteRoot, 'logs', 'autostart.stdout.log'))}</string>
|
|
72
|
+
<key>StandardErrorPath</key>
|
|
73
|
+
<string>${xmlLiteral(path.join(absoluteRoot, 'logs', 'autostart.stderr.log'))}</string>
|
|
74
|
+
</dict>
|
|
75
|
+
</plist>
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
function restorePlist(file, previous) {
|
|
79
|
+
try {
|
|
80
|
+
if (previous === undefined)
|
|
81
|
+
fs.rmSync(file, { force: true });
|
|
82
|
+
else
|
|
83
|
+
fs.writeFileSync(file, previous, { encoding: 'utf8', mode: 0o600 });
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
return error instanceof Error ? error.message : String(error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function macosAutostartInstalled() {
|
|
91
|
+
if (!isMacos())
|
|
92
|
+
return false;
|
|
93
|
+
const domain = guiDomain();
|
|
94
|
+
if (!domain)
|
|
95
|
+
return false;
|
|
96
|
+
const result = launchctl(['print', `${domain}/${MACOS_AUTOSTART_LABEL}`]);
|
|
97
|
+
return result.status === 0;
|
|
98
|
+
}
|
|
99
|
+
export function configureMacosAutostart(enabled, runtimeRoot) {
|
|
100
|
+
if (!isMacos()) {
|
|
101
|
+
return {
|
|
102
|
+
ok: !enabled,
|
|
103
|
+
enabled: false,
|
|
104
|
+
...(!enabled ? {} : { error: 'macOS 登录自启仅支持 macOS 主机' }),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const domain = guiDomain();
|
|
108
|
+
if (!domain) {
|
|
109
|
+
return { ok: false, enabled: false, error: '无法确定当前 macOS 用户 UID' };
|
|
110
|
+
}
|
|
111
|
+
const file = plistPath();
|
|
112
|
+
if (!enabled) {
|
|
113
|
+
const result = launchctl(['bootout', `${domain}/${MACOS_AUTOSTART_LABEL}`]);
|
|
114
|
+
if (result.status !== 0 && macosAutostartInstalled()) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
enabled: true,
|
|
118
|
+
error: commandError(result, '删除 macOS 登录自启失败'),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
fs.rmSync(file, { force: true });
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
// Keep the previous state coherent: a LaunchAgents plist that cannot be
|
|
126
|
+
// removed would otherwise silently re-enable the job at the next login.
|
|
127
|
+
const restored = launchctl(['bootstrap', domain, file]);
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
enabled: restored.status === 0 || macosAutostartInstalled(),
|
|
131
|
+
error: `删除 macOS 登录自启配置失败: ${error instanceof Error ? error.message : String(error)}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return { ok: true, enabled: false };
|
|
135
|
+
}
|
|
136
|
+
let previousPlist;
|
|
137
|
+
let previouslyInstalled = false;
|
|
138
|
+
try {
|
|
139
|
+
const absoluteRoot = path.resolve(runtimeRoot);
|
|
140
|
+
if (fs.existsSync(file)) {
|
|
141
|
+
previousPlist = fs.readFileSync(file, 'utf8');
|
|
142
|
+
previouslyInstalled = macosAutostartInstalled();
|
|
143
|
+
}
|
|
144
|
+
fs.mkdirSync(path.join(absoluteRoot, 'logs'), { recursive: true });
|
|
145
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
146
|
+
fs.writeFileSync(file, createPlist(absoluteRoot), { encoding: 'utf8', mode: 0o600 });
|
|
147
|
+
// Re-bootstrap so repeated `ec init --auto-start` updates the command and paths.
|
|
148
|
+
launchctl(['bootout', `${domain}/${MACOS_AUTOSTART_LABEL}`]);
|
|
149
|
+
const result = launchctl(['bootstrap', domain, file]);
|
|
150
|
+
if (result.status !== 0) {
|
|
151
|
+
const restoreError = restorePlist(file, previousPlist);
|
|
152
|
+
let restored = false;
|
|
153
|
+
if (!restoreError && previouslyInstalled && previousPlist !== undefined) {
|
|
154
|
+
const restoreResult = launchctl(['bootstrap', domain, file]);
|
|
155
|
+
restored = restoreResult.status === 0 || macosAutostartInstalled();
|
|
156
|
+
}
|
|
157
|
+
else if (!previouslyInstalled) {
|
|
158
|
+
// A failed bootstrap may still have partially loaded the job.
|
|
159
|
+
launchctl(['bootout', `${domain}/${MACOS_AUTOSTART_LABEL}`]);
|
|
160
|
+
}
|
|
161
|
+
const rollbackSuffix = restoreError ? `; 恢复原配置失败: ${restoreError}` : '';
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
enabled: restored,
|
|
165
|
+
error: `${commandError(result, '创建 macOS 登录自启失败')}${rollbackSuffix}`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return { ok: true, enabled: true };
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
const restoreError = restorePlist(file, previousPlist);
|
|
172
|
+
const rollbackSuffix = restoreError ? `; 恢复原配置失败: ${restoreError}` : '';
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
enabled: previouslyInstalled,
|
|
176
|
+
error: `${error instanceof Error ? error.message : String(error)}${rollbackSuffix}`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -115,12 +115,12 @@ function winPowerShellCreationDate(pid) {
|
|
|
115
115
|
const result = spawnSync('powershell', [
|
|
116
116
|
'-NoProfile',
|
|
117
117
|
'-Command',
|
|
118
|
-
`(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}').CreationDate`,
|
|
118
|
+
`(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}').CreationDate.ToUniversalTime().ToString('o')`,
|
|
119
119
|
], { encoding: 'utf-8', timeout: 8000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
120
120
|
const out = result.stdout?.trim();
|
|
121
121
|
if (!out)
|
|
122
122
|
return null;
|
|
123
|
-
return parseCimDate(out);
|
|
123
|
+
return parseCimDate(out) ?? parseDateString(out);
|
|
124
124
|
}
|
|
125
125
|
function winWmicCreationDate(pid) {
|
|
126
126
|
const result = spawnSync('wmic', ['process', 'where', `ProcessId=${pid}`, 'get', 'CreationDate', '/value'], { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
@@ -128,7 +128,11 @@ function winWmicCreationDate(pid) {
|
|
|
128
128
|
const m = out.match(/CreationDate=([^\r\n]+)/);
|
|
129
129
|
if (!m)
|
|
130
130
|
return null;
|
|
131
|
-
return parseCimDate(m[1].trim());
|
|
131
|
+
return parseCimDate(m[1].trim()) ?? parseDateString(m[1].trim());
|
|
132
|
+
}
|
|
133
|
+
function parseDateString(value) {
|
|
134
|
+
const parsed = Date.parse(value);
|
|
135
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
132
136
|
}
|
|
133
137
|
/**
|
|
134
138
|
* 解析 CIM/WMI 日期格式:yyyyMMddHHmmss.ffffff±TZZZ
|