@jackss119/shelf 1.0.0 → 1.2.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/bin/shelf.mjs +90 -78
- package/lib/commands/shelf.mjs +522 -175
- package/lib/manifest.mjs +2 -0
- package/package.json +1 -1
package/bin/shelf.mjs
CHANGED
|
@@ -1,78 +1,90 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
cmdShelfBrowse,
|
|
4
|
-
cmdShelfPull,
|
|
5
|
-
cmdShelfPush,
|
|
6
|
-
cmdShelfCreate,
|
|
7
|
-
cmdShelfSync,
|
|
8
|
-
cmdShelfInit,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
shelf
|
|
20
|
-
|
|
21
|
-
shelf
|
|
22
|
-
[--
|
|
23
|
-
shelf
|
|
24
|
-
|
|
25
|
-
shelf
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
case "
|
|
46
|
-
await
|
|
47
|
-
break;
|
|
48
|
-
case "
|
|
49
|
-
await
|
|
50
|
-
break;
|
|
51
|
-
case "
|
|
52
|
-
await
|
|
53
|
-
break;
|
|
54
|
-
case "
|
|
55
|
-
await
|
|
56
|
-
break;
|
|
57
|
-
case "
|
|
58
|
-
await
|
|
59
|
-
break;
|
|
60
|
-
case "
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
case "
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
cmdShelfBrowse,
|
|
4
|
+
cmdShelfPull,
|
|
5
|
+
cmdShelfPush,
|
|
6
|
+
cmdShelfCreate,
|
|
7
|
+
cmdShelfSync,
|
|
8
|
+
cmdShelfInit,
|
|
9
|
+
cmdShelfAgents,
|
|
10
|
+
cmdShelfAdopt,
|
|
11
|
+
cmdShelfHome,
|
|
12
|
+
} from "../lib/commands/shelf.mjs";
|
|
13
|
+
|
|
14
|
+
function usage() {
|
|
15
|
+
console.log(`shelf — 个人内容货架 CLI(别名 atk)
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
shelf 交互浏览货架(数字进目录,p 1,3-5 拉取,a 全部)
|
|
19
|
+
shelf pull <路径...> 按路径拉取,如 shelf pull skills/common/intj
|
|
20
|
+
[--dest <目录>]
|
|
21
|
+
shelf create <本地路径> 上架新货:全架查重名,交互选位(m <名> 建目录,
|
|
22
|
+
d 放下)[--to <货架目录>] [--yes]
|
|
23
|
+
shelf push <本地路径> 更新已有货:按记账/名字自动定位
|
|
24
|
+
[--yes] [--force] [--force-secret]
|
|
25
|
+
shelf sync [--dry-run] 按账本对账:库上新版自动更新本地;本地有改动
|
|
26
|
+
则显示 diff 由你决定推上库还是覆盖本地
|
|
27
|
+
shelf init 项目接入(幂等):交互多选 agent 目标,或
|
|
28
|
+
--agents claude,codex,kimi / all
|
|
29
|
+
shelf agents [add <名>] 查看 / 追加 agent 目标(claude / codex / kimi)
|
|
30
|
+
shelf adopt 收编外来技能:实体目录迁入正本重链 + 未记账技能
|
|
31
|
+
登记进账本 local 段(不上货架)
|
|
32
|
+
shelf home [--update] 查看货架在哪、什么模式;--update 拉取最新
|
|
33
|
+
|
|
34
|
+
货架定位:SHELF_HOME 环境变量 > 本 clone > ~/.shelfrc > 托管档口 ~/.shelf/home
|
|
35
|
+
免 clone 直跑:npx -y -p github:Jackzz119/my-workspace shelf <命令>`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const argv = process.argv.slice(2);
|
|
39
|
+
const sub = argv[0];
|
|
40
|
+
const rest = argv.slice(1);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
switch (sub) {
|
|
44
|
+
case undefined:
|
|
45
|
+
case "browse":
|
|
46
|
+
await cmdShelfBrowse();
|
|
47
|
+
break;
|
|
48
|
+
case "pull":
|
|
49
|
+
await cmdShelfPull(rest);
|
|
50
|
+
break;
|
|
51
|
+
case "create":
|
|
52
|
+
await cmdShelfCreate(rest);
|
|
53
|
+
break;
|
|
54
|
+
case "push":
|
|
55
|
+
await cmdShelfPush(rest);
|
|
56
|
+
break;
|
|
57
|
+
case "sync":
|
|
58
|
+
await cmdShelfSync(rest);
|
|
59
|
+
break;
|
|
60
|
+
case "init":
|
|
61
|
+
await cmdShelfInit(rest);
|
|
62
|
+
break;
|
|
63
|
+
case "agents":
|
|
64
|
+
await cmdShelfAgents(rest);
|
|
65
|
+
break;
|
|
66
|
+
case "adopt":
|
|
67
|
+
await cmdShelfAdopt(rest);
|
|
68
|
+
break;
|
|
69
|
+
case "home":
|
|
70
|
+
await cmdShelfHome(rest);
|
|
71
|
+
break;
|
|
72
|
+
case "-h":
|
|
73
|
+
case "--help":
|
|
74
|
+
case "help":
|
|
75
|
+
usage();
|
|
76
|
+
break;
|
|
77
|
+
case "skills":
|
|
78
|
+
case "list":
|
|
79
|
+
console.error(`'shelf ${sub}' 已随老版技能命令清退:浏览用 shelf,按路径拉取用 shelf pull <路径> --dest .claude/skills,保持最新用 shelf sync`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
break;
|
|
82
|
+
default:
|
|
83
|
+
console.error(`Unknown command: ${sub}\n`);
|
|
84
|
+
usage();
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error(err.message || err);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
package/lib/commands/shelf.mjs
CHANGED
|
@@ -732,33 +732,365 @@ export async function cmdShelfCreate(argv) {
|
|
|
732
732
|
}
|
|
733
733
|
}
|
|
734
734
|
|
|
735
|
-
// ----
|
|
735
|
+
// ---- init 植入协议(SHELF 决策 #21/#22/#23)----
|
|
736
|
+
|
|
737
|
+
// gitignore 补行:只补缺失的,其余不动;目录行同时认 ".claude" 与 ".claude/"
|
|
738
|
+
function ensureGitignore(cwd, lines) {
|
|
739
|
+
const p = path.join(cwd, ".gitignore");
|
|
740
|
+
const content = fs.existsSync(p) ? fs.readFileSync(p, "utf8") : "";
|
|
741
|
+
const NL = String.fromCharCode(10);
|
|
742
|
+
const CR = String.fromCharCode(13);
|
|
743
|
+
const have = new Set(content.split(NL).map((l) => l.replaceAll(CR, "").trim()));
|
|
744
|
+
const missing = lines.filter((l) => {
|
|
745
|
+
const bare = l.endsWith("/") ? l.slice(0, -1) : l;
|
|
746
|
+
return !have.has(bare) && !have.has(bare + "/");
|
|
747
|
+
});
|
|
748
|
+
if (missing.length === 0) return [];
|
|
749
|
+
const sep = content === "" || content.endsWith(NL) ? "" : NL;
|
|
750
|
+
fs.writeFileSync(p, content + sep + "# shelf init: agent 资产不入项目库" + NL + missing.join(NL) + NL, "utf8");
|
|
751
|
+
return missing;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// ---- agent 目标与技能正本(SHELF 决策 #24/#26)----
|
|
755
|
+
|
|
756
|
+
// 项目里技能的唯一正本;所有 agent 目录都是指向它的链接
|
|
757
|
+
const CANONICAL_SKILLS_DIR = "ai/jaSkills";
|
|
758
|
+
|
|
759
|
+
const AGENT_TARGETS = {
|
|
760
|
+
claude: { skillsDir: ".claude/skills", doc: { input: "agents/claude/CLAUDE.md", dest: "CLAUDE.md" }, ignore: [".claude/", "CLAUDE.md"] },
|
|
761
|
+
codex: { skillsDir: ".codex/skills", doc: { input: "agents/codex/AGENTS.md", dest: "AGENTS.md" }, ignore: [".codex/", "AGENTS.md"] },
|
|
762
|
+
kimi: { skillsDir: ".kimi/skills", doc: { input: "agents/codex/AGENTS.md", dest: "AGENTS.md" }, ignore: [".kimi/", "AGENTS.md"] },
|
|
763
|
+
};
|
|
764
|
+
const TARGET_PRIORITY = ["claude", "codex", "kimi"];
|
|
765
|
+
|
|
766
|
+
// "claude,codex" / "all" → 校验并按固定顺序去重
|
|
767
|
+
function parseTargetNames(spec) {
|
|
768
|
+
const names = spec === "all"
|
|
769
|
+
? [...TARGET_PRIORITY]
|
|
770
|
+
: String(spec).split(",").map((x) => x.trim()).filter(Boolean);
|
|
771
|
+
const bad = names.filter((n) => !AGENT_TARGETS[n]);
|
|
772
|
+
if (bad.length) {
|
|
773
|
+
console.error("✗ 未知 agent 目标: " + bad.join(", ") + "(可选: " + TARGET_PRIORITY.join(" / ") + " / all)");
|
|
774
|
+
process.exit(1);
|
|
775
|
+
}
|
|
776
|
+
return TARGET_PRIORITY.filter((n) => names.includes(n));
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async function askTargets() {
|
|
780
|
+
const rl = makeLineReader();
|
|
781
|
+
try {
|
|
782
|
+
console.log("要接入哪些 agent?(各自的技能目录会链接到正本 " + CANONICAL_SKILLS_DIR + "/)");
|
|
783
|
+
TARGET_PRIORITY.forEach((n, i) => {
|
|
784
|
+
const t = AGENT_TARGETS[n];
|
|
785
|
+
console.log(" " + (i + 1) + ". " + n + " (" + t.skillsDir + "/ · " + t.doc.dest + ")");
|
|
786
|
+
});
|
|
787
|
+
while (true) {
|
|
788
|
+
const raw = await rl.question("选择编号,逗号分隔(如 1,2);a=全部 > ");
|
|
789
|
+
if (raw === null) return null;
|
|
790
|
+
const ans = raw.trim().toLowerCase();
|
|
791
|
+
if (ans === "a") return [...TARGET_PRIORITY];
|
|
792
|
+
const idx = parseIndices(ans, TARGET_PRIORITY.length);
|
|
793
|
+
if (idx && idx.length) return idx.map((i) => TARGET_PRIORITY[i]);
|
|
794
|
+
console.log(" 没看懂。例如: 1,2 或 a");
|
|
795
|
+
}
|
|
796
|
+
} finally {
|
|
797
|
+
rl.close();
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// 把 agent 的技能目录做成指向正本的链接(Windows junction 免管理员 / POSIX 相对 symlink)。
|
|
802
|
+
// 遇到老式实体目录(复制镜像时代)自动迁移:内容并入正本(同名跳过),原地替换为链接。
|
|
803
|
+
// 返回 "ok"(已正确)/ "created" / "fixed"
|
|
804
|
+
function ensureSkillsLink(cwd, targetName) {
|
|
805
|
+
const rel = AGENT_TARGETS[targetName].skillsDir;
|
|
806
|
+
const linkPath = path.join(cwd, ...rel.split("/"));
|
|
807
|
+
const canonical = path.join(cwd, ...CANONICAL_SKILLS_DIR.split("/"));
|
|
808
|
+
fs.mkdirSync(canonical, { recursive: true });
|
|
809
|
+
fs.mkdirSync(path.dirname(linkPath), { recursive: true });
|
|
810
|
+
|
|
811
|
+
let st = null;
|
|
812
|
+
try { st = fs.lstatSync(linkPath); } catch { st = null; }
|
|
813
|
+
|
|
814
|
+
if (st && st.isSymbolicLink()) {
|
|
815
|
+
try {
|
|
816
|
+
if (fs.realpathSync(linkPath) === fs.realpathSync(canonical)) return "ok";
|
|
817
|
+
} catch { /* 断链 → 重建 */ }
|
|
818
|
+
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
819
|
+
} else if (st && st.isDirectory()) {
|
|
820
|
+
let moved = 0;
|
|
821
|
+
let skipped = 0;
|
|
822
|
+
for (const e of fs.readdirSync(linkPath, { withFileTypes: true })) {
|
|
823
|
+
if (IGNORE_NAMES.has(e.name)) continue;
|
|
824
|
+
const from = path.join(linkPath, e.name);
|
|
825
|
+
const to = path.join(canonical, e.name);
|
|
826
|
+
if (fs.existsSync(to)) {
|
|
827
|
+
skipped++;
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
fs.cpSync(from, to, { recursive: true });
|
|
831
|
+
moved++;
|
|
832
|
+
}
|
|
833
|
+
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
834
|
+
if (moved || skipped) {
|
|
835
|
+
console.log("↪ " + rel + " 原为实体目录:迁入正本 " + moved + " 项"
|
|
836
|
+
+ (skipped ? ",同名跳过 " + skipped + " 项" : "") + ",已替换为链接");
|
|
837
|
+
}
|
|
838
|
+
} else if (st) {
|
|
839
|
+
fs.rmSync(linkPath, { force: true });
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (process.platform === "win32") {
|
|
843
|
+
fs.symlinkSync(canonical, linkPath, "junction");
|
|
844
|
+
} else {
|
|
845
|
+
fs.symlinkSync(path.relative(path.dirname(linkPath), canonical), linkPath, "dir");
|
|
846
|
+
}
|
|
847
|
+
return st ? "fixed" : "created";
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// 植入引擎(init 与 agents add 共用):协议文档去重植入 + multica/JASKILL +
|
|
851
|
+
// common 技能拉进正本 ai/jaSkills + 各目标技能目录链接化 + gitignore
|
|
852
|
+
async function plantForTargets(ctx, manifest, targets) {
|
|
853
|
+
const plan = [];
|
|
854
|
+
const seenDoc = new Set();
|
|
855
|
+
for (const n of targets) {
|
|
856
|
+
const doc = AGENT_TARGETS[n].doc;
|
|
857
|
+
if (seenDoc.has(doc.dest)) continue; // codex/kimi 共用 AGENTS.md,只植一份
|
|
858
|
+
seenDoc.add(doc.dest);
|
|
859
|
+
plan.push({ input: doc.input, dest: doc.dest });
|
|
860
|
+
}
|
|
861
|
+
plan.push({ input: "multica", dest: "multica", optional: true });
|
|
862
|
+
plan.push({ input: "docs/JASKILL.md", dest: "ai/JASKILL.md" });
|
|
863
|
+
|
|
864
|
+
const common = resolveShelfPath(ctx.shelfDir, "skills/common");
|
|
865
|
+
if (common) {
|
|
866
|
+
for (const e of fs.readdirSync(common.abs, { withFileTypes: true })) {
|
|
867
|
+
if (!e.isDirectory() || IGNORE_NAMES.has(e.name)) continue;
|
|
868
|
+
plan.push({
|
|
869
|
+
input: common.realRel + "/" + e.name,
|
|
870
|
+
dest: CANONICAL_SKILLS_DIR + "/" + e.name,
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const counters = newCounters();
|
|
876
|
+
for (const item of plan) {
|
|
877
|
+
const hit = resolveShelfPath(ctx.shelfDir, item.input);
|
|
878
|
+
if (!hit) {
|
|
879
|
+
console.log(item.optional
|
|
880
|
+
? "- 货架上暂无 " + item.input + ",跳过"
|
|
881
|
+
: "✗ 货架上找不到 " + item.input + "(检查货架是否最新)");
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
await pullEntry(ctx, hit.realRel, manifest, counters, safeAsk, path.resolve(process.cwd(), item.dest));
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
let linksMade = 0;
|
|
888
|
+
for (const n of targets) {
|
|
889
|
+
if (ensureSkillsLink(process.cwd(), n) !== "ok") linksMade++;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const ignoreLines = [...new Set(targets.flatMap((n) => AGENT_TARGETS[n].ignore))];
|
|
893
|
+
const added = ensureGitignore(process.cwd(), ignoreLines);
|
|
894
|
+
return { counters, linksMade, added };
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function printPlantResult(targets, r) {
|
|
898
|
+
printSummary(r.counters);
|
|
899
|
+
if (r.linksMade) console.log("⛓ 建立/修复链接 " + r.linksMade + " 个");
|
|
900
|
+
if (r.added.length) console.log("✓ .gitignore 补行: " + r.added.join(", "));
|
|
901
|
+
console.log("");
|
|
902
|
+
console.log("工作区已接入货架(agent 目标: " + targets.join(" + ") + "):");
|
|
903
|
+
const docs = [...new Set(targets.map((n) => AGENT_TARGETS[n].doc.dest))].join(" / ");
|
|
904
|
+
console.log(" " + docs + " 工作协议(真源在货架,shelf sync 保持最新)");
|
|
905
|
+
console.log(" ai/JASKILL.md 基础技能名册");
|
|
906
|
+
console.log(" " + CANONICAL_SKILLS_DIR + "/ 技能唯一正本(改技能、拉技能都在这)");
|
|
907
|
+
console.log(" " + targets.map((n) => AGENT_TARGETS[n].skillsDir).join(" · ") + " → 指向正本的链接");
|
|
908
|
+
console.log(" .shelf.json 记账本(进项目 git,队友 clone 后 shelf init 即还原)");
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
export async function cmdShelfInit(argv = []) {
|
|
912
|
+
let rest = argv;
|
|
913
|
+
let agentsFlag;
|
|
914
|
+
({ args: rest, value: agentsFlag } = takeFlag(rest, "--agents", true));
|
|
736
915
|
|
|
737
|
-
export async function cmdShelfInit() {
|
|
738
916
|
const ctx = resolveShelfContext();
|
|
739
917
|
try {
|
|
918
|
+
if (path.resolve(process.cwd()) === path.resolve(ctx.root)) {
|
|
919
|
+
console.error("✗ 这里就是货架 home 本体,不能对它 init(防止把忽略规则写进货架仓库)");
|
|
920
|
+
process.exit(1);
|
|
921
|
+
}
|
|
740
922
|
const manifest = loadManifest();
|
|
741
923
|
manifest.source ??= remoteUrl(ctx.root) || toPosix(ctx.root);
|
|
742
924
|
|
|
743
|
-
|
|
744
|
-
if (
|
|
745
|
-
|
|
746
|
-
|
|
925
|
+
let targets;
|
|
926
|
+
if (agentsFlag !== undefined) {
|
|
927
|
+
targets = parseTargetNames(agentsFlag);
|
|
928
|
+
} else if (manifest.agents?.length) {
|
|
929
|
+
targets = TARGET_PRIORITY.filter((n) => manifest.agents.includes(n));
|
|
930
|
+
console.log("(沿用已配置的 agent 目标: " + targets.join(", ") + ";调整用 shelf agents add 或 --agents)");
|
|
931
|
+
} else if (INTERACTIVE) {
|
|
932
|
+
targets = await askTargets();
|
|
933
|
+
if (!targets || !targets.length) {
|
|
934
|
+
console.log("已取消。");
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
} else {
|
|
938
|
+
console.error("✗ 非交互环境首次 init 需指定 --agents claude,codex,kimi(或 all)。已中止。");
|
|
939
|
+
process.exit(2);
|
|
747
940
|
}
|
|
748
|
-
|
|
749
|
-
const
|
|
750
|
-
|
|
941
|
+
|
|
942
|
+
const result = await plantForTargets(ctx, manifest, targets);
|
|
943
|
+
manifest.agents = targets;
|
|
944
|
+
|
|
945
|
+
// 账本还原(2026-08-30 用户确认):标准植入清单之外、账上有而本地缺失的货架物品一并补拉,
|
|
946
|
+
// 换设备后一条 init 全就位,无需再跑 sync
|
|
947
|
+
let restored = 0;
|
|
948
|
+
for (const [key, rec] of Object.entries(manifest.shelf ?? {})) {
|
|
949
|
+
if (!rec.localPath) continue;
|
|
950
|
+
const destAbs = path.resolve(process.cwd(), rec.localPath);
|
|
951
|
+
if (fs.existsSync(destAbs)) continue;
|
|
952
|
+
const hit = resolveShelfPath(ctx.shelfDir, key);
|
|
953
|
+
if (!hit) {
|
|
954
|
+
console.log("⚠ 账上物品 " + displayPath(key) + " 在货架上已不在原位——跑 shelf sync 处理搬家/下架");
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
await pullEntry(ctx, hit.realRel, manifest, result.counters, safeAsk, destAbs);
|
|
958
|
+
restored++;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
await reconcileLocalSkills(manifest);
|
|
751
962
|
saveManifest(manifest);
|
|
963
|
+
printPlantResult(targets, result);
|
|
964
|
+
if (restored) console.log("↺ 按账本补还原 " + restored + " 项(标准清单之外)");
|
|
965
|
+
} finally {
|
|
966
|
+
ctx.cleanup();
|
|
967
|
+
}
|
|
968
|
+
}
|
|
752
969
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
970
|
+
// ---- 子命令:agents(查看/添加 agent 目标,SHELF 决策 #24)----
|
|
971
|
+
|
|
972
|
+
export async function cmdShelfAgents(argv) {
|
|
973
|
+
const manifest = loadManifest();
|
|
974
|
+
const current = TARGET_PRIORITY.filter((n) => (manifest.agents ?? []).includes(n));
|
|
975
|
+
const sub = argv[0];
|
|
976
|
+
|
|
977
|
+
if (sub === undefined) {
|
|
978
|
+
console.log("技能正本: " + CANONICAL_SKILLS_DIR + "/(改技能、拉技能都在这)");
|
|
979
|
+
console.log("已配置: " + (current.length ? current.join(", ") : "(无——先跑 shelf init)"));
|
|
980
|
+
for (const n of TARGET_PRIORITY) {
|
|
981
|
+
const t = AGENT_TARGETS[n];
|
|
982
|
+
console.log(" " + (current.includes(n) ? "●" : "○") + " " + n + " " + t.skillsDir + "/ → 链接 · " + t.doc.dest);
|
|
983
|
+
}
|
|
984
|
+
console.log("添加: shelf agents add <名>");
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (sub !== "add" || argv.length < 2) {
|
|
988
|
+
console.error("用法: shelf agents 查看已配置/可用目标");
|
|
989
|
+
console.error(" shelf agents add <名> 添加目标(claude / codex / kimi / all)");
|
|
990
|
+
process.exit(1);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const adding = parseTargetNames(argv.slice(1).join(","));
|
|
994
|
+
const targets = TARGET_PRIORITY.filter((n) => current.includes(n) || adding.includes(n));
|
|
995
|
+
if (targets.join() === current.join()) {
|
|
996
|
+
console.log("= 目标已包含,无需变更(" + current.join(", ") + ")");
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const ctx = resolveShelfContext();
|
|
1001
|
+
try {
|
|
1002
|
+
const result = await plantForTargets(ctx, manifest, targets);
|
|
1003
|
+
manifest.agents = targets;
|
|
1004
|
+
saveManifest(manifest);
|
|
1005
|
+
printPlantResult(targets, result);
|
|
1006
|
+
console.log("✓ agent 目标: " + (current.join(", ") || "(无)") + " → " + targets.join(", "));
|
|
757
1007
|
} finally {
|
|
758
1008
|
ctx.cleanup();
|
|
759
1009
|
}
|
|
760
1010
|
}
|
|
761
1011
|
|
|
1012
|
+
// ---- 本地技能台账(SHELF 决策 #27 · 增量对账)----
|
|
1013
|
+
|
|
1014
|
+
// local 段维护:在场→刷指纹;升格→移除;缺失→告警但保留记录(仅 adopt 交互提供清账);
|
|
1015
|
+
// 新出现的未追踪目录→登记。init / sync / adopt 共用。
|
|
1016
|
+
async function reconcileLocalSkills(manifest, { allowPrune = false } = {}) {
|
|
1017
|
+
const canonical = path.join(process.cwd(), ...CANONICAL_SKILLS_DIR.split("/"));
|
|
1018
|
+
const tracked = new Set(
|
|
1019
|
+
Object.values(manifest.shelf ?? {}).map((e) => toPosix(e.localPath ?? "")),
|
|
1020
|
+
);
|
|
1021
|
+
const prev = manifest.local ?? {};
|
|
1022
|
+
const next = {};
|
|
1023
|
+
const c = { registered: 0, refreshed: 0, missing: 0, graduated: 0 };
|
|
1024
|
+
|
|
1025
|
+
const dirs = fs.existsSync(canonical)
|
|
1026
|
+
? fs.readdirSync(canonical, { withFileTypes: true })
|
|
1027
|
+
.filter((e) => e.isDirectory() && !IGNORE_NAMES.has(e.name))
|
|
1028
|
+
.map((e) => e.name)
|
|
1029
|
+
: [];
|
|
1030
|
+
const present = new Set(dirs);
|
|
1031
|
+
|
|
1032
|
+
for (const [name, entry] of Object.entries(prev)) {
|
|
1033
|
+
if (tracked.has(CANONICAL_SKILLS_DIR + "/" + name)) {
|
|
1034
|
+
c.graduated++;
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
if (present.has(name)) {
|
|
1038
|
+
const hash = contentHash(path.join(canonical, name));
|
|
1039
|
+
if (hash !== entry.contentHash) c.refreshed++;
|
|
1040
|
+
next[name] = { ...entry, contentHash: hash };
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
c.missing++;
|
|
1044
|
+
console.log("⚠ 登记过的本地技能缺失: " + name + "(登记于 " + (entry.addedAt ?? "?")
|
|
1045
|
+
+ ",上次指纹 " + String(entry.contentHash ?? "").slice(7, 19) + "…)——重装后跑 shelf sync 即恢复入账");
|
|
1046
|
+
if (allowPrune && INTERACTIVE) {
|
|
1047
|
+
const act = await choose(" [k]保留记录 / [r]清账? ", [{ key: "k" }, { key: "r" }]);
|
|
1048
|
+
if (act === "r") continue;
|
|
1049
|
+
}
|
|
1050
|
+
next[name] = entry;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
for (const name of dirs) {
|
|
1054
|
+
if (next[name] || tracked.has(CANONICAL_SKILLS_DIR + "/" + name)) continue;
|
|
1055
|
+
next[name] = {
|
|
1056
|
+
contentHash: contentHash(path.join(canonical, name)),
|
|
1057
|
+
addedAt: todayISO(),
|
|
1058
|
+
origin: "adopted",
|
|
1059
|
+
};
|
|
1060
|
+
console.log("✚ 登记本地技能: " + name);
|
|
1061
|
+
c.registered++;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
manifest.local = next;
|
|
1065
|
+
return c;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// ---- 子命令:adopt(收编外来技能,SHELF 决策 #27)----
|
|
1069
|
+
|
|
1070
|
+
export async function cmdShelfAdopt() {
|
|
1071
|
+
const manifest = loadManifest();
|
|
1072
|
+
|
|
1073
|
+
// agent 技能目录若被外部安装器换成了实体目录:内容迁入正本,恢复链接
|
|
1074
|
+
let relinked = 0;
|
|
1075
|
+
for (const n of manifest.agents ?? []) {
|
|
1076
|
+
if (!AGENT_TARGETS[n]) continue;
|
|
1077
|
+
if (ensureSkillsLink(process.cwd(), n) !== "ok") relinked++;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const c = await reconcileLocalSkills(manifest, { allowPrune: true });
|
|
1081
|
+
saveManifest(manifest);
|
|
1082
|
+
|
|
1083
|
+
console.log("");
|
|
1084
|
+
console.log(
|
|
1085
|
+
"Adopt: " + c.registered + " 新登记, " + c.refreshed + " 指纹更新, " + c.missing + " 缺失保留, "
|
|
1086
|
+
+ c.graduated + " 已升格/移除, " + relinked + " 目录收编重链;"
|
|
1087
|
+
+ "local 段共 " + Object.keys(manifest.local).length + " 个本地技能",
|
|
1088
|
+
);
|
|
1089
|
+
if (Object.keys(manifest.local).length) {
|
|
1090
|
+
console.log("(本地技能不参与货架对账;想跨项目复用: shelf create " + CANONICAL_SKILLS_DIR + "/<名> --to skills/<包>)");
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
762
1094
|
// ---- 子命令:home(查看/更新档口)----
|
|
763
1095
|
|
|
764
1096
|
export async function cmdShelfHome(argv) {
|
|
@@ -789,165 +1121,180 @@ export async function cmdShelfHome(argv) {
|
|
|
789
1121
|
ctx.cleanup();
|
|
790
1122
|
}
|
|
791
1123
|
}
|
|
792
|
-
|
|
793
|
-
// ---- 子命令:sync(按账本与货架对账,SHELF 决策 #19)----
|
|
794
|
-
|
|
795
|
-
// 逐行内容 diff 直通终端(git 自带着色),方向:库上 → 本地
|
|
796
|
-
function showLineDiff(shelfAbs, localAbs, shown) {
|
|
797
|
-
console.log(`—— ${shown} diff(库上 → 本地)——`);
|
|
798
|
-
spawnSync("git", ["diff", "--no-index", "--color=always", "--", shelfAbs, localAbs], { stdio: "inherit" });
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
export async function cmdShelfSync(argv) {
|
|
802
|
-
let rest = argv;
|
|
803
|
-
let dryRun;
|
|
804
|
-
({ args: rest, value: dryRun } = takeFlag(rest, "--dry-run"));
|
|
805
|
-
|
|
806
|
-
const ctx = resolveShelfContext({ forWrite: true }); // 对账必须对着真最新:等同写操作,强制刷新
|
|
807
|
-
let keepEphemeral = false;
|
|
808
|
-
try {
|
|
809
|
-
const manifest = loadManifest();
|
|
810
|
-
const entries = Object.entries(manifest.shelf ?? {});
|
|
811
|
-
if (entries.length === 0) {
|
|
812
|
-
console.log("账本为空(.shelf.json 没有 shelf 段记录)——先 shelf pull / create。");
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
const c = { same: 0, updated: 0, pushed: 0, skipped: 0, cleaned: 0 };
|
|
817
|
-
const pending = [];
|
|
818
|
-
|
|
819
|
-
for (const [key, rec] of entries) {
|
|
820
|
-
let effKey = key;
|
|
821
|
-
let target = path.join(ctx.shelfDir, ...effKey.split("/"));
|
|
822
|
-
let shown = displayPath(effKey);
|
|
823
|
-
const localAbs = path.resolve(process.cwd(), rec.localPath ?? "");
|
|
824
|
-
const localExists = !!rec.localPath && fs.existsSync(localAbs);
|
|
825
|
-
|
|
826
|
-
// 情形 5:库上路径没了 → 按名字找回(搬家)或报告下架
|
|
827
|
-
if (!fs.existsSync(target)) {
|
|
828
|
-
const name = path.basename(effKey);
|
|
829
|
-
const hits = findByBasename(ctx.shelfDir, name);
|
|
830
|
-
if (hits.length === 1) {
|
|
831
|
-
if (dryRun) {
|
|
832
|
-
console.log(`↪ ${shown} 已被移动到 ${displayPath(hits[0])}(--dry-run,暂不改账)`);
|
|
833
|
-
} else {
|
|
834
|
-
delete manifest.shelf[effKey];
|
|
835
|
-
manifest.shelf[hits[0]] = rec;
|
|
836
|
-
console.log(`↪ ${shown} 已被移动到 ${displayPath(hits[0])},记账已更新`);
|
|
837
|
-
}
|
|
838
|
-
effKey = hits[0];
|
|
839
|
-
target = path.join(ctx.shelfDir, ...effKey.split("/"));
|
|
840
|
-
shown = displayPath(effKey);
|
|
841
|
-
} else {
|
|
842
|
-
const label = hits.length === 0 ? "已从货架移除" : `同名多义(${hits.length} 处)`;
|
|
843
|
-
if (!INTERACTIVE || dryRun) {
|
|
844
|
-
console.log(`⚠ 待决 ${shown}:${label}`);
|
|
845
|
-
pending.push(`${shown}(${label})`);
|
|
846
|
-
continue;
|
|
847
|
-
}
|
|
848
|
-
const act = await choose(`⚠ ${shown} ${label}。[r]清记账 / [s]跳过? `, [{ key: "r" }, { key: "s" }]);
|
|
849
|
-
if (act === "r") {
|
|
850
|
-
delete manifest.shelf[effKey];
|
|
851
|
-
console.log(`✓ 已清记账 ${shown}(本地文件未动;想重新上架用 shelf create)`);
|
|
852
|
-
c.cleaned++;
|
|
853
|
-
} else c.skipped++;
|
|
854
|
-
continue;
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// 情形 6:本地文件没了 → 孤儿记账
|
|
859
|
-
if (!localExists) {
|
|
860
|
-
if (!INTERACTIVE || dryRun) {
|
|
861
|
-
console.log(`⚠ 待决 ${shown}:本地文件不存在(${rec.localPath ?? "无 localPath"})`);
|
|
862
|
-
pending.push(`${shown}(本地文件不存在)`);
|
|
863
|
-
continue;
|
|
864
|
-
}
|
|
865
|
-
const act = await choose(
|
|
866
|
-
`⚠ ${shown} 的本地文件不存在(${rec.localPath ?? "无 localPath"})。[p]重新拉取 / [r]清记账 / [s]跳过? `,
|
|
867
|
-
[{ key: "p" }, { key: "r" }, { key: "s" }],
|
|
868
|
-
);
|
|
869
|
-
if (act === "p") {
|
|
870
|
-
fs.mkdirSync(path.dirname(localAbs), { recursive: true });
|
|
871
|
-
copyFiltered(target, localAbs);
|
|
872
|
-
manifest.shelf[effKey] = { ...rec, contentHash: contentHash(target), sourceCommit: headCommit(ctx.root), pulledAt: todayISO() };
|
|
873
|
-
console.log(`↓ 已重新拉取 ${shown}`);
|
|
874
|
-
c.updated++;
|
|
875
|
-
} else if (act === "r") {
|
|
876
|
-
delete manifest.shelf[effKey];
|
|
877
|
-
console.log(`✓ 已清记账 ${shown}`);
|
|
878
|
-
c.cleaned++;
|
|
879
|
-
} else c.skipped++;
|
|
880
|
-
continue;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
const S = contentHash(target);
|
|
884
|
-
const L = contentHash(localAbs);
|
|
885
|
-
const R = rec.contentHash;
|
|
886
|
-
|
|
887
|
-
if (S === R && L === R) { c.same++; continue; }
|
|
888
|
-
|
|
889
|
-
// 情形 2:库上有新版、本地没动 → 自动覆盖本地(零损失)
|
|
890
|
-
if (S !== R && L === R) {
|
|
891
|
-
if (dryRun) {
|
|
892
|
-
console.log(`↓ 将更新本地:${shown}`);
|
|
893
|
-
c.updated++;
|
|
894
|
-
continue;
|
|
895
|
-
}
|
|
896
|
-
fs.rmSync(localAbs, { recursive: true, force: true });
|
|
897
|
-
copyFiltered(target, localAbs);
|
|
898
|
-
manifest.shelf[effKey] = { ...rec, contentHash: S, sourceCommit: headCommit(ctx.root), pulledAt: todayISO() };
|
|
899
|
-
console.log(`↓ 已更新本地:${shown}`);
|
|
900
|
-
c.updated++;
|
|
901
|
-
continue;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
// 情形 3/4:本地有改动(本地领先,或双方都改)
|
|
905
|
-
const both = S !== R;
|
|
906
|
-
const tag = both ? "双方都改过" : "本地领先";
|
|
907
|
-
if (!INTERACTIVE || dryRun) {
|
|
908
|
-
const d = diffSummary(target, localAbs);
|
|
909
|
-
console.log(`⚠ 待决 ${shown}(${tag}):本地相对库上 新增 ${d.added.length} / 删除 ${d.removed.length} / 修改 ${d.changed.length}`);
|
|
910
|
-
pending.push(`${shown}(${tag})`);
|
|
911
|
-
continue;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
showLineDiff(target, localAbs, shown);
|
|
915
|
-
const act = await choose(`${shown}(${tag})。[p]本地推上库 / [o]库覆盖本地 / [s]跳过? `, [{ key: "p" }, { key: "o" }, { key: "s" }]);
|
|
916
|
-
if (act === "s") { c.skipped++; continue; }
|
|
917
|
-
if (act === "o") {
|
|
918
|
-
fs.rmSync(localAbs, { recursive: true, force: true });
|
|
919
|
-
copyFiltered(target, localAbs);
|
|
920
|
-
manifest.shelf[effKey] = { ...rec, contentHash: S, sourceCommit: headCommit(ctx.root), pulledAt: todayISO() };
|
|
921
|
-
console.log(`↓ 已用库上版本覆盖本地:${shown}`);
|
|
922
|
-
c.updated++;
|
|
923
|
-
continue;
|
|
924
|
-
}
|
|
925
|
-
// p:本地推上库
|
|
926
|
-
if (both) {
|
|
927
|
-
const confirm = await choose(`库上的改动将被你的本地版本覆盖,确认? [y]es / [n]o? `, [{ key: "y" }, { key: "n" }]);
|
|
928
|
-
if (confirm === "n") { c.skipped++; continue; }
|
|
929
|
-
}
|
|
930
|
-
const { secrets } = guardScan(localAbs);
|
|
931
|
-
if (secrets.length > 0) {
|
|
932
|
-
console.log(`✗ ${shown} 含疑似凭据文件(${secrets.map((x) => x.rel).join(", ")}),sync 不代推,已跳过——确要推请单独 shelf push --force-secret`);
|
|
933
|
-
c.skipped++;
|
|
934
|
-
continue;
|
|
935
|
-
}
|
|
936
|
-
keepEphemeral = applyAndCommit(ctx, effKey, localAbs, manifest, "update") || keepEphemeral;
|
|
937
|
-
c.pushed++;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1124
|
+
|
|
1125
|
+
// ---- 子命令:sync(按账本与货架对账,SHELF 决策 #19)----
|
|
1126
|
+
|
|
1127
|
+
// 逐行内容 diff 直通终端(git 自带着色),方向:库上 → 本地
|
|
1128
|
+
function showLineDiff(shelfAbs, localAbs, shown) {
|
|
1129
|
+
console.log(`—— ${shown} diff(库上 → 本地)——`);
|
|
1130
|
+
spawnSync("git", ["diff", "--no-index", "--color=always", "--", shelfAbs, localAbs], { stdio: "inherit" });
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
export async function cmdShelfSync(argv) {
|
|
1134
|
+
let rest = argv;
|
|
1135
|
+
let dryRun;
|
|
1136
|
+
({ args: rest, value: dryRun } = takeFlag(rest, "--dry-run"));
|
|
1137
|
+
|
|
1138
|
+
const ctx = resolveShelfContext({ forWrite: true }); // 对账必须对着真最新:等同写操作,强制刷新
|
|
1139
|
+
let keepEphemeral = false;
|
|
1140
|
+
try {
|
|
1141
|
+
const manifest = loadManifest();
|
|
1142
|
+
const entries = Object.entries(manifest.shelf ?? {});
|
|
1143
|
+
if (entries.length === 0) {
|
|
1144
|
+
console.log("账本为空(.shelf.json 没有 shelf 段记录)——先 shelf pull / create。");
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const c = { same: 0, updated: 0, pushed: 0, skipped: 0, cleaned: 0 };
|
|
1149
|
+
const pending = [];
|
|
1150
|
+
|
|
1151
|
+
for (const [key, rec] of entries) {
|
|
1152
|
+
let effKey = key;
|
|
1153
|
+
let target = path.join(ctx.shelfDir, ...effKey.split("/"));
|
|
1154
|
+
let shown = displayPath(effKey);
|
|
1155
|
+
const localAbs = path.resolve(process.cwd(), rec.localPath ?? "");
|
|
1156
|
+
const localExists = !!rec.localPath && fs.existsSync(localAbs);
|
|
1157
|
+
|
|
1158
|
+
// 情形 5:库上路径没了 → 按名字找回(搬家)或报告下架
|
|
1159
|
+
if (!fs.existsSync(target)) {
|
|
1160
|
+
const name = path.basename(effKey);
|
|
1161
|
+
const hits = findByBasename(ctx.shelfDir, name);
|
|
1162
|
+
if (hits.length === 1) {
|
|
1163
|
+
if (dryRun) {
|
|
1164
|
+
console.log(`↪ ${shown} 已被移动到 ${displayPath(hits[0])}(--dry-run,暂不改账)`);
|
|
1165
|
+
} else {
|
|
1166
|
+
delete manifest.shelf[effKey];
|
|
1167
|
+
manifest.shelf[hits[0]] = rec;
|
|
1168
|
+
console.log(`↪ ${shown} 已被移动到 ${displayPath(hits[0])},记账已更新`);
|
|
1169
|
+
}
|
|
1170
|
+
effKey = hits[0];
|
|
1171
|
+
target = path.join(ctx.shelfDir, ...effKey.split("/"));
|
|
1172
|
+
shown = displayPath(effKey);
|
|
1173
|
+
} else {
|
|
1174
|
+
const label = hits.length === 0 ? "已从货架移除" : `同名多义(${hits.length} 处)`;
|
|
1175
|
+
if (!INTERACTIVE || dryRun) {
|
|
1176
|
+
console.log(`⚠ 待决 ${shown}:${label}`);
|
|
1177
|
+
pending.push(`${shown}(${label})`);
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
const act = await choose(`⚠ ${shown} ${label}。[r]清记账 / [s]跳过? `, [{ key: "r" }, { key: "s" }]);
|
|
1181
|
+
if (act === "r") {
|
|
1182
|
+
delete manifest.shelf[effKey];
|
|
1183
|
+
console.log(`✓ 已清记账 ${shown}(本地文件未动;想重新上架用 shelf create)`);
|
|
1184
|
+
c.cleaned++;
|
|
1185
|
+
} else c.skipped++;
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// 情形 6:本地文件没了 → 孤儿记账
|
|
1191
|
+
if (!localExists) {
|
|
1192
|
+
if (!INTERACTIVE || dryRun) {
|
|
1193
|
+
console.log(`⚠ 待决 ${shown}:本地文件不存在(${rec.localPath ?? "无 localPath"})`);
|
|
1194
|
+
pending.push(`${shown}(本地文件不存在)`);
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
const act = await choose(
|
|
1198
|
+
`⚠ ${shown} 的本地文件不存在(${rec.localPath ?? "无 localPath"})。[p]重新拉取 / [r]清记账 / [s]跳过? `,
|
|
1199
|
+
[{ key: "p" }, { key: "r" }, { key: "s" }],
|
|
1200
|
+
);
|
|
1201
|
+
if (act === "p") {
|
|
1202
|
+
fs.mkdirSync(path.dirname(localAbs), { recursive: true });
|
|
1203
|
+
copyFiltered(target, localAbs);
|
|
1204
|
+
manifest.shelf[effKey] = { ...rec, contentHash: contentHash(target), sourceCommit: headCommit(ctx.root), pulledAt: todayISO() };
|
|
1205
|
+
console.log(`↓ 已重新拉取 ${shown}`);
|
|
1206
|
+
c.updated++;
|
|
1207
|
+
} else if (act === "r") {
|
|
1208
|
+
delete manifest.shelf[effKey];
|
|
1209
|
+
console.log(`✓ 已清记账 ${shown}`);
|
|
1210
|
+
c.cleaned++;
|
|
1211
|
+
} else c.skipped++;
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const S = contentHash(target);
|
|
1216
|
+
const L = contentHash(localAbs);
|
|
1217
|
+
const R = rec.contentHash;
|
|
1218
|
+
|
|
1219
|
+
if (S === R && L === R) { c.same++; continue; }
|
|
1220
|
+
|
|
1221
|
+
// 情形 2:库上有新版、本地没动 → 自动覆盖本地(零损失)
|
|
1222
|
+
if (S !== R && L === R) {
|
|
1223
|
+
if (dryRun) {
|
|
1224
|
+
console.log(`↓ 将更新本地:${shown}`);
|
|
1225
|
+
c.updated++;
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
fs.rmSync(localAbs, { recursive: true, force: true });
|
|
1229
|
+
copyFiltered(target, localAbs);
|
|
1230
|
+
manifest.shelf[effKey] = { ...rec, contentHash: S, sourceCommit: headCommit(ctx.root), pulledAt: todayISO() };
|
|
1231
|
+
console.log(`↓ 已更新本地:${shown}`);
|
|
1232
|
+
c.updated++;
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// 情形 3/4:本地有改动(本地领先,或双方都改)
|
|
1237
|
+
const both = S !== R;
|
|
1238
|
+
const tag = both ? "双方都改过" : "本地领先";
|
|
1239
|
+
if (!INTERACTIVE || dryRun) {
|
|
1240
|
+
const d = diffSummary(target, localAbs);
|
|
1241
|
+
console.log(`⚠ 待决 ${shown}(${tag}):本地相对库上 新增 ${d.added.length} / 删除 ${d.removed.length} / 修改 ${d.changed.length}`);
|
|
1242
|
+
pending.push(`${shown}(${tag})`);
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
showLineDiff(target, localAbs, shown);
|
|
1247
|
+
const act = await choose(`${shown}(${tag})。[p]本地推上库 / [o]库覆盖本地 / [s]跳过? `, [{ key: "p" }, { key: "o" }, { key: "s" }]);
|
|
1248
|
+
if (act === "s") { c.skipped++; continue; }
|
|
1249
|
+
if (act === "o") {
|
|
1250
|
+
fs.rmSync(localAbs, { recursive: true, force: true });
|
|
1251
|
+
copyFiltered(target, localAbs);
|
|
1252
|
+
manifest.shelf[effKey] = { ...rec, contentHash: S, sourceCommit: headCommit(ctx.root), pulledAt: todayISO() };
|
|
1253
|
+
console.log(`↓ 已用库上版本覆盖本地:${shown}`);
|
|
1254
|
+
c.updated++;
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
// p:本地推上库
|
|
1258
|
+
if (both) {
|
|
1259
|
+
const confirm = await choose(`库上的改动将被你的本地版本覆盖,确认? [y]es / [n]o? `, [{ key: "y" }, { key: "n" }]);
|
|
1260
|
+
if (confirm === "n") { c.skipped++; continue; }
|
|
1261
|
+
}
|
|
1262
|
+
const { secrets } = guardScan(localAbs);
|
|
1263
|
+
if (secrets.length > 0) {
|
|
1264
|
+
console.log(`✗ ${shown} 含疑似凭据文件(${secrets.map((x) => x.rel).join(", ")}),sync 不代推,已跳过——确要推请单独 shelf push --force-secret`);
|
|
1265
|
+
c.skipped++;
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
keepEphemeral = applyAndCommit(ctx, effKey, localAbs, manifest, "update") || keepEphemeral;
|
|
1269
|
+
c.pushed++;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// 链接完整性(决策 #26):已配置目标的技能目录必须是指向 ai/jaSkills 的链接
|
|
1273
|
+
let linksFixed = 0;
|
|
1274
|
+
if (!dryRun && manifest.agents?.length) {
|
|
1275
|
+
for (const n of manifest.agents) {
|
|
1276
|
+
if (!AGENT_TARGETS[n]) continue;
|
|
1277
|
+
if (ensureSkillsLink(process.cwd(), n) !== "ok") linksFixed++;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
let localStat = null;
|
|
1282
|
+
if (!dryRun) localStat = await reconcileLocalSkills(manifest);
|
|
1283
|
+
|
|
1284
|
+
if (!dryRun) saveManifest(manifest);
|
|
1285
|
+
console.log("");
|
|
1286
|
+
console.log(
|
|
1287
|
+
`Sync${dryRun ? "(dry-run)" : ""}: ${c.same} 一致, ${c.updated} 更新本地, ${c.pushed} 推上库, ` +
|
|
1288
|
+
`${c.cleaned} 清账, ${c.skipped} 跳过, ${linksFixed} 链接修复, ${pending.length} 待决` +
|
|
1289
|
+
(localStat && (localStat.registered || localStat.refreshed || localStat.missing)
|
|
1290
|
+
? `;本地技能: ${localStat.registered} 新登记 / ${localStat.refreshed} 指纹更新 / ${localStat.missing} 缺失`
|
|
1291
|
+
: ""),
|
|
1292
|
+
);
|
|
1293
|
+
if (pending.length) {
|
|
1294
|
+
for (const x of pending) console.log(` · ${x}`);
|
|
1295
|
+
if (!INTERACTIVE && !dryRun) process.exit(2);
|
|
1296
|
+
}
|
|
1297
|
+
} finally {
|
|
1298
|
+
if (!keepEphemeral) ctx.cleanup();
|
|
1299
|
+
}
|
|
1300
|
+
}
|
package/lib/manifest.mjs
CHANGED
|
@@ -28,11 +28,13 @@ export function loadManifest(cwd = process.cwd()) {
|
|
|
28
28
|
skillsDir: ".claude/skills",
|
|
29
29
|
skills: {},
|
|
30
30
|
shelf: {},
|
|
31
|
+
local: {},
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
34
|
const data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
34
35
|
data.skills ??= {};
|
|
35
36
|
data.shelf ??= {};
|
|
37
|
+
data.local ??= {};
|
|
36
38
|
return data;
|
|
37
39
|
}
|
|
38
40
|
|
package/package.json
CHANGED