@mison/ling 1.1.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/.agents/rules/GEMINI.md +17 -0
- package/.agents/skills/clean-code/SKILL.md +24 -14
- package/.agents/skills/doc.md +9 -5
- package/CHANGELOG.md +34 -1
- package/README.md +138 -195
- package/bin/adapters/gemini.js +6 -2
- package/bin/core/generator.js +1 -0
- package/bin/interactive.js +6 -4
- package/bin/ling-cli.js +893 -170
- package/bin/utils.js +52 -9
- package/docs/PLAN.md +21 -17
- package/docs/TECH.md +40 -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 +233 -0
- package/tests/spec-profile.test.js +84 -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
|
@@ -5,7 +5,7 @@ const os = require("os");
|
|
|
5
5
|
const path = require("path");
|
|
6
6
|
|
|
7
7
|
const pkg = require("../package.json");
|
|
8
|
-
const { readGlobalNpmDependencies, cloneBranchAgentDir } = require("./utils");
|
|
8
|
+
const { readGlobalNpmDependencies, cloneBranchAgentDir, cloneBranchSpecDir } = require("./utils");
|
|
9
9
|
const ManifestManager = require("./utils/manifest");
|
|
10
10
|
const AtomicWriter = require("./utils/atomic-writer");
|
|
11
11
|
const CodexBuilder = require("./core/builder");
|
|
@@ -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"],
|
|
@@ -55,6 +58,9 @@ const QUIET_STATUS_EXIT_CODES = {
|
|
|
55
58
|
const SPEC_STATE_VERSION = 1;
|
|
56
59
|
const SPEC_SKILL_NAMES = ["harness-engineering", "cybernetic-systems-engineering"];
|
|
57
60
|
const VERSION_TAG_PREFIX = "ling-";
|
|
61
|
+
const SPEC_TEMPLATE_REQUIRED_FILES = ["issues.template.csv", "driver-prompt.md", "review-report.md", "phase-acceptance.md", "handoff.md"];
|
|
62
|
+
const SPEC_REFERENCE_REQUIRED_FILES = ["README.md", "harness-engineering-digest.md", "gda-framework.md", "cse-quickstart.md"];
|
|
63
|
+
const SPEC_PROFILE_REQUIRED_FILES = ["codex/AGENTS.spec.md", "codex/ling.spec.rules.md", "gemini/GEMINI.spec.md"];
|
|
58
64
|
|
|
59
65
|
function nowISO() {
|
|
60
66
|
return new Date().toISOString();
|
|
@@ -167,11 +173,13 @@ function printUsage() {
|
|
|
167
173
|
console.log(` ${PRIMARY_CLI_NAME} update [--path <dir>] [--branch <name>] [--target <name>|--targets <a,b>] [--no-index] [--quiet] [--dry-run]`);
|
|
168
174
|
console.log(` ${PRIMARY_CLI_NAME} update-all [--branch <name>] [--targets <a,b>] [--prune-missing] [--quiet] [--dry-run]`);
|
|
169
175
|
console.log(` ${PRIMARY_CLI_NAME} doctor [--path <dir>] [--target <name>|--targets <a,b>] [--fix] [--quiet]`);
|
|
170
|
-
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`);
|
|
171
177
|
console.log(` ${PRIMARY_CLI_NAME} global status [--quiet]`);
|
|
172
178
|
console.log(` ${PRIMARY_CLI_NAME} spec enable [--target <name>|--targets <a,b>] [--quiet] [--dry-run]`);
|
|
173
179
|
console.log(` ${PRIMARY_CLI_NAME} spec disable [--target <name>|--targets <a,b>] [--quiet] [--dry-run]`);
|
|
174
180
|
console.log(` ${PRIMARY_CLI_NAME} spec status [--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]`);
|
|
175
183
|
console.log(` ${PRIMARY_CLI_NAME} exclude list [--quiet]`);
|
|
176
184
|
console.log(` ${PRIMARY_CLI_NAME} exclude add --path <dir> [--dry-run] [--quiet]`);
|
|
177
185
|
console.log(` ${PRIMARY_CLI_NAME} exclude remove --path <dir> [--dry-run] [--quiet]`);
|
|
@@ -197,6 +205,8 @@ function parseArgs(argv) {
|
|
|
197
205
|
nonInteractive: false,
|
|
198
206
|
noIndex: false,
|
|
199
207
|
fix: false,
|
|
208
|
+
csvOnly: false,
|
|
209
|
+
specWorkspace: false,
|
|
200
210
|
subcommand: "",
|
|
201
211
|
path: "",
|
|
202
212
|
branch: "",
|
|
@@ -247,6 +257,12 @@ function parseArgs(argv) {
|
|
|
247
257
|
} else if (arg === "--fix") {
|
|
248
258
|
providedFlags.push(arg);
|
|
249
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;
|
|
250
266
|
} else if (arg === "--path") {
|
|
251
267
|
providedFlags.push(arg);
|
|
252
268
|
if (i + 1 >= argv.length) {
|
|
@@ -290,6 +306,8 @@ const COMMAND_ALLOWED_FLAGS = {
|
|
|
290
306
|
"spec:enable": ["--target", "--targets", "--quiet", "--dry-run"],
|
|
291
307
|
"spec:disable": ["--target", "--targets", "--quiet", "--dry-run"],
|
|
292
308
|
"spec:status": ["--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"],
|
|
293
311
|
"exclude:list": ["--quiet"],
|
|
294
312
|
"exclude:add": ["--path", "--dry-run", "--quiet"],
|
|
295
313
|
"exclude:remove": ["--path", "--dry-run", "--quiet"],
|
|
@@ -382,6 +400,12 @@ function pathCompareKey(inputPath) {
|
|
|
382
400
|
return normalized;
|
|
383
401
|
}
|
|
384
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
|
+
|
|
385
409
|
function normalizePathList(items) {
|
|
386
410
|
const map = new Map();
|
|
387
411
|
for (const item of items) {
|
|
@@ -620,6 +644,95 @@ function writeWorkspaceIndex(indexPath, index) {
|
|
|
620
644
|
fs.writeFileSync(indexPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
621
645
|
}
|
|
622
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
|
+
|
|
623
736
|
function sleepSync(ms) {
|
|
624
737
|
const buffer = new SharedArrayBuffer(4);
|
|
625
738
|
const view = new Int32Array(buffer);
|
|
@@ -899,19 +1012,41 @@ function normalizeTargets(rawTargets) {
|
|
|
899
1012
|
return result;
|
|
900
1013
|
}
|
|
901
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
|
+
|
|
902
1028
|
function detectInstalledTargets(workspaceRoot) {
|
|
903
1029
|
const targets = [];
|
|
1030
|
+
const localTargets = resolveWorkspaceInstallStateTargets(workspaceRoot);
|
|
1031
|
+
const indexedTargets = resolveIndexedWorkspaceTargets(workspaceRoot);
|
|
904
1032
|
if (fs.existsSync(path.join(workspaceRoot, ".agent"))) {
|
|
905
|
-
|
|
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
|
+
}
|
|
906
1041
|
}
|
|
907
1042
|
if (fs.existsSync(path.join(workspaceRoot, ".agents")) || fs.existsSync(path.join(workspaceRoot, ".codex"))) {
|
|
908
1043
|
targets.push("codex");
|
|
909
1044
|
}
|
|
910
|
-
return targets;
|
|
1045
|
+
return normalizeTargets(targets);
|
|
911
1046
|
}
|
|
912
1047
|
|
|
913
1048
|
function isTargetInstalled(workspaceRoot, targetName) {
|
|
914
|
-
if (targetName
|
|
1049
|
+
if (SHARED_AGENT_TARGETS.includes(targetName)) {
|
|
915
1050
|
return fs.existsSync(path.join(workspaceRoot, ".agent"));
|
|
916
1051
|
}
|
|
917
1052
|
if (targetName === "codex") {
|
|
@@ -920,6 +1055,27 @@ function isTargetInstalled(workspaceRoot, targetName) {
|
|
|
920
1055
|
return false;
|
|
921
1056
|
}
|
|
922
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
|
+
|
|
923
1079
|
function setQuietStatusExitCode(state) {
|
|
924
1080
|
process.exitCode = Object.prototype.hasOwnProperty.call(QUIET_STATUS_EXIT_CODES, state)
|
|
925
1081
|
? QUIET_STATUS_EXIT_CODES[state]
|
|
@@ -1015,8 +1171,11 @@ function evaluateGlobalState() {
|
|
|
1015
1171
|
}
|
|
1016
1172
|
|
|
1017
1173
|
function createAdapter(targetName, workspaceRoot, options) {
|
|
1018
|
-
if (targetName
|
|
1019
|
-
return new GeminiAdapter(workspaceRoot,
|
|
1174
|
+
if (SHARED_AGENT_TARGETS.includes(targetName)) {
|
|
1175
|
+
return new GeminiAdapter(workspaceRoot, {
|
|
1176
|
+
...options,
|
|
1177
|
+
targetName,
|
|
1178
|
+
});
|
|
1020
1179
|
}
|
|
1021
1180
|
if (targetName === "codex") {
|
|
1022
1181
|
return new CodexAdapter(workspaceRoot, options);
|
|
@@ -1060,8 +1219,7 @@ function resolveTargetsForGlobalSync(options) {
|
|
|
1060
1219
|
if (requested.length > 0) {
|
|
1061
1220
|
return requested;
|
|
1062
1221
|
}
|
|
1063
|
-
|
|
1064
|
-
return ["codex", "gemini"];
|
|
1222
|
+
return [...SUPPORTED_TARGETS];
|
|
1065
1223
|
}
|
|
1066
1224
|
|
|
1067
1225
|
function resolveAgentInstallSource(options) {
|
|
@@ -1228,7 +1386,7 @@ function planGlobalSyncTasks(targetName, agentDir) {
|
|
|
1228
1386
|
};
|
|
1229
1387
|
}
|
|
1230
1388
|
|
|
1231
|
-
if (targetName
|
|
1389
|
+
if (SHARED_AGENT_TARGETS.includes(targetName)) {
|
|
1232
1390
|
const skillsRoot = path.join(agentDir, "skills");
|
|
1233
1391
|
const skillNames = listSkillDirectories(skillsRoot);
|
|
1234
1392
|
const tasks = [];
|
|
@@ -1310,7 +1468,7 @@ function applyGlobalSync(targetName, agentDir, timestamp, options) {
|
|
|
1310
1468
|
}
|
|
1311
1469
|
}
|
|
1312
1470
|
|
|
1313
|
-
if (targetName
|
|
1471
|
+
if (SHARED_AGENT_TARGETS.includes(targetName)) {
|
|
1314
1472
|
const skillsRoot = path.join(agentDir, "skills");
|
|
1315
1473
|
return syncGlobalSkillsFromRoot(targetName, skillsRoot, timestamp, options);
|
|
1316
1474
|
}
|
|
@@ -1447,6 +1605,10 @@ function getSpecHomeDir() {
|
|
|
1447
1605
|
return path.join(resolveGlobalRootDir(), ".ling", "spec");
|
|
1448
1606
|
}
|
|
1449
1607
|
|
|
1608
|
+
function getSpecWorkspaceDir() {
|
|
1609
|
+
return path.join(resolveGlobalRootDir(), ".ling", "spec-workspace");
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1450
1612
|
function getSpecStatePath() {
|
|
1451
1613
|
return path.join(getSpecHomeDir(), "state.json");
|
|
1452
1614
|
}
|
|
@@ -1524,6 +1686,46 @@ function ensureBundledSpecResources() {
|
|
|
1524
1686
|
}
|
|
1525
1687
|
}
|
|
1526
1688
|
|
|
1689
|
+
function listMissingFiles(rootDir, requiredRelPaths) {
|
|
1690
|
+
if (!fs.existsSync(rootDir)) {
|
|
1691
|
+
return requiredRelPaths.map((rel) => rel);
|
|
1692
|
+
}
|
|
1693
|
+
return requiredRelPaths.filter((rel) => !fs.existsSync(path.join(rootDir, ...rel.split("/"))));
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function collectSpecOrphanIssues(specHome, hasStateFile) {
|
|
1697
|
+
const issues = [];
|
|
1698
|
+
const templatesDir = path.join(specHome, "templates");
|
|
1699
|
+
const referencesDir = path.join(specHome, "references");
|
|
1700
|
+
const profilesDir = path.join(specHome, "profiles");
|
|
1701
|
+
|
|
1702
|
+
if (fs.existsSync(templatesDir)) issues.push("Detected spec templates directory");
|
|
1703
|
+
if (fs.existsSync(referencesDir)) issues.push("Detected spec references directory");
|
|
1704
|
+
if (fs.existsSync(profilesDir)) issues.push("Detected spec profiles directory");
|
|
1705
|
+
|
|
1706
|
+
for (const targetName of SUPPORTED_TARGETS) {
|
|
1707
|
+
for (const destination of getGlobalDestinations(targetName)) {
|
|
1708
|
+
for (const skillName of SPEC_SKILL_NAMES) {
|
|
1709
|
+
if (fs.existsSync(path.join(destination.skillsRoot, skillName, "SKILL.md"))) {
|
|
1710
|
+
issues.push(`Detected spec skill: ${destination.id}/${skillName}`);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
if (issues.length === 0) {
|
|
1717
|
+
return [];
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
if (!hasStateFile) {
|
|
1721
|
+
issues.unshift("Spec artifacts detected but state.json missing (run: ling spec enable to repair)");
|
|
1722
|
+
} else {
|
|
1723
|
+
issues.unshift("Spec artifacts detected but no targets enabled (run: ling spec enable to reconcile or clean manually)");
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
return issues;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1527
1729
|
function backupDirSnapshot(sourceDir, backupDir, options, label) {
|
|
1528
1730
|
if (!fs.existsSync(sourceDir)) {
|
|
1529
1731
|
return "";
|
|
@@ -1560,6 +1762,59 @@ function removeDirIfExists(targetDir, options, label) {
|
|
|
1560
1762
|
log(options, `[clean] 已删除 ${label}: ${targetDir}`);
|
|
1561
1763
|
}
|
|
1562
1764
|
|
|
1765
|
+
function atomicWriteFile(targetPath, content, options, label) {
|
|
1766
|
+
const targetDir = path.dirname(targetPath);
|
|
1767
|
+
if (options.dryRun) {
|
|
1768
|
+
log(options, `[dry-run] 将写入 ${label}: ${targetPath}`);
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
1772
|
+
const tempPath = `${targetPath}.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1773
|
+
fs.writeFileSync(tempPath, content, "utf8");
|
|
1774
|
+
try {
|
|
1775
|
+
if (fs.existsSync(targetPath)) {
|
|
1776
|
+
const backupPath = `${targetPath}.bak-${Date.now()}-${Math.random().toString(36).slice(2, 5)}`;
|
|
1777
|
+
fs.renameSync(targetPath, backupPath);
|
|
1778
|
+
try {
|
|
1779
|
+
fs.renameSync(tempPath, targetPath);
|
|
1780
|
+
} catch (err) {
|
|
1781
|
+
try {
|
|
1782
|
+
fs.renameSync(backupPath, targetPath);
|
|
1783
|
+
} catch (restoreErr) {
|
|
1784
|
+
throw new Error(`临界失败: 无法将新版本写入目标且无法恢复旧版本。旧版本位于 ${backupPath}。错误: ${err.message}`);
|
|
1785
|
+
}
|
|
1786
|
+
throw err;
|
|
1787
|
+
}
|
|
1788
|
+
try {
|
|
1789
|
+
fs.rmSync(backupPath, { force: true });
|
|
1790
|
+
} catch (cleanupErr) {
|
|
1791
|
+
log(options, `[warn] 无法清理备份文件 ${backupPath}: ${cleanupErr.message}`);
|
|
1792
|
+
}
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
fs.renameSync(tempPath, targetPath);
|
|
1796
|
+
} catch (err) {
|
|
1797
|
+
if (fs.existsSync(tempPath)) {
|
|
1798
|
+
fs.rmSync(tempPath, { force: true });
|
|
1799
|
+
}
|
|
1800
|
+
throw new Error(`原子写入失败: ${err.message}`);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
function backupFileSnapshot(sourcePath, backupPath, options, label) {
|
|
1805
|
+
if (!fs.existsSync(sourcePath)) {
|
|
1806
|
+
return "";
|
|
1807
|
+
}
|
|
1808
|
+
if (options.dryRun) {
|
|
1809
|
+
log(options, `[dry-run] 将备份 ${label}: ${sourcePath} -> ${backupPath}`);
|
|
1810
|
+
return backupPath;
|
|
1811
|
+
}
|
|
1812
|
+
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
1813
|
+
fs.copyFileSync(sourcePath, backupPath);
|
|
1814
|
+
log(options, `[backup] 已备份 ${label}: ${sourcePath} -> ${backupPath}`);
|
|
1815
|
+
return backupPath;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1563
1818
|
async function ensureSpecAssetsInstalled(state, timestamp, options, prompter) {
|
|
1564
1819
|
const specHome = getSpecHomeDir();
|
|
1565
1820
|
const assets = {
|
|
@@ -1571,14 +1826,37 @@ async function ensureSpecAssetsInstalled(state, timestamp, options, prompter) {
|
|
|
1571
1826
|
sourceDir: path.join(BUNDLED_SPEC_DIR, "references"),
|
|
1572
1827
|
destDir: path.join(specHome, "references"),
|
|
1573
1828
|
},
|
|
1829
|
+
profiles: {
|
|
1830
|
+
sourceDir: path.join(BUNDLED_SPEC_DIR, "profiles"),
|
|
1831
|
+
destDir: path.join(specHome, "profiles"),
|
|
1832
|
+
},
|
|
1574
1833
|
};
|
|
1575
1834
|
|
|
1576
1835
|
for (const [assetName, config] of Object.entries(assets)) {
|
|
1577
|
-
|
|
1836
|
+
const existingAssetState = state.assets[assetName];
|
|
1837
|
+
const hasState = existingAssetState && existingAssetState.installedAt;
|
|
1838
|
+
const exists = fs.existsSync(config.destDir);
|
|
1839
|
+
const mode = normalizeSpecAssetMode(existingAssetState);
|
|
1840
|
+
|
|
1841
|
+
if (mode === "kept" && exists) {
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
const equal = exists ? areDirectoriesEqual(config.sourceDir, config.destDir) : false;
|
|
1846
|
+
|
|
1847
|
+
if (exists && equal) {
|
|
1848
|
+
if (!hasState) {
|
|
1849
|
+
log(options, `[skip] Spec ${assetName} 已存在且一致,视为已启用: ${config.destDir}`);
|
|
1850
|
+
state.assets[assetName] = {
|
|
1851
|
+
destPath: config.destDir,
|
|
1852
|
+
backupPath: "",
|
|
1853
|
+
installedAt: nowISO(),
|
|
1854
|
+
mode: "kept",
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1578
1857
|
continue;
|
|
1579
1858
|
}
|
|
1580
1859
|
|
|
1581
|
-
const exists = fs.existsSync(config.destDir);
|
|
1582
1860
|
let action = exists ? "backup" : "";
|
|
1583
1861
|
if (exists && prompter) {
|
|
1584
1862
|
action = await prompter.resolveConflict({
|
|
@@ -1586,6 +1864,8 @@ async function ensureSpecAssetsInstalled(state, timestamp, options, prompter) {
|
|
|
1586
1864
|
label: `Spec ${assetName}`,
|
|
1587
1865
|
path: config.destDir,
|
|
1588
1866
|
});
|
|
1867
|
+
} else if (exists && !prompter && (options.nonInteractive || !process.stdin.isTTY)) {
|
|
1868
|
+
action = "backup";
|
|
1589
1869
|
}
|
|
1590
1870
|
|
|
1591
1871
|
if (action === "keep") {
|
|
@@ -1650,26 +1930,59 @@ function restoreSpecAsset(assetState, options, label) {
|
|
|
1650
1930
|
}
|
|
1651
1931
|
|
|
1652
1932
|
async function enableSpecTarget(targetName, state, timestamp, options, prompter) {
|
|
1653
|
-
if (state.targets[targetName]) {
|
|
1654
|
-
log(options, `[skip] Spec 目标已启用: ${targetName}`);
|
|
1655
|
-
return;
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
1933
|
const destinations = getGlobalDestinations(targetName);
|
|
1659
|
-
const
|
|
1934
|
+
const existingTargetState = state.targets[targetName];
|
|
1935
|
+
if (existingTargetState) {
|
|
1936
|
+
log(options, `[info] Spec 目标已启用,执行一致性修复: ${targetName}`);
|
|
1937
|
+
}
|
|
1938
|
+
const targetState = existingTargetState || {
|
|
1660
1939
|
enabledAt: nowISO(),
|
|
1661
1940
|
consumers: {},
|
|
1662
1941
|
};
|
|
1663
1942
|
|
|
1664
1943
|
for (const destination of destinations) {
|
|
1944
|
+
const existingConsumerState =
|
|
1945
|
+
targetState.consumers && targetState.consumers[destination.id] ? targetState.consumers[destination.id] : null;
|
|
1665
1946
|
const consumerState = {
|
|
1666
1947
|
skills: [],
|
|
1667
1948
|
};
|
|
1949
|
+
const existingSkills = new Map();
|
|
1950
|
+
if (existingConsumerState && Array.isArray(existingConsumerState.skills)) {
|
|
1951
|
+
for (const skill of existingConsumerState.skills) {
|
|
1952
|
+
if (skill && typeof skill.name === "string") {
|
|
1953
|
+
existingSkills.set(skill.name, skill);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1668
1957
|
|
|
1669
1958
|
for (const skillName of SPEC_SKILL_NAMES) {
|
|
1670
1959
|
const srcDir = path.join(BUNDLED_SPEC_DIR, "skills", skillName);
|
|
1671
1960
|
const destDir = path.join(destination.skillsRoot, skillName);
|
|
1961
|
+
const existingSkillState = existingSkills.get(skillName);
|
|
1962
|
+
const mode = normalizeSpecAssetMode(existingSkillState);
|
|
1672
1963
|
const exists = fs.existsSync(destDir);
|
|
1964
|
+
const equal = exists ? areDirectoriesEqual(srcDir, destDir) : false;
|
|
1965
|
+
|
|
1966
|
+
if (mode === "kept" && exists) {
|
|
1967
|
+
consumerState.skills.push({
|
|
1968
|
+
name: skillName,
|
|
1969
|
+
destPath: destDir,
|
|
1970
|
+
backupPath: existingSkillState && existingSkillState.backupPath ? existingSkillState.backupPath : "",
|
|
1971
|
+
mode: "kept",
|
|
1972
|
+
});
|
|
1973
|
+
continue;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
if (exists && equal) {
|
|
1977
|
+
consumerState.skills.push({
|
|
1978
|
+
name: skillName,
|
|
1979
|
+
destPath: destDir,
|
|
1980
|
+
backupPath: existingSkillState && existingSkillState.backupPath ? existingSkillState.backupPath : "",
|
|
1981
|
+
mode: existingSkillState && existingSkillState.mode ? existingSkillState.mode : "kept",
|
|
1982
|
+
});
|
|
1983
|
+
continue;
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1673
1986
|
let action = exists ? "backup" : "";
|
|
1674
1987
|
if (exists && prompter) {
|
|
1675
1988
|
action = await prompter.resolveConflict({
|
|
@@ -1677,6 +1990,8 @@ async function enableSpecTarget(targetName, state, timestamp, options, prompter)
|
|
|
1677
1990
|
label: `Spec Skill ${destination.id}/${skillName}`,
|
|
1678
1991
|
path: destDir,
|
|
1679
1992
|
});
|
|
1993
|
+
} else if (exists && !prompter && (options.nonInteractive || !process.stdin.isTTY)) {
|
|
1994
|
+
action = "backup";
|
|
1680
1995
|
}
|
|
1681
1996
|
|
|
1682
1997
|
if (action === "keep") {
|
|
@@ -1684,13 +1999,13 @@ async function enableSpecTarget(targetName, state, timestamp, options, prompter)
|
|
|
1684
1999
|
consumerState.skills.push({
|
|
1685
2000
|
name: skillName,
|
|
1686
2001
|
destPath: destDir,
|
|
1687
|
-
backupPath: "",
|
|
2002
|
+
backupPath: existingSkillState && existingSkillState.backupPath ? existingSkillState.backupPath : "",
|
|
1688
2003
|
mode: "kept",
|
|
1689
2004
|
});
|
|
1690
2005
|
continue;
|
|
1691
2006
|
}
|
|
1692
2007
|
|
|
1693
|
-
let backupPath = "";
|
|
2008
|
+
let backupPath = existingSkillState && existingSkillState.backupPath ? existingSkillState.backupPath : "";
|
|
1694
2009
|
if (exists && action !== "remove") {
|
|
1695
2010
|
backupPath = backupDirSnapshot(
|
|
1696
2011
|
destDir,
|
|
@@ -1742,14 +2057,28 @@ function disableSpecTarget(targetName, state, options) {
|
|
|
1742
2057
|
}
|
|
1743
2058
|
|
|
1744
2059
|
function evaluateSpecState() {
|
|
2060
|
+
const statePath = getSpecStatePath();
|
|
2061
|
+
const hasStateFile = fs.existsSync(statePath);
|
|
1745
2062
|
const { state } = readSpecState();
|
|
1746
2063
|
const targetNames = Object.keys(state.targets || {});
|
|
1747
2064
|
if (targetNames.length === 0) {
|
|
2065
|
+
const specHome = getSpecHomeDir();
|
|
2066
|
+
const orphanIssues = collectSpecOrphanIssues(specHome, hasStateFile);
|
|
2067
|
+
if (orphanIssues.length > 0) {
|
|
2068
|
+
return {
|
|
2069
|
+
state: "broken",
|
|
2070
|
+
targets: [],
|
|
2071
|
+
assets: state.assets || {},
|
|
2072
|
+
specHome,
|
|
2073
|
+
issues: orphanIssues,
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
1748
2076
|
return {
|
|
1749
2077
|
state: "missing",
|
|
1750
2078
|
targets: [],
|
|
1751
2079
|
assets: state.assets || {},
|
|
1752
|
-
specHome
|
|
2080
|
+
specHome,
|
|
2081
|
+
issues: [],
|
|
1753
2082
|
};
|
|
1754
2083
|
}
|
|
1755
2084
|
|
|
@@ -1765,10 +2094,23 @@ function evaluateSpecState() {
|
|
|
1765
2094
|
}
|
|
1766
2095
|
}
|
|
1767
2096
|
|
|
1768
|
-
|
|
2097
|
+
const specHome = getSpecHomeDir();
|
|
2098
|
+
const assetRequirements = {
|
|
2099
|
+
templates: { dir: path.join(specHome, "templates"), files: SPEC_TEMPLATE_REQUIRED_FILES },
|
|
2100
|
+
references: { dir: path.join(specHome, "references"), files: SPEC_REFERENCE_REQUIRED_FILES },
|
|
2101
|
+
profiles: { dir: path.join(specHome, "profiles"), files: SPEC_PROFILE_REQUIRED_FILES },
|
|
2102
|
+
};
|
|
2103
|
+
|
|
2104
|
+
for (const assetName of ["templates", "references", "profiles"]) {
|
|
1769
2105
|
const asset = state.assets[assetName];
|
|
1770
2106
|
if (!asset || !asset.destPath || !fs.existsSync(asset.destPath)) {
|
|
1771
2107
|
issues.push(`Missing spec asset: ${assetName}`);
|
|
2108
|
+
continue;
|
|
2109
|
+
}
|
|
2110
|
+
const requirement = assetRequirements[assetName];
|
|
2111
|
+
const missing = listMissingFiles(requirement.dir, requirement.files);
|
|
2112
|
+
for (const rel of missing) {
|
|
2113
|
+
issues.push(`Missing spec asset file: ${assetName}/${rel}`);
|
|
1772
2114
|
}
|
|
1773
2115
|
}
|
|
1774
2116
|
|
|
@@ -1777,7 +2119,7 @@ function evaluateSpecState() {
|
|
|
1777
2119
|
targets: targetNames,
|
|
1778
2120
|
issues,
|
|
1779
2121
|
assets: state.assets || {},
|
|
1780
|
-
specHome
|
|
2122
|
+
specHome,
|
|
1781
2123
|
};
|
|
1782
2124
|
}
|
|
1783
2125
|
|
|
@@ -1808,6 +2150,337 @@ function commandSpecStatus(options) {
|
|
|
1808
2150
|
setQuietStatusExitCode(summary.state);
|
|
1809
2151
|
}
|
|
1810
2152
|
|
|
2153
|
+
function stripUtf8Bom(text) {
|
|
2154
|
+
if (!text) return "";
|
|
2155
|
+
return text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
function parseCsvLine(line) {
|
|
2159
|
+
const cells = [];
|
|
2160
|
+
let current = "";
|
|
2161
|
+
let inQuotes = false;
|
|
2162
|
+
|
|
2163
|
+
for (let i = 0; i < line.length; i++) {
|
|
2164
|
+
const ch = line[i];
|
|
2165
|
+
if (ch === "\"") {
|
|
2166
|
+
if (inQuotes && line[i + 1] === "\"") {
|
|
2167
|
+
current += "\"";
|
|
2168
|
+
i += 1;
|
|
2169
|
+
continue;
|
|
2170
|
+
}
|
|
2171
|
+
inQuotes = !inQuotes;
|
|
2172
|
+
continue;
|
|
2173
|
+
}
|
|
2174
|
+
if (ch === "," && !inQuotes) {
|
|
2175
|
+
cells.push(current);
|
|
2176
|
+
current = "";
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
current += ch;
|
|
2180
|
+
}
|
|
2181
|
+
cells.push(current);
|
|
2182
|
+
return cells;
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
function analyzeIssuesCsv(issuesPath) {
|
|
2186
|
+
if (!fs.existsSync(issuesPath)) {
|
|
2187
|
+
return { status: "missing", issues: ["Missing issues.csv"], stats: { total: 0, inProgress: 0 } };
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
const raw = stripUtf8Bom(fs.readFileSync(issuesPath, "utf8"));
|
|
2191
|
+
const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
2192
|
+
if (lines.length === 0) {
|
|
2193
|
+
return { status: "broken", issues: ["issues.csv is empty"], stats: { total: 0, inProgress: 0 } };
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
const header = parseCsvLine(lines[0]).map((cell) => cell.trim());
|
|
2197
|
+
const statusIndex = header.findIndex((cell) => cell === "状态" || cell.includes("(状态)") || /状态/.test(cell));
|
|
2198
|
+
if (statusIndex < 0) {
|
|
2199
|
+
return { status: "broken", issues: ["issues.csv header missing 状态 column"], stats: { total: Math.max(0, lines.length - 1), inProgress: 0 } };
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
const allowedStates = new Set(["未开始", "进行中", "已完成"]);
|
|
2203
|
+
let total = 0;
|
|
2204
|
+
let inProgress = 0;
|
|
2205
|
+
const issues = [];
|
|
2206
|
+
|
|
2207
|
+
for (let i = 1; i < lines.length; i++) {
|
|
2208
|
+
const row = parseCsvLine(lines[i]);
|
|
2209
|
+
const isEmpty = row.every((cell) => String(cell || "").trim() === "");
|
|
2210
|
+
if (isEmpty) {
|
|
2211
|
+
continue;
|
|
2212
|
+
}
|
|
2213
|
+
total += 1;
|
|
2214
|
+
const state = String(row[statusIndex] || "").trim();
|
|
2215
|
+
if (!allowedStates.has(state)) {
|
|
2216
|
+
issues.push(`Invalid 状态 at row ${i + 1}: ${state || "(empty)"}`);
|
|
2217
|
+
continue;
|
|
2218
|
+
}
|
|
2219
|
+
if (state === "进行中") {
|
|
2220
|
+
inProgress += 1;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
if (inProgress > 1) {
|
|
2225
|
+
issues.push(`Multiple tasks in 进行中: ${inProgress}`);
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
return {
|
|
2229
|
+
status: issues.length > 0 ? "broken" : "ok",
|
|
2230
|
+
issues,
|
|
2231
|
+
stats: { total, inProgress },
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
function checkSpecProjectIntegrity(workspaceRoot) {
|
|
2236
|
+
const issues = [];
|
|
2237
|
+
const issuesPath = path.join(workspaceRoot, "issues.csv");
|
|
2238
|
+
const issuesResult = analyzeIssuesCsv(issuesPath);
|
|
2239
|
+
|
|
2240
|
+
const specDir = path.join(workspaceRoot, ".ling", "spec");
|
|
2241
|
+
const templatesDir = path.join(specDir, "templates");
|
|
2242
|
+
const referencesDir = path.join(specDir, "references");
|
|
2243
|
+
const profilesDir = path.join(specDir, "profiles");
|
|
2244
|
+
|
|
2245
|
+
const hasSpecDir = fs.existsSync(specDir);
|
|
2246
|
+
const globalSpecSummary = !hasSpecDir && issuesResult.status !== "missing" ? evaluateSpecState() : null;
|
|
2247
|
+
const canFallbackToGlobalSpec = Boolean(globalSpecSummary && globalSpecSummary.state === "installed");
|
|
2248
|
+
if (!hasSpecDir) {
|
|
2249
|
+
if (issuesResult.status !== "missing") {
|
|
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
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
} else {
|
|
2263
|
+
if (issuesResult.status === "missing") {
|
|
2264
|
+
issues.push("Missing issues.csv (run: ling spec init)");
|
|
2265
|
+
}
|
|
2266
|
+
for (const rel of SPEC_TEMPLATE_REQUIRED_FILES) {
|
|
2267
|
+
const filePath = path.join(templatesDir, rel);
|
|
2268
|
+
if (!fs.existsSync(filePath)) {
|
|
2269
|
+
issues.push(`Missing spec template: .ling/spec/templates/${rel}`);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
for (const rel of SPEC_REFERENCE_REQUIRED_FILES) {
|
|
2273
|
+
const filePath = path.join(referencesDir, rel);
|
|
2274
|
+
if (!fs.existsSync(filePath)) {
|
|
2275
|
+
issues.push(`Missing spec reference: .ling/spec/references/${rel}`);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
for (const rel of SPEC_PROFILE_REQUIRED_FILES) {
|
|
2279
|
+
const filePath = path.join(profilesDir, ...rel.split("/"));
|
|
2280
|
+
if (!fs.existsSync(filePath)) {
|
|
2281
|
+
issues.push(`Missing spec profile: .ling/spec/profiles/${rel}`);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
if (issuesResult.status === "broken") {
|
|
2287
|
+
issues.push(...issuesResult.issues);
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
const hasAnySpecSignal = hasSpecDir || issuesResult.status !== "missing";
|
|
2291
|
+
if (!hasAnySpecSignal) {
|
|
2292
|
+
return { status: "missing", issues: [], stats: { total: 0, inProgress: 0 } };
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
return {
|
|
2296
|
+
status: issues.length > 0 ? "broken" : "ok",
|
|
2297
|
+
issues,
|
|
2298
|
+
stats: issuesResult.stats,
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
function resolveWorkspaceSpecBackupRoot(workspaceRoot, timestamp) {
|
|
2303
|
+
return path.join(workspaceRoot, ".ling", "backups", "spec", timestamp, "before");
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
async function commandSpecInit(options) {
|
|
2307
|
+
const workspaceRoot = options.path
|
|
2308
|
+
? resolveWorkspaceRoot(options.path)
|
|
2309
|
+
: (options.specWorkspace ? getSpecWorkspaceDir() : resolveWorkspaceRoot());
|
|
2310
|
+
const csvOnly = Boolean(options.csvOnly);
|
|
2311
|
+
const prompter = createConflictPrompter(options);
|
|
2312
|
+
const timestamp = nowISO().replace(/[:.]/g, "-");
|
|
2313
|
+
const backupRoot = resolveWorkspaceSpecBackupRoot(workspaceRoot, timestamp);
|
|
2314
|
+
|
|
2315
|
+
const specDir = path.join(workspaceRoot, ".ling", "spec");
|
|
2316
|
+
let specSourceDir = BUNDLED_SPEC_DIR;
|
|
2317
|
+
let cleanupSpec = null;
|
|
2318
|
+
if (options.branch) {
|
|
2319
|
+
const remote = cloneBranchSpecDir(options.branch, {
|
|
2320
|
+
quiet: options.quiet,
|
|
2321
|
+
logger: log.bind(null, options),
|
|
2322
|
+
});
|
|
2323
|
+
specSourceDir = remote.specDir;
|
|
2324
|
+
cleanupSpec = remote.cleanup;
|
|
2325
|
+
}
|
|
2326
|
+
const assets = {
|
|
2327
|
+
templates: {
|
|
2328
|
+
sourceDir: path.join(specSourceDir, "templates"),
|
|
2329
|
+
destDir: path.join(specDir, "templates"),
|
|
2330
|
+
},
|
|
2331
|
+
references: {
|
|
2332
|
+
sourceDir: path.join(specSourceDir, "references"),
|
|
2333
|
+
destDir: path.join(specDir, "references"),
|
|
2334
|
+
},
|
|
2335
|
+
profiles: {
|
|
2336
|
+
sourceDir: path.join(specSourceDir, "profiles"),
|
|
2337
|
+
destDir: path.join(specDir, "profiles"),
|
|
2338
|
+
},
|
|
2339
|
+
};
|
|
2340
|
+
|
|
2341
|
+
try {
|
|
2342
|
+
fs.mkdirSync(workspaceRoot, { recursive: true });
|
|
2343
|
+
|
|
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
|
+
}
|
|
2351
|
+
|
|
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
|
+
}
|
|
2362
|
+
|
|
2363
|
+
if (action === "keep") {
|
|
2364
|
+
log(options, `[skip] 已保留 Spec ${assetName} 资产,不覆盖: ${config.destDir}`);
|
|
2365
|
+
continue;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
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}`);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
const issuesPath = path.join(workspaceRoot, "issues.csv");
|
|
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
|
+
}
|
|
2387
|
+
const issuesTemplate = stripUtf8Bom(fs.readFileSync(issuesTemplatePath, "utf8"));
|
|
2388
|
+
const hasIssues = fs.existsSync(issuesPath);
|
|
2389
|
+
if (hasIssues) {
|
|
2390
|
+
let action = "backup";
|
|
2391
|
+
if (prompter) {
|
|
2392
|
+
action = await prompter.resolveConflict({
|
|
2393
|
+
category: "spec:project:file",
|
|
2394
|
+
label: "issues.csv",
|
|
2395
|
+
path: issuesPath,
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
if (action === "keep") {
|
|
2399
|
+
log(options, "[skip] 已保留现有 issues.csv,不覆盖");
|
|
2400
|
+
} else {
|
|
2401
|
+
if (action !== "remove") {
|
|
2402
|
+
backupFileSnapshot(issuesPath, path.join(backupRoot, "issues.csv"), options, "issues.csv");
|
|
2403
|
+
} else if (!options.dryRun) {
|
|
2404
|
+
fs.rmSync(issuesPath, { force: true });
|
|
2405
|
+
}
|
|
2406
|
+
atomicWriteFile(issuesPath, `${issuesTemplate.trimEnd()}\n`, options, "issues.csv");
|
|
2407
|
+
log(options, options.dryRun ? `[dry-run] 将写入任务跟踪文件: ${issuesPath}` : `[ok] 已写入任务跟踪文件: ${issuesPath}`);
|
|
2408
|
+
}
|
|
2409
|
+
} else {
|
|
2410
|
+
atomicWriteFile(issuesPath, `${issuesTemplate.trimEnd()}\n`, options, "issues.csv");
|
|
2411
|
+
log(options, options.dryRun ? `[dry-run] 将创建任务跟踪文件: ${issuesPath}` : `[ok] 已创建任务跟踪文件: ${issuesPath}`);
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
const docsReviewsDir = path.join(workspaceRoot, "docs", "reviews");
|
|
2415
|
+
const docsHandoffDir = path.join(workspaceRoot, "docs", "handoff");
|
|
2416
|
+
if (options.dryRun) {
|
|
2417
|
+
log(options, `[dry-run] 将确保目录存在: ${docsReviewsDir}`);
|
|
2418
|
+
log(options, `[dry-run] 将确保目录存在: ${docsHandoffDir}`);
|
|
2419
|
+
} else {
|
|
2420
|
+
fs.mkdirSync(docsReviewsDir, { recursive: true });
|
|
2421
|
+
fs.mkdirSync(docsHandoffDir, { recursive: true });
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
const requestedTargets = normalizeTargets(options.targets);
|
|
2425
|
+
const isSpecWorkspaceMode = Boolean(!options.path && options.specWorkspace);
|
|
2426
|
+
const shouldInitTargets = isSpecWorkspaceMode ? true : requestedTargets.length > 0;
|
|
2427
|
+
const targets = shouldInitTargets ? (requestedTargets.length > 0 ? requestedTargets : [...SUPPORTED_TARGETS]) : [];
|
|
2428
|
+
if (targets.length > 0) {
|
|
2429
|
+
await initTargets(workspaceRoot, targets, options, prompter);
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
log(options, `[ok] Spec 初始化完成: ${workspaceRoot}`);
|
|
2433
|
+
} finally {
|
|
2434
|
+
if (cleanupSpec) cleanupSpec();
|
|
2435
|
+
if (prompter) prompter.close();
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
function commandSpecDoctor(options) {
|
|
2440
|
+
const workspaceRoot = options.path
|
|
2441
|
+
? resolveWorkspaceRoot(options.path)
|
|
2442
|
+
: (options.specWorkspace ? getSpecWorkspaceDir() : resolveWorkspaceRoot());
|
|
2443
|
+
const result = checkSpecProjectIntegrity(workspaceRoot);
|
|
2444
|
+
const state = result.status === "ok" ? "installed" : result.status;
|
|
2445
|
+
|
|
2446
|
+
if (options.quiet) {
|
|
2447
|
+
console.log(state);
|
|
2448
|
+
setQuietStatusExitCode(state);
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
if (state === "missing") {
|
|
2453
|
+
console.log("[warn] 未检测到 Spec 项目资产");
|
|
2454
|
+
console.log(` 工作区: ${workspaceRoot}`);
|
|
2455
|
+
setQuietStatusExitCode("missing");
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
console.log(state === "installed" ? "[ok] Spec 项目资产状态正常" : "[warn] Spec 项目资产存在问题");
|
|
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
|
+
}
|
|
2476
|
+
console.log(` 任务数: ${result.stats.total}`);
|
|
2477
|
+
console.log(` 进行中: ${result.stats.inProgress}`);
|
|
2478
|
+
for (const issue of result.issues || []) {
|
|
2479
|
+
console.log(` Issue: ${issue}`);
|
|
2480
|
+
}
|
|
2481
|
+
setQuietStatusExitCode(state);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
1811
2484
|
async function commandSpecEnable(options) {
|
|
1812
2485
|
ensureBundledSpecResources();
|
|
1813
2486
|
const targets = resolveTargetsForSpec(options);
|
|
@@ -1844,6 +2517,7 @@ function commandSpecDisable(options) {
|
|
|
1844
2517
|
if (remainingTargets.length === 0) {
|
|
1845
2518
|
restoreSpecAsset(state.assets.templates, options, "Spec templates");
|
|
1846
2519
|
restoreSpecAsset(state.assets.references, options, "Spec references");
|
|
2520
|
+
restoreSpecAsset(state.assets.profiles, options, "Spec profiles");
|
|
1847
2521
|
state.assets = {};
|
|
1848
2522
|
}
|
|
1849
2523
|
|
|
@@ -1851,6 +2525,14 @@ function commandSpecDisable(options) {
|
|
|
1851
2525
|
if (!options.dryRun) {
|
|
1852
2526
|
if (remainingTargets.length === 0) {
|
|
1853
2527
|
removeSpecStateFile();
|
|
2528
|
+
const specHome = getSpecHomeDir();
|
|
2529
|
+
try {
|
|
2530
|
+
if (fs.existsSync(specHome) && fs.readdirSync(specHome).length === 0) {
|
|
2531
|
+
fs.rmdirSync(specHome);
|
|
2532
|
+
}
|
|
2533
|
+
} catch (err) {
|
|
2534
|
+
log(options, `[warn] 无法清理 Spec 根目录: ${err.message}`);
|
|
2535
|
+
}
|
|
1854
2536
|
} else {
|
|
1855
2537
|
writeSpecState(statePath, state);
|
|
1856
2538
|
}
|
|
@@ -1870,87 +2552,100 @@ async function commandSpec(options) {
|
|
|
1870
2552
|
if (subcommand === "disable") {
|
|
1871
2553
|
return commandSpecDisable(options);
|
|
1872
2554
|
}
|
|
2555
|
+
if (subcommand === "init") {
|
|
2556
|
+
return await commandSpecInit(options);
|
|
2557
|
+
}
|
|
2558
|
+
if (subcommand === "doctor") {
|
|
2559
|
+
return commandSpecDoctor(options);
|
|
2560
|
+
}
|
|
1873
2561
|
throw new Error(`未知 spec 子命令: ${subcommand}`);
|
|
1874
2562
|
}
|
|
1875
2563
|
|
|
1876
|
-
async function
|
|
1877
|
-
const
|
|
1878
|
-
|
|
1879
|
-
|
|
2564
|
+
async function initTargets(workspaceRoot, targets, options, prompter) {
|
|
2565
|
+
for (const group of groupTargetsByInstallSurface(targets)) {
|
|
2566
|
+
const runOptions = { ...options };
|
|
2567
|
+
const conflicts = [];
|
|
1880
2568
|
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
2569
|
+
if (group.installSurface === ".agent") {
|
|
2570
|
+
const agentDir = path.join(workspaceRoot, ".agent");
|
|
2571
|
+
if (fs.existsSync(agentDir)) {
|
|
2572
|
+
conflicts.push({
|
|
2573
|
+
category: "project:shared-agent",
|
|
2574
|
+
label: ".agent",
|
|
2575
|
+
path: agentDir,
|
|
2576
|
+
target: group.logicalTargets.join(","),
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
1885
2580
|
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
2581
|
+
if (group.adapterTarget === "codex") {
|
|
2582
|
+
const managedDir = path.join(workspaceRoot, ".agents");
|
|
2583
|
+
const legacyDir = path.join(workspaceRoot, ".codex");
|
|
2584
|
+
if (fs.existsSync(managedDir) || fs.existsSync(legacyDir)) {
|
|
2585
|
+
if (fs.existsSync(managedDir)) {
|
|
1889
2586
|
conflicts.push({
|
|
1890
|
-
category: "project:
|
|
1891
|
-
label: ".
|
|
1892
|
-
path:
|
|
2587
|
+
category: "project:codex",
|
|
2588
|
+
label: ".agents",
|
|
2589
|
+
path: managedDir,
|
|
1893
2590
|
target,
|
|
1894
2591
|
});
|
|
1895
2592
|
}
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
conflicts.push({
|
|
1904
|
-
category: "project:codex",
|
|
1905
|
-
label: ".agents",
|
|
1906
|
-
path: managedDir,
|
|
1907
|
-
target,
|
|
1908
|
-
});
|
|
1909
|
-
}
|
|
1910
|
-
if (fs.existsSync(legacyDir)) {
|
|
1911
|
-
conflicts.push({
|
|
1912
|
-
category: "project:codex",
|
|
1913
|
-
label: ".codex",
|
|
1914
|
-
path: legacyDir,
|
|
1915
|
-
target,
|
|
1916
|
-
});
|
|
1917
|
-
}
|
|
2593
|
+
if (fs.existsSync(legacyDir)) {
|
|
2594
|
+
conflicts.push({
|
|
2595
|
+
category: "project:codex",
|
|
2596
|
+
label: ".codex",
|
|
2597
|
+
path: legacyDir,
|
|
2598
|
+
target,
|
|
2599
|
+
});
|
|
1918
2600
|
}
|
|
1919
2601
|
}
|
|
2602
|
+
}
|
|
1920
2603
|
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
2604
|
+
if (conflicts.length > 0) {
|
|
2605
|
+
if (!prompter && !runOptions.force) {
|
|
2606
|
+
throw new Error("检测到已有资产且当前环境不可交互,请使用 --force 或在交互终端中重试。");
|
|
2607
|
+
}
|
|
1925
2608
|
|
|
1926
|
-
|
|
1927
|
-
|
|
2609
|
+
const timestamp = nowISO().replace(/[:.]/g, "-");
|
|
2610
|
+
let shouldSkip = false;
|
|
1928
2611
|
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
}
|
|
1936
|
-
if (action === "backup") {
|
|
1937
|
-
const backupRootName = conflict.label === ".agent" ? ".agent-backup" : ".agents-backup";
|
|
1938
|
-
backupWorkspaceDir(workspaceRoot, conflict.path, backupRootName, timestamp, options, `工作区资产 ${conflict.label}`);
|
|
1939
|
-
}
|
|
1940
|
-
// remove/backup 都需要强制覆盖才能继续
|
|
1941
|
-
runOptions.force = true;
|
|
2612
|
+
for (const conflict of conflicts) {
|
|
2613
|
+
const action = prompter ? await prompter.resolveConflict(conflict) : "backup";
|
|
2614
|
+
if (action === "keep") {
|
|
2615
|
+
log(options, `[skip] 已保留现有资产,跳过初始化: ${conflict.label}`);
|
|
2616
|
+
shouldSkip = true;
|
|
2617
|
+
break;
|
|
1942
2618
|
}
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
2619
|
+
if (action === "backup") {
|
|
2620
|
+
const backupRootName = conflict.label === ".agent" ? ".agent-backup" : ".agents-backup";
|
|
2621
|
+
backupWorkspaceDir(workspaceRoot, conflict.path, backupRootName, timestamp, options, `工作区资产 ${conflict.label}`);
|
|
1946
2622
|
}
|
|
2623
|
+
// remove/backup 都需要强制覆盖才能继续
|
|
2624
|
+
runOptions.force = true;
|
|
1947
2625
|
}
|
|
1948
2626
|
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
2627
|
+
if (shouldSkip) {
|
|
2628
|
+
continue;
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
const adapter = createAdapter(group.adapterTarget, workspaceRoot, runOptions);
|
|
2633
|
+
log(options, `[sync] 正在初始化目标 [${group.logicalTargets.join(", ")}] ...`);
|
|
2634
|
+
adapter.install(BUNDLED_AGENT_DIR);
|
|
2635
|
+
recordWorkspaceInstallTargets(workspaceRoot, group.logicalTargets, runOptions);
|
|
2636
|
+
for (const target of group.logicalTargets) {
|
|
1952
2637
|
registerWorkspaceTarget(workspaceRoot, target, runOptions);
|
|
1953
2638
|
}
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
async function commandInit(options) {
|
|
2643
|
+
const workspaceRoot = resolveWorkspaceRoot(options.path);
|
|
2644
|
+
const targets = await resolveTargetsForInit(options);
|
|
2645
|
+
const prompter = createConflictPrompter(options);
|
|
2646
|
+
|
|
2647
|
+
try {
|
|
2648
|
+
await initTargets(workspaceRoot, targets, options, prompter);
|
|
1954
2649
|
} finally {
|
|
1955
2650
|
if (prompter) prompter.close();
|
|
1956
2651
|
}
|
|
@@ -2025,71 +2720,73 @@ async function commandUpdate(options) {
|
|
|
2025
2720
|
|
|
2026
2721
|
let updatedAny = false;
|
|
2027
2722
|
try {
|
|
2028
|
-
for (const
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
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
|
+
}
|
|
2036
2731
|
|
|
2037
|
-
|
|
2732
|
+
const runOptions = { ...options, force: true };
|
|
2733
|
+
const timestamp = nowISO().replace(/[:.]/g, "-");
|
|
2038
2734
|
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
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
|
+
}
|
|
2058
2754
|
}
|
|
2059
2755
|
}
|
|
2060
|
-
}
|
|
2061
2756
|
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
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
|
+
}
|
|
2084
2779
|
}
|
|
2085
2780
|
}
|
|
2086
|
-
}
|
|
2087
2781
|
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
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;
|
|
2093
2790
|
}
|
|
2094
2791
|
} finally {
|
|
2095
2792
|
if (prompter) prompter.close();
|
|
@@ -2182,7 +2879,7 @@ async function commandUpdateAll(options) {
|
|
|
2182
2879
|
const installedTargets = detectInstalledTargets(workspacePath);
|
|
2183
2880
|
let targets = [];
|
|
2184
2881
|
if (requestedTargets.length > 0) {
|
|
2185
|
-
targets =
|
|
2882
|
+
targets = requestedTargets.filter((target) => isTargetInstalled(workspacePath, target));
|
|
2186
2883
|
} else {
|
|
2187
2884
|
targets = [...Object.keys(item.targets || {}), ...installedTargets];
|
|
2188
2885
|
}
|
|
@@ -2198,9 +2895,9 @@ async function commandUpdateAll(options) {
|
|
|
2198
2895
|
log(options, `[sync] [${i + 1}/${records.length}] 更新: ${workspacePath} [${targets.join(", ")}]`);
|
|
2199
2896
|
|
|
2200
2897
|
const updatedTargets = [];
|
|
2201
|
-
for (const
|
|
2202
|
-
if (!isTargetInstalled(workspacePath,
|
|
2203
|
-
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(", ")}`);
|
|
2204
2901
|
continue;
|
|
2205
2902
|
}
|
|
2206
2903
|
|
|
@@ -2214,19 +2911,19 @@ async function commandUpdateAll(options) {
|
|
|
2214
2911
|
|
|
2215
2912
|
const timestampForBackup = nowISO().replace(/[:.]/g, "-");
|
|
2216
2913
|
|
|
2217
|
-
if (
|
|
2914
|
+
if (group.installSurface === ".agent") {
|
|
2218
2915
|
const agentDir = path.join(workspacePath, ".agent");
|
|
2219
2916
|
if (fs.existsSync(agentDir) && !areDirectoriesEqual(BUNDLED_AGENT_DIR, agentDir)) {
|
|
2220
2917
|
let action = "backup";
|
|
2221
2918
|
if (prompter) {
|
|
2222
2919
|
action = await prompter.resolveConflict({
|
|
2223
|
-
category: "update-all:project:
|
|
2920
|
+
category: "update-all:project:shared-agent",
|
|
2224
2921
|
label: `.agent (${workspacePath})`,
|
|
2225
2922
|
path: agentDir,
|
|
2226
2923
|
});
|
|
2227
2924
|
}
|
|
2228
2925
|
if (action === "keep") {
|
|
2229
|
-
log(options, `[skip] [${i + 1}/${records.length}] 已保留现有资产,跳过更新: ${workspacePath} [
|
|
2926
|
+
log(options, `[skip] [${i + 1}/${records.length}] 已保留现有资产,跳过更新: ${workspacePath} [${group.logicalTargets.join(", ")}]`);
|
|
2230
2927
|
continue;
|
|
2231
2928
|
}
|
|
2232
2929
|
if (action === "backup") {
|
|
@@ -2235,7 +2932,7 @@ async function commandUpdateAll(options) {
|
|
|
2235
2932
|
}
|
|
2236
2933
|
}
|
|
2237
2934
|
|
|
2238
|
-
if (
|
|
2935
|
+
if (group.adapterTarget === "codex") {
|
|
2239
2936
|
const managedDir = path.join(workspacePath, ".agents");
|
|
2240
2937
|
const legacyDir = path.join(workspacePath, ".codex");
|
|
2241
2938
|
const activeDir = fs.existsSync(managedDir) ? managedDir : legacyDir;
|
|
@@ -2259,13 +2956,14 @@ async function commandUpdateAll(options) {
|
|
|
2259
2956
|
}
|
|
2260
2957
|
}
|
|
2261
2958
|
|
|
2262
|
-
const adapter = createAdapter(
|
|
2959
|
+
const adapter = createAdapter(group.adapterTarget, workspacePath, runOptions);
|
|
2263
2960
|
adapter.update(BUNDLED_AGENT_DIR);
|
|
2264
|
-
|
|
2961
|
+
recordWorkspaceInstallTargets(workspacePath, group.logicalTargets, runOptions);
|
|
2962
|
+
updatedTargets.push(...group.logicalTargets);
|
|
2265
2963
|
} catch (err) {
|
|
2266
2964
|
failed += 1;
|
|
2267
2965
|
if (!options.quiet) {
|
|
2268
|
-
console.error(`[error] 更新失败: ${workspacePath} [${
|
|
2966
|
+
console.error(`[error] 更新失败: ${workspacePath} [${group.logicalTargets.join(", ")}]`);
|
|
2269
2967
|
console.error(` ${err.message}`);
|
|
2270
2968
|
}
|
|
2271
2969
|
}
|
|
@@ -2385,6 +3083,22 @@ async function commandDoctor(options) {
|
|
|
2385
3083
|
}
|
|
2386
3084
|
}
|
|
2387
3085
|
|
|
3086
|
+
const specResult = checkSpecProjectIntegrity(workspaceRoot);
|
|
3087
|
+
if (specResult.status !== "missing") {
|
|
3088
|
+
out(`\n[SPEC] 检查 Spec 项目资产...`);
|
|
3089
|
+
if (specResult.status === "ok") {
|
|
3090
|
+
out(" [ok] 状态正常");
|
|
3091
|
+
out(` - 任务数: ${specResult.stats.total}, 进行中: ${specResult.stats.inProgress}`);
|
|
3092
|
+
} else {
|
|
3093
|
+
out(` [error] 状态: ${specResult.status}`);
|
|
3094
|
+
out(` - 任务数: ${specResult.stats.total}, 进行中: ${specResult.stats.inProgress}`);
|
|
3095
|
+
for (const issue of specResult.issues || []) {
|
|
3096
|
+
out(` - ${issue}`);
|
|
3097
|
+
}
|
|
3098
|
+
hasIssue = true;
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
|
|
2388
3102
|
if (hasIssue) {
|
|
2389
3103
|
process.exitCode = 1;
|
|
2390
3104
|
}
|
|
@@ -2554,6 +3268,25 @@ function countSkillsRecursive(dir) {
|
|
|
2554
3268
|
return count;
|
|
2555
3269
|
}
|
|
2556
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
|
+
|
|
2557
3290
|
function commandStatus(options) {
|
|
2558
3291
|
const workspaceRoot = resolveWorkspaceRoot(options.path);
|
|
2559
3292
|
const summary = evaluateWorkspaceState(workspaceRoot, options);
|
|
@@ -2584,22 +3317,12 @@ function commandStatus(options) {
|
|
|
2584
3317
|
|
|
2585
3318
|
const geminiState = summary.targets.find((item) => item.targetName === "gemini");
|
|
2586
3319
|
if (geminiState) {
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
console.log(` 路径: ${agentDir}`);
|
|
2594
|
-
if (geminiState.state === "installed") {
|
|
2595
|
-
console.log(` Agents: ${agentsCount}`);
|
|
2596
|
-
console.log(` Skills: ${skillsCount}`);
|
|
2597
|
-
console.log(` Workflows: ${workflowsCount}`);
|
|
2598
|
-
} else {
|
|
2599
|
-
for (const issue of geminiState.integrity.issues || []) {
|
|
2600
|
-
console.log(` Issue: ${issue}`);
|
|
2601
|
-
}
|
|
2602
|
-
}
|
|
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");
|
|
2603
3326
|
}
|
|
2604
3327
|
|
|
2605
3328
|
const codexState = summary.targets.find((item) => item.targetName === "codex");
|