@mison/ling 1.1.1 → 1.2.2
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 +35 -1
- package/README.md +139 -194
- package/bin/adapters/gemini.js +6 -2
- package/bin/interactive.js +6 -4
- package/bin/ling-cli.js +552 -143
- package/docs/PLAN.md +23 -18
- package/docs/TECH.md +30 -17
- package/package.json +1 -1
- package/scripts/ci-verify.js +6 -3
- package/scripts/health-check.js +5 -14
- package/tests/cli-smoke.test.js +115 -0
- package/tests/global-sync.test.js +82 -7
- package/tests/spec-init-doctor.test.js +58 -0
- package/tests/spec-profile.test.js +67 -4
- package/tests/standards-compliance.test.js +33 -2
- package/.agents/skills/vulnerability-scanner/scripts/__pycache__/security_scan.cpython-310.pyc +0 -0
package/bin/ling-cli.js
CHANGED
|
@@ -19,7 +19,8 @@ const PRIMARY_CLI_NAME = "ling";
|
|
|
19
19
|
const WORKSPACE_INDEX_VERSION = 2;
|
|
20
20
|
const UPSTREAM_GLOBAL_PACKAGE = "@vudovn/ag-kit";
|
|
21
21
|
const TOOLKIT_PACKAGE_NAMES = new Set(["@mison/ling", "@mison/ag-kit-cn", "antigravity-kit-cn", "antigravity-kit"]);
|
|
22
|
-
const SUPPORTED_TARGETS = ["gemini", "codex"];
|
|
22
|
+
const SUPPORTED_TARGETS = ["gemini", "antigravity", "codex"];
|
|
23
|
+
const SHARED_AGENT_TARGETS = ["gemini", "antigravity"];
|
|
23
24
|
const LEGACY_INDEX_TARGET_ALIASES = {
|
|
24
25
|
full: "gemini",
|
|
25
26
|
};
|
|
@@ -27,8 +28,8 @@ const GLOBAL_TARGET_DESTINATIONS = {
|
|
|
27
28
|
codex: [
|
|
28
29
|
{
|
|
29
30
|
id: "codex",
|
|
30
|
-
rootParts: [".
|
|
31
|
-
skillsParts: [".
|
|
31
|
+
rootParts: [".agents"],
|
|
32
|
+
skillsParts: [".agents", "skills"],
|
|
32
33
|
},
|
|
33
34
|
],
|
|
34
35
|
gemini: [
|
|
@@ -37,6 +38,8 @@ const GLOBAL_TARGET_DESTINATIONS = {
|
|
|
37
38
|
rootParts: [".gemini", "skills"],
|
|
38
39
|
skillsParts: [".gemini", "skills"],
|
|
39
40
|
},
|
|
41
|
+
],
|
|
42
|
+
antigravity: [
|
|
40
43
|
{
|
|
41
44
|
id: "antigravity",
|
|
42
45
|
rootParts: [".gemini", "antigravity"],
|
|
@@ -58,6 +61,7 @@ const VERSION_TAG_PREFIX = "ling-";
|
|
|
58
61
|
const SPEC_TEMPLATE_REQUIRED_FILES = ["issues.template.csv", "driver-prompt.md", "review-report.md", "phase-acceptance.md", "handoff.md"];
|
|
59
62
|
const SPEC_REFERENCE_REQUIRED_FILES = ["README.md", "harness-engineering-digest.md", "gda-framework.md", "cse-quickstart.md"];
|
|
60
63
|
const SPEC_PROFILE_REQUIRED_FILES = ["codex/AGENTS.spec.md", "codex/ling.spec.rules.md", "gemini/GEMINI.spec.md"];
|
|
64
|
+
const LEGACY_CODEX_GLOBAL_SKILLS_BACKUP_TARGET = "codex-legacy";
|
|
61
65
|
|
|
62
66
|
function nowISO() {
|
|
63
67
|
return new Date().toISOString();
|
|
@@ -170,13 +174,13 @@ function printUsage() {
|
|
|
170
174
|
console.log(` ${PRIMARY_CLI_NAME} update [--path <dir>] [--branch <name>] [--target <name>|--targets <a,b>] [--no-index] [--quiet] [--dry-run]`);
|
|
171
175
|
console.log(` ${PRIMARY_CLI_NAME} update-all [--branch <name>] [--targets <a,b>] [--prune-missing] [--quiet] [--dry-run]`);
|
|
172
176
|
console.log(` ${PRIMARY_CLI_NAME} doctor [--path <dir>] [--target <name>|--targets <a,b>] [--fix] [--quiet]`);
|
|
173
|
-
console.log(` ${PRIMARY_CLI_NAME} global sync [--target <name>|--targets <a,b>] [--branch <name>] [--quiet] [--dry-run] # 默认同步 codex + gemini
|
|
177
|
+
console.log(` ${PRIMARY_CLI_NAME} global sync [--target <name>|--targets <a,b>] [--branch <name>] [--quiet] [--dry-run] # 默认同步 codex + gemini + antigravity`);
|
|
174
178
|
console.log(` ${PRIMARY_CLI_NAME} global status [--quiet]`);
|
|
175
179
|
console.log(` ${PRIMARY_CLI_NAME} spec enable [--target <name>|--targets <a,b>] [--quiet] [--dry-run]`);
|
|
176
180
|
console.log(` ${PRIMARY_CLI_NAME} spec disable [--target <name>|--targets <a,b>] [--quiet] [--dry-run]`);
|
|
177
181
|
console.log(` ${PRIMARY_CLI_NAME} spec status [--quiet]`);
|
|
178
|
-
console.log(` ${PRIMARY_CLI_NAME} spec init [--path <dir>] [--target <name>|--targets <a,b>] [--branch <name>] [--force] [--non-interactive] [--no-index] [--quiet] [--dry-run]`);
|
|
179
|
-
console.log(` ${PRIMARY_CLI_NAME} spec doctor [--path <dir>] [--quiet]`);
|
|
182
|
+
console.log(` ${PRIMARY_CLI_NAME} spec init [--path <dir>] [--spec-workspace] [--csv-only] [--target <name>|--targets <a,b>] [--branch <name>] [--force] [--non-interactive] [--no-index] [--quiet] [--dry-run]`);
|
|
183
|
+
console.log(` ${PRIMARY_CLI_NAME} spec doctor [--path <dir>] [--spec-workspace] [--quiet]`);
|
|
180
184
|
console.log(` ${PRIMARY_CLI_NAME} exclude list [--quiet]`);
|
|
181
185
|
console.log(` ${PRIMARY_CLI_NAME} exclude add --path <dir> [--dry-run] [--quiet]`);
|
|
182
186
|
console.log(` ${PRIMARY_CLI_NAME} exclude remove --path <dir> [--dry-run] [--quiet]`);
|
|
@@ -202,6 +206,8 @@ function parseArgs(argv) {
|
|
|
202
206
|
nonInteractive: false,
|
|
203
207
|
noIndex: false,
|
|
204
208
|
fix: false,
|
|
209
|
+
csvOnly: false,
|
|
210
|
+
specWorkspace: false,
|
|
205
211
|
subcommand: "",
|
|
206
212
|
path: "",
|
|
207
213
|
branch: "",
|
|
@@ -252,6 +258,12 @@ function parseArgs(argv) {
|
|
|
252
258
|
} else if (arg === "--fix") {
|
|
253
259
|
providedFlags.push(arg);
|
|
254
260
|
options.fix = true;
|
|
261
|
+
} else if (arg === "--csv-only") {
|
|
262
|
+
providedFlags.push(arg);
|
|
263
|
+
options.csvOnly = true;
|
|
264
|
+
} else if (arg === "--spec-workspace") {
|
|
265
|
+
providedFlags.push(arg);
|
|
266
|
+
options.specWorkspace = true;
|
|
255
267
|
} else if (arg === "--path") {
|
|
256
268
|
providedFlags.push(arg);
|
|
257
269
|
if (i + 1 >= argv.length) {
|
|
@@ -295,8 +307,8 @@ const COMMAND_ALLOWED_FLAGS = {
|
|
|
295
307
|
"spec:enable": ["--target", "--targets", "--quiet", "--dry-run"],
|
|
296
308
|
"spec:disable": ["--target", "--targets", "--quiet", "--dry-run"],
|
|
297
309
|
"spec:status": ["--quiet"],
|
|
298
|
-
"spec:init": ["--force", "--path", "--branch", "--target", "--targets", "--non-interactive", "--no-index", "--quiet", "--dry-run"],
|
|
299
|
-
"spec:doctor": ["--path", "--quiet"],
|
|
310
|
+
"spec:init": ["--force", "--path", "--spec-workspace", "--branch", "--csv-only", "--target", "--targets", "--non-interactive", "--no-index", "--quiet", "--dry-run"],
|
|
311
|
+
"spec:doctor": ["--path", "--spec-workspace", "--quiet"],
|
|
300
312
|
"exclude:list": ["--quiet"],
|
|
301
313
|
"exclude:add": ["--path", "--dry-run", "--quiet"],
|
|
302
314
|
"exclude:remove": ["--path", "--dry-run", "--quiet"],
|
|
@@ -389,6 +401,12 @@ function pathCompareKey(inputPath) {
|
|
|
389
401
|
return normalized;
|
|
390
402
|
}
|
|
391
403
|
|
|
404
|
+
function findWorkspaceRecord(index, workspaceRoot) {
|
|
405
|
+
const normalizedPath = normalizeAbsolutePath(workspaceRoot);
|
|
406
|
+
const targetKey = pathCompareKey(normalizedPath);
|
|
407
|
+
return (index.workspaces || []).find((item) => pathCompareKey(item.path) === targetKey) || null;
|
|
408
|
+
}
|
|
409
|
+
|
|
392
410
|
function normalizePathList(items) {
|
|
393
411
|
const map = new Map();
|
|
394
412
|
for (const item of items) {
|
|
@@ -627,6 +645,95 @@ function writeWorkspaceIndex(indexPath, index) {
|
|
|
627
645
|
fs.writeFileSync(indexPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
628
646
|
}
|
|
629
647
|
|
|
648
|
+
function getWorkspaceInstallStatePath(workspaceRoot) {
|
|
649
|
+
return path.join(normalizeAbsolutePath(workspaceRoot), ".ling", "install-state.json");
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function createEmptyWorkspaceInstallState() {
|
|
653
|
+
return {
|
|
654
|
+
version: 1,
|
|
655
|
+
updatedAt: "",
|
|
656
|
+
targets: {},
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function readWorkspaceInstallState(workspaceRoot) {
|
|
661
|
+
const statePath = getWorkspaceInstallStatePath(workspaceRoot);
|
|
662
|
+
if (!fs.existsSync(statePath)) {
|
|
663
|
+
return { statePath, state: createEmptyWorkspaceInstallState() };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const raw = fs.readFileSync(statePath, "utf8").trim();
|
|
667
|
+
if (!raw) {
|
|
668
|
+
return { statePath, state: createEmptyWorkspaceInstallState() };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
let parsed;
|
|
672
|
+
try {
|
|
673
|
+
parsed = JSON.parse(raw);
|
|
674
|
+
} catch (_err) {
|
|
675
|
+
return { statePath, state: createEmptyWorkspaceInstallState() };
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const state = createEmptyWorkspaceInstallState();
|
|
679
|
+
state.updatedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : "";
|
|
680
|
+
if (parsed && parsed.targets && typeof parsed.targets === "object") {
|
|
681
|
+
for (const [targetName, targetState] of Object.entries(parsed.targets)) {
|
|
682
|
+
const normalizedTargetName = normalizeIndexTargetName(targetName);
|
|
683
|
+
const normalizedTargetState = normalizeTargetState(targetState);
|
|
684
|
+
if (normalizedTargetName && normalizedTargetState) {
|
|
685
|
+
state.targets[normalizedTargetName] = normalizedTargetState;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return { statePath, state };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function writeWorkspaceInstallState(statePath, state) {
|
|
694
|
+
const payload = {
|
|
695
|
+
version: 1,
|
|
696
|
+
updatedAt: state.updatedAt || nowISO(),
|
|
697
|
+
targets: state.targets || {},
|
|
698
|
+
};
|
|
699
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
700
|
+
fs.writeFileSync(statePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function recordWorkspaceInstallTargets(workspaceRoot, targetNames, options) {
|
|
704
|
+
const logicalTargets = normalizeTargets(targetNames).filter((targetName) => SHARED_AGENT_TARGETS.includes(targetName));
|
|
705
|
+
if (logicalTargets.length === 0) {
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (options.dryRun) {
|
|
710
|
+
log(options, `[dry-run] 将写入工作区安装状态: ${getWorkspaceInstallStatePath(workspaceRoot)} [${logicalTargets.join(", ")}]`);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const { statePath, state } = readWorkspaceInstallState(workspaceRoot);
|
|
715
|
+
const timestamp = nowISO();
|
|
716
|
+
for (const targetName of logicalTargets) {
|
|
717
|
+
const prev = normalizeTargetState(state.targets[targetName]) || {
|
|
718
|
+
version: "",
|
|
719
|
+
installedAt: "",
|
|
720
|
+
updatedAt: "",
|
|
721
|
+
};
|
|
722
|
+
state.targets[targetName] = {
|
|
723
|
+
version: pkg.version,
|
|
724
|
+
installedAt: prev.installedAt || timestamp,
|
|
725
|
+
updatedAt: timestamp,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
state.updatedAt = timestamp;
|
|
729
|
+
writeWorkspaceInstallState(statePath, state);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function resolveWorkspaceInstallStateTargets(workspaceRoot) {
|
|
733
|
+
const { state } = readWorkspaceInstallState(workspaceRoot);
|
|
734
|
+
return normalizeTargets(Object.keys(state.targets || {}));
|
|
735
|
+
}
|
|
736
|
+
|
|
630
737
|
function sleepSync(ms) {
|
|
631
738
|
const buffer = new SharedArrayBuffer(4);
|
|
632
739
|
const view = new Int32Array(buffer);
|
|
@@ -906,19 +1013,41 @@ function normalizeTargets(rawTargets) {
|
|
|
906
1013
|
return result;
|
|
907
1014
|
}
|
|
908
1015
|
|
|
1016
|
+
function resolveIndexedWorkspaceTargets(workspaceRoot) {
|
|
1017
|
+
try {
|
|
1018
|
+
const { index } = readWorkspaceIndex();
|
|
1019
|
+
const record = findWorkspaceRecord(index, workspaceRoot);
|
|
1020
|
+
if (!record) {
|
|
1021
|
+
return [];
|
|
1022
|
+
}
|
|
1023
|
+
return normalizeTargets(Object.keys(record.targets || {}));
|
|
1024
|
+
} catch (_err) {
|
|
1025
|
+
return [];
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
909
1029
|
function detectInstalledTargets(workspaceRoot) {
|
|
910
1030
|
const targets = [];
|
|
1031
|
+
const localTargets = resolveWorkspaceInstallStateTargets(workspaceRoot);
|
|
1032
|
+
const indexedTargets = resolveIndexedWorkspaceTargets(workspaceRoot);
|
|
911
1033
|
if (fs.existsSync(path.join(workspaceRoot, ".agent"))) {
|
|
912
|
-
|
|
1034
|
+
const sharedTargets = localTargets
|
|
1035
|
+
.filter((target) => SHARED_AGENT_TARGETS.includes(target))
|
|
1036
|
+
.concat(indexedTargets.filter((target) => SHARED_AGENT_TARGETS.includes(target)));
|
|
1037
|
+
if (sharedTargets.length > 0) {
|
|
1038
|
+
targets.push(...sharedTargets);
|
|
1039
|
+
} else {
|
|
1040
|
+
targets.push("gemini");
|
|
1041
|
+
}
|
|
913
1042
|
}
|
|
914
1043
|
if (fs.existsSync(path.join(workspaceRoot, ".agents")) || fs.existsSync(path.join(workspaceRoot, ".codex"))) {
|
|
915
1044
|
targets.push("codex");
|
|
916
1045
|
}
|
|
917
|
-
return targets;
|
|
1046
|
+
return normalizeTargets(targets);
|
|
918
1047
|
}
|
|
919
1048
|
|
|
920
1049
|
function isTargetInstalled(workspaceRoot, targetName) {
|
|
921
|
-
if (targetName
|
|
1050
|
+
if (SHARED_AGENT_TARGETS.includes(targetName)) {
|
|
922
1051
|
return fs.existsSync(path.join(workspaceRoot, ".agent"));
|
|
923
1052
|
}
|
|
924
1053
|
if (targetName === "codex") {
|
|
@@ -927,6 +1056,27 @@ function isTargetInstalled(workspaceRoot, targetName) {
|
|
|
927
1056
|
return false;
|
|
928
1057
|
}
|
|
929
1058
|
|
|
1059
|
+
function groupTargetsByInstallSurface(targets) {
|
|
1060
|
+
const normalizedTargets = normalizeTargets(targets);
|
|
1061
|
+
const groups = [];
|
|
1062
|
+
const sharedTargets = normalizedTargets.filter((target) => SHARED_AGENT_TARGETS.includes(target));
|
|
1063
|
+
if (sharedTargets.length > 0) {
|
|
1064
|
+
groups.push({
|
|
1065
|
+
installSurface: ".agent",
|
|
1066
|
+
adapterTarget: sharedTargets.includes("gemini") ? "gemini" : "antigravity",
|
|
1067
|
+
logicalTargets: sharedTargets,
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
if (normalizedTargets.includes("codex")) {
|
|
1071
|
+
groups.push({
|
|
1072
|
+
installSurface: ".agents",
|
|
1073
|
+
adapterTarget: "codex",
|
|
1074
|
+
logicalTargets: ["codex"],
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
return groups;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
930
1080
|
function setQuietStatusExitCode(state) {
|
|
931
1081
|
process.exitCode = Object.prototype.hasOwnProperty.call(QUIET_STATUS_EXIT_CODES, state)
|
|
932
1082
|
? QUIET_STATUS_EXIT_CODES[state]
|
|
@@ -1005,6 +1155,22 @@ function evaluateGlobalState() {
|
|
|
1005
1155
|
};
|
|
1006
1156
|
}).filter((item) => item.state !== "missing");
|
|
1007
1157
|
|
|
1158
|
+
const legacyCodexSkillsRoot = path.join(globalRoot, ".codex", "skills");
|
|
1159
|
+
const hasCurrentCodexRoot = targetStates.some((item) => item.targetName === "codex");
|
|
1160
|
+
if (!hasCurrentCodexRoot && fs.existsSync(legacyCodexSkillsRoot)) {
|
|
1161
|
+
targetStates.push({
|
|
1162
|
+
targetName: "codex",
|
|
1163
|
+
family: "codex",
|
|
1164
|
+
state: "broken",
|
|
1165
|
+
rootDir: path.join(globalRoot, ".codex"),
|
|
1166
|
+
skillsRoot: legacyCodexSkillsRoot,
|
|
1167
|
+
skillsCount: countSkillsRecursive(legacyCodexSkillsRoot),
|
|
1168
|
+
issues: [
|
|
1169
|
+
"检测到旧版 Codex 全局 Skill 目录;当前版本改用 ~/.agents/skills(运行: ling global sync --target codex)",
|
|
1170
|
+
],
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1008
1174
|
if (targetStates.length === 0) {
|
|
1009
1175
|
return {
|
|
1010
1176
|
globalRoot,
|
|
@@ -1022,8 +1188,11 @@ function evaluateGlobalState() {
|
|
|
1022
1188
|
}
|
|
1023
1189
|
|
|
1024
1190
|
function createAdapter(targetName, workspaceRoot, options) {
|
|
1025
|
-
if (targetName
|
|
1026
|
-
return new GeminiAdapter(workspaceRoot,
|
|
1191
|
+
if (SHARED_AGENT_TARGETS.includes(targetName)) {
|
|
1192
|
+
return new GeminiAdapter(workspaceRoot, {
|
|
1193
|
+
...options,
|
|
1194
|
+
targetName,
|
|
1195
|
+
});
|
|
1027
1196
|
}
|
|
1028
1197
|
if (targetName === "codex") {
|
|
1029
1198
|
return new CodexAdapter(workspaceRoot, options);
|
|
@@ -1067,8 +1236,7 @@ function resolveTargetsForGlobalSync(options) {
|
|
|
1067
1236
|
if (requested.length > 0) {
|
|
1068
1237
|
return requested;
|
|
1069
1238
|
}
|
|
1070
|
-
|
|
1071
|
-
return ["codex", "gemini"];
|
|
1239
|
+
return [...SUPPORTED_TARGETS];
|
|
1072
1240
|
}
|
|
1073
1241
|
|
|
1074
1242
|
function resolveAgentInstallSource(options) {
|
|
@@ -1120,6 +1288,74 @@ function backupSkillDirectory(targetName, skillName, sourceDir, timestamp, optio
|
|
|
1120
1288
|
log(options, `[backup] 已备份 ${targetName} 全局 Skill: ${skillName} -> ${backupDir}`);
|
|
1121
1289
|
}
|
|
1122
1290
|
|
|
1291
|
+
function migrateLegacyCodexGlobalSkills(timestamp, options) {
|
|
1292
|
+
const globalRoot = resolveGlobalRootDir();
|
|
1293
|
+
const legacyRoot = path.join(globalRoot, ".codex", "skills");
|
|
1294
|
+
if (!fs.existsSync(legacyRoot)) {
|
|
1295
|
+
return { migrated: 0, removed: 0, backedUp: 0, conflicts: 0, remaining: 0 };
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
const destRoot = path.join(globalRoot, ".agents", "skills");
|
|
1299
|
+
const legacyDirEntries = fs.readdirSync(legacyRoot, { withFileTypes: true });
|
|
1300
|
+
const legacyEntries = legacyDirEntries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
1301
|
+
if (legacyEntries.length === 0) {
|
|
1302
|
+
if (legacyDirEntries.length === 0) {
|
|
1303
|
+
removeDirIfExists(legacyRoot, options, "遗留 Codex skills 根目录");
|
|
1304
|
+
} else {
|
|
1305
|
+
log(options, `[warn] 遗留 Codex skills 根目录包含非目录条目,已跳过迁移清理: ${legacyRoot}`);
|
|
1306
|
+
}
|
|
1307
|
+
return { migrated: 0, removed: 0, backedUp: 0, conflicts: 0, remaining: legacyDirEntries.length };
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
let migrated = 0;
|
|
1311
|
+
let removed = 0;
|
|
1312
|
+
let backedUp = 0;
|
|
1313
|
+
let conflicts = 0;
|
|
1314
|
+
|
|
1315
|
+
for (const entryName of legacyEntries) {
|
|
1316
|
+
const legacyDir = path.join(legacyRoot, entryName);
|
|
1317
|
+
const destDir = path.join(destRoot, entryName);
|
|
1318
|
+
const destExists = fs.existsSync(destDir);
|
|
1319
|
+
|
|
1320
|
+
if (!destExists) {
|
|
1321
|
+
if (options.dryRun) {
|
|
1322
|
+
log(options, `[dry-run] 将迁移遗留 Codex Skill 目录: ${legacyDir} -> ${destDir}`);
|
|
1323
|
+
} else {
|
|
1324
|
+
const logger = options.quiet ? (() => {}) : log.bind(null, options);
|
|
1325
|
+
AtomicWriter.atomicCopyDir(legacyDir, destDir, { logger });
|
|
1326
|
+
log(options, `[ok] 已迁移遗留 Codex Skill 目录: ${legacyDir} -> ${destDir}`);
|
|
1327
|
+
}
|
|
1328
|
+
migrated += 1;
|
|
1329
|
+
} else if (!areDirectoriesEqual(legacyDir, destDir)) {
|
|
1330
|
+
conflicts += 1;
|
|
1331
|
+
if (options.dryRun) {
|
|
1332
|
+
log(options, `[dry-run] 将备份并移除遗留 Codex Skill(与现有目录冲突): ${legacyDir}`);
|
|
1333
|
+
} else {
|
|
1334
|
+
backupSkillDirectory(LEGACY_CODEX_GLOBAL_SKILLS_BACKUP_TARGET, entryName, legacyDir, timestamp, options);
|
|
1335
|
+
backedUp += 1;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
if (options.dryRun) {
|
|
1340
|
+
log(options, `[dry-run] 将删除遗留 Codex Skill 目录: ${legacyDir}`);
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
fs.rmSync(legacyDir, { recursive: true, force: true });
|
|
1345
|
+
log(options, `[clean] 已删除遗留 Codex Skill 目录: ${legacyDir}`);
|
|
1346
|
+
removed += 1;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
const remaining = fs.existsSync(legacyRoot) ? fs.readdirSync(legacyRoot).length : 0;
|
|
1350
|
+
if (remaining === 0) {
|
|
1351
|
+
removeDirIfExists(legacyRoot, options, "遗留 Codex skills 根目录");
|
|
1352
|
+
} else {
|
|
1353
|
+
log(options, `[warn] 遗留 Codex skills 根目录仍有残留条目(${remaining}),已保留: ${legacyRoot}`);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
return { migrated, removed, backedUp, conflicts, remaining };
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1123
1359
|
function syncSkillDirectory(destination, srcDir, destDir, timestamp, options) {
|
|
1124
1360
|
const exists = fs.existsSync(destDir);
|
|
1125
1361
|
if (exists) {
|
|
@@ -1235,7 +1471,7 @@ function planGlobalSyncTasks(targetName, agentDir) {
|
|
|
1235
1471
|
};
|
|
1236
1472
|
}
|
|
1237
1473
|
|
|
1238
|
-
if (targetName
|
|
1474
|
+
if (SHARED_AGENT_TARGETS.includes(targetName)) {
|
|
1239
1475
|
const skillsRoot = path.join(agentDir, "skills");
|
|
1240
1476
|
const skillNames = listSkillDirectories(skillsRoot);
|
|
1241
1477
|
const tasks = [];
|
|
@@ -1317,7 +1553,7 @@ function applyGlobalSync(targetName, agentDir, timestamp, options) {
|
|
|
1317
1553
|
}
|
|
1318
1554
|
}
|
|
1319
1555
|
|
|
1320
|
-
if (targetName
|
|
1556
|
+
if (SHARED_AGENT_TARGETS.includes(targetName)) {
|
|
1321
1557
|
const skillsRoot = path.join(agentDir, "skills");
|
|
1322
1558
|
return syncGlobalSkillsFromRoot(targetName, skillsRoot, timestamp, options);
|
|
1323
1559
|
}
|
|
@@ -1386,6 +1622,10 @@ async function commandGlobalSync(options) {
|
|
|
1386
1622
|
log(options, ` - ${item.targetName}: ${item.destRoot}(每目标 ${item.total} 个 Skills)`);
|
|
1387
1623
|
}
|
|
1388
1624
|
}
|
|
1625
|
+
|
|
1626
|
+
if (target === "codex") {
|
|
1627
|
+
migrateLegacyCodexGlobalSkills(timestamp, options);
|
|
1628
|
+
}
|
|
1389
1629
|
} finally {
|
|
1390
1630
|
if (plan.cleanup) plan.cleanup();
|
|
1391
1631
|
}
|
|
@@ -1475,15 +1715,121 @@ function createEmptySpecState() {
|
|
|
1475
1715
|
};
|
|
1476
1716
|
}
|
|
1477
1717
|
|
|
1718
|
+
function rewriteLegacyCodexGlobalSkillPath(targetPath) {
|
|
1719
|
+
if (typeof targetPath !== "string" || targetPath === "") {
|
|
1720
|
+
return targetPath;
|
|
1721
|
+
}
|
|
1722
|
+
const normalizedPath = targetPath.replace(/\\/g, "/");
|
|
1723
|
+
if (!normalizedPath.includes("/.codex/skills")) {
|
|
1724
|
+
return targetPath;
|
|
1725
|
+
}
|
|
1726
|
+
return targetPath.replace(/([\\/])\.codex([\\/])skills/g, "$1.agents$2skills");
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
function normalizeSpecSkillState(targetName, consumerId, skillState) {
|
|
1730
|
+
if (!skillState || typeof skillState !== "object" || typeof skillState.name !== "string" || !skillState.name) {
|
|
1731
|
+
return null;
|
|
1732
|
+
}
|
|
1733
|
+
const destPath = typeof skillState.destPath === "string" ? skillState.destPath : "";
|
|
1734
|
+
return {
|
|
1735
|
+
name: skillState.name,
|
|
1736
|
+
destPath: targetName === "codex" && consumerId === "codex"
|
|
1737
|
+
? rewriteLegacyCodexGlobalSkillPath(destPath)
|
|
1738
|
+
: destPath,
|
|
1739
|
+
backupPath: typeof skillState.backupPath === "string" ? skillState.backupPath : "",
|
|
1740
|
+
mode: normalizeSpecAssetMode(skillState),
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
function normalizeSpecConsumerState(targetName, consumerId, consumerState) {
|
|
1745
|
+
const skills = [];
|
|
1746
|
+
let migrated = false;
|
|
1747
|
+
for (const skillState of Array.isArray(consumerState && consumerState.skills) ? consumerState.skills : []) {
|
|
1748
|
+
const normalizedSkill = normalizeSpecSkillState(targetName, consumerId, skillState);
|
|
1749
|
+
if (normalizedSkill) {
|
|
1750
|
+
skills.push(normalizedSkill);
|
|
1751
|
+
if (normalizedSkill.destPath !== (typeof skillState.destPath === "string" ? skillState.destPath : "")) {
|
|
1752
|
+
migrated = true;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
return { skills, migrated };
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
function normalizeSpecTargetState(targetName, targetState) {
|
|
1760
|
+
const allowedConsumers = new Set(getGlobalDestinations(targetName).map((destination) => destination.id));
|
|
1761
|
+
const consumers = {};
|
|
1762
|
+
let migrated = false;
|
|
1763
|
+
for (const consumerId of allowedConsumers) {
|
|
1764
|
+
if (targetState && targetState.consumers && targetState.consumers[consumerId]) {
|
|
1765
|
+
const normalizedConsumerState = normalizeSpecConsumerState(targetName, consumerId, targetState.consumers[consumerId]);
|
|
1766
|
+
consumers[consumerId] = { skills: normalizedConsumerState.skills };
|
|
1767
|
+
if (normalizedConsumerState.migrated) {
|
|
1768
|
+
migrated = true;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
return {
|
|
1773
|
+
enabledAt: targetState && typeof targetState.enabledAt === "string" ? targetState.enabledAt : "",
|
|
1774
|
+
consumers,
|
|
1775
|
+
migrated,
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function normalizeSpecTargets(rawTargets) {
|
|
1780
|
+
const normalizedTargets = {};
|
|
1781
|
+
let migrated = false;
|
|
1782
|
+
|
|
1783
|
+
for (const targetName of SUPPORTED_TARGETS) {
|
|
1784
|
+
if (rawTargets && rawTargets[targetName] && typeof rawTargets[targetName] === "object") {
|
|
1785
|
+
normalizedTargets[targetName] = rawTargets[targetName];
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
const geminiState = normalizedTargets.gemini;
|
|
1790
|
+
if (
|
|
1791
|
+
geminiState
|
|
1792
|
+
&& geminiState.consumers
|
|
1793
|
+
&& geminiState.consumers.antigravity
|
|
1794
|
+
&& !normalizedTargets.antigravity
|
|
1795
|
+
) {
|
|
1796
|
+
normalizedTargets.antigravity = {
|
|
1797
|
+
enabledAt: typeof geminiState.enabledAt === "string" ? geminiState.enabledAt : "",
|
|
1798
|
+
consumers: {
|
|
1799
|
+
antigravity: geminiState.consumers.antigravity,
|
|
1800
|
+
},
|
|
1801
|
+
};
|
|
1802
|
+
delete geminiState.consumers.antigravity;
|
|
1803
|
+
migrated = true;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
const finalTargets = {};
|
|
1807
|
+
for (const targetName of Object.keys(normalizedTargets)) {
|
|
1808
|
+
const normalizedTargetState = normalizeSpecTargetState(targetName, normalizedTargets[targetName]);
|
|
1809
|
+
const consumerIds = Object.keys(normalizedTargetState.consumers);
|
|
1810
|
+
const rawConsumerIds = Object.keys((normalizedTargets[targetName] && normalizedTargets[targetName].consumers) || {});
|
|
1811
|
+
if (consumerIds.length !== rawConsumerIds.length || rawConsumerIds.some((consumerId) => !consumerIds.includes(consumerId))) {
|
|
1812
|
+
migrated = true;
|
|
1813
|
+
}
|
|
1814
|
+
if (normalizedTargetState.migrated) {
|
|
1815
|
+
migrated = true;
|
|
1816
|
+
}
|
|
1817
|
+
finalTargets[targetName] = normalizedTargetState;
|
|
1818
|
+
delete finalTargets[targetName].migrated;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
return { targets: finalTargets, migrated };
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1478
1824
|
function readSpecState() {
|
|
1479
1825
|
const statePath = getSpecStatePath();
|
|
1480
1826
|
if (!fs.existsSync(statePath)) {
|
|
1481
|
-
return { statePath, state: createEmptySpecState() };
|
|
1827
|
+
return { statePath, state: createEmptySpecState(), migrated: false };
|
|
1482
1828
|
}
|
|
1483
1829
|
|
|
1484
1830
|
const raw = fs.readFileSync(statePath, "utf8").trim();
|
|
1485
1831
|
if (!raw) {
|
|
1486
|
-
return { statePath, state: createEmptySpecState() };
|
|
1832
|
+
return { statePath, state: createEmptySpecState(), migrated: false };
|
|
1487
1833
|
}
|
|
1488
1834
|
|
|
1489
1835
|
let parsed;
|
|
@@ -1495,9 +1841,10 @@ function readSpecState() {
|
|
|
1495
1841
|
|
|
1496
1842
|
const state = createEmptySpecState();
|
|
1497
1843
|
state.updatedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : "";
|
|
1498
|
-
|
|
1844
|
+
const normalizedTargets = normalizeSpecTargets(parsed && typeof parsed.targets === "object" && parsed.targets ? parsed.targets : {});
|
|
1845
|
+
state.targets = normalizedTargets.targets;
|
|
1499
1846
|
state.assets = parsed && typeof parsed.assets === "object" && parsed.assets ? parsed.assets : {};
|
|
1500
|
-
return { statePath, state };
|
|
1847
|
+
return { statePath, state, migrated: normalizedTargets.migrated };
|
|
1501
1848
|
}
|
|
1502
1849
|
|
|
1503
1850
|
function writeSpecState(statePath, state) {
|
|
@@ -2092,9 +2439,21 @@ function checkSpecProjectIntegrity(workspaceRoot) {
|
|
|
2092
2439
|
const profilesDir = path.join(specDir, "profiles");
|
|
2093
2440
|
|
|
2094
2441
|
const hasSpecDir = fs.existsSync(specDir);
|
|
2442
|
+
const globalSpecSummary = !hasSpecDir && issuesResult.status !== "missing" ? evaluateSpecState() : null;
|
|
2443
|
+
const canFallbackToGlobalSpec = Boolean(globalSpecSummary && globalSpecSummary.state === "installed");
|
|
2095
2444
|
if (!hasSpecDir) {
|
|
2096
2445
|
if (issuesResult.status !== "missing") {
|
|
2097
|
-
|
|
2446
|
+
if (!canFallbackToGlobalSpec) {
|
|
2447
|
+
issues.push("Missing .ling/spec directory (run: ling spec init)");
|
|
2448
|
+
if (globalSpecSummary && globalSpecSummary.state === "missing") {
|
|
2449
|
+
issues.push("Global Spec is not enabled (run: ling spec enable)");
|
|
2450
|
+
} else if (globalSpecSummary && globalSpecSummary.state === "broken") {
|
|
2451
|
+
issues.push("Global Spec is broken (run: ling spec enable)");
|
|
2452
|
+
for (const issue of globalSpecSummary.issues || []) {
|
|
2453
|
+
issues.push(`Global Spec issue: ${issue}`);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2098
2457
|
}
|
|
2099
2458
|
} else {
|
|
2100
2459
|
if (issuesResult.status === "missing") {
|
|
@@ -2141,7 +2500,10 @@ function resolveWorkspaceSpecBackupRoot(workspaceRoot, timestamp) {
|
|
|
2141
2500
|
}
|
|
2142
2501
|
|
|
2143
2502
|
async function commandSpecInit(options) {
|
|
2144
|
-
const workspaceRoot = options.path
|
|
2503
|
+
const workspaceRoot = options.path
|
|
2504
|
+
? resolveWorkspaceRoot(options.path)
|
|
2505
|
+
: (options.specWorkspace ? getSpecWorkspaceDir() : resolveWorkspaceRoot());
|
|
2506
|
+
const csvOnly = Boolean(options.csvOnly);
|
|
2145
2507
|
const prompter = createConflictPrompter(options);
|
|
2146
2508
|
const timestamp = nowISO().replace(/[:.]/g, "-");
|
|
2147
2509
|
const backupRoot = resolveWorkspaceSpecBackupRoot(workspaceRoot, timestamp);
|
|
@@ -2175,39 +2537,49 @@ async function commandSpecInit(options) {
|
|
|
2175
2537
|
try {
|
|
2176
2538
|
fs.mkdirSync(workspaceRoot, { recursive: true });
|
|
2177
2539
|
|
|
2178
|
-
|
|
2179
|
-
const
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2540
|
+
if (!csvOnly) {
|
|
2541
|
+
for (const [assetName, config] of Object.entries(assets)) {
|
|
2542
|
+
const exists = fs.existsSync(config.destDir);
|
|
2543
|
+
if (exists && areDirectoriesEqual(config.sourceDir, config.destDir)) {
|
|
2544
|
+
log(options, `[skip] Spec ${assetName} 已最新,无需覆盖: ${config.destDir}`);
|
|
2545
|
+
continue;
|
|
2546
|
+
}
|
|
2184
2547
|
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2548
|
+
let action = exists ? "backup" : "";
|
|
2549
|
+
if (exists && prompter) {
|
|
2550
|
+
action = await prompter.resolveConflict({
|
|
2551
|
+
category: "spec:project:assets",
|
|
2552
|
+
label: `Spec ${assetName}`,
|
|
2553
|
+
path: config.destDir,
|
|
2554
|
+
});
|
|
2555
|
+
} else if (exists && !prompter && (options.force || options.nonInteractive || !process.stdin.isTTY)) {
|
|
2556
|
+
action = "backup";
|
|
2557
|
+
}
|
|
2195
2558
|
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2559
|
+
if (action === "keep") {
|
|
2560
|
+
log(options, `[skip] 已保留 Spec ${assetName} 资产,不覆盖: ${config.destDir}`);
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2200
2563
|
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2564
|
+
if (exists && action !== "remove") {
|
|
2565
|
+
backupDirSnapshot(config.destDir, path.join(backupRoot, "assets", assetName), options, `Spec ${assetName}`);
|
|
2566
|
+
} else if (exists && action === "remove") {
|
|
2567
|
+
removeDirIfExists(config.destDir, options, `Spec ${assetName}`);
|
|
2568
|
+
}
|
|
2569
|
+
applyDirSnapshot(config.sourceDir, config.destDir, options, `Spec ${assetName}`);
|
|
2205
2570
|
}
|
|
2206
|
-
applyDirSnapshot(config.sourceDir, config.destDir, options, `Spec ${assetName}`);
|
|
2207
2571
|
}
|
|
2208
2572
|
|
|
2209
2573
|
const issuesPath = path.join(workspaceRoot, "issues.csv");
|
|
2210
|
-
|
|
2574
|
+
let issuesTemplatePath = path.join(specSourceDir, "templates", "issues.template.csv");
|
|
2575
|
+
if (csvOnly && !options.branch) {
|
|
2576
|
+
const globalTemplatePath = path.join(getSpecHomeDir(), "templates", "issues.template.csv");
|
|
2577
|
+
if (fs.existsSync(globalTemplatePath)) {
|
|
2578
|
+
issuesTemplatePath = globalTemplatePath;
|
|
2579
|
+
} else if (!options.quiet) {
|
|
2580
|
+
log(options, "[warn] 未检测到全局 Spec templates,已使用内置模板生成 issues.csv。可执行: ling spec enable");
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2211
2583
|
const issuesTemplate = stripUtf8Bom(fs.readFileSync(issuesTemplatePath, "utf8"));
|
|
2212
2584
|
const hasIssues = fs.existsSync(issuesPath);
|
|
2213
2585
|
if (hasIssues) {
|
|
@@ -2246,7 +2618,8 @@ async function commandSpecInit(options) {
|
|
|
2246
2618
|
}
|
|
2247
2619
|
|
|
2248
2620
|
const requestedTargets = normalizeTargets(options.targets);
|
|
2249
|
-
const
|
|
2621
|
+
const isSpecWorkspaceMode = Boolean(!options.path && options.specWorkspace);
|
|
2622
|
+
const shouldInitTargets = isSpecWorkspaceMode ? true : requestedTargets.length > 0;
|
|
2250
2623
|
const targets = shouldInitTargets ? (requestedTargets.length > 0 ? requestedTargets : [...SUPPORTED_TARGETS]) : [];
|
|
2251
2624
|
if (targets.length > 0) {
|
|
2252
2625
|
await initTargets(workspaceRoot, targets, options, prompter);
|
|
@@ -2260,7 +2633,9 @@ async function commandSpecInit(options) {
|
|
|
2260
2633
|
}
|
|
2261
2634
|
|
|
2262
2635
|
function commandSpecDoctor(options) {
|
|
2263
|
-
const workspaceRoot = options.path
|
|
2636
|
+
const workspaceRoot = options.path
|
|
2637
|
+
? resolveWorkspaceRoot(options.path)
|
|
2638
|
+
: (options.specWorkspace ? getSpecWorkspaceDir() : resolveWorkspaceRoot());
|
|
2264
2639
|
const result = checkSpecProjectIntegrity(workspaceRoot);
|
|
2265
2640
|
const state = result.status === "ok" ? "installed" : result.status;
|
|
2266
2641
|
|
|
@@ -2279,6 +2654,21 @@ function commandSpecDoctor(options) {
|
|
|
2279
2654
|
|
|
2280
2655
|
console.log(state === "installed" ? "[ok] Spec 项目资产状态正常" : "[warn] Spec 项目资产存在问题");
|
|
2281
2656
|
console.log(` 工作区: ${workspaceRoot}`);
|
|
2657
|
+
const specDir = path.join(workspaceRoot, ".ling", "spec");
|
|
2658
|
+
const hasSpecDir = fs.existsSync(specDir);
|
|
2659
|
+
const issuesPath = path.join(workspaceRoot, "issues.csv");
|
|
2660
|
+
const hasIssues = fs.existsSync(issuesPath);
|
|
2661
|
+
if (hasSpecDir) {
|
|
2662
|
+
console.log(" 模式: project");
|
|
2663
|
+
console.log(` Spec 根目录: ${specDir}`);
|
|
2664
|
+
} else if (hasIssues) {
|
|
2665
|
+
const globalSpecSummary = evaluateSpecState();
|
|
2666
|
+
const fallback = globalSpecSummary.state === "installed";
|
|
2667
|
+
console.log(` 模式: csv-only${fallback ? " (global fallback)" : ""}`);
|
|
2668
|
+
if (fallback) {
|
|
2669
|
+
console.log(` Spec 根目录: ${getSpecHomeDir()}`);
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2282
2672
|
console.log(` 任务数: ${result.stats.total}`);
|
|
2283
2673
|
console.log(` 进行中: ${result.stats.inProgress}`);
|
|
2284
2674
|
for (const issue of result.issues || []) {
|
|
@@ -2308,6 +2698,10 @@ async function commandSpecEnable(options) {
|
|
|
2308
2698
|
writeSpecState(statePath, state);
|
|
2309
2699
|
}
|
|
2310
2700
|
|
|
2701
|
+
if (targets.includes("codex")) {
|
|
2702
|
+
migrateLegacyCodexGlobalSkills(timestamp, options);
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2311
2705
|
log(options, `[ok] Spec Profile 已启用 (Targets: ${targets.join(", ")})`);
|
|
2312
2706
|
}
|
|
2313
2707
|
|
|
@@ -2368,23 +2762,23 @@ async function commandSpec(options) {
|
|
|
2368
2762
|
}
|
|
2369
2763
|
|
|
2370
2764
|
async function initTargets(workspaceRoot, targets, options, prompter) {
|
|
2371
|
-
for (const
|
|
2765
|
+
for (const group of groupTargetsByInstallSurface(targets)) {
|
|
2372
2766
|
const runOptions = { ...options };
|
|
2373
2767
|
const conflicts = [];
|
|
2374
2768
|
|
|
2375
|
-
if (
|
|
2769
|
+
if (group.installSurface === ".agent") {
|
|
2376
2770
|
const agentDir = path.join(workspaceRoot, ".agent");
|
|
2377
2771
|
if (fs.existsSync(agentDir)) {
|
|
2378
2772
|
conflicts.push({
|
|
2379
|
-
category: "project:
|
|
2773
|
+
category: "project:shared-agent",
|
|
2380
2774
|
label: ".agent",
|
|
2381
2775
|
path: agentDir,
|
|
2382
|
-
target,
|
|
2776
|
+
target: group.logicalTargets.join(","),
|
|
2383
2777
|
});
|
|
2384
2778
|
}
|
|
2385
2779
|
}
|
|
2386
2780
|
|
|
2387
|
-
if (
|
|
2781
|
+
if (group.adapterTarget === "codex") {
|
|
2388
2782
|
const managedDir = path.join(workspaceRoot, ".agents");
|
|
2389
2783
|
const legacyDir = path.join(workspaceRoot, ".codex");
|
|
2390
2784
|
if (fs.existsSync(managedDir) || fs.existsSync(legacyDir)) {
|
|
@@ -2435,10 +2829,13 @@ async function initTargets(workspaceRoot, targets, options, prompter) {
|
|
|
2435
2829
|
}
|
|
2436
2830
|
}
|
|
2437
2831
|
|
|
2438
|
-
const adapter = createAdapter(
|
|
2439
|
-
log(options, `[sync] 正在初始化目标 [${
|
|
2832
|
+
const adapter = createAdapter(group.adapterTarget, workspaceRoot, runOptions);
|
|
2833
|
+
log(options, `[sync] 正在初始化目标 [${group.logicalTargets.join(", ")}] ...`);
|
|
2440
2834
|
adapter.install(BUNDLED_AGENT_DIR);
|
|
2441
|
-
|
|
2835
|
+
recordWorkspaceInstallTargets(workspaceRoot, group.logicalTargets, runOptions);
|
|
2836
|
+
for (const target of group.logicalTargets) {
|
|
2837
|
+
registerWorkspaceTarget(workspaceRoot, target, runOptions);
|
|
2838
|
+
}
|
|
2442
2839
|
}
|
|
2443
2840
|
}
|
|
2444
2841
|
|
|
@@ -2523,71 +2920,73 @@ async function commandUpdate(options) {
|
|
|
2523
2920
|
|
|
2524
2921
|
let updatedAny = false;
|
|
2525
2922
|
try {
|
|
2526
|
-
for (const
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2923
|
+
for (const group of groupTargetsByInstallSurface(targets)) {
|
|
2924
|
+
if (!isTargetInstalled(workspaceRoot, group.adapterTarget) && options.targets.length > 0) {
|
|
2925
|
+
throw new Error(`目标未安装: ${group.logicalTargets.join(", ")}`);
|
|
2926
|
+
}
|
|
2927
|
+
if (!isTargetInstalled(workspaceRoot, group.adapterTarget)) {
|
|
2928
|
+
log(options, `[skip] 目标未安装,跳过: ${group.logicalTargets.join(", ")}`);
|
|
2929
|
+
continue;
|
|
2930
|
+
}
|
|
2534
2931
|
|
|
2535
|
-
|
|
2932
|
+
const runOptions = { ...options, force: true };
|
|
2933
|
+
const timestamp = nowISO().replace(/[:.]/g, "-");
|
|
2536
2934
|
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
}
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2935
|
+
if (group.installSurface === ".agent") {
|
|
2936
|
+
const agentDir = path.join(workspaceRoot, ".agent");
|
|
2937
|
+
if (fs.existsSync(agentDir) && !areDirectoriesEqual(BUNDLED_AGENT_DIR, agentDir)) {
|
|
2938
|
+
let action = "backup";
|
|
2939
|
+
if (prompter) {
|
|
2940
|
+
action = await prompter.resolveConflict({
|
|
2941
|
+
category: "project:shared-agent",
|
|
2942
|
+
label: ".agent",
|
|
2943
|
+
path: agentDir,
|
|
2944
|
+
target: group.logicalTargets.join(","),
|
|
2945
|
+
});
|
|
2946
|
+
}
|
|
2947
|
+
if (action === "keep") {
|
|
2948
|
+
log(options, "[skip] 已保留现有资产,跳过更新: .agent");
|
|
2949
|
+
continue;
|
|
2950
|
+
}
|
|
2951
|
+
if (action === "backup") {
|
|
2952
|
+
backupWorkspaceDir(workspaceRoot, agentDir, ".agent-backup", timestamp, options, "工作区资产 .agent");
|
|
2953
|
+
}
|
|
2556
2954
|
}
|
|
2557
2955
|
}
|
|
2558
|
-
}
|
|
2559
2956
|
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2957
|
+
if (group.adapterTarget === "codex") {
|
|
2958
|
+
const managedDir = path.join(workspaceRoot, ".agents");
|
|
2959
|
+
const legacyDir = path.join(workspaceRoot, ".codex");
|
|
2960
|
+
const activeDir = fs.existsSync(managedDir) ? managedDir : legacyDir;
|
|
2961
|
+
const conflict = evaluateCodexUpdateConflict(activeDir);
|
|
2962
|
+
if (conflict.hasConflict) {
|
|
2963
|
+
let action = "backup";
|
|
2964
|
+
if (prompter) {
|
|
2965
|
+
action = await prompter.resolveConflict({
|
|
2966
|
+
category: "project:codex",
|
|
2967
|
+
label: fs.existsSync(managedDir) ? ".agents" : ".codex",
|
|
2968
|
+
path: activeDir,
|
|
2969
|
+
target: group.logicalTargets.join(","),
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
if (action === "keep") {
|
|
2973
|
+
log(options, `[skip] 已保留现有资产,跳过更新: ${path.basename(activeDir)}`);
|
|
2974
|
+
continue;
|
|
2975
|
+
}
|
|
2976
|
+
if (action === "backup") {
|
|
2977
|
+
backupWorkspaceDir(workspaceRoot, activeDir, ".agents-backup", timestamp, options, `工作区资产 ${path.basename(activeDir)}`);
|
|
2978
|
+
}
|
|
2582
2979
|
}
|
|
2583
2980
|
}
|
|
2584
|
-
}
|
|
2585
2981
|
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2982
|
+
const adapter = createAdapter(group.adapterTarget, workspaceRoot, runOptions);
|
|
2983
|
+
log(options, `[sync] 更新 [${group.logicalTargets.join(", ")}] ...`);
|
|
2984
|
+
adapter.update(BUNDLED_AGENT_DIR);
|
|
2985
|
+
recordWorkspaceInstallTargets(workspaceRoot, group.logicalTargets, runOptions);
|
|
2986
|
+
for (const target of group.logicalTargets) {
|
|
2987
|
+
registerWorkspaceTarget(workspaceRoot, target, runOptions);
|
|
2988
|
+
}
|
|
2989
|
+
updatedAny = true;
|
|
2591
2990
|
}
|
|
2592
2991
|
} finally {
|
|
2593
2992
|
if (prompter) prompter.close();
|
|
@@ -2680,7 +3079,7 @@ async function commandUpdateAll(options) {
|
|
|
2680
3079
|
const installedTargets = detectInstalledTargets(workspacePath);
|
|
2681
3080
|
let targets = [];
|
|
2682
3081
|
if (requestedTargets.length > 0) {
|
|
2683
|
-
targets =
|
|
3082
|
+
targets = requestedTargets.filter((target) => isTargetInstalled(workspacePath, target));
|
|
2684
3083
|
} else {
|
|
2685
3084
|
targets = [...Object.keys(item.targets || {}), ...installedTargets];
|
|
2686
3085
|
}
|
|
@@ -2696,9 +3095,9 @@ async function commandUpdateAll(options) {
|
|
|
2696
3095
|
log(options, `[sync] [${i + 1}/${records.length}] 更新: ${workspacePath} [${targets.join(", ")}]`);
|
|
2697
3096
|
|
|
2698
3097
|
const updatedTargets = [];
|
|
2699
|
-
for (const
|
|
2700
|
-
if (!isTargetInstalled(workspacePath,
|
|
2701
|
-
log(options, `[skip] [${i + 1}/${records.length}] 目标未安装,跳过: ${
|
|
3098
|
+
for (const group of groupTargetsByInstallSurface(targets)) {
|
|
3099
|
+
if (!isTargetInstalled(workspacePath, group.adapterTarget)) {
|
|
3100
|
+
log(options, `[skip] [${i + 1}/${records.length}] 目标未安装,跳过: ${group.logicalTargets.join(", ")}`);
|
|
2702
3101
|
continue;
|
|
2703
3102
|
}
|
|
2704
3103
|
|
|
@@ -2712,19 +3111,19 @@ async function commandUpdateAll(options) {
|
|
|
2712
3111
|
|
|
2713
3112
|
const timestampForBackup = nowISO().replace(/[:.]/g, "-");
|
|
2714
3113
|
|
|
2715
|
-
if (
|
|
3114
|
+
if (group.installSurface === ".agent") {
|
|
2716
3115
|
const agentDir = path.join(workspacePath, ".agent");
|
|
2717
3116
|
if (fs.existsSync(agentDir) && !areDirectoriesEqual(BUNDLED_AGENT_DIR, agentDir)) {
|
|
2718
3117
|
let action = "backup";
|
|
2719
3118
|
if (prompter) {
|
|
2720
3119
|
action = await prompter.resolveConflict({
|
|
2721
|
-
category: "update-all:project:
|
|
3120
|
+
category: "update-all:project:shared-agent",
|
|
2722
3121
|
label: `.agent (${workspacePath})`,
|
|
2723
3122
|
path: agentDir,
|
|
2724
3123
|
});
|
|
2725
3124
|
}
|
|
2726
3125
|
if (action === "keep") {
|
|
2727
|
-
log(options, `[skip] [${i + 1}/${records.length}] 已保留现有资产,跳过更新: ${workspacePath} [
|
|
3126
|
+
log(options, `[skip] [${i + 1}/${records.length}] 已保留现有资产,跳过更新: ${workspacePath} [${group.logicalTargets.join(", ")}]`);
|
|
2728
3127
|
continue;
|
|
2729
3128
|
}
|
|
2730
3129
|
if (action === "backup") {
|
|
@@ -2733,7 +3132,7 @@ async function commandUpdateAll(options) {
|
|
|
2733
3132
|
}
|
|
2734
3133
|
}
|
|
2735
3134
|
|
|
2736
|
-
if (
|
|
3135
|
+
if (group.adapterTarget === "codex") {
|
|
2737
3136
|
const managedDir = path.join(workspacePath, ".agents");
|
|
2738
3137
|
const legacyDir = path.join(workspacePath, ".codex");
|
|
2739
3138
|
const activeDir = fs.existsSync(managedDir) ? managedDir : legacyDir;
|
|
@@ -2757,13 +3156,14 @@ async function commandUpdateAll(options) {
|
|
|
2757
3156
|
}
|
|
2758
3157
|
}
|
|
2759
3158
|
|
|
2760
|
-
const adapter = createAdapter(
|
|
3159
|
+
const adapter = createAdapter(group.adapterTarget, workspacePath, runOptions);
|
|
2761
3160
|
adapter.update(BUNDLED_AGENT_DIR);
|
|
2762
|
-
|
|
3161
|
+
recordWorkspaceInstallTargets(workspacePath, group.logicalTargets, runOptions);
|
|
3162
|
+
updatedTargets.push(...group.logicalTargets);
|
|
2763
3163
|
} catch (err) {
|
|
2764
3164
|
failed += 1;
|
|
2765
3165
|
if (!options.quiet) {
|
|
2766
|
-
console.error(`[error] 更新失败: ${workspacePath} [${
|
|
3166
|
+
console.error(`[error] 更新失败: ${workspacePath} [${group.logicalTargets.join(", ")}]`);
|
|
2767
3167
|
console.error(` ${err.message}`);
|
|
2768
3168
|
}
|
|
2769
3169
|
}
|
|
@@ -3068,6 +3468,25 @@ function countSkillsRecursive(dir) {
|
|
|
3068
3468
|
return count;
|
|
3069
3469
|
}
|
|
3070
3470
|
|
|
3471
|
+
function printSharedAgentTargetStatus(workspaceRoot, targetState, label) {
|
|
3472
|
+
const agentDir = path.join(workspaceRoot, ".agent");
|
|
3473
|
+
const agentsCount = countFilesIfExists(path.join(agentDir, "agents"), (name) => name.endsWith(".md"));
|
|
3474
|
+
const workflowsCount = countFilesIfExists(path.join(agentDir, "workflows"), (name) => name.endsWith(".md"));
|
|
3475
|
+
const skillsCount = countSkillsRecursive(path.join(agentDir, "skills"));
|
|
3476
|
+
console.log(`\n[${label}]`);
|
|
3477
|
+
console.log(` 状态: ${targetState.state}`);
|
|
3478
|
+
console.log(` 路径: ${agentDir}`);
|
|
3479
|
+
if (targetState.state === "installed") {
|
|
3480
|
+
console.log(` Agents: ${agentsCount}`);
|
|
3481
|
+
console.log(` Skills: ${skillsCount}`);
|
|
3482
|
+
console.log(` Workflows: ${workflowsCount}`);
|
|
3483
|
+
return;
|
|
3484
|
+
}
|
|
3485
|
+
for (const issue of targetState.integrity.issues || []) {
|
|
3486
|
+
console.log(` Issue: ${issue}`);
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3071
3490
|
function commandStatus(options) {
|
|
3072
3491
|
const workspaceRoot = resolveWorkspaceRoot(options.path);
|
|
3073
3492
|
const summary = evaluateWorkspaceState(workspaceRoot, options);
|
|
@@ -3098,22 +3517,12 @@ function commandStatus(options) {
|
|
|
3098
3517
|
|
|
3099
3518
|
const geminiState = summary.targets.find((item) => item.targetName === "gemini");
|
|
3100
3519
|
if (geminiState) {
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
console.log(` 路径: ${agentDir}`);
|
|
3108
|
-
if (geminiState.state === "installed") {
|
|
3109
|
-
console.log(` Agents: ${agentsCount}`);
|
|
3110
|
-
console.log(` Skills: ${skillsCount}`);
|
|
3111
|
-
console.log(` Workflows: ${workflowsCount}`);
|
|
3112
|
-
} else {
|
|
3113
|
-
for (const issue of geminiState.integrity.issues || []) {
|
|
3114
|
-
console.log(` Issue: ${issue}`);
|
|
3115
|
-
}
|
|
3116
|
-
}
|
|
3520
|
+
printSharedAgentTargetStatus(workspaceRoot, geminiState, "gemini");
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
const antigravityState = summary.targets.find((item) => item.targetName === "antigravity");
|
|
3524
|
+
if (antigravityState) {
|
|
3525
|
+
printSharedAgentTargetStatus(workspaceRoot, antigravityState, "antigravity");
|
|
3117
3526
|
}
|
|
3118
3527
|
|
|
3119
3528
|
const codexState = summary.targets.find((item) => item.targetName === "codex");
|