evolcore 0.0.7 → 0.0.8

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.
@@ -33,7 +33,7 @@ const execFileAsync = promisify(execFile);
33
33
  function printNoSelfAgentHints(options) {
34
34
  const { daemonConfig, ecwebStarted, skipped } = options;
35
35
  console.log('\nℹ 未配置任何 self-agent,Control Plane 已启动。');
36
- console.log(' 命令行:ec agent new <aid>.agentid.pub');
36
+ console.log(' 命令行:ec agent new <aid>.example.com');
37
37
  if (daemonConfig.aid && (daemonConfig.owners?.length ?? 0) > 0) {
38
38
  console.log(' Evol App:可通过进程级菜单创建 agent');
39
39
  }
@@ -301,9 +301,10 @@ export async function cmdStart(opts = {}) {
301
301
  if (ans.trim() === '2') {
302
302
  const { suppressSdkLogs } = await import('../aun/aid/index.js');
303
303
  suppressSdkLogs();
304
- const { generateControlAid } = await import('../aun/aid/control-aid.js');
305
- const result = await generateControlAid();
306
- saveDaemonConfig({ ...loadDaemonConfig(), aid: result.aid });
304
+ const { generateControlAid, resolveControlAidDomain } = await import('../aun/aid/control-aid.js');
305
+ const cfg = loadDaemonConfig();
306
+ const result = await generateControlAid(resolveControlAidDomain(cfg.aun?.defaultAidDomain));
307
+ saveDaemonConfig({ ...cfg, aid: result.aid });
307
308
  console.log(`✓ 新控制 AID: ${result.aid}`);
308
309
  }
309
310
  }
@@ -349,8 +350,19 @@ export async function cmdStart(opts = {}) {
349
350
  await sleep(2000);
350
351
  }
351
352
  }
352
- // HOME 孤儿(未登记进程)只警告,不动
353
- reportOrphans(findOrphanProcesses());
353
+ // A missing/stale instance record must not turn into a duplicate daemon.
354
+ // On Windows/macOS the process environment may be unavailable, so an
355
+ // unregistered process from this package is conservatively treated as a
356
+ // blocker when its HOME cannot be identified.
357
+ const orphans = findOrphanProcesses();
358
+ const blockingOrphans = orphans.filter(o => isRuntimeOrphan(o));
359
+ if (blockingOrphans.length > 0) {
360
+ console.error(`❌ 检测到未登记的 EvolCore 进程(PID: ${blockingOrphans.map(o => o.pid).join(', ')}),已停止启动以避免重复连接 AUN。`);
361
+ console.error(' 请先停止这些进程,再重新执行 ec start。');
362
+ process.exitCode = 1;
363
+ return;
364
+ }
365
+ reportOrphans(orphans);
354
366
  console.log('🚀 Starting EvolCore...');
355
367
  const stdoutRotation = rotateStdoutLog(p.logs);
