@mison/ling 1.1.1 → 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/CHANGELOG.md +20 -1
- package/README.md +138 -195
- package/bin/adapters/gemini.js +6 -2
- package/bin/interactive.js +6 -4
- package/bin/ling-cli.js +346 -137
- package/docs/PLAN.md +21 -17
- package/docs/TECH.md +25 -13
- package/package.json +1 -1
- package/scripts/ci-verify.js +4 -1
- package/scripts/health-check.js +4 -13
- package/tests/cli-smoke.test.js +115 -0
- package/tests/global-sync.test.js +15 -2
- package/tests/spec-init-doctor.test.js +58 -0
- package/tests/spec-profile.test.js +16 -0
- package/tests/standards-compliance.test.js +31 -0
- 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
|
};
|
|
@@ -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"],
|
|
@@ -170,13 +173,13 @@ function printUsage() {
|
|
|
170
173
|
console.log(` ${PRIMARY_CLI_NAME} update [--path <dir>] [--branch <name>] [--target <name>|--targets <a,b>] [--no-index] [--quiet] [--dry-run]`);
|
|
171
174
|
console.log(` ${PRIMARY_CLI_NAME} update-all [--branch <name>] [--targets <a,b>] [--prune-missing] [--quiet] [--dry-run]`);
|
|
172
175
|
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
|
|
176
|
+
console.log(` ${PRIMARY_CLI_NAME} global sync [--target <name>|--targets <a,b>] [--branch <name>] [--quiet] [--dry-run] # 默认同步 codex + gemini + antigravity`);
|
|
174
177
|
console.log(` ${PRIMARY_CLI_NAME} global status [--quiet]`);
|
|
175
178
|
console.log(` ${PRIMARY_CLI_NAME} spec enable [--target <name>|--targets <a,b>] [--quiet] [--dry-run]`);
|
|
176
179
|
console.log(` ${PRIMARY_CLI_NAME} spec disable [--target <name>|--targets <a,b>] [--quiet] [--dry-run]`);
|
|
177
180
|
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]`);
|
|
181
|
+
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]`);
|
|
182
|
+
console.log(` ${PRIMARY_CLI_NAME} spec doctor [--path <dir>] [--spec-workspace] [--quiet]`);
|
|
180
183
|
console.log(` ${PRIMARY_CLI_NAME} exclude list [--quiet]`);
|
|
181
184
|
console.log(` ${PRIMARY_CLI_NAME} exclude add --path <dir> [--dry-run] [--quiet]`);
|
|
182
185
|
console.log(` ${PRIMARY_CLI_NAME} exclude remove --path <dir> [--dry-run] [--quiet]`);
|
|
@@ -202,6 +205,8 @@ function parseArgs(argv) {
|
|
|
202
205
|
nonInteractive: false,
|
|
203
206
|
noIndex: false,
|
|
204
207
|
fix: false,
|
|
208
|
+
csvOnly: false,
|
|
209
|
+
specWorkspace: false,
|
|
205
210
|
subcommand: "",
|
|
206
211
|
path: "",
|
|
207
212
|
branch: "",
|
|
@@ -252,6 +257,12 @@ function parseArgs(argv) {
|
|
|
252
257
|
} else if (arg === "--fix") {
|
|
253
258
|
providedFlags.push(arg);
|
|
254
259
|
options.fix = true;
|
|
260
|
+
} else if (arg === "--csv-only") {
|
|
261
|
+
providedFlags.push(arg);
|
|
262
|
+
options.csvOnly = true;
|
|
263
|
+
} else if (arg === "--spec-workspace") {
|
|
264
|
+
providedFlags.push(arg);
|
|
265
|
+
options.specWorkspace = true;
|
|
255
266
|
} else if (arg === "--path") {
|
|
256
267
|
providedFlags.push(arg);
|
|
257
268
|
if (i + 1 >= argv.length) {
|
|
@@ -295,8 +306,8 @@ const COMMAND_ALLOWED_FLAGS = {
|
|
|
295
306
|
"spec:enable": ["--target", "--targets", "--quiet", "--dry-run"],
|
|
296
307
|
"spec:disable": ["--target", "--targets", "--quiet", "--dry-run"],
|
|
297
308
|
"spec:status": ["--quiet"],
|
|
298
|
-
"spec:init": ["--force", "--path", "--branch", "--target", "--targets", "--non-interactive", "--no-index", "--quiet", "--dry-run"],
|
|
299
|
-
"spec:doctor": ["--path", "--quiet"],
|
|
309
|
+
"spec:init": ["--force", "--path", "--spec-workspace", "--branch", "--csv-only", "--target", "--targets", "--non-interactive", "--no-index", "--quiet", "--dry-run"],
|
|
310
|
+
"spec:doctor": ["--path", "--spec-workspace", "--quiet"],
|
|
300
311
|
"exclude:list": ["--quiet"],
|
|
301
312
|
"exclude:add": ["--path", "--dry-run", "--quiet"],
|
|
302
313
|
"exclude:remove": ["--path", "--dry-run", "--quiet"],
|
|
@@ -389,6 +400,12 @@ function pathCompareKey(inputPath) {
|
|
|
389
400
|
return normalized;
|
|
390
401
|
}
|
|
391
402
|
|
|
403
|
+
function findWorkspaceRecord(index, workspaceRoot) {
|
|
404
|
+
const normalizedPath = normalizeAbsolutePath(workspaceRoot);
|
|
405
|
+
const targetKey = pathCompareKey(normalizedPath);
|
|
406
|
+
return (index.workspaces || []).find((item) => pathCompareKey(item.path) === targetKey) || null;
|
|
407
|
+
}
|
|
408
|
+
|
|
392
409
|
function normalizePathList(items) {
|
|
393
410
|
const map = new Map();
|
|
394
411
|
for (const item of items) {
|
|
@@ -627,6 +644,95 @@ function writeWorkspaceIndex(indexPath, index) {
|
|
|
627
644
|
fs.writeFileSync(indexPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
628
645
|
}
|
|
629
646
|
|
|
647
|
+
function getWorkspaceInstallStatePath(workspaceRoot) {
|
|
648
|
+
return path.join(normalizeAbsolutePath(workspaceRoot), ".ling", "install-state.json");
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function createEmptyWorkspaceInstallState() {
|
|
652
|
+
return {
|
|
653
|
+
version: 1,
|
|
654
|
+
updatedAt: "",
|
|
655
|
+
targets: {},
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function readWorkspaceInstallState(workspaceRoot) {
|
|
660
|
+
const statePath = getWorkspaceInstallStatePath(workspaceRoot);
|
|
661
|
+
if (!fs.existsSync(statePath)) {
|
|
662
|
+
return { statePath, state: createEmptyWorkspaceInstallState() };
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const raw = fs.readFileSync(statePath, "utf8").trim();
|
|
666
|
+
if (!raw) {
|
|
667
|
+
return { statePath, state: createEmptyWorkspaceInstallState() };
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
let parsed;
|
|
671
|
+
try {
|
|
672
|
+
parsed = JSON.parse(raw);
|
|
673
|
+
} catch (_err) {
|
|
674
|
+
return { statePath, state: createEmptyWorkspaceInstallState() };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const state = createEmptyWorkspaceInstallState();
|
|
678
|
+
state.updatedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : "";
|
|
679
|
+
if (parsed && parsed.targets && typeof parsed.targets === "object") {
|
|
680
|
+
for (const [targetName, targetState] of Object.entries(parsed.targets)) {
|
|
681
|
+
const normalizedTargetName = normalizeIndexTargetName(targetName);
|
|
682
|
+
const normalizedTargetState = normalizeTargetState(targetState);
|
|
683
|
+
if (normalizedTargetName && normalizedTargetState) {
|
|
684
|
+
state.targets[normalizedTargetName] = normalizedTargetState;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return { statePath, state };
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function writeWorkspaceInstallState(statePath, state) {
|
|
693
|
+
const payload = {
|
|
694
|
+
version: 1,
|
|
695
|
+
updatedAt: state.updatedAt || nowISO(),
|
|
696
|
+
targets: state.targets || {},
|
|
697
|
+
};
|
|
698
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
699
|
+
fs.writeFileSync(statePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function recordWorkspaceInstallTargets(workspaceRoot, targetNames, options) {
|
|
703
|
+
const logicalTargets = normalizeTargets(targetNames).filter((targetName) => SHARED_AGENT_TARGETS.includes(targetName));
|
|
704
|
+
if (logicalTargets.length === 0) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (options.dryRun) {
|
|
709
|
+
log(options, `[dry-run] 将写入工作区安装状态: ${getWorkspaceInstallStatePath(workspaceRoot)} [${logicalTargets.join(", ")}]`);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const { statePath, state } = readWorkspaceInstallState(workspaceRoot);
|
|
714
|
+
const timestamp = nowISO();
|
|
715
|
+
for (const targetName of logicalTargets) {
|
|
716
|
+
const prev = normalizeTargetState(state.targets[targetName]) || {
|
|
717
|
+
version: "",
|
|
718
|
+
installedAt: "",
|
|
719
|
+
updatedAt: "",
|
|
720
|
+
};
|
|
721
|
+
state.targets[targetName] = {
|
|
722
|
+
version: pkg.version,
|
|
723
|
+
installedAt: prev.installedAt || timestamp,
|
|
724
|
+
updatedAt: timestamp,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
state.updatedAt = timestamp;
|
|
728
|
+
writeWorkspaceInstallState(statePath, state);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function resolveWorkspaceInstallStateTargets(workspaceRoot) {
|
|
732
|
+
const { state } = readWorkspaceInstallState(workspaceRoot);
|
|
733
|
+
return normalizeTargets(Object.keys(state.targets || {}));
|
|
734
|
+
}
|
|
735
|
+
|
|
630
736
|
function sleepSync(ms) {
|
|
631
737
|
const buffer = new SharedArrayBuffer(4);
|
|
632
738
|
const view = new Int32Array(buffer);
|
|
@@ -906,19 +1012,41 @@ function normalizeTargets(rawTargets) {
|
|
|
906
1012
|
return result;
|
|
907
1013
|
}
|
|
908
1014
|
|
|
1015
|
+
function resolveIndexedWorkspaceTargets(workspaceRoot) {
|
|
1016
|
+
try {
|
|
1017
|
+
const { index } = readWorkspaceIndex();
|
|
1018
|
+
const record = findWorkspaceRecord(index, workspaceRoot);
|
|
1019
|
+
if (!record) {
|
|
1020
|
+
return [];
|
|
1021
|
+
}
|
|
1022
|
+
return normalizeTargets(Object.keys(record.targets || {}));
|
|
1023
|
+
} catch (_err) {
|
|
1024
|
+
return [];
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
909
1028
|
function detectInstalledTargets(workspaceRoot) {
|
|
910
1029
|
const targets = [];
|
|
1030
|
+
const localTargets = resolveWorkspaceInstallStateTargets(workspaceRoot);
|
|
1031
|
+
const indexedTargets = resolveIndexedWorkspaceTargets(workspaceRoot);
|
|
911
1032
|
if (fs.existsSync(path.join(workspaceRoot, ".agent"))) {
|
|
912
|
-
|
|
1033
|
+
const sharedTargets = localTargets
|
|
1034
|
+
.filter((target) => SHARED_AGENT_TARGETS.includes(target))
|
|
1035
|
+
.concat(indexedTargets.filter((target) => SHARED_AGENT_TARGETS.includes(target)));
|
|
1036
|
+
if (sharedTargets.length > 0) {
|
|
1037
|
+
targets.push(...sharedTargets);
|
|
1038
|
+
} else {
|
|
1039
|
+
targets.push("gemini");
|
|
1040
|
+
}
|
|
913
1041
|
}
|
|
914
1042
|
if (fs.existsSync(path.join(workspaceRoot, ".agents")) || fs.existsSync(path.join(workspaceRoot, ".codex"))) {
|
|
915
1043
|
targets.push("codex");
|
|
916
1044
|
}
|
|
917
|
-
return targets;
|
|
1045
|
+
return normalizeTargets(targets);
|
|
918
1046
|
}
|
|
919
1047
|
|
|
920
1048
|
function isTargetInstalled(workspaceRoot, targetName) {
|
|
921
|
-
if (targetName
|
|
1049
|
+
if (SHARED_AGENT_TARGETS.includes(targetName)) {
|
|
922
1050
|
return fs.existsSync(path.join(workspaceRoot, ".agent"));
|
|
923
1051
|
}
|
|
924
1052
|
if (targetName === "codex") {
|
|
@@ -927,6 +1055,27 @@ function isTargetInstalled(workspaceRoot, targetName) {
|
|
|
927
1055
|
return false;
|
|
928
1056
|
}
|
|
929
1057
|
|
|
1058
|
+
function groupTargetsByInstallSurface(targets) {
|
|
1059
|
+
const normalizedTargets = normalizeTargets(targets);
|
|
1060
|
+
const groups = [];
|
|
1061
|
+
const sharedTargets = normalizedTargets.filter((target) => SHARED_AGENT_TARGETS.includes(target));
|
|
1062
|
+
if (sharedTargets.length > 0) {
|
|
1063
|
+
groups.push({
|
|
1064
|
+
installSurface: ".agent",
|
|
1065
|
+
adapterTarget: sharedTargets.includes("gemini") ? "gemini" : "antigravity",
|
|
1066
|
+
logicalTargets: sharedTargets,
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
if (normalizedTargets.includes("codex")) {
|
|
1070
|
+
groups.push({
|
|
1071
|
+
installSurface: ".agents",
|
|
1072
|
+
adapterTarget: "codex",
|
|
1073
|
+
logicalTargets: ["codex"],
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
return groups;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
930
1079
|
function setQuietStatusExitCode(state) {
|
|
931
1080
|
process.exitCode = Object.prototype.hasOwnProperty.call(QUIET_STATUS_EXIT_CODES, state)
|
|
932
1081
|
? QUIET_STATUS_EXIT_CODES[state]
|
|
@@ -1022,8 +1171,11 @@ function evaluateGlobalState() {
|
|
|
1022
1171
|
}
|
|
1023
1172
|
|
|
1024
1173
|
function createAdapter(targetName, workspaceRoot, options) {
|
|
1025
|
-
if (targetName
|
|
1026
|
-
return new GeminiAdapter(workspaceRoot,
|
|
1174
|
+
if (SHARED_AGENT_TARGETS.includes(targetName)) {
|
|
1175
|
+
return new GeminiAdapter(workspaceRoot, {
|
|
1176
|
+
...options,
|
|
1177
|
+
targetName,
|
|
1178
|
+
});
|
|
1027
1179
|
}
|
|
1028
1180
|
if (targetName === "codex") {
|
|
1029
1181
|
return new CodexAdapter(workspaceRoot, options);
|
|
@@ -1067,8 +1219,7 @@ function resolveTargetsForGlobalSync(options) {
|
|
|
1067
1219
|
if (requested.length > 0) {
|
|
1068
1220
|
return requested;
|
|
1069
1221
|
}
|
|
1070
|
-
|
|
1071
|
-
return ["codex", "gemini"];
|
|
1222
|
+
return [...SUPPORTED_TARGETS];
|
|
1072
1223
|
}
|
|
1073
1224
|
|
|
1074
1225
|
function resolveAgentInstallSource(options) {
|
|
@@ -1235,7 +1386,7 @@ function planGlobalSyncTasks(targetName, agentDir) {
|
|
|
1235
1386
|
};
|
|
1236
1387
|
}
|
|
1237
1388
|
|
|
1238
|
-
if (targetName
|
|
1389
|
+
if (SHARED_AGENT_TARGETS.includes(targetName)) {
|
|
1239
1390
|
const skillsRoot = path.join(agentDir, "skills");
|
|
1240
1391
|
const skillNames = listSkillDirectories(skillsRoot);
|
|
1241
1392
|
const tasks = [];
|
|
@@ -1317,7 +1468,7 @@ function applyGlobalSync(targetName, agentDir, timestamp, options) {
|
|
|
1317
1468
|
}
|
|
1318
1469
|
}
|
|
1319
1470
|
|
|
1320
|
-
if (targetName
|
|
1471
|
+
if (SHARED_AGENT_TARGETS.includes(targetName)) {
|
|
1321
1472
|
const skillsRoot = path.join(agentDir, "skills");
|
|
1322
1473
|
return syncGlobalSkillsFromRoot(targetName, skillsRoot, timestamp, options);
|
|
1323
1474
|
}
|
|
@@ -2092,9 +2243,21 @@ function checkSpecProjectIntegrity(workspaceRoot) {
|
|
|
2092
2243
|
const profilesDir = path.join(specDir, "profiles");
|
|
2093
2244
|
|
|
2094
2245
|
const hasSpecDir = fs.existsSync(specDir);
|
|
2246
|
+
const globalSpecSummary = !hasSpecDir && issuesResult.status !== "missing" ? evaluateSpecState() : null;
|
|
2247
|
+
const canFallbackToGlobalSpec = Boolean(globalSpecSummary && globalSpecSummary.state === "installed");
|
|
2095
2248
|
if (!hasSpecDir) {
|
|
2096
2249
|
if (issuesResult.status !== "missing") {
|
|
2097
|
-
|
|
2250
|
+
if (!canFallbackToGlobalSpec) {
|
|
2251
|
+
issues.push("Missing .ling/spec directory (run: ling spec init)");
|
|
2252
|
+
if (globalSpecSummary && globalSpecSummary.state === "missing") {
|
|
2253
|
+
issues.push("Global Spec is not enabled (run: ling spec enable)");
|
|
2254
|
+
} else if (globalSpecSummary && globalSpecSummary.state === "broken") {
|
|
2255
|
+
issues.push("Global Spec is broken (run: ling spec enable)");
|
|
2256
|
+
for (const issue of globalSpecSummary.issues || []) {
|
|
2257
|
+
issues.push(`Global Spec issue: ${issue}`);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2098
2261
|
}
|
|
2099
2262
|
} else {
|
|
2100
2263
|
if (issuesResult.status === "missing") {
|
|
@@ -2141,7 +2304,10 @@ function resolveWorkspaceSpecBackupRoot(workspaceRoot, timestamp) {
|
|
|
2141
2304
|
}
|
|
2142
2305
|
|
|
2143
2306
|
async function commandSpecInit(options) {
|
|
2144
|
-
const workspaceRoot = options.path
|
|
2307
|
+
const workspaceRoot = options.path
|
|
2308
|
+
? resolveWorkspaceRoot(options.path)
|
|
2309
|
+
: (options.specWorkspace ? getSpecWorkspaceDir() : resolveWorkspaceRoot());
|
|
2310
|
+
const csvOnly = Boolean(options.csvOnly);
|
|
2145
2311
|
const prompter = createConflictPrompter(options);
|
|
2146
2312
|
const timestamp = nowISO().replace(/[:.]/g, "-");
|
|
2147
2313
|
const backupRoot = resolveWorkspaceSpecBackupRoot(workspaceRoot, timestamp);
|
|
@@ -2175,39 +2341,49 @@ async function commandSpecInit(options) {
|
|
|
2175
2341
|
try {
|
|
2176
2342
|
fs.mkdirSync(workspaceRoot, { recursive: true });
|
|
2177
2343
|
|
|
2178
|
-
|
|
2179
|
-
const
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2344
|
+
if (!csvOnly) {
|
|
2345
|
+
for (const [assetName, config] of Object.entries(assets)) {
|
|
2346
|
+
const exists = fs.existsSync(config.destDir);
|
|
2347
|
+
if (exists && areDirectoriesEqual(config.sourceDir, config.destDir)) {
|
|
2348
|
+
log(options, `[skip] Spec ${assetName} 已最新,无需覆盖: ${config.destDir}`);
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2184
2351
|
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2352
|
+
let action = exists ? "backup" : "";
|
|
2353
|
+
if (exists && prompter) {
|
|
2354
|
+
action = await prompter.resolveConflict({
|
|
2355
|
+
category: "spec:project:assets",
|
|
2356
|
+
label: `Spec ${assetName}`,
|
|
2357
|
+
path: config.destDir,
|
|
2358
|
+
});
|
|
2359
|
+
} else if (exists && !prompter && (options.force || options.nonInteractive || !process.stdin.isTTY)) {
|
|
2360
|
+
action = "backup";
|
|
2361
|
+
}
|
|
2195
2362
|
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2363
|
+
if (action === "keep") {
|
|
2364
|
+
log(options, `[skip] 已保留 Spec ${assetName} 资产,不覆盖: ${config.destDir}`);
|
|
2365
|
+
continue;
|
|
2366
|
+
}
|
|
2200
2367
|
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2368
|
+
if (exists && action !== "remove") {
|
|
2369
|
+
backupDirSnapshot(config.destDir, path.join(backupRoot, "assets", assetName), options, `Spec ${assetName}`);
|
|
2370
|
+
} else if (exists && action === "remove") {
|
|
2371
|
+
removeDirIfExists(config.destDir, options, `Spec ${assetName}`);
|
|
2372
|
+
}
|
|
2373
|
+
applyDirSnapshot(config.sourceDir, config.destDir, options, `Spec ${assetName}`);
|
|
2205
2374
|
}
|
|
2206
|
-
applyDirSnapshot(config.sourceDir, config.destDir, options, `Spec ${assetName}`);
|
|
2207
2375
|
}
|
|
2208
2376
|
|
|
2209
2377
|
const issuesPath = path.join(workspaceRoot, "issues.csv");
|
|
2210
|
-
|
|
2378
|
+
let issuesTemplatePath = path.join(specSourceDir, "templates", "issues.template.csv");
|
|
2379
|
+
if (csvOnly && !options.branch) {
|
|
2380
|
+
const globalTemplatePath = path.join(getSpecHomeDir(), "templates", "issues.template.csv");
|
|
2381
|
+
if (fs.existsSync(globalTemplatePath)) {
|
|
2382
|
+
issuesTemplatePath = globalTemplatePath;
|
|
2383
|
+
} else if (!options.quiet) {
|
|
2384
|
+
log(options, "[warn] 未检测到全局 Spec templates,已使用内置模板生成 issues.csv。可执行: ling spec enable");
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2211
2387
|
const issuesTemplate = stripUtf8Bom(fs.readFileSync(issuesTemplatePath, "utf8"));
|
|
2212
2388
|
const hasIssues = fs.existsSync(issuesPath);
|
|
2213
2389
|
if (hasIssues) {
|
|
@@ -2246,7 +2422,8 @@ async function commandSpecInit(options) {
|
|
|
2246
2422
|
}
|
|
2247
2423
|
|
|
2248
2424
|
const requestedTargets = normalizeTargets(options.targets);
|
|
2249
|
-
const
|
|
2425
|
+
const isSpecWorkspaceMode = Boolean(!options.path && options.specWorkspace);
|
|
2426
|
+
const shouldInitTargets = isSpecWorkspaceMode ? true : requestedTargets.length > 0;
|
|
2250
2427
|
const targets = shouldInitTargets ? (requestedTargets.length > 0 ? requestedTargets : [...SUPPORTED_TARGETS]) : [];
|
|
2251
2428
|
if (targets.length > 0) {
|
|
2252
2429
|
await initTargets(workspaceRoot, targets, options, prompter);
|
|
@@ -2260,7 +2437,9 @@ async function commandSpecInit(options) {
|
|
|
2260
2437
|
}
|
|
2261
2438
|
|
|
2262
2439
|
function commandSpecDoctor(options) {
|
|
2263
|
-
const workspaceRoot = options.path
|
|
2440
|
+
const workspaceRoot = options.path
|
|
2441
|
+
? resolveWorkspaceRoot(options.path)
|
|
2442
|
+
: (options.specWorkspace ? getSpecWorkspaceDir() : resolveWorkspaceRoot());
|
|
2264
2443
|
const result = checkSpecProjectIntegrity(workspaceRoot);
|
|
2265
2444
|
const state = result.status === "ok" ? "installed" : result.status;
|
|
2266
2445
|
|
|
@@ -2279,6 +2458,21 @@ function commandSpecDoctor(options) {
|
|
|
2279
2458
|
|
|
2280
2459
|
console.log(state === "installed" ? "[ok] Spec 项目资产状态正常" : "[warn] Spec 项目资产存在问题");
|
|
2281
2460
|
console.log(` 工作区: ${workspaceRoot}`);
|
|
2461
|
+
const specDir = path.join(workspaceRoot, ".ling", "spec");
|
|
2462
|
+
const hasSpecDir = fs.existsSync(specDir);
|
|
2463
|
+
const issuesPath = path.join(workspaceRoot, "issues.csv");
|
|
2464
|
+
const hasIssues = fs.existsSync(issuesPath);
|
|
2465
|
+
if (hasSpecDir) {
|
|
2466
|
+
console.log(" 模式: project");
|
|
2467
|
+
console.log(` Spec 根目录: ${specDir}`);
|
|
2468
|
+
} else if (hasIssues) {
|
|
2469
|
+
const globalSpecSummary = evaluateSpecState();
|
|
2470
|
+
const fallback = globalSpecSummary.state === "installed";
|
|
2471
|
+
console.log(` 模式: csv-only${fallback ? " (global fallback)" : ""}`);
|
|
2472
|
+
if (fallback) {
|
|
2473
|
+
console.log(` Spec 根目录: ${getSpecHomeDir()}`);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2282
2476
|
console.log(` 任务数: ${result.stats.total}`);
|
|
2283
2477
|
console.log(` 进行中: ${result.stats.inProgress}`);
|
|
2284
2478
|
for (const issue of result.issues || []) {
|
|
@@ -2368,23 +2562,23 @@ async function commandSpec(options) {
|
|
|
2368
2562
|
}
|
|
2369
2563
|
|
|
2370
2564
|
async function initTargets(workspaceRoot, targets, options, prompter) {
|
|
2371
|
-
for (const
|
|
2565
|
+
for (const group of groupTargetsByInstallSurface(targets)) {
|
|
2372
2566
|
const runOptions = { ...options };
|
|
2373
2567
|
const conflicts = [];
|
|
2374
2568
|
|
|
2375
|
-
if (
|
|
2569
|
+
if (group.installSurface === ".agent") {
|
|
2376
2570
|
const agentDir = path.join(workspaceRoot, ".agent");
|
|
2377
2571
|
if (fs.existsSync(agentDir)) {
|
|
2378
2572
|
conflicts.push({
|
|
2379
|
-
category: "project:
|
|
2573
|
+
category: "project:shared-agent",
|
|
2380
2574
|
label: ".agent",
|
|
2381
2575
|
path: agentDir,
|
|
2382
|
-
target,
|
|
2576
|
+
target: group.logicalTargets.join(","),
|
|
2383
2577
|
});
|
|
2384
2578
|
}
|
|
2385
2579
|
}
|
|
2386
2580
|
|
|
2387
|
-
if (
|
|
2581
|
+
if (group.adapterTarget === "codex") {
|
|
2388
2582
|
const managedDir = path.join(workspaceRoot, ".agents");
|
|
2389
2583
|
const legacyDir = path.join(workspaceRoot, ".codex");
|
|
2390
2584
|
if (fs.existsSync(managedDir) || fs.existsSync(legacyDir)) {
|
|
@@ -2435,10 +2629,13 @@ async function initTargets(workspaceRoot, targets, options, prompter) {
|
|
|
2435
2629
|
}
|
|
2436
2630
|
}
|
|
2437
2631
|
|
|
2438
|
-
const adapter = createAdapter(
|
|
2439
|
-
log(options, `[sync] 正在初始化目标 [${
|
|
2632
|
+
const adapter = createAdapter(group.adapterTarget, workspaceRoot, runOptions);
|
|
2633
|
+
log(options, `[sync] 正在初始化目标 [${group.logicalTargets.join(", ")}] ...`);
|
|
2440
2634
|
adapter.install(BUNDLED_AGENT_DIR);
|
|
2441
|
-
|
|
2635
|
+
recordWorkspaceInstallTargets(workspaceRoot, group.logicalTargets, runOptions);
|
|
2636
|
+
for (const target of group.logicalTargets) {
|
|
2637
|
+
registerWorkspaceTarget(workspaceRoot, target, runOptions);
|
|
2638
|
+
}
|
|
2442
2639
|
}
|
|
2443
2640
|
}
|
|
2444
2641
|
|
|
@@ -2523,71 +2720,73 @@ async function commandUpdate(options) {
|
|
|
2523
2720
|
|
|
2524
2721
|
let updatedAny = false;
|
|
2525
2722
|
try {
|
|
2526
|
-
for (const
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2723
|
+
for (const group of groupTargetsByInstallSurface(targets)) {
|
|
2724
|
+
if (!isTargetInstalled(workspaceRoot, group.adapterTarget) && options.targets.length > 0) {
|
|
2725
|
+
throw new Error(`目标未安装: ${group.logicalTargets.join(", ")}`);
|
|
2726
|
+
}
|
|
2727
|
+
if (!isTargetInstalled(workspaceRoot, group.adapterTarget)) {
|
|
2728
|
+
log(options, `[skip] 目标未安装,跳过: ${group.logicalTargets.join(", ")}`);
|
|
2729
|
+
continue;
|
|
2730
|
+
}
|
|
2534
2731
|
|
|
2535
|
-
|
|
2732
|
+
const runOptions = { ...options, force: true };
|
|
2733
|
+
const timestamp = nowISO().replace(/[:.]/g, "-");
|
|
2536
2734
|
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
}
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2735
|
+
if (group.installSurface === ".agent") {
|
|
2736
|
+
const agentDir = path.join(workspaceRoot, ".agent");
|
|
2737
|
+
if (fs.existsSync(agentDir) && !areDirectoriesEqual(BUNDLED_AGENT_DIR, agentDir)) {
|
|
2738
|
+
let action = "backup";
|
|
2739
|
+
if (prompter) {
|
|
2740
|
+
action = await prompter.resolveConflict({
|
|
2741
|
+
category: "project:shared-agent",
|
|
2742
|
+
label: ".agent",
|
|
2743
|
+
path: agentDir,
|
|
2744
|
+
target: group.logicalTargets.join(","),
|
|
2745
|
+
});
|
|
2746
|
+
}
|
|
2747
|
+
if (action === "keep") {
|
|
2748
|
+
log(options, "[skip] 已保留现有资产,跳过更新: .agent");
|
|
2749
|
+
continue;
|
|
2750
|
+
}
|
|
2751
|
+
if (action === "backup") {
|
|
2752
|
+
backupWorkspaceDir(workspaceRoot, agentDir, ".agent-backup", timestamp, options, "工作区资产 .agent");
|
|
2753
|
+
}
|
|
2556
2754
|
}
|
|
2557
2755
|
}
|
|
2558
|
-
}
|
|
2559
2756
|
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2757
|
+
if (group.adapterTarget === "codex") {
|
|
2758
|
+
const managedDir = path.join(workspaceRoot, ".agents");
|
|
2759
|
+
const legacyDir = path.join(workspaceRoot, ".codex");
|
|
2760
|
+
const activeDir = fs.existsSync(managedDir) ? managedDir : legacyDir;
|
|
2761
|
+
const conflict = evaluateCodexUpdateConflict(activeDir);
|
|
2762
|
+
if (conflict.hasConflict) {
|
|
2763
|
+
let action = "backup";
|
|
2764
|
+
if (prompter) {
|
|
2765
|
+
action = await prompter.resolveConflict({
|
|
2766
|
+
category: "project:codex",
|
|
2767
|
+
label: fs.existsSync(managedDir) ? ".agents" : ".codex",
|
|
2768
|
+
path: activeDir,
|
|
2769
|
+
target: group.logicalTargets.join(","),
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
if (action === "keep") {
|
|
2773
|
+
log(options, `[skip] 已保留现有资产,跳过更新: ${path.basename(activeDir)}`);
|
|
2774
|
+
continue;
|
|
2775
|
+
}
|
|
2776
|
+
if (action === "backup") {
|
|
2777
|
+
backupWorkspaceDir(workspaceRoot, activeDir, ".agents-backup", timestamp, options, `工作区资产 ${path.basename(activeDir)}`);
|
|
2778
|
+
}
|
|
2582
2779
|
}
|
|
2583
2780
|
}
|
|
2584
|
-
}
|
|
2585
2781
|
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2782
|
+
const adapter = createAdapter(group.adapterTarget, workspaceRoot, runOptions);
|
|
2783
|
+
log(options, `[sync] 更新 [${group.logicalTargets.join(", ")}] ...`);
|
|
2784
|
+
adapter.update(BUNDLED_AGENT_DIR);
|
|
2785
|
+
recordWorkspaceInstallTargets(workspaceRoot, group.logicalTargets, runOptions);
|
|
2786
|
+
for (const target of group.logicalTargets) {
|
|
2787
|
+
registerWorkspaceTarget(workspaceRoot, target, runOptions);
|
|
2788
|
+
}
|
|
2789
|
+
updatedAny = true;
|
|
2591
2790
|
}
|
|
2592
2791
|
} finally {
|
|
2593
2792
|
if (prompter) prompter.close();
|
|
@@ -2680,7 +2879,7 @@ async function commandUpdateAll(options) {
|
|
|
2680
2879
|
const installedTargets = detectInstalledTargets(workspacePath);
|
|
2681
2880
|
let targets = [];
|
|
2682
2881
|
if (requestedTargets.length > 0) {
|
|
2683
|
-
targets =
|
|
2882
|
+
targets = requestedTargets.filter((target) => isTargetInstalled(workspacePath, target));
|
|
2684
2883
|
} else {
|
|
2685
2884
|
targets = [...Object.keys(item.targets || {}), ...installedTargets];
|
|
2686
2885
|
}
|
|
@@ -2696,9 +2895,9 @@ async function commandUpdateAll(options) {
|
|
|
2696
2895
|
log(options, `[sync] [${i + 1}/${records.length}] 更新: ${workspacePath} [${targets.join(", ")}]`);
|
|
2697
2896
|
|
|
2698
2897
|
const updatedTargets = [];
|
|
2699
|
-
for (const
|
|
2700
|
-
if (!isTargetInstalled(workspacePath,
|
|
2701
|
-
log(options, `[skip] [${i + 1}/${records.length}] 目标未安装,跳过: ${
|
|
2898
|
+
for (const group of groupTargetsByInstallSurface(targets)) {
|
|
2899
|
+
if (!isTargetInstalled(workspacePath, group.adapterTarget)) {
|
|
2900
|
+
log(options, `[skip] [${i + 1}/${records.length}] 目标未安装,跳过: ${group.logicalTargets.join(", ")}`);
|
|
2702
2901
|
continue;
|
|
2703
2902
|
}
|
|
2704
2903
|
|
|
@@ -2712,19 +2911,19 @@ async function commandUpdateAll(options) {
|
|
|
2712
2911
|
|
|
2713
2912
|
const timestampForBackup = nowISO().replace(/[:.]/g, "-");
|
|
2714
2913
|
|
|
2715
|
-
if (
|
|
2914
|
+
if (group.installSurface === ".agent") {
|
|
2716
2915
|
const agentDir = path.join(workspacePath, ".agent");
|
|
2717
2916
|
if (fs.existsSync(agentDir) && !areDirectoriesEqual(BUNDLED_AGENT_DIR, agentDir)) {
|
|
2718
2917
|
let action = "backup";
|
|
2719
2918
|
if (prompter) {
|
|
2720
2919
|
action = await prompter.resolveConflict({
|
|
2721
|
-
category: "update-all:project:
|
|
2920
|
+
category: "update-all:project:shared-agent",
|
|
2722
2921
|
label: `.agent (${workspacePath})`,
|
|
2723
2922
|
path: agentDir,
|
|
2724
2923
|
});
|
|
2725
2924
|
}
|
|
2726
2925
|
if (action === "keep") {
|
|
2727
|
-
log(options, `[skip] [${i + 1}/${records.length}] 已保留现有资产,跳过更新: ${workspacePath} [
|
|
2926
|
+
log(options, `[skip] [${i + 1}/${records.length}] 已保留现有资产,跳过更新: ${workspacePath} [${group.logicalTargets.join(", ")}]`);
|
|
2728
2927
|
continue;
|
|
2729
2928
|
}
|
|
2730
2929
|
if (action === "backup") {
|
|
@@ -2733,7 +2932,7 @@ async function commandUpdateAll(options) {
|
|
|
2733
2932
|
}
|
|
2734
2933
|
}
|
|
2735
2934
|
|
|
2736
|
-
if (
|
|
2935
|
+
if (group.adapterTarget === "codex") {
|
|
2737
2936
|
const managedDir = path.join(workspacePath, ".agents");
|
|
2738
2937
|
const legacyDir = path.join(workspacePath, ".codex");
|
|
2739
2938
|
const activeDir = fs.existsSync(managedDir) ? managedDir : legacyDir;
|
|
@@ -2757,13 +2956,14 @@ async function commandUpdateAll(options) {
|
|
|
2757
2956
|
}
|
|
2758
2957
|
}
|
|
2759
2958
|
|
|
2760
|
-
const adapter = createAdapter(
|
|
2959
|
+
const adapter = createAdapter(group.adapterTarget, workspacePath, runOptions);
|
|
2761
2960
|
adapter.update(BUNDLED_AGENT_DIR);
|
|
2762
|
-
|
|
2961
|
+
recordWorkspaceInstallTargets(workspacePath, group.logicalTargets, runOptions);
|
|
2962
|
+
updatedTargets.push(...group.logicalTargets);
|
|
2763
2963
|
} catch (err) {
|
|
2764
2964
|
failed += 1;
|
|
2765
2965
|
if (!options.quiet) {
|
|
2766
|
-
console.error(`[error] 更新失败: ${workspacePath} [${
|
|
2966
|
+
console.error(`[error] 更新失败: ${workspacePath} [${group.logicalTargets.join(", ")}]`);
|
|
2767
2967
|
console.error(` ${err.message}`);
|
|
2768
2968
|
}
|
|
2769
2969
|
}
|
|
@@ -3068,6 +3268,25 @@ function countSkillsRecursive(dir) {
|
|
|
3068
3268
|
return count;
|
|
3069
3269
|
}
|
|
3070
3270
|
|
|
3271
|
+
function printSharedAgentTargetStatus(workspaceRoot, targetState, label) {
|
|
3272
|
+
const agentDir = path.join(workspaceRoot, ".agent");
|
|
3273
|
+
const agentsCount = countFilesIfExists(path.join(agentDir, "agents"), (name) => name.endsWith(".md"));
|
|
3274
|
+
const workflowsCount = countFilesIfExists(path.join(agentDir, "workflows"), (name) => name.endsWith(".md"));
|
|
3275
|
+
const skillsCount = countSkillsRecursive(path.join(agentDir, "skills"));
|
|
3276
|
+
console.log(`\n[${label}]`);
|
|
3277
|
+
console.log(` 状态: ${targetState.state}`);
|
|
3278
|
+
console.log(` 路径: ${agentDir}`);
|
|
3279
|
+
if (targetState.state === "installed") {
|
|
3280
|
+
console.log(` Agents: ${agentsCount}`);
|
|
3281
|
+
console.log(` Skills: ${skillsCount}`);
|
|
3282
|
+
console.log(` Workflows: ${workflowsCount}`);
|
|
3283
|
+
return;
|
|
3284
|
+
}
|
|
3285
|
+
for (const issue of targetState.integrity.issues || []) {
|
|
3286
|
+
console.log(` Issue: ${issue}`);
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3071
3290
|
function commandStatus(options) {
|
|
3072
3291
|
const workspaceRoot = resolveWorkspaceRoot(options.path);
|
|
3073
3292
|
const summary = evaluateWorkspaceState(workspaceRoot, options);
|
|
@@ -3098,22 +3317,12 @@ function commandStatus(options) {
|
|
|
3098
3317
|
|
|
3099
3318
|
const geminiState = summary.targets.find((item) => item.targetName === "gemini");
|
|
3100
3319
|
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
|
-
}
|
|
3320
|
+
printSharedAgentTargetStatus(workspaceRoot, geminiState, "gemini");
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
const antigravityState = summary.targets.find((item) => item.targetName === "antigravity");
|
|
3324
|
+
if (antigravityState) {
|
|
3325
|
+
printSharedAgentTargetStatus(workspaceRoot, antigravityState, "antigravity");
|
|
3117
3326
|
}
|
|
3118
3327
|
|
|
3119
3328
|
const codexState = summary.targets.find((item) => item.targetName === "codex");
|