@netpilot/skills 0.4.0 → 0.6.0
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/scripts/sync.mjs +354 -17
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
"owner": {
|
|
5
5
|
"name": "NetPilot"
|
|
6
6
|
},
|
|
7
|
-
"description": "NetPilot
|
|
7
|
+
"description": "NetPilot AI 工程协作插件目录",
|
|
8
8
|
"plugins": [
|
|
9
9
|
{
|
|
10
10
|
"name": "netpilot-skills",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "中文工程协作 skills 与可组合工作流",
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.6.0",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "NetPilot"
|
|
16
16
|
},
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "netpilot-skills",
|
|
4
4
|
"displayName": "NetPilot Skills",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.6.0",
|
|
6
6
|
"description": "面向 Claude Code 的中文工程协作 skills 与可组合工作流",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "NetPilot"
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.6.0
|
|
8
|
+
|
|
9
|
+
- 将用户级与项目级安装状态迁移到中性的 `.agents/.state/skills-installer`;旧 `.netpilot-skills` 在 apply 时经锁保护、目录身份复核后原子迁移,preview 仅报告计划。旧版客户端留下的精确空目录可安全自愈,真实双状态或目标冲突仍整批停止。
|
|
10
|
+
- 项目安装继续只写入 `.agents/skills`,不安装项目级 Agents;用户级默认仍安装 Codex Skills 与少量 Codex Agents。
|
|
11
|
+
- 与 `@netpilot/harness` 协调发布同一 `v0.6.0` 标签,便于在新电脑或新项目中识别兼容的一组安装器能力;两个包仍保持独立职责和发布契约。
|
|
12
|
+
|
|
7
13
|
## 0.4.0
|
|
8
14
|
|
|
9
15
|
- 将工程与生产力方法扩展为 21 个去个人化 skills,按上游原有结构恢复完整工作流、步骤内门禁、方法自身的反模式,以及有实际价值的 `references/` 和跨平台脚本;不再为每个 skill 强加统一尾部章节。
|
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ npm install --global @netpilot/skills
|
|
|
42
42
|
netpilot-skills
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
安装器不会覆盖未受管理或用户修改过的同名文件;发生冲突时整批停止。同步状态保存在 `.agents/.state/skills-installer`:用户安装位于 home,项目安装位于项目根;旧 `.netpilot-skills` 会在下一次实际安装时原子迁移,预览只展示计划。旧版安装器在新状态旁留下的空旧目录会安全自愈,含内容的双状态仍会阻断。项目范围只安装 Skills,项目级 Agents 仍由 [NetPilot Harness](https://github.com/netpilot-z/harness) 管理。
|
|
46
46
|
|
|
47
47
|
## 核心用法
|
|
48
48
|
|
package/package.json
CHANGED
package/scripts/sync.mjs
CHANGED
|
@@ -24,6 +24,10 @@ const HOST_DESTINATIONS = {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
const CODEX_AGENT_DESTINATION = [".codex", "agents"];
|
|
27
|
+
const CURRENT_STATE_DESTINATION = [".agents", ".state", "skills-installer"];
|
|
28
|
+
const LEGACY_STATE_DESTINATION = [".netpilot-skills"];
|
|
29
|
+
const LAYOUT_LOCK_DIRECTORY = [".agents", ".state"];
|
|
30
|
+
const LAYOUT_LOCK_NAME = "skills-installer.layout.lock";
|
|
27
31
|
|
|
28
32
|
const ALL_HOSTS = Object.freeze(Object.keys(HOST_DESTINATIONS));
|
|
29
33
|
const ALL_COMPONENTS = Object.freeze(["skills", "agents"]);
|
|
@@ -354,6 +358,100 @@ async function assertSafeHomeRoot(homeDir) {
|
|
|
354
358
|
]);
|
|
355
359
|
}
|
|
356
360
|
|
|
361
|
+
function resolveStatePaths(homeDir) {
|
|
362
|
+
const currentStateDir = path.join(homeDir, ...CURRENT_STATE_DESTINATION);
|
|
363
|
+
const legacyStateDir = path.join(homeDir, ...LEGACY_STATE_DESTINATION);
|
|
364
|
+
return {
|
|
365
|
+
currentStateDir,
|
|
366
|
+
currentManifestPath: path.join(currentStateDir, "manifest.json"),
|
|
367
|
+
legacyStateDir,
|
|
368
|
+
legacyManifestPath: path.join(legacyStateDir, "manifest.json"),
|
|
369
|
+
layoutLockDir: path.join(homeDir, ...LAYOUT_LOCK_DIRECTORY),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function stateLayoutConflict(homeDir, reason, targetPath = homeDir) {
|
|
374
|
+
return new SyncConflictError([
|
|
375
|
+
{
|
|
376
|
+
host: "state",
|
|
377
|
+
component: "layout",
|
|
378
|
+
assetName: "state-layout",
|
|
379
|
+
skillName: "state-layout",
|
|
380
|
+
relativePath: ".",
|
|
381
|
+
targetPath,
|
|
382
|
+
action: "conflict",
|
|
383
|
+
reason,
|
|
384
|
+
},
|
|
385
|
+
]);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function inspectStateDirectory(homeDir, targetPath) {
|
|
389
|
+
const ancestor = await inspectAncestorPath(homeDir, targetPath);
|
|
390
|
+
if (!ancestor.safe) {
|
|
391
|
+
throw stateLayoutConflict(
|
|
392
|
+
homeDir,
|
|
393
|
+
`${ancestor.reason}:${ancestor.path}`,
|
|
394
|
+
targetPath,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const stats = await lstat(targetPath);
|
|
399
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
400
|
+
throw stateLayoutConflict(
|
|
401
|
+
homeDir,
|
|
402
|
+
"状态路径必须是非符号链接目录",
|
|
403
|
+
targetPath,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
exists: true,
|
|
408
|
+
path: targetPath,
|
|
409
|
+
identity: { dev: stats.dev, ino: stats.ino },
|
|
410
|
+
};
|
|
411
|
+
} catch (error) {
|
|
412
|
+
if (error.code === "ENOENT") return { exists: false, path: targetPath };
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function sameStateDirectoryIdentity(left, right) {
|
|
418
|
+
return left?.dev === right?.dev && left?.ino === right?.ino;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function inspectStateLayout(homeDir, paths = resolveStatePaths(homeDir)) {
|
|
422
|
+
const [current, legacy] = await Promise.all([
|
|
423
|
+
inspectStateDirectory(homeDir, paths.currentStateDir),
|
|
424
|
+
inspectStateDirectory(homeDir, paths.legacyStateDir),
|
|
425
|
+
]);
|
|
426
|
+
if (current.exists && legacy.exists) {
|
|
427
|
+
const legacyEntries = await readdir(legacy.path);
|
|
428
|
+
const latestLegacy = await inspectStateDirectory(homeDir, legacy.path);
|
|
429
|
+
if (
|
|
430
|
+
legacyEntries.length === 0 &&
|
|
431
|
+
latestLegacy.exists &&
|
|
432
|
+
sameStateDirectoryIdentity(legacy.identity, latestLegacy.identity)
|
|
433
|
+
) {
|
|
434
|
+
return {
|
|
435
|
+
kind: "current-with-empty-legacy",
|
|
436
|
+
current,
|
|
437
|
+
legacy: latestLegacy,
|
|
438
|
+
paths,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
throw stateLayoutConflict(
|
|
442
|
+
homeDir,
|
|
443
|
+
"新旧状态目录同时存在,拒绝自动合并 ownership",
|
|
444
|
+
paths.currentStateDir,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
kind: current.exists ? "current" : legacy.exists ? "legacy" : "fresh",
|
|
449
|
+
current,
|
|
450
|
+
legacy,
|
|
451
|
+
paths,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
357
455
|
function parseLockRecord(content) {
|
|
358
456
|
const trimmed = content.trim();
|
|
359
457
|
if (/^[1-9][0-9]*$/u.test(trimmed)) {
|
|
@@ -411,9 +509,13 @@ async function moveLockForCompare({ lockPath, expectedContent, stateDir, purpose
|
|
|
411
509
|
throw error;
|
|
412
510
|
}
|
|
413
511
|
|
|
414
|
-
async function acquireSyncLock({
|
|
415
|
-
|
|
416
|
-
|
|
512
|
+
async function acquireSyncLock({
|
|
513
|
+
homeDir,
|
|
514
|
+
stateDir,
|
|
515
|
+
lockName = "sync.lock",
|
|
516
|
+
beforeLockPublish,
|
|
517
|
+
}) {
|
|
518
|
+
let stateSafety = await inspectAncestorPath(homeDir, stateDir);
|
|
417
519
|
if (!stateSafety.safe) {
|
|
418
520
|
throw new SyncConflictError([
|
|
419
521
|
{
|
|
@@ -421,15 +523,24 @@ async function acquireSyncLock({ homeDir, stateDir, transactionHooks = {} }) {
|
|
|
421
523
|
component: "lock",
|
|
422
524
|
assetName: "sync",
|
|
423
525
|
skillName: "sync",
|
|
424
|
-
relativePath:
|
|
425
|
-
targetPath: path.join(stateDir,
|
|
526
|
+
relativePath: lockName,
|
|
527
|
+
targetPath: path.join(stateDir, lockName),
|
|
426
528
|
action: "conflict",
|
|
427
529
|
reason: `${stateSafety.reason}:${stateSafety.path}`,
|
|
428
530
|
},
|
|
429
531
|
]);
|
|
430
532
|
}
|
|
533
|
+
await mkdir(stateDir, { recursive: true });
|
|
534
|
+
stateSafety = await inspectAncestorPath(homeDir, stateDir);
|
|
535
|
+
if (!stateSafety.safe) {
|
|
536
|
+
throw stateLayoutConflict(
|
|
537
|
+
homeDir,
|
|
538
|
+
`${stateSafety.reason}:${stateSafety.path}`,
|
|
539
|
+
stateDir,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
431
542
|
|
|
432
|
-
const lockPath = path.join(stateDir,
|
|
543
|
+
const lockPath = path.join(stateDir, lockName);
|
|
433
544
|
const owner = randomUUID();
|
|
434
545
|
const recordContent = `${JSON.stringify({
|
|
435
546
|
schemaVersion: 1,
|
|
@@ -437,7 +548,10 @@ async function acquireSyncLock({ homeDir, stateDir, transactionHooks = {} }) {
|
|
|
437
548
|
owner,
|
|
438
549
|
createdAt: new Date().toISOString(),
|
|
439
550
|
})}\n`;
|
|
440
|
-
const stagedLockPath = path.join(
|
|
551
|
+
const stagedLockPath = path.join(
|
|
552
|
+
stateDir,
|
|
553
|
+
`${lockName.replaceAll(".", "-")}-${owner}.tmp`,
|
|
554
|
+
);
|
|
441
555
|
await writeFile(stagedLockPath, recordContent, { encoding: "utf8", flag: "wx" });
|
|
442
556
|
if ((await readFile(stagedLockPath, "utf8")) !== recordContent) {
|
|
443
557
|
await rm(stagedLockPath, { force: true });
|
|
@@ -445,7 +559,7 @@ async function acquireSyncLock({ homeDir, stateDir, transactionHooks = {} }) {
|
|
|
445
559
|
}
|
|
446
560
|
|
|
447
561
|
try {
|
|
448
|
-
await
|
|
562
|
+
await beforeLockPublish?.();
|
|
449
563
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
450
564
|
try {
|
|
451
565
|
await link(stagedLockPath, lockPath);
|
|
@@ -1303,6 +1417,111 @@ async function applyOperationsTransactional({
|
|
|
1303
1417
|
}
|
|
1304
1418
|
}
|
|
1305
1419
|
|
|
1420
|
+
async function migrateLegacyState({
|
|
1421
|
+
homeDir,
|
|
1422
|
+
paths,
|
|
1423
|
+
transactionHooks,
|
|
1424
|
+
}) {
|
|
1425
|
+
let syncLock = await acquireSyncLock({
|
|
1426
|
+
homeDir,
|
|
1427
|
+
stateDir: paths.legacyStateDir,
|
|
1428
|
+
beforeLockPublish: transactionHooks.beforeLockPublish,
|
|
1429
|
+
});
|
|
1430
|
+
let moved = false;
|
|
1431
|
+
try {
|
|
1432
|
+
const freshLayout = await inspectStateLayout(homeDir, paths);
|
|
1433
|
+
if (freshLayout.kind !== "legacy") {
|
|
1434
|
+
throw stateLayoutConflict(
|
|
1435
|
+
homeDir,
|
|
1436
|
+
"获取旧状态锁后布局发生变化,拒绝继续迁移",
|
|
1437
|
+
paths.legacyStateDir,
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
const lockedSourceIdentity = freshLayout.legacy.identity;
|
|
1441
|
+
const legacyManifest = await inspectTarget(paths.legacyManifestPath);
|
|
1442
|
+
if (!legacyManifest.exists || !legacyManifest.regularFile) {
|
|
1443
|
+
throw stateLayoutConflict(
|
|
1444
|
+
homeDir,
|
|
1445
|
+
"旧状态目录缺少可信的普通 manifest.json",
|
|
1446
|
+
paths.legacyManifestPath,
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
await readManifest(paths.legacyManifestPath);
|
|
1450
|
+
await transactionHooks.beforeStateMigrationRename?.();
|
|
1451
|
+
const beforeRename = await inspectStateLayout(homeDir, paths);
|
|
1452
|
+
if (
|
|
1453
|
+
beforeRename.kind !== "legacy" ||
|
|
1454
|
+
!sameStateDirectoryIdentity(
|
|
1455
|
+
lockedSourceIdentity,
|
|
1456
|
+
beforeRename.legacy.identity,
|
|
1457
|
+
)
|
|
1458
|
+
) {
|
|
1459
|
+
throw stateLayoutConflict(
|
|
1460
|
+
homeDir,
|
|
1461
|
+
"旧状态目录身份在迁移提交前发生变化",
|
|
1462
|
+
paths.currentStateDir,
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
try {
|
|
1466
|
+
await rename(paths.legacyStateDir, paths.currentStateDir);
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
if (error.code === "EXDEV") {
|
|
1469
|
+
throw stateLayoutConflict(
|
|
1470
|
+
homeDir,
|
|
1471
|
+
"新旧状态目录不在同一文件系统,拒绝退化为非原子复制",
|
|
1472
|
+
paths.currentStateDir,
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
if (["EEXIST", "ENOTEMPTY"].includes(error.code)) {
|
|
1476
|
+
throw stateLayoutConflict(
|
|
1477
|
+
homeDir,
|
|
1478
|
+
"新状态目录在原子迁移前已出现",
|
|
1479
|
+
paths.currentStateDir,
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
throw error;
|
|
1483
|
+
}
|
|
1484
|
+
moved = true;
|
|
1485
|
+
syncLock = {
|
|
1486
|
+
...syncLock,
|
|
1487
|
+
stateDir: paths.currentStateDir,
|
|
1488
|
+
lockPath: path.join(paths.currentStateDir, "sync.lock"),
|
|
1489
|
+
};
|
|
1490
|
+
const migratedLayout = await inspectStateLayout(homeDir, paths);
|
|
1491
|
+
if (
|
|
1492
|
+
migratedLayout.kind !== "current" ||
|
|
1493
|
+
!sameStateDirectoryIdentity(
|
|
1494
|
+
lockedSourceIdentity,
|
|
1495
|
+
migratedLayout.current.identity,
|
|
1496
|
+
)
|
|
1497
|
+
) {
|
|
1498
|
+
const error = new Error("状态目录迁移后的目标身份不一致");
|
|
1499
|
+
error.preserveEvidence = true;
|
|
1500
|
+
throw error;
|
|
1501
|
+
}
|
|
1502
|
+
const migratedManifest = await inspectTarget(paths.currentManifestPath);
|
|
1503
|
+
if (
|
|
1504
|
+
!migratedManifest.regularFile ||
|
|
1505
|
+
migratedManifest.hash !== legacyManifest.hash
|
|
1506
|
+
) {
|
|
1507
|
+
const error = new Error("状态目录迁移后 manifest digest 不一致");
|
|
1508
|
+
error.preserveEvidence = true;
|
|
1509
|
+
throw error;
|
|
1510
|
+
}
|
|
1511
|
+
await transactionHooks.afterStateMigrationRename?.();
|
|
1512
|
+
return syncLock;
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
try {
|
|
1515
|
+
await releaseSyncLock(syncLock);
|
|
1516
|
+
} catch (releaseError) {
|
|
1517
|
+
error.releaseError = releaseError;
|
|
1518
|
+
error.preserveEvidence = true;
|
|
1519
|
+
}
|
|
1520
|
+
if (moved) error.stateMigrationCommitted = true;
|
|
1521
|
+
throw error;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1306
1525
|
export async function syncSkills({
|
|
1307
1526
|
rootDir,
|
|
1308
1527
|
homeDir,
|
|
@@ -1329,14 +1548,114 @@ export async function syncSkills({
|
|
|
1329
1548
|
const resolvedRoot = path.resolve(rootDir);
|
|
1330
1549
|
const resolvedHome = path.resolve(homeDir);
|
|
1331
1550
|
await assertSafeHomeRoot(resolvedHome);
|
|
1332
|
-
const
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1551
|
+
const statePaths = resolveStatePaths(resolvedHome);
|
|
1552
|
+
let stateLayout = await inspectStateLayout(resolvedHome, statePaths);
|
|
1553
|
+
let stateDir =
|
|
1554
|
+
stateLayout.kind === "legacy"
|
|
1555
|
+
? statePaths.legacyStateDir
|
|
1556
|
+
: statePaths.currentStateDir;
|
|
1557
|
+
let manifestPath =
|
|
1558
|
+
stateLayout.kind === "legacy"
|
|
1559
|
+
? statePaths.legacyManifestPath
|
|
1560
|
+
: statePaths.currentManifestPath;
|
|
1561
|
+
let stateMigration =
|
|
1562
|
+
stateLayout.kind === "legacy"
|
|
1563
|
+
? {
|
|
1564
|
+
action: "move",
|
|
1565
|
+
from: statePaths.legacyStateDir,
|
|
1566
|
+
to: statePaths.currentStateDir,
|
|
1567
|
+
}
|
|
1568
|
+
: null;
|
|
1569
|
+
let stateRepair =
|
|
1570
|
+
stateLayout.kind === "current-with-empty-legacy"
|
|
1571
|
+
? {
|
|
1572
|
+
action: "remove-empty-legacy-state",
|
|
1573
|
+
target: statePaths.legacyStateDir,
|
|
1574
|
+
}
|
|
1575
|
+
: null;
|
|
1576
|
+
let layoutLock = null;
|
|
1577
|
+
let syncLock = null;
|
|
1337
1578
|
let syncError = null;
|
|
1338
1579
|
|
|
1339
1580
|
try {
|
|
1581
|
+
if (apply) {
|
|
1582
|
+
layoutLock = await acquireSyncLock({
|
|
1583
|
+
homeDir: resolvedHome,
|
|
1584
|
+
stateDir: statePaths.layoutLockDir,
|
|
1585
|
+
lockName: LAYOUT_LOCK_NAME,
|
|
1586
|
+
beforeLockPublish: transactionHooks.beforeLayoutLockPublish,
|
|
1587
|
+
});
|
|
1588
|
+
stateLayout = await inspectStateLayout(resolvedHome, statePaths);
|
|
1589
|
+
if (stateLayout.kind === "current-with-empty-legacy") {
|
|
1590
|
+
stateRepair = {
|
|
1591
|
+
action: "remove-empty-legacy-state",
|
|
1592
|
+
target: statePaths.legacyStateDir,
|
|
1593
|
+
};
|
|
1594
|
+
const expectedCurrentIdentity = stateLayout.current.identity;
|
|
1595
|
+
try {
|
|
1596
|
+
await rmdir(statePaths.legacyStateDir);
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
if (["ENOTEMPTY", "EEXIST"].includes(error.code)) {
|
|
1599
|
+
throw stateLayoutConflict(
|
|
1600
|
+
resolvedHome,
|
|
1601
|
+
"空旧状态目录在清理前出现内容,拒绝继续",
|
|
1602
|
+
statePaths.legacyStateDir,
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
throw error;
|
|
1606
|
+
}
|
|
1607
|
+
const repairedLayout = await inspectStateLayout(
|
|
1608
|
+
resolvedHome,
|
|
1609
|
+
statePaths,
|
|
1610
|
+
);
|
|
1611
|
+
if (
|
|
1612
|
+
repairedLayout.kind !== "current" ||
|
|
1613
|
+
!sameStateDirectoryIdentity(
|
|
1614
|
+
expectedCurrentIdentity,
|
|
1615
|
+
repairedLayout.current.identity,
|
|
1616
|
+
)
|
|
1617
|
+
) {
|
|
1618
|
+
throw stateLayoutConflict(
|
|
1619
|
+
resolvedHome,
|
|
1620
|
+
"清理旧状态残留后当前状态目录身份发生变化",
|
|
1621
|
+
statePaths.currentStateDir,
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
stateLayout = repairedLayout;
|
|
1625
|
+
}
|
|
1626
|
+
if (stateLayout.kind === "legacy") {
|
|
1627
|
+
stateMigration = {
|
|
1628
|
+
action: "move",
|
|
1629
|
+
from: statePaths.legacyStateDir,
|
|
1630
|
+
to: statePaths.currentStateDir,
|
|
1631
|
+
};
|
|
1632
|
+
syncLock = await migrateLegacyState({
|
|
1633
|
+
homeDir: resolvedHome,
|
|
1634
|
+
paths: statePaths,
|
|
1635
|
+
transactionHooks,
|
|
1636
|
+
});
|
|
1637
|
+
} else {
|
|
1638
|
+
syncLock = await acquireSyncLock({
|
|
1639
|
+
homeDir: resolvedHome,
|
|
1640
|
+
stateDir: statePaths.currentStateDir,
|
|
1641
|
+
beforeLockPublish: transactionHooks.beforeLockPublish,
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
const lockedLayout = await inspectStateLayout(
|
|
1645
|
+
resolvedHome,
|
|
1646
|
+
statePaths,
|
|
1647
|
+
);
|
|
1648
|
+
if (lockedLayout.kind !== "current") {
|
|
1649
|
+
throw stateLayoutConflict(
|
|
1650
|
+
resolvedHome,
|
|
1651
|
+
"获取同步锁后项目状态布局发生变化,拒绝继续",
|
|
1652
|
+
statePaths.currentStateDir,
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
stateLayout = lockedLayout;
|
|
1656
|
+
stateDir = statePaths.currentStateDir;
|
|
1657
|
+
manifestPath = statePaths.currentManifestPath;
|
|
1658
|
+
}
|
|
1340
1659
|
if (syncLock) await transactionHooks.afterLockAcquired?.();
|
|
1341
1660
|
const stateSafety = await inspectAncestorPath(resolvedHome, stateDir);
|
|
1342
1661
|
if (!stateSafety.safe) {
|
|
@@ -1468,7 +1787,12 @@ export async function syncSkills({
|
|
|
1468
1787
|
const result = {
|
|
1469
1788
|
mode: apply ? "apply" : "dry-run",
|
|
1470
1789
|
homeDir: resolvedHome,
|
|
1471
|
-
manifestPath,
|
|
1790
|
+
manifestPath: statePaths.currentManifestPath,
|
|
1791
|
+
...(manifestPath !== statePaths.currentManifestPath
|
|
1792
|
+
? { sourceManifestPath: manifestPath }
|
|
1793
|
+
: {}),
|
|
1794
|
+
...(stateMigration ? { stateMigration } : {}),
|
|
1795
|
+
...(stateRepair ? { stateRepair } : {}),
|
|
1472
1796
|
operations,
|
|
1473
1797
|
conflicts,
|
|
1474
1798
|
};
|
|
@@ -1488,18 +1812,21 @@ export async function syncSkills({
|
|
|
1488
1812
|
syncError = error;
|
|
1489
1813
|
throw error;
|
|
1490
1814
|
} finally {
|
|
1491
|
-
|
|
1815
|
+
let releaseFailure = null;
|
|
1816
|
+
for (const lock of [syncLock, layoutLock]) {
|
|
1817
|
+
if (!lock) continue;
|
|
1492
1818
|
try {
|
|
1493
|
-
await releaseSyncLock(
|
|
1819
|
+
await releaseSyncLock(lock);
|
|
1494
1820
|
} catch (releaseError) {
|
|
1495
1821
|
if (syncError) {
|
|
1496
1822
|
syncError.releaseError = releaseError;
|
|
1497
1823
|
syncError.preserveEvidence = true;
|
|
1498
1824
|
} else {
|
|
1499
|
-
|
|
1825
|
+
releaseFailure ??= releaseError;
|
|
1500
1826
|
}
|
|
1501
1827
|
}
|
|
1502
1828
|
}
|
|
1829
|
+
if (!syncError && releaseFailure) throw releaseFailure;
|
|
1503
1830
|
}
|
|
1504
1831
|
}
|
|
1505
1832
|
|
|
@@ -1518,6 +1845,16 @@ Claude Code 目标:~/.claude/skills(选择 --host claude 时默认只安装
|
|
|
1518
1845
|
}
|
|
1519
1846
|
|
|
1520
1847
|
function printPlan(result) {
|
|
1848
|
+
if (result.stateRepair) {
|
|
1849
|
+
console.log(
|
|
1850
|
+
`[remove-empty] state ${result.stateRepair.target}`,
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
if (result.stateMigration) {
|
|
1854
|
+
console.log(
|
|
1855
|
+
`[move] state ${result.stateMigration.from} -> ${result.stateMigration.to}`,
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1521
1858
|
for (const operation of result.operations) {
|
|
1522
1859
|
const reason = operation.reason ? `(${operation.reason})` : "";
|
|
1523
1860
|
const component = operation.component === "skills" ? "skills" : operation.component;
|