356
368
  if (stdoutRotation.rotatedPath) {
@@ -549,36 +561,54 @@ export async function cmdStart(opts = {}) {
549
561
  async function stopPid(pid) {
550
562
  console.log(`🛑 Stopping EvolCore (PID: ${pid})...`);
551
563
  platform.killProcess(pid);
552
- await new Promise((resolve) => {
553
- let waited = 0;
554
- const check = setInterval(() => {
555
- waited++;
556
- if (!platform.isProcessRunning(pid)) {
557
- clearInterval(check);
558
- console.log('✓ EvolCore stopped');
559
- resolve();
560
- return;
561
- }
562
- if (waited >= 10) {
563
- clearInterval(check);
564
- platform.killProcess(pid, true);
565
- console.log('✓ EvolCore stopped (forced)');
566
- resolve();
567
- }
568
- }, 1000);
569
- });
564
+ if (await platform.waitForProcessExit(pid, 10_000)) {
565
+ console.log('✓ EvolCore stopped');
566
+ return true;
567
+ }
568
+ console.log(`⚠ EvolCore PID ${pid} did not exit gracefully; forcing termination...`);
569
+ platform.killProcess(pid, true);
570
+ if (await platform.waitForProcessExit(pid, 5_000)) {
571
+ console.log('✓ EvolCore stopped (forced)');
572
+ return true;
573
+ }
574
+ console.error(`❌ EvolCore PID ${pid} is still running; restart aborted`);
575
+ return false;
576
+ }
577
+ function isRuntimeOrphan(orphan, currentHome = resolveRoot()) {
578
+ if (orphan.confirmedTestDaemon)
579
+ return false;
580
+ const homeMatches = orphan.evolcoreHome
581
+ && normalizeRuntimePath(orphan.evolcoreHome) === normalizeRuntimePath(currentHome);
582
+ // Windows/macOS may not expose another process's environment. In that case
583
+ // the exact installed package path is the conservative ownership proof.
584
+ const packageMatches = !orphan.evolcoreHome
585
+ && normalizeRuntimePath(orphan.cmdline).includes(normalizeRuntimePath(getPackageRoot()));
586
+ return Boolean(homeMatches || packageMatches);
570
587
  }
571
588
  export async function cmdStop() {
572
589
  const status = scanInstances();
573
- const ping = await probeDaemon(resolvePaths().socket);
574
- if (rejectPidIsolatedLifecycle('stop', status, ping))
575
- return;
590
+ const p = resolvePaths();
591
+ const initialOrphans = findOrphanProcesses();
592
+ const runtimeOrphans = initialOrphans.filter(orphan => isRuntimeOrphan(orphan, p.root));
593
+ const ping = await probeDaemon(p.socket);
594
+ if (ping && !hasVisibleDaemonPid(status, ping) && runtimeOrphans.length === 0) {
595
+ if (rejectPidIsolatedLifecycle('stop', status, ping))
596
+ return;
597
+ }
576
598
  const aliveMains = status.mains.filter(m => m.alive);
577
- if (aliveMains.length === 0) {
599
+ if (aliveMains.length === 0 && runtimeOrphans.length === 0) {
578
600
  console.log('⚠ EvolCore is not running');
579
601
  return;
580
602
  }
581
- await Promise.all(aliveMains.map(m => stopPid(m.record.pid)));
603
+ const pids = new Set([...aliveMains.map(entry => entry.record.pid), ...runtimeOrphans.map(orphan => orphan.pid)]);
604
+ if (runtimeOrphans.length > 0) {
605
+ console.log(`⚠ 检测到未登记的 EvolCore 进程,将一并停止: ${runtimeOrphans.map(orphan => orphan.pid).join(', ')}`);
606
+ }
607
+ const stopped = await Promise.all([...pids].map(pid => stopPid(pid)));
608
+ if (stopped.some(result => !result)) {
609
+ process.exitCode = 1;
610
+ return;
611
+ }
582
612
  await sleep(500);
583
613
  cleanupInstances();
584
614
  if (aliveMains.length > 1) {
@@ -589,14 +619,16 @@ export async function cmdRestart(opts = {}) {
589
619
  const cmdStartedAt = Date.now();
590
620
  console.log('🔄 Restarting EvolCore...');
591
621
  const initialStatus = scanInstances();
622
+ const initialOrphans = findOrphanProcesses();
623
+ const initialRuntimeOrphans = initialOrphans.filter(orphan => isRuntimeOrphan(orphan));
592
624
  const socketPath = resolvePaths().socket;
593
625
  const ping = await probeDaemon(socketPath);
594
626
  if (ping && !hasVisibleDaemonPid(initialStatus, ping)) {
595
627
  if (await tryDelegatedDaemonRestart(socketPath))
596
628
  return;
629
+ if (initialRuntimeOrphans.length === 0 && rejectPidIsolatedLifecycle('restart', initialStatus, ping))
630
+ return;
597
631
  }
598
- if (rejectPidIsolatedLifecycle('restart', initialStatus, ping))
599
- return;
600
632
  if (!requireFreshSourceBuild())
601
633
  return;
602
634
  // 版本检查与自动升级
@@ -651,29 +683,49 @@ export async function cmdRestart(opts = {}) {
651
683
  if (aliveMains.length > 1) {
652
684
  console.log(`⚠ 检测到 ${aliveMains.length} 个 main 实例,将一并停止: ${aliveMains.map(m => m.record.pid).join(', ')}`);
653
685
  }
654
- await Promise.all(aliveMains.map(m => stopPid(m.record.pid)));
686
+ const stopped = await Promise.all(aliveMains.map(m => stopPid(m.record.pid)));
687
+ if (stopped.some(result => !result)) {
688
+ console.error('❌ Restart aborted because an existing EvolCore process could not be stopped');
689
+ process.exitCode = 1;
690
+ return;
691
+ }
655
692
  await sleep(500);
656
693
  }
657
694
  cleanupInstances();
658
- // 孤儿处理:同 HOME 的孤儿无条件 kill(restart 必须替换旧实例);
695
+ const remainingMainProcesses = scanInstances().mains.filter(entry => entry.alive);
696
+ if (remainingMainProcesses.length > 0) {
697
+ console.error(`❌ Restart aborted: EvolCore process still registered and alive (PID: ${remainingMainProcesses.map(entry => entry.record.pid).join(', ')})`);
698
+ process.exitCode = 1;
699
+ return;
700
+ }
701
+ // 孤儿处理:同 HOME/同安装包的孤儿属于本运行时,先确认退出再替换;
659
702
  // 跨 HOME 的孤儿只在 --clear 时 kill,否则仅警告。
660
703
  {
661
704
  const orphans = findOrphanProcesses();
662
705
  const confirmedTestDaemons = orphans.filter(o => o.confirmedTestDaemon);
663
- const nonTestOrphans = orphans.filter(o => !o.confirmedTestDaemon);
664
706
  if (confirmedTestDaemons.length > 0) {
665
707
  const killed = killOrphans(confirmedTestDaemons);
666
708
  console.log(`☠ 已清理 ${killed.length} 个泄漏的测试 daemon: ${killed.join(', ')}`);
667
709
  await sleep(500);
668
710
  }
669
- const currentHome = resolveRoot();
670
- const sameHome = nonTestOrphans.filter(o => o.evolcoreHome === currentHome);
671
- const otherHome = nonTestOrphans.filter(o => o.evolcoreHome !== currentHome);
672
- if (sameHome.length > 0) {
673
- const killed = killOrphans(sameHome);
674
- console.log(`☠ SIGKILL ${killed.length} 个同 HOME 孤儿进程: ${killed.join(', ')}`);
711
+ const runtimeOrphans = findOrphanProcesses().filter(o => isRuntimeOrphan(o));
712
+ if (runtimeOrphans.length > 0) {
713
+ console.log(`⚠ 检测到未登记的 EvolCore 进程,将一并停止: ${runtimeOrphans.map(o => o.pid).join(', ')}`);
714
+ const stopped = await Promise.all(runtimeOrphans.map(o => stopPid(o.pid)));
715
+ if (stopped.some(result => !result)) {
716
+ console.error('❌ Restart aborted because an unregistered EvolCore process could not be stopped');
717
+ process.exitCode = 1;
718
+ return;
719
+ }
675
720
  await sleep(500);
676
721
  }
722
+ const remainingRuntimeOrphans = findOrphanProcesses().filter(o => isRuntimeOrphan(o));
723
+ if (remainingRuntimeOrphans.length > 0) {
724
+ console.error(`❌ Restart aborted: EvolCore process still running (PID: ${remainingRuntimeOrphans.map(o => o.pid).join(', ')})`);
725
+ process.exitCode = 1;
726
+ return;
727
+ }
728
+ const otherHome = findOrphanProcesses().filter(o => !o.confirmedTestDaemon && !isRuntimeOrphan(o));
677
729
  if (opts.clear && otherHome.length > 0) {
678
730
  const killed = killOrphans(otherHome);
679
731
  console.log(`☠ 已 SIGKILL ${killed.length} 个跨 HOME 孤儿进程: ${killed.join(', ')}`);
@@ -707,6 +759,9 @@ export async function cmdRestart(opts = {}) {
707
759
  printCodeStats(getPackageRoot(), resolvePaths().logs);
708
760
  }
709
761
  }
762
+ function normalizeRuntimePath(value) {
763
+ return value.replace(/[\\/]+/g, '/').toLowerCase();
764
+ }
710
765
  function formatTimeAgo(ms) {
711
766
  const sec = Math.floor(ms / 1000);
712
767
  if (sec < 60)
@@ -2382,8 +2437,22 @@ async function printEcwebStatus(p) {
2382
2437
  // still provide an authoritative runtime status without local config access.
2383
2438
  }
2384
2439
  const entries = readEcwebInstanceEntries(p);
2385
- let readyEntry;
2440
+ const aliveEntries = [];
2386
2441
  for (const entry of entries) {
2442
+ if (platform.isProcessRunning(entry.record.pid)) {
2443
+ aliveEntries.push(entry);
2444
+ }
2445
+ else {
2446
+ // A stale EC Web record must never be used to report the PID of an
2447
+ // unrelated process that later inherited the port.
2448
+ try {
2449
+ fs.unlinkSync(entry.filePath);
2450
+ }
2451
+ catch { }
2452
+ }
2453
+ }
2454
+ let readyEntry;
2455
+ for (const entry of aliveEntries) {
2387
2456
  if (await probeEcwebReady(entry.record.port)) {
2388
2457
  readyEntry = entry;
2389
2458
  break;
@@ -2394,11 +2463,19 @@ async function printEcwebStatus(p) {
2394
2463
  console.log(`🔭 ECWeb: 运行中 (PID: ${readyEntry.record.pid}) http://localhost:${readyEntry.record.port}`);
2395
2464
  return;
2396
2465
  }
2397
- const visibleEntry = entries.find(entry => platform.isProcessRunning(entry.record.pid));
2466
+ const occupiedPids = new Set();
2467
+ for (const entry of entries) {
2468
+ for (const pid of platform.findProcessByPort(entry.record.port))
2469
+ occupiedPids.add(pid);
2470
+ }
2398
2471
  console.log('');
2399
- if (visibleEntry) {
2472
+ if (aliveEntries.length > 0) {
2473
+ const visibleEntry = aliveEntries[0];
2400
2474
  console.log(`🔭 ECWeb: 进程存活 (PID: ${visibleEntry.record.pid}) 但 HTTP 未就绪 (端口 ${visibleEntry.record.port}),查看 logs/watch-web.log`);
2401
2475
  }
2476
+ else if (occupiedPids.size > 0) {
2477
+ console.log(`🔭 ECWeb: 端口仍被未登记进程占用 (PID: ${[...occupiedPids].join(', ')}),请先执行 ec restart`);
2478
+ }
2402
2479
  else if (entries.length > 0) {
2403
2480
  console.log('🔭 ECWeb: instance 记录存在,但宿主 PID 不可见且 HTTP 未就绪');
2404
2481
  }
@@ -2432,31 +2509,34 @@ function stopCodexAppServerOrphans() {
2432
2509
  }
2433
2510
  return killed;
2434
2511
  }
2435
- /** 若 ecweb 在运行则杀掉并清理 pid 文件,返回是否成功 kill。 */
2436
- function stopEcwebIfRunning(p) {
2512
+ /** 若 ecweb 在运行则杀掉并确认 pid/端口都已释放。 */
2513
+ async function stopEcwebIfRunning(p) {
2437
2514
  const alive = findAliveEcweb(p);
2438
- let killed = false;
2439
- if (alive) {
2440
- try {
2441
- platform.killProcess(alive.pid, true);
2442
- }
2443
- catch { }
2444
- killed = true;
2515
+ const port = loadDaemonConfig().ecweb?.port ?? 42705;
2516
+ const pids = new Set([
2517
+ ...(alive ? [alive.pid] : []),
2518
+ ...platform.findProcessByPort(port),
2519
+ ]);
2520
+ for (const pid of pids)
2521
+ platform.killProcess(pid, true);
2522
+ const exited = await Promise.all([...pids].map(pid => platform.waitForProcessExit(pid, 5_000)));
2523
+ const portReleased = await waitForPortRelease(port, 5_000);
2524
+ if (exited.some(result => !result) || !portReleased) {
2525
+ throw new Error(`EC Web old process or port ${port} could not be released`);
2445
2526
  }
2446
2527
  // 清理 pid 文件(仅 ecweb-<pid>.json / watch-web-<pid>.json,
2447
2528
  // 不碰 ecweb-tokens.json 等非 pid 文件,否则会清空配对 token 库导致每次重启都要重新配对)
2448
2529
  removeEcwebInstanceFiles(p);
2449
- // 端口兜底:杀掉任何仍占用 ecweb 端口的残留进程(含手动启动、未登记 pid 文件的)。
2450
- // 仅靠 pid 文件无法清理这类进程,会导致下次启动端口被占。
2451
- const port = loadDaemonConfig().ecweb?.port ?? 42705;
2452
- for (const pid of platform.findProcessByPort(port)) {
2453
- try {
2454
- platform.killProcess(pid, true);
2455
- killed = true;
2456
- }
2457
- catch { }
2530
+ return pids.size > 0;
2531
+ }
2532
+ async function waitForPortRelease(port, timeoutMs) {
2533
+ const deadline = Date.now() + timeoutMs;
2534
+ while (Date.now() < deadline) {
2535
+ if (platform.findProcessByPort(port).length === 0)
2536
+ return true;
2537
+ await sleep(250);
2458
2538
  }
2459
- return killed;
2539
+ return platform.findProcessByPort(port).length === 0;
2460
2540
  }
2461
2541
  /**
2462
2542
  * 后台 detached 启动 ecweb;若已运行则先停再启(确保加载最新代码)。
@@ -2539,7 +2619,14 @@ export async function startEcwebIfEnabled(p) {
2539
2619
  const cfg = loadDaemonConfig();
2540
2620
  if (!cfg.ecweb?.enabled)
2541
2621
  return false;
2542
- const wasRunning = stopEcwebIfRunning(p); // 先停旧进程(有则停),保证加载最新代码
2622
+ let wasRunning = false;
2623
+ try {
2624
+ wasRunning = await stopEcwebIfRunning(p); // 先停旧进程(有则停),保证加载最新代码
2625
+ }
2626
+ catch (error) {
2627
+ console.log(`❌ EC Web 旧进程未清理干净,取消启动: ${error instanceof Error ? error.message : String(error)}`);
2628
+ return false;
2629
+ }
2543
2630
  const port = cfg.ecweb.port ?? 42705;
2544
2631
  const args = ['--home', p.root, '--port', String(port)];
2545
2632
  const launch = resolveEcwebLaunchCommand(args);
@@ -2553,6 +2640,7 @@ export async function startEcwebIfEnabled(p) {
2553
2640
  console.log(` command: ${launch.command}, args: ${JSON.stringify(launch.args)}`);
2554
2641
  return false;
2555
2642
  }
2643
+ let startedPid;
2556
2644
  try {
2557
2645
  const child = spawn(launch.command, launch.args, {
2558
2646
  detached: true,
@@ -2565,6 +2653,7 @@ export async function startEcwebIfEnabled(p) {
2565
2653
  });
2566
2654
  child.unref();
2567
2655
  const pid = child.pid;
2656
+ startedPid = pid;
2568
2657
  if (!pid) {
2569
2658
  console.log('❌ ECWeb 启动失败(进程创建失败)');
2570
2659
  return false;
@@ -2592,9 +2681,17 @@ export async function startEcwebIfEnabled(p) {
2592
2681
  return true;
2593
2682
  }
2594
2683
  console.log(`❌ ECWeb 启动失败:端口 ${port} 未就绪(进程 PID ${pid} 可能已退出,查看 logs/watch-web.log)`);
2684
+ platform.killProcess(pid, true);
2685
+ await platform.waitForProcessExit(pid, 5_000);
2686
+ removeEcwebInstanceFiles(p);
2595
2687
  return false;
2596
2688
  }
2597
2689
  catch (err) {
2690
+ if (startedPid) {
2691
+ platform.killProcess(startedPid, true);
2692
+ await platform.waitForProcessExit(startedPid, 5_000);
2693
+ }
2694
+ removeEcwebInstanceFiles(p);
2598
2695
  console.log(`⚠ ECWeb 启动失败: ${err instanceof Error ? err.message : String(err)}`);
2599
2696
  console.log(` command: ${launch.command}`);
2600
2697
  console.log(` args: ${JSON.stringify(launch.args)}`);
@@ -2771,8 +2868,15 @@ export async function cmdDiagnose() {
2771
2868
  try {
2772
2869
  const instStatus = scanInstances();
2773
2870
  const aliveMains = instStatus.mains.filter(m => m.alive);
2871
+ const orphanMains = findOrphanProcesses();
2774
2872
  if (aliveMains.length > 0) {
2775
2873
  console.log(`[diagnose] ⚠️ 已有进程运行中: PID ${aliveMains.map(m => m.record.pid).join(', ')}`);
2874
+ hasError = true;
2875
+ }
2876
+ else if (orphanMains.length > 0) {
2877
+ console.error(`[diagnose] ❌ 发现未登记的 EvolCore 进程: PID ${orphanMains.map(o => o.pid).join(', ')}`);
2878
+ console.error('[diagnose] 请先执行 ec stop 或手动结束这些 PID,再运行 ec start。');
2879
+ hasError = true;
2776
2880
  }
2777
2881
  else {
2778
2882
  console.log(`[diagnose] ✓ 无残留进程`);
@@ -2781,7 +2885,28 @@ export async function cmdDiagnose() {
2781
2885
  catch {
2782
2886
  console.log(`[diagnose] ✓ 无 instance 文件`);
2783
2887
  }
2784
- // 6. 检查关键文件
2888
+ // 6. 检查 EC Web 端口与实例记录是否一致。
2889
+ try {
2890
+ const cfg = loadDaemonConfig();
2891
+ if (cfg.ecweb?.enabled) {
2892
+ const port = cfg.ecweb.port ?? 42705;
2893
+ const portPids = platform.findProcessByPort(port);
2894
+ const recordedPids = new Set(readEcwebInstanceEntries(p).map(entry => entry.record.pid));
2895
+ const unregisteredPids = portPids.filter(pid => !recordedPids.has(pid));
2896
+ if (unregisteredPids.length > 0) {
2897
+ console.error(`[diagnose] ❌ EC Web 端口 ${port} 被未登记进程占用: PID ${unregisteredPids.join(', ')}`);
2898
+ console.error('[diagnose] 请执行 ec restart 清理 EC Web 后再启动。');
2899
+ hasError = true;
2900
+ }
2901
+ else {
2902
+ console.log(`[diagnose] ✓ EC Web 端口 ${port} 未发现未登记占用`);
2903
+ }
2904
+ }
2905
+ }
2906
+ catch (e) {
2907
+ console.error(`[diagnose] ⚠️ EC Web 端口检查失败: ${e instanceof Error ? e.message : String(e)}`);
2908
+ }
2909
+ // 7. 检查关键文件
2785
2910
  const appMain = path.join(getPackageRoot(), 'dist', 'index.js');
2786
2911
  if (!fs.existsSync(appMain)) {
2787
2912
  console.error(`[diagnose] ❌ 编译产物不存在: ${appMain}`);
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { resolvePaths, resolveRoot } from '../paths.js';
4
- import { applyDataMigration, planDataMigration, readDataMigration, verifyDataMigration } from '../core/data-migration.js';
4
+ import { applyDataMigration, finalizeDataMigration, planDataMigration, readDataMigration, verifyDataMigration } from '../core/data-migration.js';
5
5
  import { scanInstances } from '../utils/instance-registry.js';
6
6
  import { isProcessRunning, killProcess } from '../utils/cross-platform.js';
7
7
  import { ipcQuery } from '../ipc.js';
@@ -11,9 +11,11 @@ function printSummary(manifest) {
11
11
  result[operation.status] = (result[operation.status] ?? 0) + 1;
12
12
  return result;
13
13
  }, {});
14
+ const pending = manifest.operations.filter(operation => !['committed', 'skipped', 'acknowledged'].includes(operation.status)).length;
14
15
  console.log(`migration: ${manifest.id}`);
15
16
  console.log(`state: ${manifest.state}`);
16
17
  console.log(`operations: ${manifest.operations.length}`);
18
+ console.log(`pending: ${pending}`);
17
19
  console.log(`status: ${Object.entries(counts).map(([key, value]) => `${key}=${value}`).join(', ') || 'none'}`);
18
20
  if (manifest.warnings.length) {
19
21
  console.log('warnings:');
@@ -77,19 +79,72 @@ async function waitForMaintenanceWindow() {
77
79
  }
78
80
  export async function cmdData(args) {
79
81
  if (args[0] !== 'migrate' || args.includes('--help')) {
80
- console.log('用法: ec data migrate --dry-run | --apply | --resume <migration-id> | --verify [migration-id]');
82
+ console.log('用法: ec data migrate --dry-run | --apply [migration-id] | --verify [migration-id]');
81
83
  return;
82
84
  }
83
85
  const root = resolveRoot();
84
86
  if (args.includes('--dry-run')) {
87
+ if (args.length !== 2)
88
+ throw new Error('--dry-run cannot be combined with another migration action');
85
89
  printSummary(planDataMigration(root));
86
90
  return;
87
91
  }
88
- const resumeIndex = args.indexOf('--resume');
89
92
  const verify = args.includes('--verify');
90
- const requestedId = resumeIndex >= 0 ? args[resumeIndex + 1] : args.find(value => value.startsWith('data-'));
91
- if (resumeIndex >= 0 && !requestedId)
92
- throw new Error('--resume requires a migration id');
93
+ const requestedId = args.find(value => value.startsWith('data-'));
94
+ if (args.includes('--apply')) {
95
+ if (verify) {
96
+ throw new Error('--apply cannot be combined with another migration action');
97
+ }
98
+ // Planning is read-only, so avoid an unnecessary service interruption
99
+ // when there is no legacy source to process.
100
+ const existing = requestedId ? readDataMigration(root, requestedId) : undefined;
101
+ let manifest = existing ?? planDataMigration(root);
102
+ if (manifest.operations.length === 0 && Object.keys(manifest.supersededSourceHashes ?? {}).length === 0) {
103
+ printSummary(manifest);
104
+ console.log('No legacy data requires migration.');
105
+ return;
106
+ }
107
+ const initialRuntime = liveRuntime();
108
+ const wasRunning = initialRuntime.mainCount > 0
109
+ || initialRuntime.monitorPids.length > 0
110
+ || await daemonIpcReachable();
111
+ if (wasRunning) {
112
+ console.log('Stopping daemon and all Agent runtimes for data migration...');
113
+ await stopRestartMonitors(initialRuntime.monitorPids);
114
+ await cmdStop();
115
+ await waitForMaintenanceWindow();
116
+ }
117
+ try {
118
+ if (existing?.state !== 'completed') {
119
+ manifest = applyDataMigration(manifest);
120
+ manifest = verifyDataMigration(manifest, { strictTargetContent: true });
121
+ printSummary(manifest);
122
+ if (manifest.state !== 'completed') {
123
+ throw new Error(`migration is not fully verified; fix the listed operations and run ec data migrate --apply ${manifest.id}`);
124
+ }
125
+ }
126
+ else {
127
+ // A completed manifest may come from the previous copy-only workflow,
128
+ // or from an earlier successful --apply. The finalization routine is
129
+ // idempotent and verifies the archive receipt before returning it.
130
+ printSummary(manifest);
131
+ }
132
+ const finalization = finalizeDataMigration(root, manifest);
133
+ console.log(`archive: ${finalization.archivePath}`);
134
+ console.log(`archive-sha256: ${finalization.archiveSha256}`);
135
+ console.log(`removed legacy sources: ${finalization.sourcePaths.length}`);
136
+ if (wasRunning) {
137
+ console.log('Starting daemon and Agent runtimes after data migration...');
138
+ await cmdStart();
139
+ }
140
+ }
141
+ catch (error) {
142
+ if (wasRunning)
143
+ console.error('Data migration failed; daemon remains stopped.');
144
+ throw error;
145
+ }
146
+ return;
147
+ }
93
148
  if (verify) {
94
149
  // A completed migration's destination becomes live runtime data. Operator
95
150
  // verification therefore validates source integrity and target structure
@@ -100,33 +155,5 @@ export async function cmdData(args) {
100
155
  process.exitCode = 1;
101
156
  return;
102
157
  }
103
- if (!args.includes('--apply') && resumeIndex < 0)
104
- throw new Error('select --dry-run, --apply, --resume, or --verify');
105
- const initialRuntime = liveRuntime();
106
- const wasRunning = initialRuntime.mainCount > 0
107
- || initialRuntime.monitorPids.length > 0
108
- || await daemonIpcReachable();
109
- if (wasRunning) {
110
- console.log('Stopping daemon and all Agent runtimes for data migration...');
111
- // A restart monitor would otherwise relaunch the daemon in the middle of
112
- // a copy transaction, so it must leave the maintenance window first.
113
- await stopRestartMonitors(initialRuntime.monitorPids);
114
- await cmdStop();
115
- await waitForMaintenanceWindow();
116
- }
117
- let manifest = resumeIndex >= 0
118
- ? applyDataMigration(readDataMigration(root, requestedId))
119
- : applyDataMigration(planDataMigration(root));
120
- manifest = verifyDataMigration(manifest);
121
- printSummary(manifest);
122
- // A partial migration must be inspected before any runtime can write the new
123
- // layout. All independent operations have still been attempted and recorded.
124
- if (wasRunning && manifest.state === 'completed') {
125
- console.log('Starting daemon and Agent runtimes after verified migration...');
126
- await cmdStart();
127
- }
128
- else if (wasRunning) {
129
- console.error('Migration has errors; daemon remains stopped. Fix or resume the listed operations before restarting.');
130
- process.exitCode = 1;
131
- }
158
+ throw new Error('select --dry-run, --apply, or --verify');
132
159
  }
@@ -1250,7 +1250,7 @@ async function pickAgentForChannel(rl) {
1250
1250
  const { agents } = loadAllAgents();
1251
1251
  if (agents.length === 0) {
1252
1252
  console.log('❌ 暂无 agent,请先创建:');
1253
- console.log(' ec agent new <aid>.agentid.pub');
1253
+ console.log(' ec agent new <aid>.example.com');
1254
1254
  return null;
1255
1255
  }
1256
1256
  const letters = 'abcdefghijklmnopqrstuvwxyz';
package/dist/cli/init.js CHANGED
@@ -6,7 +6,7 @@ import { resolvePaths, ensureDataDirs } from '../paths.js';
6
6
  import { commandExists } from '../utils/cross-platform.js';
7
7
  import { scanInstances } from '../utils/instance-registry.js';
8
8
  import { saveDefaultsSafe, loadAllAgents, loadDaemonConfig, saveDaemonConfig } from '../config-store.js';
9
- import { generateControlAid, resolveControlIssuer } from '../aun/aid/control-aid.js';
9
+ import { generateControlAid, resolveControlAidDomain } from '../aun/aid/control-aid.js';
10
10
  import { getCodexAppServerAvailability, isCodexAppServerAvailable } from '../agents/codex-runner.js';
11
11
  import { resolveEcagentConfig } from '../agents/baseagent.js';
12
12
  import { defaultProjectsRoot } from '../utils/project-path.js';
@@ -313,29 +313,24 @@ export async function initTail(options = {}) {
313
313
  const { agents } = loadAllAgents();
314
314
  if (agents.length === 0 && !options.invokedByStart) {
315
315
  console.log('\n提示:尚无 agent,运行以下命令创建:');
316
- console.log(' ec agent new <aid>.agentid.pub');
316
+ console.log(' ec agent new <aid>.example.com');
317
317
  }
318
318
  // 控制 AID:daemon 进程身份。缺失则生成并写回 daemon.json(幂等:已存在则跳过)。
319
319
  const daemonConfig = loadDaemonConfig();
320
320
  let controlAidReady = !!daemonConfig.aid;
321
321
  if (daemonConfig.aid) {
322
- const targetIssuer = resolveControlIssuer();
323
- const currentIssuer = daemonConfig.aid.split('.').slice(1).join('.');
324
- if (currentIssuer !== targetIssuer) {
325
- console.log(`⚠️ 控制 AID issuer (${currentIssuer}) 与目标 issuer (${targetIssuer}) 不一致`);
326
- console.log(` 如需切换,请删除 daemon.json 中的 aid 字段后重新运行 init`);
327
- }
328
322
  console.log(`✓ 控制 AID 已存在: ${daemonConfig.aid}`);
329
323
  }
330
324
  else {
331
325
  try {
332
- const { aid } = await generateControlAid();
326
+ const aidDomain = resolveControlAidDomain(daemonConfig.aun?.defaultAidDomain);
327
+ const { aid } = await generateControlAid(aidDomain);
333
328
  saveDaemonConfig({ ...daemonConfig, $schema_version: daemonConfig.$schema_version ?? 1, aid });
334
329
  controlAidReady = true;
335
330
  console.log(`✓ 已生成控制 AID: ${aid}`);
336
331
  }
337
332
  catch (e) {
338
- console.error(`⚠️ 控制 AID 生成失败(Gateway 不可达?联网后重跑 ec init 补全): ${e?.message || e}`);
333
+ console.error(`⚠️ 控制 AID 生成失败(请检查 AID domain 配置或 Gateway 连通性): ${e?.message || e}`);
339
334
  controlAidReady = false;
340
335
  }
341
336
  }
@@ -446,7 +441,7 @@ export async function initTail(options = {}) {
446
441
  const { agents: finalAgents } = loadAllAgents();
447
442
  if (finalAgents.length === 0) {
448
443
  console.log('\n📌 下一步:创建 agent');
449
- console.log(' ec agent new <your-aid>.agentid.pub');
444
+ console.log(' ec agent new <your-aid>.example.com');
450
445
  console.log(' ec init feishu # 绑定飞书');
451
446
  console.log(' ec start # 启动服务');
452
447
  }
@@ -632,11 +627,12 @@ export async function cmdInitNonInteractive(opts, dependencies = {}) {
632
627
  let controlAid = existingCfg.aid;
633
628
  if (!controlAid) {
634
629
  try {
635
- const { aid } = await generateControlAid();
630
+ const aidDomain = resolveControlAidDomain(existingCfg.aun?.defaultAidDomain);
631
+ const { aid } = await generateControlAid(aidDomain);
636
632
  controlAid = aid;
637
633
  }
638
634
  catch (e) {
639
- fail('CONTROL_AID_CREATE_FAILED', `gateway unreachable: ${e?.message || e}`, EXIT_RUNTIME, format);
635
+ fail('CONTROL_AID_CREATE_FAILED', `control AID creation failed: ${e?.message || e}`, EXIT_RUNTIME, format);
640
636
  }
641
637
  }
642
638
  // ── 9. 写 daemon.json(aid + owners + ecweb)──