evolcore 0.0.35 → 0.0.36
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 +31 -0
- package/README.md +6 -0
- package/bin/install-codex-managed-hooks.mjs +20 -3
- package/dist/agents/baseagent-availability.js +1 -1
- package/dist/agents/baseagent.js +88 -0
- package/dist/agents/claude-runner.js +59 -12
- package/dist/agents/codex/app-server-client.js +18 -1
- package/dist/agents/codex-runner.js +262 -21
- package/dist/agents/ecagent-runner.js +47 -11
- package/dist/agents/gemini-runner.js +36 -12
- package/dist/aun/aid/client.js +4 -3
- package/dist/aun/aid/control-aid.js +270 -8
- package/dist/aun/aid/identity.js +6 -3
- package/dist/aun/aid/ownership-operation.js +58 -0
- package/dist/aun/aid/store.js +1 -0
- package/dist/aun/group/fs-download.js +67 -2
- package/dist/aun/group/history.js +20 -5
- package/dist/aun/group/operations.js +8 -2
- package/dist/aun/menu/client.js +39 -2
- package/dist/aun/menu/transport.js +1 -1
- package/dist/aun/msg/history.js +43 -4
- package/dist/aun/msg/managed-operation.js +30 -53
- package/dist/aun/msg/p2p.js +51 -15
- package/dist/aun/outbox.js +4 -0
- package/dist/channels/aun.js +260 -73
- package/dist/channels/daemon.js +6 -4
- package/dist/channels/feishu.js +3 -0
- package/dist/cli/agent-command.js +161 -223
- package/dist/cli/agent-create.js +190 -223
- package/dist/cli/agent.js +3 -1
- package/dist/cli/aid-bind-command.js +53 -0
- package/dist/cli/aun-commands.js +384 -91
- package/dist/cli/bench.js +1 -1
- package/dist/cli/cli-argv.js +74 -0
- package/dist/cli/daemon-commands.js +105 -74
- package/dist/cli/diag.js +1 -1
- package/dist/cli/fs-command.js +18 -19
- package/dist/cli/getmeta-command.js +159 -6
- package/dist/cli/index.js +6 -12
- package/dist/cli/init-channel.js +8 -55
- package/dist/cli/init.js +125 -218
- package/dist/cli/queue-command.js +11 -8
- package/dist/cli/restart-monitor.js +134 -48
- package/dist/cli/session-command.js +1 -1
- package/dist/cli/stats.js +7 -5
- package/dist/cli/thread-command.js +103 -30
- package/dist/cli/trigger-command.js +1 -1
- package/dist/cli/watch-msg-diag.js +1 -0
- package/dist/config/access-policy.js +2 -2
- package/dist/config/boot-log.js +14 -11
- package/dist/config/config-field-policy.js +9 -1
- package/dist/config/config-manager.js +176 -90
- package/dist/config/config-operation-service.js +7 -14
- package/dist/config/contact-book-store.js +142 -26
- package/dist/config/contact-book.js +4 -2
- package/dist/config/lifecycle.js +1 -1
- package/dist/config/owner-config.js +96 -0
- package/dist/config/peer-role-resolver.js +12 -18
- package/dist/config/providers-config.js +382 -0
- package/dist/config/providers-import.js +74 -0
- package/dist/config/providers-management.js +328 -0
- package/dist/config/providers.js +22 -0
- package/dist/config/resolved-config-op.js +17 -14
- package/dist/config/snapshot.js +28 -16
- package/dist/config-store.js +18 -14
- package/dist/core/agent-ipc-registration.js +21 -63
- package/dist/core/agent-runtime.js +16 -5
- package/dist/core/auth/agent-delegation.js +199 -25
- package/dist/core/auth/auth-gateway.js +44 -2
- package/dist/core/auth/authorization-denial.js +1 -1
- package/dist/core/auth/debug-context.js +2 -1
- package/dist/core/auth/delegation-authorization.js +2 -2
- package/dist/core/auth/delegation-diagnostics.js +4 -1
- package/dist/core/auth/operation-authorizer.js +10 -1
- package/dist/core/auth/operation-catalog.js +19 -1
- package/dist/core/bootstrap-service.js +43 -20
- package/dist/core/channel-loader.js +0 -1
- package/dist/core/channel-runtime.js +30 -4
- package/dist/core/command/agent-control.js +196 -37
- package/dist/core/command/cli-intent-parser.js +60 -4
- package/dist/core/command/command-handler.js +212 -60
- package/dist/core/command/connect-menu.js +1 -0
- package/dist/core/command/diag-service.js +5 -4
- package/dist/core/command/file-search.js +368 -0
- package/dist/core/command/menu-catalog.js +2 -1
- package/dist/core/command/menu-handler.js +292 -107
- package/dist/core/command/menu-protocol.js +23 -0
- package/dist/core/command/role-menu.js +7 -19
- package/dist/core/command/skill-slash.js +130 -25
- package/dist/core/command/slash-handler.js +101 -53
- package/dist/core/command/topic-slash.js +16 -5
- package/dist/core/daemon-bootstrap.js +7 -1
- package/dist/core/daemon-control.js +19 -1
- package/dist/core/daemon-lifecycle.js +2 -1
- package/dist/core/event-catalog.js +2 -0
- package/dist/core/evolagent-registry.js +13 -2
- package/dist/core/evolagent.js +3 -6
- package/dist/core/handoff/ipc-registration.js +2 -2
- package/dist/core/handoff/runtime.js +40 -6
- package/dist/core/handoff/store.js +1 -0
- package/dist/core/inference/text-inference.js +95 -6
- package/dist/core/ipc-registration.js +43 -24
- package/dist/core/memory/consolidation-context.js +4 -1
- package/dist/core/memory/consolidation.js +25 -14
- package/dist/core/message/aun-ipc-sender.js +296 -88
- package/dist/core/message/aun-request-registry.js +226 -0
- package/dist/core/message/create-status.js +33 -4
- package/dist/core/message/message-bridge.js +62 -13
- package/dist/core/message/message-log.js +0 -1
- package/dist/core/message/message-queue.js +4 -1
- package/dist/core/message/message-utils.js +8 -1
- package/dist/core/message/peer-mode.js +17 -9
- package/dist/core/message/response-engine.js +266 -124
- package/dist/core/message/retry-scheduler.js +11 -8
- package/dist/core/model/catalog-service.js +60 -18
- package/dist/core/model/model-catalog.js +112 -9
- package/dist/core/model/model-fallback.js +94 -26
- package/dist/core/ownership/owner-resolver.js +687 -0
- package/dist/core/permission/approval-gateway.js +196 -62
- package/dist/core/permission/ec-command-parser.js +138 -25
- package/dist/core/permission/filesystem-approval-scope.js +141 -0
- package/dist/core/permission/protected-paths.js +14 -1
- package/dist/core/permission/sandbox-runtime.js +171 -18
- package/dist/core/permission/tool-policy.js +13 -2
- package/dist/core/role/runtime-policy.js +3 -4
- package/dist/core/session/session-idle-monitor.js +10 -8
- package/dist/core/session/session-manager.js +72 -45
- package/dist/core/session/session-mapper.js +18 -7
- package/dist/core/session/session-renew.js +5 -4
- package/dist/core/session/session-title.js +2 -1
- package/dist/core/session/session-turn-coordinator.js +4 -2
- package/dist/core/skill/index-generator.js +2 -16
- package/dist/core/task/task-recovery-service.js +15 -2
- package/dist/core/thread/command-parser.js +28 -13
- package/dist/core/thread/index.js +1 -0
- package/dist/core/thread/ipc-operation.js +11 -8
- package/dist/core/thread/thread-operation.js +79 -44
- package/dist/core/thread/thread-route.js +54 -6
- package/dist/core/thread/thread-service.js +236 -45
- package/dist/core/thread/topic-id.js +248 -0
- package/dist/eck/kit-renderer.js +1 -1
- package/dist/eck/message-renderer.js +12 -2
- package/dist/index.js +50 -27
- package/dist/ipc.js +59 -6
- package/dist/paths.js +1 -0
- package/dist/product.js +1 -0
- package/dist/response-system/engines/v1/proactive-flow.js +10 -3
- package/dist/stats/db.js +8 -0
- package/dist/stats/writer.js +26 -4
- package/dist/trigger/authorization.js +48 -3
- package/dist/trigger/feedback.js +28 -10
- package/dist/trigger/ipc-service.js +86 -10
- package/dist/trigger/local-files.js +55 -23
- package/dist/trigger/scheduler.js +57 -11
- package/dist/trigger/system-definitions.js +1 -0
- package/dist/trigger/validation.js +44 -15
- package/dist/utils/aid-bind.js +17 -297
- package/dist/utils/avatar-upload.js +16 -4
- package/dist/utils/cross-platform.js +109 -34
- package/dist/utils/ecweb-supervisor.js +1 -1
- package/dist/utils/ecweb-utils.js +8 -1
- package/dist/utils/error-utils.js +44 -6
- package/dist/utils/instance-registry.js +12 -2
- package/dist/utils/npm-ops.js +102 -27
- package/dist/utils/package-version-info.js +72 -27
- package/dist/utils/restart-safety.js +50 -0
- package/dist/utils/upgrade-jobs.js +304 -0
- package/dist/utils/windows-autostart.js +17 -10
- package/kits/docs/INDEX.md +5 -0
- package/kits/docs/aun/ABILITY_CALLING.md +239 -0
- package/kits/docs/evolcore/INDEX.md +14 -3
- package/kits/docs/evolcore/agent-create.md +55 -141
- package/kits/docs/evolcore/agent.md +38 -16
- package/kits/docs/evolcore/aid.md +34 -0
- package/kits/docs/evolcore/config.md +20 -1
- package/kits/docs/evolcore/event.md +2 -2
- package/kits/docs/evolcore/fs.md +18 -2
- package/kits/docs/evolcore/group-collaboration.md +13 -246
- package/kits/docs/evolcore/group.md +2 -1
- package/kits/docs/evolcore/memory-consolidation-batches.md +1 -1
- package/kits/docs/evolcore/menu-protocol.md +65 -1
- package/kits/docs/evolcore/msg.md +64 -2
- package/kits/docs/evolcore/skill.md +86 -0
- package/kits/docs/evolcore/stats.md +6 -0
- package/kits/docs/evolcore/task.md +2 -0
- package/kits/docs/evolcore/thread-chain.md +145 -51
- package/kits/docs/evolcore/trigger.md +48 -3
- package/kits/eck_manifest.json +21 -0
- package/kits/eck_message_manifest.json +4 -4
- package/kits/migrations/migrate-role-config-v5.mjs +20 -7
- package/kits/rules/01-overview.md +2 -1
- package/kits/rules/03-identity.md +3 -1
- package/kits/rules/04-relation.md +1 -1
- package/kits/rules/07-memory-and-thread.md +1 -1
- package/kits/schemas/_meta.json +16 -1
- package/kits/schemas/agent-config.schema.13.json +44 -2
- package/kits/schemas/contact-book.schema.3.json +5 -0
- package/kits/schemas/daemon.schema.6.json +3 -1
- package/kits/schemas/daemon.schema.8.json +329 -0
- package/kits/schemas/defaults.schema.5.json +39 -3
- package/kits/schemas/providers.schema.1.json +76 -0
- package/kits/schemas/relation-config.schema.9.json +5 -1
- package/kits/skills/.switches.json +28 -0
- package/kits/skills/aid-ability/SKILL.md +214 -0
- package/kits/skills/ec-agent-create/SKILL.md +162 -0
- package/kits/skills/ec-group/SKILL.md +2 -1
- package/kits/skills/ec-group-collaboration/SKILL.md +228 -0
- package/kits/skills/ec-identity/SKILL.md +143 -0
- package/kits/skills/ec-skill/scripts/cli.mjs +36 -3
- package/kits/skills/ec-skill/scripts/skill.mjs +202 -15
- package/kits/skills/ec-trigger/SKILL.md +167 -0
- package/kits/skills/ec-trigger/references/examples.md +153 -0
- package/kits/skills/thread-ops/SKILL.md +108 -420
- package/kits/templates/message-fragments/thread-context.md +1 -2
- package/kits/templates/message-fragments/trigger-thread-check.md +1 -1
- package/kits/templates/roles/member.json +1 -1
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +1 -0
- package/kits/templates/system-fragments/channel.md +2 -1
- package/kits/templates/system-fragments/commands.md +5 -4
- package/kits/templates/system-fragments/identity.md +2 -0
- package/kits/templates/system-fragments/session.md +1 -1
- package/kits/templates/system-fragments/venue.md +1 -1
- package/package.json +4 -3
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { getPackageRoot, resolvePaths } from '../paths.js';
|
|
4
4
|
import { logger } from './logger.js';
|
|
5
|
-
import { isSandboxInitializationFailure } from '../core/permission/sandbox-runtime.js';
|
|
5
|
+
import { CODEX_SANDBOX_USER_NAMESPACE_DISABLED_BY_APPARMOR, CODEX_SANDBOX_USER_NAMESPACE_UNAVAILABLE, isClaudeSandboxDependencyFailure, isCodexSandboxUserNamespaceFailure, isSandboxInitializationFailure, } from '../core/permission/sandbox-runtime.js';
|
|
6
6
|
/** A deadline imposed by an outer executor, handled by the ordinary task timeout path. */
|
|
7
7
|
export class ExecutionTimeoutError extends Error {
|
|
8
8
|
reason;
|
|
@@ -87,6 +87,8 @@ export var ErrorType;
|
|
|
87
87
|
ErrorType["MODEL_UNAVAILABLE"] = "model_unavailable";
|
|
88
88
|
ErrorType["SESSION_UNAVAILABLE"] = "session_unavailable";
|
|
89
89
|
ErrorType["SANDBOX_INITIALIZATION_FAILED"] = "sandbox_initialization_failed";
|
|
90
|
+
ErrorType["SANDBOX_DEPENDENCIES_MISSING"] = "sandbox_dependencies_missing";
|
|
91
|
+
ErrorType["CODEX_SANDBOX_USER_NAMESPACE_UNAVAILABLE"] = "codex_sandbox_userns_unavailable";
|
|
90
92
|
ErrorType["CLAUDE_STARTUP_ARGUMENTS_TOO_LONG"] = "claude_startup_arguments_too_long";
|
|
91
93
|
ErrorType["CLAUDE_CODE_STARTUP_FAILED"] = "claude_code_startup_failed";
|
|
92
94
|
ErrorType["UNKNOWN"] = "unknown";
|
|
@@ -440,6 +442,16 @@ function hasRetryableHttpStatus(text) {
|
|
|
440
442
|
export function classifyError(error) {
|
|
441
443
|
if (error?.code === 'RUNNER_INTERRUPT_UNCONFIRMED')
|
|
442
444
|
return ErrorType.SESSION_UNAVAILABLE;
|
|
445
|
+
if (isClaudeSandboxDependencyFailure(error))
|
|
446
|
+
return ErrorType.SANDBOX_DEPENDENCIES_MISSING;
|
|
447
|
+
if (error?.code === CODEX_SANDBOX_USER_NAMESPACE_DISABLED_BY_APPARMOR
|
|
448
|
+
|| error?.code === CODEX_SANDBOX_USER_NAMESPACE_UNAVAILABLE
|
|
449
|
+
|| (isCodexSandboxUserNamespaceFailure(error)
|
|
450
|
+
&& /\b(?:bwrap|bubblewrap)\b/i.test(String(error?.message ?? error)))) {
|
|
451
|
+
return ErrorType.CODEX_SANDBOX_USER_NAMESPACE_UNAVAILABLE;
|
|
452
|
+
}
|
|
453
|
+
if (isSandboxInitializationFailure(error))
|
|
454
|
+
return ErrorType.SANDBOX_INITIALIZATION_FAILED;
|
|
443
455
|
const msg = (error?.message || '').toLowerCase();
|
|
444
456
|
// A damaged ecagent JSONL transcript is deterministic local state. Retrying
|
|
445
457
|
// the provider request cannot repair it and can append more entries to the
|
|
@@ -481,9 +493,6 @@ export function classifyError(error) {
|
|
|
481
493
|
|| /api error: 404\b/.test(msg)) {
|
|
482
494
|
return ErrorType.MODEL_UNAVAILABLE;
|
|
483
495
|
}
|
|
484
|
-
if (isSandboxInitializationFailure(error)) {
|
|
485
|
-
return ErrorType.SANDBOX_INITIALIZATION_FAILED;
|
|
486
|
-
}
|
|
487
496
|
if (msg.includes('401') || msg.includes('authentication_error') || msg.includes('unauthorized')) {
|
|
488
497
|
return ErrorType.AUTH_ERROR;
|
|
489
498
|
}
|
|
@@ -509,6 +518,8 @@ export function classifyError(error) {
|
|
|
509
518
|
export function isRetryableError(error) {
|
|
510
519
|
if (error?.code === 'RUNNER_INTERRUPT_UNCONFIRMED')
|
|
511
520
|
return false;
|
|
521
|
+
if (isClaudeSandboxDependencyFailure(error) || isSandboxInitializationFailure(error))
|
|
522
|
+
return false;
|
|
512
523
|
// Replaying the same request does not increase its output budget.
|
|
513
524
|
if (isOutputTokenLimitError(error))
|
|
514
525
|
return false;
|
|
@@ -530,6 +541,28 @@ export function isRetryableError(error) {
|
|
|
530
541
|
return true;
|
|
531
542
|
return false;
|
|
532
543
|
}
|
|
544
|
+
function sandboxInitializationErrorMessage(error, prefix) {
|
|
545
|
+
const stderr = typeof error?.stderr === 'string'
|
|
546
|
+
? [error.stderr]
|
|
547
|
+
: Array.isArray(error?.stderr) ? error.stderr.filter((line) => typeof line === 'string') : [];
|
|
548
|
+
const diagnostic = [error?.message ?? String(error ?? ''), ...stderr].join('\n')
|
|
549
|
+
.match(/bwrap:\s*[^\r\n]+: read-only file system/i)?.[0];
|
|
550
|
+
if (error?.code === CODEX_SANDBOX_USER_NAMESPACE_DISABLED_BY_APPARMOR
|
|
551
|
+
|| /codex_sandbox_userns_disabled_by_apparmor/i.test(String(error?.message ?? ''))
|
|
552
|
+
|| (isCodexSandboxUserNamespaceFailure({ message: error?.message, stderr })
|
|
553
|
+
&& /apparmor_restrict_unprivileged_userns\s*=\s*1|apparmor/i.test([error?.message ?? '', ...stderr].join('\n')))) {
|
|
554
|
+
return `${prefix}Codex Linux 沙箱无法创建 user namespace(${CODEX_SANDBOX_USER_NAMESPACE_DISABLED_BY_APPARMOR}),当前操作未执行;请让宿主为该 EvolCore/Codex 服务配置受限的 AppArmor 允许规则,或使用等价隔离后端。不要关闭沙箱或自动降级为全权限,并运行 ec diagnose 获取探针结果`;
|
|
555
|
+
}
|
|
556
|
+
if (error?.code === CODEX_SANDBOX_USER_NAMESPACE_UNAVAILABLE
|
|
557
|
+
|| isCodexSandboxUserNamespaceFailure({ message: error?.message, stderr })) {
|
|
558
|
+
return `${prefix}Codex Linux 沙箱无法创建 user namespace(${CODEX_SANDBOX_USER_NAMESPACE_UNAVAILABLE}),当前操作未执行;请检查内核/AppArmor/容器策略并运行 ec diagnose。不会关闭沙箱或自动降级为全权限`;
|
|
559
|
+
}
|
|
560
|
+
if (diagnostic) {
|
|
561
|
+
const target = /mkdir parents for/i.test(diagnostic) ? '挂载目标的父目录' : '挂载目标';
|
|
562
|
+
return `${prefix}执行沙箱文件系统挂载失败(sandbox_initialization_failed),当前操作未执行;只读文件系统阻止创建${target},请检查文件系统挂载规则`;
|
|
563
|
+
}
|
|
564
|
+
return `${prefix}执行沙箱启动失败(sandbox_initialization_failed),当前操作未执行;请查看原始诊断并运行 ec diagnose 排查`;
|
|
565
|
+
}
|
|
533
566
|
export function getErrorMessage(error, terminalReason, includeEmoji = true) {
|
|
534
567
|
if (error?.code === 'RUNNER_INTERRUPT_UNCONFIRMED')
|
|
535
568
|
return `${includeEmoji ? '⚠️ ' : ''}${error.message}`;
|
|
@@ -566,7 +599,9 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
|
|
|
566
599
|
case 'permission_denied':
|
|
567
600
|
return `${prefix}权限被拒绝,操作已取消`;
|
|
568
601
|
case 'sandbox_initialization_failed':
|
|
569
|
-
return
|
|
602
|
+
return sandboxInitializationErrorMessage(error, warnPrefix);
|
|
603
|
+
case 'sandbox_dependencies_missing':
|
|
604
|
+
return `${warnPrefix}Claude 执行沙箱缺少系统依赖(sandbox_dependencies_missing),当前操作未执行;请安装 bubblewrap 和 socat 后运行 ec diagnose。不会自动关闭沙箱或安装系统包`;
|
|
570
605
|
case 'claude_startup_arguments_too_long':
|
|
571
606
|
return `${warnPrefix}当前 Claude 启动参数过长,任务未执行`;
|
|
572
607
|
case 'claude_code_startup_failed':
|
|
@@ -580,6 +615,9 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
|
|
|
580
615
|
const msg = resolveErrorMessage(error);
|
|
581
616
|
const warnPrefix = includeEmoji ? '⚠️ ' : '';
|
|
582
617
|
const errPrefix = includeEmoji ? '❌ ' : '';
|
|
618
|
+
if (isClaudeSandboxDependencyFailure(error)) {
|
|
619
|
+
return `${warnPrefix}Claude 执行沙箱缺少系统依赖(sandbox_dependencies_missing),当前操作未执行;请安装 bubblewrap 和 socat 后运行 ec diagnose。不会自动关闭沙箱或安装系统包`;
|
|
620
|
+
}
|
|
583
621
|
if (/invalid (?:jsonl )?session file/i.test(msg)) {
|
|
584
622
|
return `${errPrefix}ecagent 会话文件损坏,已停止自动重试;请使用 /repair 或 /new 恢复会话`;
|
|
585
623
|
}
|
|
@@ -605,7 +643,7 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
|
|
|
605
643
|
: `${warnPrefix}上下文过长,自动压缩失败,请手动输入 /compact 重试`;
|
|
606
644
|
}
|
|
607
645
|
if (isSandboxInitializationFailure(error)) {
|
|
608
|
-
return
|
|
646
|
+
return sandboxInitializationErrorMessage(error, warnPrefix);
|
|
609
647
|
}
|
|
610
648
|
if (msg.includes('401') || msg.includes('authentication_error') || msg.toLowerCase().includes('unauthorized')) {
|
|
611
649
|
return `${errPrefix}API Key 无效,请检查密钥配置。使用 /status 查看当前配置`;
|
|
@@ -16,6 +16,7 @@ import { isProcessRunning, killProcess, isWindows, findProcessesWithCommandLine
|
|
|
16
16
|
import { getProcessStartTime, startTimeMatches } from './process-introspect.js';
|
|
17
17
|
import { isConfirmedLeakedTestDaemon, runtimeHomeFromEnv } from './restart-safety.js';
|
|
18
18
|
import { decodeWindowsOutput } from './windows-output.js';
|
|
19
|
+
import { atomicWriteJson } from './atomic-write.js';
|
|
19
20
|
// ── Helpers ──
|
|
20
21
|
function instanceDir() {
|
|
21
22
|
return resolvePaths().instanceDir;
|
|
@@ -58,7 +59,7 @@ export function writeMain(launchedBy) {
|
|
|
58
59
|
runMode: isDevMode() ? 'dev' : 'install',
|
|
59
60
|
};
|
|
60
61
|
const filePath = path.join(dir, mainFileName(process.pid));
|
|
61
|
-
|
|
62
|
+
atomicWriteJson(filePath, record);
|
|
62
63
|
return filePath;
|
|
63
64
|
}
|
|
64
65
|
export function removeMain(pid) {
|
|
@@ -99,7 +100,7 @@ export function isMainWinner() {
|
|
|
99
100
|
function restartMonitorFileName(pid) {
|
|
100
101
|
return `restart-monitor-${pid}.json`;
|
|
101
102
|
}
|
|
102
|
-
export function writeRestartMonitor() {
|
|
103
|
+
export function writeRestartMonitor(requestId, phase) {
|
|
103
104
|
const dir = instanceDir();
|
|
104
105
|
fs.mkdirSync(dir, { recursive: true });
|
|
105
106
|
const startedAt = getProcessStartTime(process.pid) ?? Date.now();
|
|
@@ -108,11 +109,20 @@ export function writeRestartMonitor() {
|
|
|
108
109
|
startedAt,
|
|
109
110
|
startedAtIso: new Date(startedAt).toISOString(),
|
|
110
111
|
launchedBy: 'restart-monitor',
|
|
112
|
+
...(requestId ? { requestId } : {}),
|
|
113
|
+
...(phase ? { phase } : {}),
|
|
111
114
|
};
|
|
112
115
|
const filePath = path.join(dir, restartMonitorFileName(process.pid));
|
|
113
116
|
writeAtomic(filePath, JSON.stringify(record, null, 2));
|
|
114
117
|
return filePath;
|
|
115
118
|
}
|
|
119
|
+
export function updateRestartMonitorPhase(pid, phase) {
|
|
120
|
+
const filePath = path.join(instanceDir(), restartMonitorFileName(pid));
|
|
121
|
+
const record = safeParseJson(filePath);
|
|
122
|
+
if (!record)
|
|
123
|
+
return;
|
|
124
|
+
atomicWriteJson(filePath, { ...record, phase });
|
|
125
|
+
}
|
|
116
126
|
export function removeRestartMonitor(pid) {
|
|
117
127
|
const target = pid ?? process.pid;
|
|
118
128
|
const filePath = path.join(instanceDir(), restartMonitorFileName(target));
|
package/dist/utils/npm-ops.js
CHANGED
|
@@ -64,6 +64,33 @@ function resolveNpmInvocation() {
|
|
|
64
64
|
}
|
|
65
65
|
return { command: fs.existsSync(sibling) ? sibling : 'npm', argsPrefix: [] };
|
|
66
66
|
}
|
|
67
|
+
function npmConfigArgs(config = {}) {
|
|
68
|
+
return [
|
|
69
|
+
...(config.cacheDir ? ['--cache', config.cacheDir] : []),
|
|
70
|
+
...(config.userConfig ? ['--userconfig', config.userConfig] : []),
|
|
71
|
+
...(config.globalConfig ? ['--globalconfig', config.globalConfig] : []),
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
function normalizeNpmPath(value) {
|
|
75
|
+
if (!value || /^(?:https?|file):/i.test(value) || ['false', 'undefined', 'null'].includes(value))
|
|
76
|
+
return undefined;
|
|
77
|
+
return path.resolve(value);
|
|
78
|
+
}
|
|
79
|
+
async function readNpmConfigValue(key, signal) {
|
|
80
|
+
const npm = resolveNpmInvocation();
|
|
81
|
+
try {
|
|
82
|
+
const result = await execFileAsync(npm.command, [...npm.argsPrefix, 'config', 'get', key === 'cacheDir' ? 'cache' : key === 'userConfig' ? 'userconfig' : 'globalconfig'], {
|
|
83
|
+
timeout: 5000,
|
|
84
|
+
windowsHide: isWindows,
|
|
85
|
+
signal,
|
|
86
|
+
});
|
|
87
|
+
const value = String(result.stdout ?? '').trim();
|
|
88
|
+
return normalizeNpmPath(value);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
67
94
|
/**
|
|
68
95
|
* Read the effective registry from the npm belonging to the current Node.
|
|
69
96
|
* An empty value means npm has no explicit registry and uses the public
|
|
@@ -72,6 +99,8 @@ function resolveNpmInvocation() {
|
|
|
72
99
|
*/
|
|
73
100
|
export async function resolveUpgradeRegistry(signal) {
|
|
74
101
|
const npm = resolveNpmInvocation();
|
|
102
|
+
let registry = DEFAULT_NPM_REGISTRY;
|
|
103
|
+
let useNpmCli = false;
|
|
75
104
|
try {
|
|
76
105
|
const result = await execFileAsync(npm.command, [...npm.argsPrefix, 'config', 'get', 'registry'], {
|
|
77
106
|
timeout: 5000,
|
|
@@ -79,22 +108,27 @@ export async function resolveUpgradeRegistry(signal) {
|
|
|
79
108
|
signal,
|
|
80
109
|
});
|
|
81
110
|
const value = String(result.stdout ?? '').trim();
|
|
82
|
-
if (
|
|
83
|
-
|
|
111
|
+
if (value && value !== 'undefined' && value !== 'null') {
|
|
112
|
+
registry = normalizeRegistry(value);
|
|
113
|
+
useNpmCli = true;
|
|
84
114
|
}
|
|
85
|
-
return { registry: normalizeRegistry(value), useNpmCli: true };
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return { registry: DEFAULT_NPM_REGISTRY, useNpmCli: false };
|
|
89
115
|
}
|
|
116
|
+
catch { }
|
|
117
|
+
const [cacheDir, userConfig, globalConfig] = await Promise.all([
|
|
118
|
+
readNpmConfigValue('cacheDir', signal),
|
|
119
|
+
readNpmConfigValue('userConfig', signal),
|
|
120
|
+
readNpmConfigValue('globalConfig', signal),
|
|
121
|
+
]);
|
|
122
|
+
const npmConfig = { ...(cacheDir ? { cacheDir } : {}), ...(userConfig ? { userConfig } : {}), ...(globalConfig ? { globalConfig } : {}) };
|
|
123
|
+
return { registry, useNpmCli, npmConfig };
|
|
90
124
|
}
|
|
91
|
-
async function checkLatestVersionViaNpm(pkg, timeoutMs, signal, registry) {
|
|
125
|
+
async function checkLatestVersionViaNpm(pkg, timeoutMs, signal, registry, distTag = 'latest') {
|
|
92
126
|
const npm = resolveNpmInvocation();
|
|
93
127
|
try {
|
|
94
128
|
const result = await execFileAsync(npm.command, [
|
|
95
129
|
...npm.argsPrefix,
|
|
96
130
|
'view',
|
|
97
|
-
`${pkg}
|
|
131
|
+
`${pkg}@${distTag}`,
|
|
98
132
|
'version',
|
|
99
133
|
'--json',
|
|
100
134
|
'--registry',
|
|
@@ -160,7 +194,7 @@ async function canSudoNonInteractively() {
|
|
|
160
194
|
export async function npmInstallGlobal(pkg, options = {}) {
|
|
161
195
|
const npm = resolveNpmInvocation();
|
|
162
196
|
const registry = options.registry ? normalizeRegistry(options.registry) : undefined;
|
|
163
|
-
const npmArgs = ['install', '-g', pkg, ...(registry ? ['--registry', registry] : [])];
|
|
197
|
+
const npmArgs = ['install', '-g', pkg, ...(registry ? ['--registry', registry] : []), ...npmConfigArgs(options.npmConfig), ...(options.preferOffline ? ['--prefer-offline'] : [])];
|
|
164
198
|
try {
|
|
165
199
|
await execFileAsync(npm.command, [...npm.argsPrefix, ...npmArgs], {
|
|
166
200
|
timeout: options.timeoutMs ?? 180000,
|
|
@@ -179,7 +213,8 @@ export async function npmInstallGlobal(pkg, options = {}) {
|
|
|
179
213
|
const registryArg = registry ? ` --registry ${registry}` : '';
|
|
180
214
|
throw new Error(`权限不足,且 sudo 需要密码(当前进程无法交互输入)。请使用当前 Node 对应的 npm 手动执行: sudo npm install -g ${pkg}${registryArg}`);
|
|
181
215
|
}
|
|
182
|
-
|
|
216
|
+
const preserveEnv = ['NPM_TOKEN', 'NODE_AUTH_TOKEN'].filter(key => process.env[key]);
|
|
217
|
+
await execFileAsync('sudo', ['-n', ...(preserveEnv.length ? [`--preserve-env=${preserveEnv.join(',')}`] : []), npm.command, ...npm.argsPrefix, ...npmArgs], {
|
|
183
218
|
timeout: options.timeoutMs ?? 180000,
|
|
184
219
|
windowsHide: false,
|
|
185
220
|
signal: options.signal,
|
|
@@ -295,7 +330,7 @@ export function getLocalVersion() {
|
|
|
295
330
|
}
|
|
296
331
|
/**
|
|
297
332
|
* 解析全局安装包的已装版本(通过 `npm root -g` 定位全局 node_modules)。
|
|
298
|
-
*
|
|
333
|
+
* EC Web 从 EvolCore 固定依赖解析;开发链接时使用同仓 ecweb 子项目。
|
|
299
334
|
* 未安装或查询失败返回 null。
|
|
300
335
|
*/
|
|
301
336
|
export function resolveGlobalPkg(pkgName) {
|
|
@@ -304,8 +339,15 @@ export function resolveGlobalPkg(pkgName) {
|
|
|
304
339
|
const globalRoot = execFileSync(npm.command, [...npm.argsPrefix, 'root', '-g'], {
|
|
305
340
|
encoding: 'utf-8', timeout: 10000, windowsHide: true,
|
|
306
341
|
}).trim();
|
|
307
|
-
const
|
|
308
|
-
|
|
342
|
+
const packagePaths = pkgName === WEB_PACKAGE_NAME
|
|
343
|
+
? [
|
|
344
|
+
...(isLinkedInstall() ? [path.join(getPackageRoot(), 'ecweb', 'package.json')] : []),
|
|
345
|
+
path.join(globalRoot, MAIN_PACKAGE_NAME, 'node_modules', ...pkgName.split('/'), 'package.json'),
|
|
346
|
+
]
|
|
347
|
+
: [path.join(globalRoot, ...pkgName.split('/'), 'package.json')];
|
|
348
|
+
for (const pkgPath of packagePaths) {
|
|
349
|
+
if (!fs.existsSync(pkgPath))
|
|
350
|
+
continue;
|
|
309
351
|
const data = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
310
352
|
return { version: data.version, path: pkgPath };
|
|
311
353
|
}
|
|
@@ -318,7 +360,7 @@ export function resolveGlobalPkg(pkgName) {
|
|
|
318
360
|
* 使用 HTTP fetch 直接查 registry API,不依赖 npm CLI。
|
|
319
361
|
* 超时 10 秒,失败返回 null。
|
|
320
362
|
*/
|
|
321
|
-
async function fetchLatestVersion(pkg = 'evolcore', timeoutMs = 10000, signal, registry = DEFAULT_NPM_REGISTRY) {
|
|
363
|
+
async function fetchLatestVersion(pkg = 'evolcore', timeoutMs = 10000, signal, registry = DEFAULT_NPM_REGISTRY, distTag = 'latest') {
|
|
322
364
|
const controller = new AbortController();
|
|
323
365
|
const abort = () => controller.abort();
|
|
324
366
|
if (signal?.aborted)
|
|
@@ -327,7 +369,7 @@ async function fetchLatestVersion(pkg = 'evolcore', timeoutMs = 10000, signal, r
|
|
|
327
369
|
signal?.addEventListener('abort', abort, { once: true });
|
|
328
370
|
const timer = setTimeout(abort, timeoutMs);
|
|
329
371
|
try {
|
|
330
|
-
const res = await fetch(`${normalizeRegistry(registry)}/${encodeURIComponent(pkg)}
|
|
372
|
+
const res = await fetch(`${normalizeRegistry(registry)}/${encodeURIComponent(pkg)}/${distTag}`, {
|
|
331
373
|
signal: controller.signal,
|
|
332
374
|
headers: { 'Accept': 'application/json' },
|
|
333
375
|
});
|
|
@@ -344,18 +386,49 @@ async function fetchLatestVersion(pkg = 'evolcore', timeoutMs = 10000, signal, r
|
|
|
344
386
|
signal?.removeEventListener('abort', abort);
|
|
345
387
|
}
|
|
346
388
|
}
|
|
389
|
+
export async function preparePackageCache(target, registry, options = {}) {
|
|
390
|
+
if (!isPackageVersion(target) || normalizeVersionRegistry(registry) !== registry)
|
|
391
|
+
throw new Error('Invalid cache target');
|
|
392
|
+
const npm = resolveNpmInvocation();
|
|
393
|
+
const spec = `${MAIN_PACKAGE_NAME}@${target}`;
|
|
394
|
+
const run = (args) => new Promise((resolve, reject) => {
|
|
395
|
+
let failure = null;
|
|
396
|
+
let output = '';
|
|
397
|
+
const child = execFile(npm.command, [...npm.argsPrefix, ...args, '--registry', registry, ...npmConfigArgs(options.npmConfig), '--ignore-scripts'], {
|
|
398
|
+
timeout: 180_000, windowsHide: isWindows, signal: options.signal, maxBuffer: 8 * 1024 * 1024,
|
|
399
|
+
}, (error, stdout) => { failure = error; output = String(stdout ?? ''); });
|
|
400
|
+
child.once('error', error => { failure = error; });
|
|
401
|
+
child.once('close', code => {
|
|
402
|
+
if (failure || code !== 0 || options.signal?.aborted)
|
|
403
|
+
reject(new Error(options.signal?.aborted
|
|
404
|
+
? 'Package cache preparation timed out or was cancelled' : 'npm cache preparation failed; check registry connectivity, credentials, disk space and permissions'));
|
|
405
|
+
else
|
|
406
|
+
resolve(output);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
options.onPhase?.('downloading');
|
|
410
|
+
await run(['cache', 'add', spec, '--prefer-offline']);
|
|
411
|
+
options.onPhase?.('tarball-check');
|
|
412
|
+
const metadata = JSON.parse(await run(['view', spec, 'name', 'version', 'dist', '--json', '--offline']));
|
|
413
|
+
const [packed] = JSON.parse(await run(['pack', spec, '--dry-run', '--json', '--offline']));
|
|
414
|
+
if (metadata?.name !== MAIN_PACKAGE_NAME || metadata.version !== target || packed?.name !== MAIN_PACKAGE_NAME || packed.version !== target
|
|
415
|
+
|| !(typeof metadata.dist?.integrity === 'string' ? packed.integrity === metadata.dist.integrity
|
|
416
|
+
: typeof metadata.dist?.shasum === 'string' && packed.shasum === metadata.dist.shasum)) {
|
|
417
|
+
throw new Error('Cached package identity or integrity does not match the selected EvolCore version');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
347
420
|
/** Query an already selected source, retaining npm proxy/CA/auth behavior. */
|
|
348
|
-
export async function queryPackageVersion(pkg, selection, signal) {
|
|
421
|
+
export async function queryPackageVersion(pkg, selection, signal, distTag = 'latest') {
|
|
349
422
|
const registry = normalizeVersionRegistry(selection.registry);
|
|
350
|
-
if (!registry)
|
|
423
|
+
if (!registry || !['latest', 'test'].includes(distTag))
|
|
351
424
|
return null;
|
|
352
425
|
const version = selection.useNpmCli
|
|
353
|
-
? await checkLatestVersionViaNpm(pkg, 10000, signal, registry)
|
|
354
|
-
: await fetchLatestVersion(pkg, 10000, signal, registry);
|
|
355
|
-
return isPackageVersion(version) ? { version, registry } : null;
|
|
426
|
+
? await checkLatestVersionViaNpm(pkg, 10000, signal, registry, distTag)
|
|
427
|
+
: await fetchLatestVersion(pkg, 10000, signal, registry, distTag);
|
|
428
|
+
return isPackageVersion(version) ? { version, registry, npmConfig: selection.npmConfig } : null;
|
|
356
429
|
}
|
|
357
430
|
/** No registry lookup: callers must validate cache freshness before each install. */
|
|
358
|
-
export async function installExactPackageVersion(pkg, target, registry, resolveInstalled) {
|
|
431
|
+
export async function installExactPackageVersion(pkg, target, registry, resolveInstalled, npmConfig) {
|
|
359
432
|
if (![MAIN_PACKAGE_NAME, WEB_PACKAGE_NAME].includes(pkg)
|
|
360
433
|
|| !isPackageVersion(target) || normalizeVersionRegistry(registry) !== registry) {
|
|
361
434
|
return { status: 'failed', error: 'Invalid cached install target' };
|
|
@@ -374,7 +447,7 @@ export async function installExactPackageVersion(pkg, target, registry, resolveI
|
|
|
374
447
|
}
|
|
375
448
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
376
449
|
try {
|
|
377
|
-
await npmInstallGlobal(`${pkg}@${target}`, { registry });
|
|
450
|
+
await npmInstallGlobal(`${pkg}@${target}`, { registry, preferOffline: true, npmConfig });
|
|
378
451
|
const actual = resolveInstalled()?.version;
|
|
379
452
|
if (!actual || !isPackageVersion(actual) || comparePackageVersions(actual, target) < 0) {
|
|
380
453
|
throw new Error('Installed version did not reach the cached target');
|
|
@@ -407,7 +480,7 @@ async function checkLatestVersionWithFallback(pkg, timeoutMs, signal, explicitRe
|
|
|
407
480
|
const version = selection.useNpmCli
|
|
408
481
|
? await checkLatestVersionViaNpm(pkg, timeoutMs, signal, selection.registry)
|
|
409
482
|
: await fetchLatestVersion(pkg, timeoutMs, signal, selection.registry);
|
|
410
|
-
return version ? { version, registry: selection.registry } : null;
|
|
483
|
+
return version ? { version, registry: selection.registry, npmConfig: selection.npmConfig } : null;
|
|
411
484
|
}
|
|
412
485
|
/**
|
|
413
486
|
* Query the registry selected from the running Node/npm configuration, or the
|
|
@@ -434,7 +507,7 @@ export async function tryUpgrade(options = {}) {
|
|
|
434
507
|
}
|
|
435
508
|
const { version: remoteVer, registry } = checked;
|
|
436
509
|
// 版本比较
|
|
437
|
-
if (
|
|
510
|
+
if (comparePackageVersions(localVer, remoteVer) >= 0) {
|
|
438
511
|
return { status: 'no-update', from: localVer };
|
|
439
512
|
}
|
|
440
513
|
// 有新版本,执行升级(失败重试一次)
|
|
@@ -447,10 +520,11 @@ export async function tryUpgrade(options = {}) {
|
|
|
447
520
|
timeoutMs: options.installTimeoutMs,
|
|
448
521
|
signal: options.signal,
|
|
449
522
|
registry,
|
|
523
|
+
npmConfig: checked.npmConfig,
|
|
450
524
|
});
|
|
451
525
|
installedVer = getLocalVersion();
|
|
452
526
|
});
|
|
453
|
-
if (!installedVer ||
|
|
527
|
+
if (!installedVer || comparePackageVersions(installedVer, remoteVer) < 0) {
|
|
454
528
|
throw new Error(`npm install completed but evolcore is still ${installedVer ?? 'not installed'} (target ${remoteVer})`);
|
|
455
529
|
}
|
|
456
530
|
return { status: 'upgraded', from: localVer, to: installedVer };
|
|
@@ -484,7 +558,7 @@ export async function tryUpgradeGlobalPkg(resolveInstalled, pkgName, options = {
|
|
|
484
558
|
: { status: 'skipped', error: 'Failed to check remote version' };
|
|
485
559
|
}
|
|
486
560
|
const { version: remoteVer, registry } = checked;
|
|
487
|
-
if (
|
|
561
|
+
if (comparePackageVersions(localVer, remoteVer) >= 0) {
|
|
488
562
|
return { status: 'no-update', from: localVer };
|
|
489
563
|
}
|
|
490
564
|
let lastError;
|
|
@@ -496,10 +570,11 @@ export async function tryUpgradeGlobalPkg(resolveInstalled, pkgName, options = {
|
|
|
496
570
|
timeoutMs: options.installTimeoutMs,
|
|
497
571
|
signal: options.signal,
|
|
498
572
|
registry,
|
|
573
|
+
npmConfig: checked.npmConfig,
|
|
499
574
|
});
|
|
500
575
|
installedVer = resolveInstalled()?.version;
|
|
501
576
|
});
|
|
502
|
-
if (!installedVer ||
|
|
577
|
+
if (!installedVer || comparePackageVersions(installedVer, remoteVer) < 0) {
|
|
503
578
|
throw new Error(`npm install completed but ${pkgName} is still ${installedVer ?? 'not installed'} (target ${remoteVer})`);
|
|
504
579
|
}
|
|
505
580
|
return { status: 'upgraded', from: localVer, to: installedVer };
|
|
@@ -2,13 +2,20 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { getPackageRoot, resolvePaths } from '../paths.js';
|
|
4
4
|
import { MAIN_PACKAGE_NAME, WEB_PACKAGE_NAME, TARGET_PACKAGE_ROOT_ENV } from '../product.js';
|
|
5
|
-
import { getLocalVersion, isLinkedInstall, resolveGlobalPkg, resolveUpgradeRegistry, queryPackageVersion, installExactPackageVersion, } from './npm-ops.js';
|
|
5
|
+
import { DEFAULT_NPM_REGISTRY, getLocalVersion, isLinkedInstall, resolveGlobalPkg, resolveUpgradeRegistry, queryPackageVersion, installExactPackageVersion, } from './npm-ops.js';
|
|
6
6
|
import { comparePackageVersions, isPackageVersion, normalizeVersionRegistry } from './npm-ops.js';
|
|
7
7
|
import { atomicReplaceJson, withFileLock } from './atomic-write.js';
|
|
8
|
+
import { loadDaemonConfig } from '../config-store.js';
|
|
8
9
|
export const VERSION_INFO_TTL_MS = 24 * 60 * 60 * 1000;
|
|
10
|
+
export const VERSION_INFO_SCHEMA_VERSION = 2;
|
|
9
11
|
export const VERSION_INFO_PACKAGES = [MAIN_PACKAGE_NAME, WEB_PACKAGE_NAME, '@agentunion/fastaun'];
|
|
10
|
-
export const AUTO_UPGRADE_PACKAGES = [MAIN_PACKAGE_NAME
|
|
11
|
-
|
|
12
|
+
export const AUTO_UPGRADE_PACKAGES = [MAIN_PACKAGE_NAME];
|
|
13
|
+
export function resolveUpgradeDistTag() {
|
|
14
|
+
return loadDaemonConfig().upgrade?.testPackages === true ? 'test' : 'latest';
|
|
15
|
+
}
|
|
16
|
+
const linkedInstallSelection = { registry: DEFAULT_NPM_REGISTRY, useNpmCli: false, npmConfig: {} };
|
|
17
|
+
const snapshotKey = (name, registry, distTag) => JSON.stringify([name, registry, distTag]);
|
|
18
|
+
const empty = () => ({ $schema_version: VERSION_INFO_SCHEMA_VERSION, packages: {} });
|
|
12
19
|
const timestamp = (value) => typeof value === 'string' ? Date.parse(value) : NaN;
|
|
13
20
|
const allowed = (name) => VERSION_INFO_PACKAGES.includes(name);
|
|
14
21
|
const filePath = () => path.join(resolvePaths().dataDir, 'package-version-info.json');
|
|
@@ -17,10 +24,22 @@ const writeLockPath = () => `${filePath()}.lock`;
|
|
|
17
24
|
export function readPackageVersionInfo() {
|
|
18
25
|
try {
|
|
19
26
|
const raw = JSON.parse(fs.readFileSync(filePath(), 'utf8'));
|
|
20
|
-
if (raw?.$schema_version !==
|
|
27
|
+
if (raw?.$schema_version !== VERSION_INFO_SCHEMA_VERSION || !raw.packages || Array.isArray(raw.packages)
|
|
21
28
|
|| typeof raw.packages !== 'object' || Object.keys(raw.packages).some(name => !allowed(name)))
|
|
22
29
|
return empty();
|
|
23
30
|
const result = empty();
|
|
31
|
+
if (raw.snapshots && typeof raw.snapshots === 'object' && !Array.isArray(raw.snapshots)) {
|
|
32
|
+
result.snapshots = {};
|
|
33
|
+
for (const [key, value] of Object.entries(raw.snapshots)) {
|
|
34
|
+
const entry = value;
|
|
35
|
+
if (entry && allowed(entry.packageName) && entry.registry && normalizeVersionRegistry(entry.registry) === entry.registry
|
|
36
|
+
&& ['latest', 'test'].includes(entry.distTag ?? 'latest')
|
|
37
|
+
&& key === snapshotKey(entry.packageName, entry.registry, entry.distTag ?? 'latest')
|
|
38
|
+
&& (entry.currentVersion === null || isPackageVersion(entry.currentVersion))
|
|
39
|
+
&& (entry.latestVersion == null || isPackageVersion(entry.latestVersion)))
|
|
40
|
+
result.snapshots[key] = entry;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
24
43
|
for (const [name, value] of Object.entries(raw.packages)) {
|
|
25
44
|
const entry = value;
|
|
26
45
|
if (!entry || entry.packageName !== name || !['ok', 'unavailable', 'skipped', 'not-installed'].includes(entry.lastAttemptStatus)
|
|
@@ -46,11 +65,12 @@ export async function readPackageVersionInfoConsistent(timeoutMs = 30000) {
|
|
|
46
65
|
}
|
|
47
66
|
return readPackageVersionInfo();
|
|
48
67
|
}
|
|
49
|
-
export function isPackageVersionInfoFresh(entry, now = Date.now(), ttlMs = VERSION_INFO_TTL_MS, registry) {
|
|
68
|
+
export function isPackageVersionInfoFresh(entry, now = Date.now(), ttlMs = VERSION_INFO_TTL_MS, registry, distTag = 'latest') {
|
|
50
69
|
if (!entry || !allowed(entry.packageName) || !isPackageVersion(entry.currentVersion)
|
|
51
70
|
|| !isPackageVersion(entry.latestVersion) || !entry.registry
|
|
52
71
|
|| normalizeVersionRegistry(entry.registry) !== entry.registry
|
|
53
|
-
|| (registry !== undefined && entry.registry !== registry)
|
|
72
|
+
|| (registry !== undefined && entry.registry !== registry)
|
|
73
|
+
|| (entry.distTag ?? 'latest') !== distTag)
|
|
54
74
|
return false;
|
|
55
75
|
const checked = timestamp(entry.checkedAt);
|
|
56
76
|
const observed = timestamp(entry.currentVersionObservedAt);
|
|
@@ -69,7 +89,9 @@ export async function mergePackageVersionInfo(patch) {
|
|
|
69
89
|
const file = filePath();
|
|
70
90
|
return withFileLock(writeLockPath(), async () => {
|
|
71
91
|
const data = readPackageVersionInfo();
|
|
72
|
-
const
|
|
92
|
+
const key = patch.registry ? snapshotKey(patch.packageName, patch.registry, patch.distTag ?? 'latest') : undefined;
|
|
93
|
+
const previous = key ? data.snapshots?.[key] ?? ((data.packages[patch.packageName]?.distTag ?? 'latest') === (patch.distTag ?? 'latest')
|
|
94
|
+
&& data.packages[patch.packageName]?.registry === patch.registry ? data.packages[patch.packageName] : undefined) : data.packages[patch.packageName];
|
|
73
95
|
const entry = previous ? { ...previous } : {
|
|
74
96
|
packageName: patch.packageName, currentVersion: null, lastAttemptStatus: patch.lastAttemptStatus,
|
|
75
97
|
};
|
|
@@ -83,6 +105,8 @@ export async function mergePackageVersionInfo(patch) {
|
|
|
83
105
|
entry.latestVersion = patch.latestVersion;
|
|
84
106
|
entry.checkedAt = patch.checkedAt;
|
|
85
107
|
entry.registry = patch.registry;
|
|
108
|
+
entry.distTag = patch.distTag ?? 'latest';
|
|
109
|
+
entry.npmConfig = patch.npmConfig;
|
|
86
110
|
}
|
|
87
111
|
if (newer(patch.lastAttemptAt, entry.lastAttemptAt)) {
|
|
88
112
|
entry.lastAttemptStatus = patch.lastAttemptStatus;
|
|
@@ -95,6 +119,15 @@ export async function mergePackageVersionInfo(patch) {
|
|
|
95
119
|
if (patch.lastUpgrade && newer(patch.lastUpgrade.at, entry.lastUpgrade?.at))
|
|
96
120
|
entry.lastUpgrade = patch.lastUpgrade;
|
|
97
121
|
data.packages[patch.packageName] = entry;
|
|
122
|
+
if (key) {
|
|
123
|
+
entry.registry = patch.registry;
|
|
124
|
+
entry.distTag = patch.distTag ?? 'latest';
|
|
125
|
+
data.snapshots ??= {};
|
|
126
|
+
data.snapshots[key] = entry;
|
|
127
|
+
const keys = Object.keys(data.snapshots);
|
|
128
|
+
for (const expired of keys.sort((left, right) => timestamp(data.snapshots[left].lastAttemptAt) - timestamp(data.snapshots[right].lastAttemptAt)).slice(0, Math.max(0, keys.length - 48)))
|
|
129
|
+
delete data.snapshots[expired];
|
|
130
|
+
}
|
|
98
131
|
await atomicReplaceJson(file, data);
|
|
99
132
|
return entry;
|
|
100
133
|
});
|
|
@@ -115,8 +148,8 @@ export function resolveVersionInfoPackage(name) {
|
|
|
115
148
|
return null;
|
|
116
149
|
}
|
|
117
150
|
}
|
|
118
|
-
function hasUpdate(entry, registry, now = Date.now()) {
|
|
119
|
-
return isPackageVersionInfoFresh(entry, now, VERSION_INFO_TTL_MS, registry)
|
|
151
|
+
function hasUpdate(entry, registry, now = Date.now(), distTag = 'latest') {
|
|
152
|
+
return isPackageVersionInfoFresh(entry, now, VERSION_INFO_TTL_MS, registry, distTag)
|
|
120
153
|
&& comparePackageVersions(entry.currentVersion, entry.latestVersion) < 0;
|
|
121
154
|
}
|
|
122
155
|
async function checkPackage(name, force, options) {
|
|
@@ -124,37 +157,44 @@ async function checkPackage(name, force, options) {
|
|
|
124
157
|
throw new Error('Unmanaged package');
|
|
125
158
|
const now = options.now ?? Date.now();
|
|
126
159
|
const at = new Date(now).toISOString();
|
|
127
|
-
const
|
|
160
|
+
const distTag = options.distTag ?? resolveUpgradeDistTag();
|
|
161
|
+
const selection = options.selection ?? (isLinkedInstall() && !options.includeUninstalled
|
|
162
|
+
? linkedInstallSelection
|
|
163
|
+
: await resolveUpgradeRegistry(options.signal));
|
|
164
|
+
const registry = normalizeVersionRegistry(selection.registry);
|
|
165
|
+
const data = await readPackageVersionInfoConsistent();
|
|
166
|
+
const legacy = data.packages[name];
|
|
167
|
+
const old = registry ? data.snapshots?.[snapshotKey(name, registry, distTag)]
|
|
168
|
+
?? (legacy?.registry === registry && (legacy.distTag ?? 'latest') === distTag ? legacy : undefined) : undefined;
|
|
128
169
|
const observed = {
|
|
129
170
|
packageName: name, currentVersion: null, currentVersionObservedAt: at,
|
|
130
171
|
lastAttemptStatus: 'unavailable',
|
|
172
|
+
distTag, npmConfig: selection.npmConfig, ...(registry ? { registry } : {}),
|
|
131
173
|
};
|
|
132
174
|
const log = (message) => options.log?.(`[${options.source ?? 'version-check'}] ${name}: ${message}`);
|
|
133
175
|
let source = 'refresh';
|
|
134
176
|
try {
|
|
135
177
|
const linked = isLinkedInstall();
|
|
136
|
-
const local =
|
|
178
|
+
const local = resolveVersionInfoPackage(name);
|
|
137
179
|
if (local && !isPackageVersion(local.version))
|
|
138
180
|
throw new Error('Invalid installed package version');
|
|
139
181
|
observed.currentVersion = local?.version ?? null;
|
|
140
|
-
if (linked)
|
|
182
|
+
if (linked && !options.includeUninstalled)
|
|
141
183
|
observed.lastAttemptStatus = 'skipped';
|
|
142
|
-
else if (!local)
|
|
184
|
+
else if (!local && !options.includeUninstalled)
|
|
143
185
|
observed.lastAttemptStatus = 'not-installed';
|
|
144
186
|
else {
|
|
145
|
-
const selection = options.selection ?? await resolveUpgradeRegistry(options.signal);
|
|
146
|
-
const registry = normalizeVersionRegistry(selection.registry);
|
|
147
187
|
if (!registry)
|
|
148
188
|
throw new Error('Registry configuration is not safe for version caching');
|
|
149
|
-
if (!force && isPackageVersionInfoFresh(old, now, VERSION_INFO_TTL_MS, registry)) {
|
|
189
|
+
if (!force && isPackageVersionInfoFresh(old, now, VERSION_INFO_TTL_MS, registry, distTag)) {
|
|
150
190
|
source = 'cache';
|
|
151
191
|
observed.lastAttemptStatus = 'ok';
|
|
152
192
|
const entry = await mergePackageVersionInfo(observed);
|
|
153
193
|
log(`Using cached version information (age ${Math.floor((now - timestamp(old.checkedAt)) / 60000)}m)`);
|
|
154
|
-
return { packageName: name, status: 'ok', source, entry, hasUpdate: hasUpdate(entry, registry, now) };
|
|
194
|
+
return { packageName: name, status: 'ok', source, entry, hasUpdate: hasUpdate(entry, registry, now, distTag), npmConfig: selection.npmConfig };
|
|
155
195
|
}
|
|
156
196
|
log(force ? 'Refreshing version information' : 'Version information missing/invalid/expired; refreshing');
|
|
157
|
-
const checked = await queryPackageVersion(name, selection, options.signal);
|
|
197
|
+
const checked = await queryPackageVersion(name, selection, options.signal, distTag);
|
|
158
198
|
if (!checked)
|
|
159
199
|
throw new Error('Version registry unavailable or returned an invalid version');
|
|
160
200
|
observed.latestVersion = checked.version;
|
|
@@ -167,7 +207,7 @@ async function checkPackage(name, force, options) {
|
|
|
167
207
|
}
|
|
168
208
|
catch {
|
|
169
209
|
observed.lastAttemptStatus = 'unavailable';
|
|
170
|
-
observed.lastError =
|
|
210
|
+
observed.lastError = `Cannot resolve ${name}@${distTag}; registry unavailable, tag missing or invalid version`;
|
|
171
211
|
log(observed.lastError);
|
|
172
212
|
}
|
|
173
213
|
observed.lastAttemptAt = at;
|
|
@@ -181,20 +221,25 @@ async function checkPackage(name, force, options) {
|
|
|
181
221
|
}
|
|
182
222
|
return {
|
|
183
223
|
packageName: name, source, entry, status: observed.lastAttemptStatus,
|
|
184
|
-
hasUpdate: observed.lastAttemptStatus === 'ok' && hasUpdate(entry, observed.registry, now),
|
|
224
|
+
hasUpdate: observed.lastAttemptStatus === 'ok' && hasUpdate(entry, observed.registry, now, distTag),
|
|
185
225
|
...(observed.lastError ? { error: observed.lastError } : {}),
|
|
226
|
+
npmConfig: selection.npmConfig,
|
|
186
227
|
};
|
|
187
228
|
}
|
|
188
229
|
export async function refreshPackageVersionInfo(name, options = {}) {
|
|
189
230
|
return checkPackage(name, true, options);
|
|
190
231
|
}
|
|
191
232
|
export async function ensureFreshPackageVersionInfo(names = AUTO_UPGRADE_PACKAGES, options = {}) {
|
|
192
|
-
const selection = options.selection ?? (isLinkedInstall()
|
|
193
|
-
|
|
233
|
+
const selection = options.selection ?? (isLinkedInstall() && !options.includeUninstalled
|
|
234
|
+
? linkedInstallSelection
|
|
235
|
+
: await resolveUpgradeRegistry(options.signal));
|
|
236
|
+
const distTag = options.distTag ?? resolveUpgradeDistTag();
|
|
237
|
+
return Promise.all(names.map(name => checkPackage(name, false, { ...options, selection, distTag })));
|
|
194
238
|
}
|
|
195
239
|
export async function refreshManagedPackageVersions(names = AUTO_UPGRADE_PACKAGES, options = {}) {
|
|
196
|
-
const selection = options.selection ??
|
|
197
|
-
|
|
240
|
+
const selection = options.selection ?? await resolveUpgradeRegistry(options.signal);
|
|
241
|
+
const distTag = options.distTag ?? resolveUpgradeDistTag();
|
|
242
|
+
return Promise.all(names.map(name => checkPackage(name, true, { ...options, selection, distTag })));
|
|
198
243
|
}
|
|
199
244
|
export async function recordPackageUpgrade(name, upgrade, log) {
|
|
200
245
|
try {
|
|
@@ -219,14 +264,14 @@ export async function applyCachedPackageUpgrades(names = AUTO_UPGRADE_PACKAGES,
|
|
|
219
264
|
if (!AUTO_UPGRADE_PACKAGES.includes(name))
|
|
220
265
|
throw new Error('Package is not automatically managed');
|
|
221
266
|
try {
|
|
222
|
-
const entry = (await readPackageVersionInfoConsistent(0)).packages[name];
|
|
267
|
+
const entry = options.target ?? (await readPackageVersionInfoConsistent(0)).packages[name];
|
|
223
268
|
const registry = entry?.registry ? normalizeVersionRegistry(entry.registry) : null;
|
|
224
|
-
if (!registry || !isPackageVersionInfoFresh(entry, Date.now(), VERSION_INFO_TTL_MS, registry)) {
|
|
269
|
+
if (!registry || entry.packageName !== name || !isPackageVersionInfoFresh(entry, Date.now(), VERSION_INFO_TTL_MS, registry, options.distTag ?? resolveUpgradeDistTag())) {
|
|
225
270
|
result = { status: 'skipped', error: 'No valid cached version information' };
|
|
226
271
|
}
|
|
227
272
|
else {
|
|
228
273
|
options.log?.(`Applying cached version information: ${name}@${entry.latestVersion}`);
|
|
229
|
-
result = await installExactPackageVersion(name, entry.latestVersion, registry, () => resolveVersionInfoPackage(name));
|
|
274
|
+
result = await installExactPackageVersion(name, entry.latestVersion, registry, () => resolveVersionInfoPackage(name), options.npmConfig ?? entry.npmConfig);
|
|
230
275
|
}
|
|
231
276
|
}
|
|
232
277
|
catch {
|