@mison/ling 1.0.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.
Files changed (268) hide show
  1. package/.agents/.shared/ui-ux-pro-max/data/charts.csv +26 -0
  2. package/.agents/.shared/ui-ux-pro-max/data/colors.csv +97 -0
  3. package/.agents/.shared/ui-ux-pro-max/data/icons.csv +101 -0
  4. package/.agents/.shared/ui-ux-pro-max/data/landing.csv +31 -0
  5. package/.agents/.shared/ui-ux-pro-max/data/products.csv +97 -0
  6. package/.agents/.shared/ui-ux-pro-max/data/prompts.csv +24 -0
  7. package/.agents/.shared/ui-ux-pro-max/data/react-performance.csv +45 -0
  8. package/.agents/.shared/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  9. package/.agents/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  10. package/.agents/.shared/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  11. package/.agents/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  12. package/.agents/.shared/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  13. package/.agents/.shared/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  14. package/.agents/.shared/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  15. package/.agents/.shared/ui-ux-pro-max/data/stacks/react.csv +54 -0
  16. package/.agents/.shared/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  17. package/.agents/.shared/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  18. package/.agents/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  19. package/.agents/.shared/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  20. package/.agents/.shared/ui-ux-pro-max/data/styles.csv +59 -0
  21. package/.agents/.shared/ui-ux-pro-max/data/typography.csv +58 -0
  22. package/.agents/.shared/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  23. package/.agents/.shared/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  24. package/.agents/.shared/ui-ux-pro-max/data/web-interface.csv +31 -0
  25. package/.agents/.shared/ui-ux-pro-max/scripts/core.py +258 -0
  26. package/.agents/.shared/ui-ux-pro-max/scripts/design_system.py +1067 -0
  27. package/.agents/.shared/ui-ux-pro-max/scripts/search.py +106 -0
  28. package/.agents/ARCHITECTURE.md +285 -0
  29. package/.agents/agents/backend-specialist.md +268 -0
  30. package/.agents/agents/code-archaeologist.md +106 -0
  31. package/.agents/agents/database-architect.md +225 -0
  32. package/.agents/agents/debugger.md +225 -0
  33. package/.agents/agents/devops-engineer.md +242 -0
  34. package/.agents/agents/documentation-writer.md +104 -0
  35. package/.agents/agents/explorer-agent.md +73 -0
  36. package/.agents/agents/frontend-specialist.md +618 -0
  37. package/.agents/agents/game-developer.md +162 -0
  38. package/.agents/agents/mobile-developer.md +382 -0
  39. package/.agents/agents/orchestrator.md +436 -0
  40. package/.agents/agents/penetration-tester.md +188 -0
  41. package/.agents/agents/performance-optimizer.md +187 -0
  42. package/.agents/agents/product-manager.md +112 -0
  43. package/.agents/agents/product-owner.md +95 -0
  44. package/.agents/agents/project-planner.md +405 -0
  45. package/.agents/agents/qa-automation-engineer.md +103 -0
  46. package/.agents/agents/security-auditor.md +170 -0
  47. package/.agents/agents/seo-specialist.md +111 -0
  48. package/.agents/agents/test-engineer.md +158 -0
  49. package/.agents/mcp_config.json +22 -0
  50. package/.agents/rules/GEMINI.md +273 -0
  51. package/.agents/scripts/auto_preview.py +148 -0
  52. package/.agents/scripts/checklist.py +217 -0
  53. package/.agents/scripts/session_manager.py +120 -0
  54. package/.agents/scripts/verify_all.py +327 -0
  55. package/.agents/skills/api-patterns/SKILL.md +84 -0
  56. package/.agents/skills/api-patterns/api-style.md +42 -0
  57. package/.agents/skills/api-patterns/auth.md +24 -0
  58. package/.agents/skills/api-patterns/documentation.md +26 -0
  59. package/.agents/skills/api-patterns/graphql.md +41 -0
  60. package/.agents/skills/api-patterns/rate-limiting.md +31 -0
  61. package/.agents/skills/api-patterns/response.md +37 -0
  62. package/.agents/skills/api-patterns/rest.md +40 -0
  63. package/.agents/skills/api-patterns/scripts/api_validator.py +211 -0
  64. package/.agents/skills/api-patterns/security-testing.md +122 -0
  65. package/.agents/skills/api-patterns/trpc.md +41 -0
  66. package/.agents/skills/api-patterns/versioning.md +22 -0
  67. package/.agents/skills/app-builder/SKILL.md +75 -0
  68. package/.agents/skills/app-builder/agent-coordination.md +74 -0
  69. package/.agents/skills/app-builder/feature-building.md +53 -0
  70. package/.agents/skills/app-builder/project-detection.md +34 -0
  71. package/.agents/skills/app-builder/scaffolding.md +118 -0
  72. package/.agents/skills/app-builder/tech-stack.md +40 -0
  73. package/.agents/skills/app-builder/templates/SKILL.md +39 -0
  74. package/.agents/skills/app-builder/templates/astro-static/TEMPLATE.md +76 -0
  75. package/.agents/skills/app-builder/templates/chrome-extension/TEMPLATE.md +92 -0
  76. package/.agents/skills/app-builder/templates/cli-tool/TEMPLATE.md +88 -0
  77. package/.agents/skills/app-builder/templates/electron-desktop/TEMPLATE.md +88 -0
  78. package/.agents/skills/app-builder/templates/express-api/TEMPLATE.md +83 -0
  79. package/.agents/skills/app-builder/templates/flutter-app/TEMPLATE.md +90 -0
  80. package/.agents/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +90 -0
  81. package/.agents/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +122 -0
  82. package/.agents/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +122 -0
  83. package/.agents/skills/app-builder/templates/nextjs-static/TEMPLATE.md +169 -0
  84. package/.agents/skills/app-builder/templates/nuxt-app/TEMPLATE.md +134 -0
  85. package/.agents/skills/app-builder/templates/python-fastapi/TEMPLATE.md +83 -0
  86. package/.agents/skills/app-builder/templates/react-native-app/TEMPLATE.md +119 -0
  87. package/.agents/skills/architecture/SKILL.md +57 -0
  88. package/.agents/skills/architecture/context-discovery.md +43 -0
  89. package/.agents/skills/architecture/examples.md +94 -0
  90. package/.agents/skills/architecture/pattern-selection.md +68 -0
  91. package/.agents/skills/architecture/patterns-reference.md +50 -0
  92. package/.agents/skills/architecture/trade-off-analysis.md +77 -0
  93. package/.agents/skills/bash-linux/SKILL.md +201 -0
  94. package/.agents/skills/behavioral-modes/SKILL.md +264 -0
  95. package/.agents/skills/brainstorming/SKILL.md +164 -0
  96. package/.agents/skills/brainstorming/dynamic-questioning.md +359 -0
  97. package/.agents/skills/clean-code/SKILL.md +200 -0
  98. package/.agents/skills/code-review-checklist/SKILL.md +125 -0
  99. package/.agents/skills/database-design/SKILL.md +54 -0
  100. package/.agents/skills/database-design/database-selection.md +43 -0
  101. package/.agents/skills/database-design/indexing.md +39 -0
  102. package/.agents/skills/database-design/migrations.md +50 -0
  103. package/.agents/skills/database-design/optimization.md +36 -0
  104. package/.agents/skills/database-design/orm-selection.md +30 -0
  105. package/.agents/skills/database-design/schema-design.md +56 -0
  106. package/.agents/skills/database-design/scripts/schema_validator.py +172 -0
  107. package/.agents/skills/deployment-procedures/SKILL.md +241 -0
  108. package/.agents/skills/doc.md +177 -0
  109. package/.agents/skills/documentation-templates/SKILL.md +194 -0
  110. package/.agents/skills/frontend-design/SKILL.md +418 -0
  111. package/.agents/skills/frontend-design/animation-guide.md +331 -0
  112. package/.agents/skills/frontend-design/color-system.md +307 -0
  113. package/.agents/skills/frontend-design/decision-trees.md +418 -0
  114. package/.agents/skills/frontend-design/motion-graphics.md +306 -0
  115. package/.agents/skills/frontend-design/scripts/accessibility_checker.py +183 -0
  116. package/.agents/skills/frontend-design/scripts/ux_audit.py +727 -0
  117. package/.agents/skills/frontend-design/typography-system.md +345 -0
  118. package/.agents/skills/frontend-design/ux-psychology.md +1118 -0
  119. package/.agents/skills/frontend-design/visual-effects.md +383 -0
  120. package/.agents/skills/game-development/2d-games/SKILL.md +119 -0
  121. package/.agents/skills/game-development/3d-games/SKILL.md +135 -0
  122. package/.agents/skills/game-development/SKILL.md +167 -0
  123. package/.agents/skills/game-development/game-art/SKILL.md +185 -0
  124. package/.agents/skills/game-development/game-audio/SKILL.md +190 -0
  125. package/.agents/skills/game-development/game-design/SKILL.md +129 -0
  126. package/.agents/skills/game-development/mobile-games/SKILL.md +108 -0
  127. package/.agents/skills/game-development/multiplayer/SKILL.md +132 -0
  128. package/.agents/skills/game-development/pc-games/SKILL.md +144 -0
  129. package/.agents/skills/game-development/vr-ar/SKILL.md +123 -0
  130. package/.agents/skills/game-development/web-games/SKILL.md +150 -0
  131. package/.agents/skills/geo-fundamentals/SKILL.md +155 -0
  132. package/.agents/skills/geo-fundamentals/scripts/geo_checker.py +289 -0
  133. package/.agents/skills/i18n-localization/SKILL.md +154 -0
  134. package/.agents/skills/i18n-localization/scripts/i18n_checker.py +241 -0
  135. package/.agents/skills/intelligent-routing/SKILL.md +335 -0
  136. package/.agents/skills/lint-and-validate/SKILL.md +44 -0
  137. package/.agents/skills/lint-and-validate/scripts/lint_runner.py +184 -0
  138. package/.agents/skills/lint-and-validate/scripts/type_coverage.py +173 -0
  139. package/.agents/skills/mcp-builder/SKILL.md +176 -0
  140. package/.agents/skills/mobile-design/SKILL.md +394 -0
  141. package/.agents/skills/mobile-design/decision-trees.md +516 -0
  142. package/.agents/skills/mobile-design/mobile-backend.md +491 -0
  143. package/.agents/skills/mobile-design/mobile-color-system.md +420 -0
  144. package/.agents/skills/mobile-design/mobile-debugging.md +122 -0
  145. package/.agents/skills/mobile-design/mobile-design-thinking.md +355 -0
  146. package/.agents/skills/mobile-design/mobile-navigation.md +458 -0
  147. package/.agents/skills/mobile-design/mobile-performance.md +767 -0
  148. package/.agents/skills/mobile-design/mobile-testing.md +356 -0
  149. package/.agents/skills/mobile-design/mobile-typography.md +432 -0
  150. package/.agents/skills/mobile-design/platform-android.md +666 -0
  151. package/.agents/skills/mobile-design/platform-ios.md +561 -0
  152. package/.agents/skills/mobile-design/scripts/mobile_audit.py +670 -0
  153. package/.agents/skills/mobile-design/touch-psychology.md +537 -0
  154. package/.agents/skills/nextjs-react-expert/1-async-eliminating-waterfalls.md +311 -0
  155. package/.agents/skills/nextjs-react-expert/2-bundle-bundle-size-optimization.md +241 -0
  156. package/.agents/skills/nextjs-react-expert/3-server-server-side-performance.md +489 -0
  157. package/.agents/skills/nextjs-react-expert/4-client-client-side-data-fetching.md +263 -0
  158. package/.agents/skills/nextjs-react-expert/5-rerender-re-render-optimization.md +581 -0
  159. package/.agents/skills/nextjs-react-expert/6-rendering-rendering-performance.md +431 -0
  160. package/.agents/skills/nextjs-react-expert/7-js-javascript-performance.md +683 -0
  161. package/.agents/skills/nextjs-react-expert/8-advanced-advanced-patterns.md +149 -0
  162. package/.agents/skills/nextjs-react-expert/SKILL.md +286 -0
  163. package/.agents/skills/nextjs-react-expert/scripts/convert_rules.py +222 -0
  164. package/.agents/skills/nextjs-react-expert/scripts/react_performance_checker.py +252 -0
  165. package/.agents/skills/nodejs-best-practices/SKILL.md +333 -0
  166. package/.agents/skills/parallel-agents/SKILL.md +193 -0
  167. package/.agents/skills/performance-profiling/SKILL.md +149 -0
  168. package/.agents/skills/performance-profiling/scripts/lighthouse_audit.py +120 -0
  169. package/.agents/skills/plan-writing/SKILL.md +152 -0
  170. package/.agents/skills/powershell-windows/SKILL.md +166 -0
  171. package/.agents/skills/python-patterns/SKILL.md +441 -0
  172. package/.agents/skills/red-team-tactics/SKILL.md +203 -0
  173. package/.agents/skills/refactoring-patterns/SKILL.md +43 -0
  174. package/.agents/skills/rust-pro/SKILL.md +190 -0
  175. package/.agents/skills/seo-fundamentals/SKILL.md +135 -0
  176. package/.agents/skills/seo-fundamentals/scripts/seo_checker.py +215 -0
  177. package/.agents/skills/server-management/SKILL.md +161 -0
  178. package/.agents/skills/systematic-debugging/SKILL.md +114 -0
  179. package/.agents/skills/tailwind-patterns/SKILL.md +269 -0
  180. package/.agents/skills/tdd-workflow/SKILL.md +149 -0
  181. package/.agents/skills/testing-patterns/SKILL.md +178 -0
  182. package/.agents/skills/testing-patterns/scripts/test_runner.py +219 -0
  183. package/.agents/skills/vulnerability-scanner/SKILL.md +276 -0
  184. package/.agents/skills/vulnerability-scanner/checklists.md +131 -0
  185. package/.agents/skills/vulnerability-scanner/scripts/__pycache__/security_scan.cpython-310.pyc +0 -0
  186. package/.agents/skills/vulnerability-scanner/scripts/security_scan.py +524 -0
  187. package/.agents/skills/web-design-guidelines/SKILL.md +57 -0
  188. package/.agents/skills/webapp-testing/SKILL.md +187 -0
  189. package/.agents/skills/webapp-testing/scripts/playwright_runner.py +173 -0
  190. package/.agents/workflows/brainstorm.md +113 -0
  191. package/.agents/workflows/create.md +59 -0
  192. package/.agents/workflows/debug.md +103 -0
  193. package/.agents/workflows/deploy.md +176 -0
  194. package/.agents/workflows/enhance.md +63 -0
  195. package/.agents/workflows/orchestrate.md +242 -0
  196. package/.agents/workflows/plan.md +89 -0
  197. package/.agents/workflows/preview.md +80 -0
  198. package/.agents/workflows/restore-localize-compat.md +525 -0
  199. package/.agents/workflows/status.md +86 -0
  200. package/.agents/workflows/test.md +144 -0
  201. package/.agents/workflows/ui-ux-pro-max.md +295 -0
  202. package/.spec/profiles/codex/AGENTS.spec.md +7 -0
  203. package/.spec/profiles/codex/ling.spec.rules.md +4 -0
  204. package/.spec/profiles/gemini/GEMINI.spec.md +5 -0
  205. package/.spec/references/README.md +36 -0
  206. package/.spec/references/cse-quickstart.md +96 -0
  207. package/.spec/references/gda-framework.md +394 -0
  208. package/.spec/references/harness-engineering-digest.md +93 -0
  209. package/.spec/skills/cybernetic-systems-engineering/SKILL.md +792 -0
  210. package/.spec/skills/cybernetic-systems-engineering/agents/openai.yaml +5 -0
  211. package/.spec/skills/cybernetic-systems-engineering/assets/quickstart.md +96 -0
  212. package/.spec/skills/cybernetic-systems-engineering/references/README.md +36 -0
  213. package/.spec/skills/cybernetic-systems-engineering/references/gda-framework.md +394 -0
  214. package/.spec/skills/cybernetic-systems-engineering/scripts/issues.csv +20 -0
  215. package/.spec/skills/harness-engineering/SKILL.md +100 -0
  216. package/.spec/skills/harness-engineering/agents/openai.yaml +4 -0
  217. package/.spec/skills/harness-engineering/references/harness-engineering-digest.md +93 -0
  218. package/.spec/templates/driver-prompt.md +7 -0
  219. package/.spec/templates/handoff.md +9 -0
  220. package/.spec/templates/issues.template.csv +2 -0
  221. package/.spec/templates/phase-acceptance.md +9 -0
  222. package/.spec/templates/review-report.md +9 -0
  223. package/AGENT_FLOW.md +609 -0
  224. package/CHANGELOG.md +43 -0
  225. package/LICENSE +21 -0
  226. package/README.md +359 -0
  227. package/bin/adapters/base.js +63 -0
  228. package/bin/adapters/codex.js +421 -0
  229. package/bin/adapters/gemini.js +157 -0
  230. package/bin/ag-kit.js +2266 -0
  231. package/bin/core/builder.js +80 -0
  232. package/bin/core/generator.js +59 -0
  233. package/bin/core/resource-loader.js +64 -0
  234. package/bin/core/transformer.js +208 -0
  235. package/bin/interactive.js +65 -0
  236. package/bin/ling.js +3 -0
  237. package/bin/utils/atomic-writer.js +97 -0
  238. package/bin/utils/git-helper.js +68 -0
  239. package/bin/utils/managed-block.js +65 -0
  240. package/bin/utils/manifest.js +244 -0
  241. package/bin/utils.js +89 -0
  242. package/docs/PLAN.md +54 -0
  243. package/docs/TECH.md +191 -0
  244. package/package.json +56 -0
  245. package/scripts/ci-verify.js +110 -0
  246. package/scripts/clean.js +123 -0
  247. package/scripts/health-check.js +143 -0
  248. package/scripts/health-check.sh +6 -0
  249. package/scripts/postinstall-check.js +112 -0
  250. package/scripts/run-tests.js +49 -0
  251. package/tests/atomic-writer.test.js +47 -0
  252. package/tests/clean-script.test.js +77 -0
  253. package/tests/cli-smoke.test.js +479 -0
  254. package/tests/codex-adapter.test.js +132 -0
  255. package/tests/doctor.test.js +94 -0
  256. package/tests/gemini-adapter.test.js +30 -0
  257. package/tests/generator.test.js +48 -0
  258. package/tests/git-helper.test.js +53 -0
  259. package/tests/global-sync.test.js +133 -0
  260. package/tests/health-check-script.test.js +34 -0
  261. package/tests/managed-block.test.js +41 -0
  262. package/tests/manifest.test.js +97 -0
  263. package/tests/package-tarball.test.js +33 -0
  264. package/tests/phase-c.test.js +107 -0
  265. package/tests/spec-profile.test.js +86 -0
  266. package/tests/standards-compliance.test.js +303 -0
  267. package/tests/transformer.test.js +74 -0
  268. package/tests/versioning.test.js +51 -0
package/bin/ag-kit.js ADDED
@@ -0,0 +1,2266 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const path = require("path");
6
+
7
+ const pkg = require("../package.json");
8
+ const { readGlobalNpmDependencies, cloneBranchAgentDir } = require("./utils");
9
+ const ManifestManager = require("./utils/manifest");
10
+ const AtomicWriter = require("./utils/atomic-writer");
11
+ const CodexBuilder = require("./core/builder");
12
+ const GeminiAdapter = require("./adapters/gemini");
13
+ const CodexAdapter = require("./adapters/codex");
14
+ const { selectTargets } = require("./interactive");
15
+
16
+ const BUNDLED_AGENT_DIR = path.resolve(__dirname, "../.agents");
17
+ const BUNDLED_SPEC_DIR = path.resolve(__dirname, "../.spec");
18
+ const PRIMARY_CLI_NAME = "ling";
19
+ const LEGACY_CLI_NAME = "ag-kit";
20
+ const CURRENT_CLI_BASENAME = path.basename(process.argv[1] || "", path.extname(process.argv[1] || "")) || PRIMARY_CLI_NAME;
21
+ const WORKSPACE_INDEX_VERSION = 2;
22
+ const UPSTREAM_GLOBAL_PACKAGE = "@vudovn/ag-kit";
23
+ const TOOLKIT_PACKAGE_NAMES = new Set(["@mison/ling", "@mison/ag-kit-cn", "antigravity-kit-cn", "antigravity-kit"]);
24
+ const SUPPORTED_TARGETS = ["gemini", "codex"];
25
+ const LEGACY_INDEX_TARGET_ALIASES = {
26
+ full: "gemini",
27
+ };
28
+ const GLOBAL_TARGET_DESTINATIONS = {
29
+ codex: [
30
+ {
31
+ id: "codex",
32
+ rootParts: [".codex"],
33
+ skillsParts: [".codex", "skills"],
34
+ },
35
+ ],
36
+ gemini: [
37
+ {
38
+ id: "gemini-cli",
39
+ rootParts: [".gemini", "skills"],
40
+ skillsParts: [".gemini", "skills"],
41
+ },
42
+ {
43
+ id: "antigravity",
44
+ rootParts: [".gemini", "antigravity"],
45
+ skillsParts: [".gemini", "antigravity", "skills"],
46
+ },
47
+ ],
48
+ };
49
+ const INDEX_LOCK_RETRY_MS = 50;
50
+ const INDEX_LOCK_TIMEOUT_MS = 3000;
51
+ const INDEX_LOCK_STALE_MS = 30000;
52
+ const QUIET_STATUS_EXIT_CODES = {
53
+ installed: 0,
54
+ broken: 1,
55
+ missing: 2,
56
+ };
57
+ const SPEC_STATE_VERSION = 1;
58
+ const SPEC_SKILL_NAMES = ["harness-engineering", "cybernetic-systems-engineering"];
59
+ const VERSION_TAG_PREFIX = "ling-";
60
+
61
+ function nowISO() {
62
+ return new Date().toISOString();
63
+ }
64
+
65
+ function getVersionTag() {
66
+ return `${VERSION_TAG_PREFIX}${pkg.version}`;
67
+ }
68
+
69
+ function getControlHomeDir() {
70
+ const preferred = path.join(os.homedir(), ".ling");
71
+ const legacy = path.join(os.homedir(), ".ag-kit");
72
+
73
+ if (fs.existsSync(preferred)) {
74
+ return preferred;
75
+ }
76
+ if (fs.existsSync(legacy)) {
77
+ return legacy;
78
+ }
79
+ return preferred;
80
+ }
81
+
82
+ function migrateLegacyControlHomeDir() {
83
+ if (process.env.LING_INDEX_PATH || process.env.AG_KIT_INDEX_PATH) {
84
+ return null;
85
+ }
86
+
87
+ const preferred = path.join(os.homedir(), ".ling");
88
+ const legacy = path.join(os.homedir(), ".ag-kit");
89
+
90
+ if (fs.existsSync(preferred) || !fs.existsSync(legacy)) {
91
+ return null;
92
+ }
93
+
94
+ copyDirRecursive(legacy, preferred);
95
+ return { from: legacy, to: preferred };
96
+ }
97
+
98
+ function getWorkspaceIndexPath() {
99
+ const customPath = process.env.LING_INDEX_PATH || process.env.AG_KIT_INDEX_PATH;
100
+ if (customPath) {
101
+ return path.resolve(process.cwd(), customPath);
102
+ }
103
+ return path.join(getControlHomeDir(), "workspaces.json");
104
+ }
105
+
106
+ function createEmptyWorkspaceIndex() {
107
+ return {
108
+ version: WORKSPACE_INDEX_VERSION,
109
+ updatedAt: "",
110
+ workspaces: [],
111
+ excludedPaths: [],
112
+ };
113
+ }
114
+
115
+ function resolveGlobalRootDir() {
116
+ const customRoot = process.env.LING_GLOBAL_ROOT || process.env.AG_KIT_GLOBAL_ROOT;
117
+ if (typeof customRoot === "string" && customRoot.trim()) {
118
+ return path.resolve(process.cwd(), customRoot);
119
+ }
120
+ return os.homedir();
121
+ }
122
+
123
+ function getGlobalDestinations(targetName, globalRoot = resolveGlobalRootDir()) {
124
+ const config = GLOBAL_TARGET_DESTINATIONS[targetName];
125
+ if (!config) {
126
+ throw new Error(`未知目标: ${targetName}`);
127
+ }
128
+ return config.map((item) => ({
129
+ ...item,
130
+ targetName,
131
+ rootDir: path.join(globalRoot, ...item.rootParts),
132
+ skillsRoot: path.join(globalRoot, ...item.skillsParts),
133
+ }));
134
+ }
135
+
136
+ function listGlobalDestinations(globalRoot = resolveGlobalRootDir()) {
137
+ return Object.keys(GLOBAL_TARGET_DESTINATIONS).flatMap((targetName) => getGlobalDestinations(targetName, globalRoot));
138
+ }
139
+
140
+ function resolveGlobalBackupRoot(timestamp) {
141
+ const globalRoot = resolveGlobalRootDir();
142
+ return path.join(globalRoot, ".ling", "backups", "global", timestamp);
143
+ }
144
+
145
+ function copyDirRecursive(src, dest) {
146
+ fs.mkdirSync(dest, { recursive: true });
147
+ const entries = fs.readdirSync(src, { withFileTypes: true });
148
+ for (const entry of entries) {
149
+ const srcPath = path.join(src, entry.name);
150
+ const destPath = path.join(dest, entry.name);
151
+ if (entry.isDirectory()) {
152
+ copyDirRecursive(srcPath, destPath);
153
+ } else {
154
+ fs.copyFileSync(srcPath, destPath);
155
+ }
156
+ }
157
+ }
158
+
159
+ function areDirectoriesEqual(leftDir, rightDir) {
160
+ const left = ManifestManager.generateFromDir(leftDir);
161
+ const right = ManifestManager.generateFromDir(rightDir);
162
+ const leftKeys = Object.keys(left);
163
+ const rightKeys = Object.keys(right);
164
+ if (leftKeys.length !== rightKeys.length) {
165
+ return false;
166
+ }
167
+ for (const key of leftKeys) {
168
+ if (left[key] !== right[key]) {
169
+ return false;
170
+ }
171
+ }
172
+ return true;
173
+ }
174
+
175
+ function printUsage() {
176
+ console.log("用法:");
177
+ console.log(` ${PRIMARY_CLI_NAME} init [--force] [--path <dir>] [--branch <name>] [--target <name>|--targets <a,b>] [--non-interactive] [--no-index] [--quiet] [--dry-run]`);
178
+ console.log(` ${PRIMARY_CLI_NAME} update [--path <dir>] [--branch <name>] [--target <name>|--targets <a,b>] [--no-index] [--quiet] [--dry-run]`);
179
+ console.log(` ${PRIMARY_CLI_NAME} update-all [--branch <name>] [--targets <a,b>] [--prune-missing] [--quiet] [--dry-run]`);
180
+ console.log(` ${PRIMARY_CLI_NAME} doctor [--path <dir>] [--target <name>|--targets <a,b>] [--fix] [--quiet]`);
181
+ console.log(` ${PRIMARY_CLI_NAME} global sync [--target <name>|--targets <a,b>] [--branch <name>] [--quiet] [--dry-run] # 默认同步 codex + gemini(cli+antigravity)`);
182
+ console.log(` ${PRIMARY_CLI_NAME} global status [--quiet]`);
183
+ console.log(` ${PRIMARY_CLI_NAME} spec enable [--target <name>|--targets <a,b>] [--quiet] [--dry-run]`);
184
+ console.log(` ${PRIMARY_CLI_NAME} spec disable [--target <name>|--targets <a,b>] [--quiet] [--dry-run]`);
185
+ console.log(` ${PRIMARY_CLI_NAME} spec status [--quiet]`);
186
+ console.log(` ${PRIMARY_CLI_NAME} exclude list [--quiet]`);
187
+ console.log(` ${PRIMARY_CLI_NAME} exclude add --path <dir> [--dry-run] [--quiet]`);
188
+ console.log(` ${PRIMARY_CLI_NAME} exclude remove --path <dir> [--dry-run] [--quiet]`);
189
+ console.log(` ${PRIMARY_CLI_NAME} status [--path <dir>] [--quiet]`);
190
+ console.log(` ${PRIMARY_CLI_NAME} --version`);
191
+ }
192
+
193
+ function printVersion() {
194
+ console.log(`${PRIMARY_CLI_NAME} version ${getVersionTag()}`);
195
+ }
196
+
197
+ function parseArgs(argv) {
198
+ if (argv.length === 0) {
199
+ return { command: "", options: {}, providedFlags: [] };
200
+ }
201
+
202
+ const command = argv[0];
203
+ const options = {
204
+ force: false,
205
+ quiet: false,
206
+ dryRun: false,
207
+ pruneMissing: false,
208
+ nonInteractive: false,
209
+ noIndex: false,
210
+ fix: false,
211
+ subcommand: "",
212
+ path: "",
213
+ branch: "",
214
+ targets: [],
215
+ };
216
+ const providedFlags = [];
217
+
218
+ let startIndex = 1;
219
+ if (command === "exclude" || command === "global") {
220
+ if (argv.length > 1 && !argv[1].startsWith("--")) {
221
+ options.subcommand = argv[1];
222
+ startIndex = 2;
223
+ } else {
224
+ options.subcommand = command === "global" ? "status" : "list";
225
+ startIndex = 1;
226
+ }
227
+ } else if (command === "spec") {
228
+ if (argv.length > 1 && !argv[1].startsWith("--")) {
229
+ options.subcommand = argv[1];
230
+ startIndex = 2;
231
+ } else {
232
+ options.subcommand = "status";
233
+ startIndex = 1;
234
+ }
235
+ }
236
+
237
+ for (let i = startIndex; i < argv.length; i++) {
238
+ const arg = argv[i];
239
+
240
+ if (arg === "--force") {
241
+ providedFlags.push(arg);
242
+ options.force = true;
243
+ } else if (arg === "--quiet") {
244
+ providedFlags.push(arg);
245
+ options.quiet = true;
246
+ } else if (arg === "--dry-run") {
247
+ providedFlags.push(arg);
248
+ options.dryRun = true;
249
+ } else if (arg === "--prune-missing") {
250
+ providedFlags.push(arg);
251
+ options.pruneMissing = true;
252
+ } else if (arg === "--non-interactive") {
253
+ providedFlags.push(arg);
254
+ options.nonInteractive = true;
255
+ } else if (arg === "--no-index") {
256
+ providedFlags.push(arg);
257
+ options.noIndex = true;
258
+ } else if (arg === "--fix") {
259
+ providedFlags.push(arg);
260
+ options.fix = true;
261
+ } else if (arg === "--path") {
262
+ providedFlags.push(arg);
263
+ if (i + 1 >= argv.length) {
264
+ throw new Error("--path 需要一个目录参数");
265
+ }
266
+ options.path = argv[++i];
267
+ } else if (arg === "--branch") {
268
+ providedFlags.push(arg);
269
+ if (i + 1 >= argv.length) {
270
+ throw new Error("--branch 需要一个分支名参数");
271
+ }
272
+ options.branch = argv[++i];
273
+ } else if (arg === "--target") {
274
+ providedFlags.push(arg);
275
+ if (i + 1 >= argv.length) {
276
+ throw new Error("--target 需要一个目标参数");
277
+ }
278
+ options.targets.push(argv[++i]);
279
+ } else if (arg === "--targets") {
280
+ providedFlags.push(arg);
281
+ if (i + 1 >= argv.length) {
282
+ throw new Error("--targets 需要一个参数");
283
+ }
284
+ options.targets.push(...String(argv[++i]).split(","));
285
+ } else {
286
+ throw new Error(`未知参数: ${arg}`);
287
+ }
288
+ }
289
+
290
+ return { command, options, providedFlags };
291
+ }
292
+
293
+ const COMMAND_ALLOWED_FLAGS = {
294
+ init: ["--force", "--path", "--branch", "--target", "--targets", "--non-interactive", "--no-index", "--quiet", "--dry-run"],
295
+ update: ["--path", "--branch", "--target", "--targets", "--no-index", "--quiet", "--dry-run"],
296
+ "update-all": ["--branch", "--targets", "--prune-missing", "--quiet", "--dry-run"],
297
+ doctor: ["--path", "--target", "--targets", "--fix", "--quiet"],
298
+ status: ["--path", "--quiet"],
299
+ "global:sync": ["--target", "--targets", "--branch", "--quiet", "--dry-run"],
300
+ "global:status": ["--quiet"],
301
+ "spec:enable": ["--target", "--targets", "--quiet", "--dry-run"],
302
+ "spec:disable": ["--target", "--targets", "--quiet", "--dry-run"],
303
+ "spec:status": ["--quiet"],
304
+ "exclude:list": ["--quiet"],
305
+ "exclude:add": ["--path", "--dry-run", "--quiet"],
306
+ "exclude:remove": ["--path", "--dry-run", "--quiet"],
307
+ };
308
+
309
+ function resolveAllowedFlags(command, options) {
310
+ if (command === "exclude") {
311
+ const subcommand = String(options.subcommand || "list").toLowerCase();
312
+ const key = `exclude:${subcommand}`;
313
+ return COMMAND_ALLOWED_FLAGS[key] || null;
314
+ }
315
+ if (command === "global") {
316
+ const subcommand = String(options.subcommand || "status").toLowerCase();
317
+ const key = `global:${subcommand}`;
318
+ return COMMAND_ALLOWED_FLAGS[key] || null;
319
+ }
320
+ if (command === "spec") {
321
+ const subcommand = String(options.subcommand || "status").toLowerCase();
322
+ const key = `spec:${subcommand}`;
323
+ return COMMAND_ALLOWED_FLAGS[key] || null;
324
+ }
325
+ return COMMAND_ALLOWED_FLAGS[command] || null;
326
+ }
327
+
328
+ function resolveCommandLabel(command, options) {
329
+ if (command === "exclude") {
330
+ const subcommand = String(options.subcommand || "list").toLowerCase();
331
+ return `exclude ${subcommand}`;
332
+ }
333
+ if (command === "global") {
334
+ const subcommand = String(options.subcommand || "status").toLowerCase();
335
+ return `global ${subcommand}`;
336
+ }
337
+ if (command === "spec") {
338
+ const subcommand = String(options.subcommand || "status").toLowerCase();
339
+ return `spec ${subcommand}`;
340
+ }
341
+ return command;
342
+ }
343
+
344
+ function validateOptionScope(command, options, providedFlags) {
345
+ const allowedFlags = resolveAllowedFlags(command, options);
346
+ if (!allowedFlags) {
347
+ return;
348
+ }
349
+
350
+ const allowedSet = new Set(allowedFlags);
351
+ const unsupported = [];
352
+ const seen = new Set();
353
+ for (const flag of providedFlags || []) {
354
+ if (allowedSet.has(flag)) {
355
+ continue;
356
+ }
357
+ if (!seen.has(flag)) {
358
+ unsupported.push(flag);
359
+ seen.add(flag);
360
+ }
361
+ }
362
+
363
+ if (unsupported.length === 0) {
364
+ return;
365
+ }
366
+
367
+ const commandLabel = resolveCommandLabel(command, options);
368
+ throw new Error(`命令 ${commandLabel} 不支持参数: ${unsupported.join(", ")}。可用参数: ${allowedFlags.join(", ")}`);
369
+ }
370
+
371
+ function resolveWorkspaceRoot(customPath) {
372
+ if (!customPath) {
373
+ return process.cwd();
374
+ }
375
+ return path.resolve(process.cwd(), customPath);
376
+ }
377
+
378
+ function log(options, message) {
379
+ if (!options.quiet) {
380
+ console.log(message);
381
+ }
382
+ }
383
+
384
+ function normalizeAbsolutePath(inputPath) {
385
+ return path.normalize(path.resolve(inputPath));
386
+ }
387
+
388
+ function pathCompareKey(inputPath) {
389
+ const normalized = normalizeAbsolutePath(inputPath);
390
+ if (process.platform === "win32") {
391
+ return normalized.toLowerCase();
392
+ }
393
+ return normalized;
394
+ }
395
+
396
+ function normalizePathList(items) {
397
+ const map = new Map();
398
+ for (const item of items) {
399
+ if (typeof item !== "string" || item.trim() === "") {
400
+ continue;
401
+ }
402
+ const normalizedPath = normalizeAbsolutePath(item);
403
+ const key = pathCompareKey(normalizedPath);
404
+ if (!map.has(key)) {
405
+ map.set(key, normalizedPath);
406
+ }
407
+ }
408
+ return Array.from(map.values()).sort((a, b) => a.localeCompare(b));
409
+ }
410
+
411
+ function isPathInOrUnder(basePath, targetPath) {
412
+ const normalizedBase = normalizeAbsolutePath(basePath);
413
+ const normalizedTarget = normalizeAbsolutePath(targetPath);
414
+ const baseKey = pathCompareKey(normalizedBase);
415
+ const targetKey = pathCompareKey(normalizedTarget);
416
+
417
+ if (targetKey === baseKey) {
418
+ return true;
419
+ }
420
+
421
+ const prefix = baseKey.endsWith(path.sep) ? baseKey : `${baseKey}${path.sep}`;
422
+ return targetKey.startsWith(prefix);
423
+ }
424
+
425
+ function isPathExcludedByList(excludedPaths, workspacePath) {
426
+ return excludedPaths.some((excludedPath) => isPathInOrUnder(excludedPath, workspacePath));
427
+ }
428
+
429
+ function isToolkitSourceDirectory(workspacePath) {
430
+ const packageJsonPath = path.join(workspacePath, "package.json");
431
+ const legacyCliPath = path.join(workspacePath, "bin", "ag-kit.js");
432
+ const primaryCliPath = path.join(workspacePath, "bin", "ling.js");
433
+
434
+ if (!fs.existsSync(packageJsonPath) || (!fs.existsSync(legacyCliPath) && !fs.existsSync(primaryCliPath))) {
435
+ return false;
436
+ }
437
+
438
+ try {
439
+ const content = fs.readFileSync(packageJsonPath, "utf8");
440
+ const parsed = JSON.parse(content);
441
+ const name = typeof parsed.name === "string" ? parsed.name : "";
442
+ return TOOLKIT_PACKAGE_NAMES.has(name);
443
+ } catch (err) {
444
+ return false;
445
+ }
446
+ }
447
+
448
+ function getSystemTempRoots() {
449
+ const rawRoots = [
450
+ os.tmpdir(),
451
+ process.env.TMPDIR,
452
+ process.env.TMP,
453
+ process.env.TEMP,
454
+ ];
455
+
456
+ // Add common POSIX temp roots explicitly.
457
+ // On macOS, os.tmpdir() usually resolves to /var/folders/... and may not cover /tmp.
458
+ if (process.platform !== "win32") {
459
+ rawRoots.push("/tmp", "/var/tmp");
460
+ if (process.platform === "darwin") {
461
+ rawRoots.push("/private/tmp", "/private/var/tmp");
462
+ }
463
+ }
464
+
465
+ const expandedRoots = [];
466
+
467
+ for (const root of rawRoots) {
468
+ if (typeof root !== "string" || root.trim() === "") {
469
+ continue;
470
+ }
471
+
472
+ expandedRoots.push(root);
473
+
474
+ const normalized = normalizeAbsolutePath(root);
475
+ try {
476
+ const realPath = fs.realpathSync.native
477
+ ? fs.realpathSync.native(normalized)
478
+ : fs.realpathSync(normalized);
479
+ expandedRoots.push(realPath);
480
+ } catch (err) {
481
+ // Ignore missing or inaccessible tmp roots from environment variables.
482
+ }
483
+
484
+ if (process.platform === "darwin") {
485
+ if (normalized === "/var" || normalized.startsWith("/var/")) {
486
+ expandedRoots.push(normalized.replace(/^\/var\b/, "/private/var"));
487
+ } else if (normalized === "/private/var" || normalized.startsWith("/private/var/")) {
488
+ expandedRoots.push(normalized.replace(/^\/private\/var\b/, "/var"));
489
+ }
490
+ }
491
+ }
492
+
493
+ return normalizePathList(expandedRoots);
494
+ }
495
+
496
+ const SYSTEM_TEMP_ROOTS = getSystemTempRoots();
497
+
498
+ function isSystemTempDirectory(workspacePath) {
499
+ return SYSTEM_TEMP_ROOTS.some((tempRoot) => isPathInOrUnder(tempRoot, workspacePath));
500
+ }
501
+
502
+ function normalizeTargetState(value) {
503
+ if (!value || typeof value !== "object") {
504
+ return null;
505
+ }
506
+ return {
507
+ version: typeof value.version === "string" ? value.version : "",
508
+ installedAt: typeof value.installedAt === "string" ? value.installedAt : "",
509
+ updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : "",
510
+ };
511
+ }
512
+
513
+ function normalizeIndexTargetName(targetName) {
514
+ if (typeof targetName !== "string") {
515
+ return null;
516
+ }
517
+ const normalized = targetName.trim().toLowerCase();
518
+ if (!normalized) {
519
+ return null;
520
+ }
521
+ if (Object.prototype.hasOwnProperty.call(LEGACY_INDEX_TARGET_ALIASES, normalized)) {
522
+ return LEGACY_INDEX_TARGET_ALIASES[normalized];
523
+ }
524
+ if (SUPPORTED_TARGETS.includes(normalized)) {
525
+ return normalized;
526
+ }
527
+ return null;
528
+ }
529
+
530
+ function normalizeWorkspaceRecordV2(item, normalizedPath) {
531
+ const targets = {};
532
+ if (item && item.targets && typeof item.targets === "object") {
533
+ for (const [targetName, state] of Object.entries(item.targets)) {
534
+ const normalizedTargetName = normalizeIndexTargetName(targetName);
535
+ if (!normalizedTargetName) {
536
+ continue;
537
+ }
538
+ const normalizedState = normalizeTargetState(state);
539
+ if (normalizedState) {
540
+ targets[normalizedTargetName] = normalizedState;
541
+ }
542
+ }
543
+ }
544
+ return {
545
+ path: normalizedPath,
546
+ targets,
547
+ };
548
+ }
549
+
550
+ function migrateRecordV1ToV2(item, normalizedPath) {
551
+ const targets = {};
552
+ const installedAt = typeof item.installedAt === "string" ? item.installedAt : "";
553
+ if (installedAt) {
554
+ targets.gemini = {
555
+ version: typeof item.cliVersion === "string" ? item.cliVersion : "",
556
+ installedAt,
557
+ updatedAt: typeof item.lastUpdatedAt === "string" ? item.lastUpdatedAt : installedAt,
558
+ };
559
+ }
560
+ return {
561
+ path: normalizedPath,
562
+ targets,
563
+ };
564
+ }
565
+
566
+ function readWorkspaceIndex() {
567
+ const indexPath = getWorkspaceIndexPath();
568
+ if (!fs.existsSync(indexPath)) {
569
+ return { indexPath, index: createEmptyWorkspaceIndex() };
570
+ }
571
+
572
+ const raw = fs.readFileSync(indexPath, "utf8").trim();
573
+ if (!raw) {
574
+ return { indexPath, index: createEmptyWorkspaceIndex() };
575
+ }
576
+
577
+ let parsed;
578
+ try {
579
+ parsed = JSON.parse(raw);
580
+ } catch (err) {
581
+ throw new Error(`工作区索引文件解析失败: ${indexPath}`);
582
+ }
583
+
584
+ const normalized = createEmptyWorkspaceIndex();
585
+ normalized.updatedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : "";
586
+
587
+ const records = Array.isArray(parsed.workspaces) ? parsed.workspaces : [];
588
+ const dedupMap = new Map();
589
+ const isV1 = !parsed.version || parsed.version === 1;
590
+
591
+ for (const item of records) {
592
+ if (!item || typeof item.path !== "string" || item.path.trim() === "") {
593
+ continue;
594
+ }
595
+
596
+ const workspacePath = normalizeAbsolutePath(item.path);
597
+ const key = pathCompareKey(workspacePath);
598
+ const record = isV1
599
+ ? migrateRecordV1ToV2(item, workspacePath)
600
+ : normalizeWorkspaceRecordV2(item, workspacePath);
601
+
602
+ dedupMap.set(key, record);
603
+ }
604
+
605
+ normalized.workspaces = Array.from(dedupMap.values()).sort((a, b) => a.path.localeCompare(b.path));
606
+ normalized.excludedPaths = normalizePathList(Array.isArray(parsed.excludedPaths) ? parsed.excludedPaths : []);
607
+ return { indexPath, index: normalized };
608
+ }
609
+
610
+ function writeWorkspaceIndex(indexPath, index) {
611
+ const workspaceMap = new Map();
612
+
613
+ for (const item of Array.isArray(index.workspaces) ? index.workspaces : []) {
614
+ if (!item || typeof item.path !== "string" || item.path.trim() === "") {
615
+ continue;
616
+ }
617
+
618
+ const normalizedPath = normalizeAbsolutePath(item.path);
619
+ const normalizedRecord = normalizeWorkspaceRecordV2(item, normalizedPath);
620
+ workspaceMap.set(pathCompareKey(normalizedPath), normalizedRecord);
621
+ }
622
+
623
+ const payload = {
624
+ version: WORKSPACE_INDEX_VERSION,
625
+ updatedAt: index.updatedAt || nowISO(),
626
+ workspaces: Array.from(workspaceMap.values()).sort((a, b) => a.path.localeCompare(b.path)),
627
+ excludedPaths: normalizePathList(Array.isArray(index.excludedPaths) ? index.excludedPaths : []),
628
+ };
629
+
630
+ fs.mkdirSync(path.dirname(indexPath), { recursive: true });
631
+ fs.writeFileSync(indexPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
632
+ }
633
+
634
+ function sleepSync(ms) {
635
+ const buffer = new SharedArrayBuffer(4);
636
+ const view = new Int32Array(buffer);
637
+ Atomics.wait(view, 0, 0, ms);
638
+ }
639
+
640
+ function acquireWorkspaceIndexLock(indexPath) {
641
+ const lockPath = `${indexPath}.lock`;
642
+ const startedAt = Date.now();
643
+
644
+ while (true) {
645
+ try {
646
+ fs.mkdirSync(path.dirname(indexPath), { recursive: true });
647
+ const fd = fs.openSync(lockPath, "wx");
648
+ fs.writeFileSync(fd, `${process.pid}\n${nowISO()}\n`, "utf8");
649
+ return { lockPath, fd };
650
+ } catch (err) {
651
+ if (err && err.code !== "EEXIST") {
652
+ throw new Error(`索引锁创建失败: ${lockPath}`);
653
+ }
654
+
655
+ let removedStale = false;
656
+ try {
657
+ const stat = fs.statSync(lockPath);
658
+ if (Date.now() - stat.mtimeMs > INDEX_LOCK_STALE_MS) {
659
+ fs.rmSync(lockPath, { force: true });
660
+ removedStale = true;
661
+ }
662
+ } catch (_statErr) {
663
+ removedStale = false;
664
+ }
665
+ if (removedStale) {
666
+ continue;
667
+ }
668
+
669
+ if (Date.now() - startedAt >= INDEX_LOCK_TIMEOUT_MS) {
670
+ throw new Error(`工作区索引正被其他进程占用: ${indexPath}`);
671
+ }
672
+ sleepSync(INDEX_LOCK_RETRY_MS);
673
+ }
674
+ }
675
+ }
676
+
677
+ function releaseWorkspaceIndexLock(lockHandle) {
678
+ if (!lockHandle) return;
679
+ try {
680
+ if (typeof lockHandle.fd === "number") {
681
+ fs.closeSync(lockHandle.fd);
682
+ }
683
+ } catch (_closeErr) {
684
+ // ignore
685
+ }
686
+ try {
687
+ fs.rmSync(lockHandle.lockPath, { force: true });
688
+ } catch (_rmErr) {
689
+ // ignore
690
+ }
691
+ }
692
+
693
+ function withWorkspaceIndexLock(indexPath, fn) {
694
+ const lockHandle = acquireWorkspaceIndexLock(indexPath);
695
+ try {
696
+ return fn();
697
+ } finally {
698
+ releaseWorkspaceIndexLock(lockHandle);
699
+ }
700
+ }
701
+
702
+ function evaluateWorkspaceExclusion(index, workspaceRoot) {
703
+ const normalizedPath = normalizeAbsolutePath(workspaceRoot);
704
+ const excludedPaths = Array.isArray(index.excludedPaths) ? index.excludedPaths : [];
705
+
706
+ if (isPathExcludedByList(excludedPaths, normalizedPath)) {
707
+ return {
708
+ excluded: true,
709
+ reason: "命中用户排除清单",
710
+ path: normalizedPath,
711
+ };
712
+ }
713
+
714
+ if (isToolkitSourceDirectory(normalizedPath)) {
715
+ return {
716
+ excluded: true,
717
+ reason: "检测为灵轨工具包源码目录(默认排除)",
718
+ path: normalizedPath,
719
+ };
720
+ }
721
+
722
+ if (isSystemTempDirectory(normalizedPath)) {
723
+ return {
724
+ excluded: true,
725
+ reason: "检测为系统临时目录(默认排除)",
726
+ path: normalizedPath,
727
+ };
728
+ }
729
+
730
+ return {
731
+ excluded: false,
732
+ reason: "",
733
+ path: normalizedPath,
734
+ };
735
+ }
736
+
737
+ function removeWorkspaceRecord(index, workspaceRoot) {
738
+ const normalizedPath = normalizeAbsolutePath(workspaceRoot);
739
+ const targetKey = pathCompareKey(normalizedPath);
740
+ const before = index.workspaces.length;
741
+ index.workspaces = index.workspaces.filter((item) => pathCompareKey(item.path) !== targetKey);
742
+ return before - index.workspaces.length;
743
+ }
744
+
745
+ function upsertWorkspaceTarget(index, workspaceRoot, targetName, timestamp) {
746
+ const normalizedPath = normalizeAbsolutePath(workspaceRoot);
747
+ const targetKey = pathCompareKey(normalizedPath);
748
+
749
+ let record = index.workspaces.find((item) => pathCompareKey(item.path) === targetKey);
750
+ if (!record) {
751
+ record = { path: normalizedPath, targets: {} };
752
+ index.workspaces.push(record);
753
+ }
754
+
755
+ if (!record.targets || typeof record.targets !== "object") {
756
+ record.targets = {};
757
+ }
758
+
759
+ const prev = normalizeTargetState(record.targets[targetName]) || {
760
+ version: "",
761
+ installedAt: "",
762
+ updatedAt: "",
763
+ };
764
+
765
+ record.targets[targetName] = {
766
+ version: pkg.version,
767
+ installedAt: prev.installedAt || timestamp,
768
+ updatedAt: timestamp,
769
+ };
770
+ }
771
+
772
+ function previewWorkspaceIndexRegistration(workspaceRoot, targetName, options) {
773
+ const { indexPath, index } = readWorkspaceIndex();
774
+ const exclusion = evaluateWorkspaceExclusion(index, workspaceRoot);
775
+ const normalizedPath = normalizeAbsolutePath(workspaceRoot);
776
+
777
+ if (exclusion.excluded) {
778
+ const exists = index.workspaces.some((item) => pathCompareKey(item.path) === pathCompareKey(normalizedPath));
779
+ log(options, `[dry-run] 索引登记已跳过: ${exclusion.reason}`);
780
+ if (exists) {
781
+ log(options, `[dry-run] 将从索引中移除已存在记录: ${normalizedPath}`);
782
+ }
783
+ return;
784
+ }
785
+
786
+ const exists = index.workspaces.some((item) => pathCompareKey(item.path) === pathCompareKey(normalizedPath));
787
+ if (exists) {
788
+ log(options, `[dry-run] 将刷新工作区索引记录: ${normalizedPath} [${targetName}]`);
789
+ } else {
790
+ log(options, `[dry-run] 将登记工作区到全局索引: ${normalizedPath} [${targetName}]`);
791
+ }
792
+ log(options, `[dry-run] 索引文件: ${indexPath}`);
793
+ }
794
+
795
+ function registerWorkspaceTarget(workspaceRoot, targetName, options) {
796
+ if (options.noIndex) {
797
+ if (!options.silentIndexLog) {
798
+ log(options, `[skip] 已跳过索引登记: ${normalizeAbsolutePath(workspaceRoot)}`);
799
+ log(options, " 原因: 启用了 --no-index");
800
+ }
801
+ return;
802
+ }
803
+
804
+ if (options.dryRun) {
805
+ const normalizedPath = normalizeAbsolutePath(workspaceRoot);
806
+ previewWorkspaceIndexRegistration(normalizedPath, targetName, options);
807
+ return;
808
+ }
809
+
810
+ const normalizedPath = normalizeAbsolutePath(workspaceRoot);
811
+ const indexPath = getWorkspaceIndexPath();
812
+ const timestamp = nowISO();
813
+ let removedCount = 0;
814
+ let exclusionReason = "";
815
+ let excluded = false;
816
+
817
+ withWorkspaceIndexLock(indexPath, () => {
818
+ const { index } = readWorkspaceIndex();
819
+ const exclusion = evaluateWorkspaceExclusion(index, normalizedPath);
820
+ excluded = exclusion.excluded;
821
+ exclusionReason = exclusion.reason;
822
+
823
+ if (exclusion.excluded) {
824
+ removedCount = removeWorkspaceRecord(index, normalizedPath);
825
+ if (removedCount > 0) {
826
+ index.updatedAt = timestamp;
827
+ writeWorkspaceIndex(indexPath, index);
828
+ }
829
+ return;
830
+ }
831
+
832
+ upsertWorkspaceTarget(index, normalizedPath, targetName, timestamp);
833
+ index.updatedAt = timestamp;
834
+ writeWorkspaceIndex(indexPath, index);
835
+ });
836
+
837
+ if (excluded) {
838
+ if (!options.silentIndexLog) {
839
+ log(options, `[skip] 已跳过索引登记: ${normalizedPath}`);
840
+ log(options, ` 原因: ${exclusionReason}`);
841
+ if (removedCount > 0) {
842
+ log(options, `[clean] 已清理旧索引记录: ${normalizedPath}`);
843
+ }
844
+ log(options, ` 索引文件: ${indexPath}`);
845
+ }
846
+ return;
847
+ }
848
+
849
+ if (!options.silentIndexLog) {
850
+ log(options, `[index] 已登记工作区索引: ${normalizedPath} [${targetName}]`);
851
+ log(options, ` 索引文件: ${indexPath}`);
852
+ }
853
+ }
854
+
855
+ function maybeWarnUpstreamGlobalConflict(command, options) {
856
+ if (options.quiet) {
857
+ return;
858
+ }
859
+ if (process.env.LING_SKIP_UPSTREAM_CHECK === "1" || process.env.AG_KIT_SKIP_UPSTREAM_CHECK === "1") {
860
+ return;
861
+ }
862
+ const shouldWarn =
863
+ command === "init"
864
+ || command === "update"
865
+ || command === "update-all"
866
+ || (command === "global" && String(options.subcommand || "").toLowerCase() === "sync")
867
+ || (command === "spec" && String(options.subcommand || "").toLowerCase() === "enable");
868
+ if (!shouldWarn) {
869
+ return;
870
+ }
871
+
872
+ const deps = readGlobalNpmDependencies();
873
+ if (!deps) {
874
+ return;
875
+ }
876
+
877
+ if (!Object.prototype.hasOwnProperty.call(deps, UPSTREAM_GLOBAL_PACKAGE)) {
878
+ return;
879
+ }
880
+
881
+ log(options, `[warn] 检测到全局已安装上游英文版 ${UPSTREAM_GLOBAL_PACKAGE}。`);
882
+ log(options, `[warn] 上游英文版与当前版本共用 \`${LEGACY_CLI_NAME}\` 兼容命令名,后安装者会覆盖该入口。`);
883
+ log(options, `[hint] 建议执行: npm uninstall -g ${UPSTREAM_GLOBAL_PACKAGE}`);
884
+ log(options, `[info] 正式命令已切换为 \`${PRIMARY_CLI_NAME}\`。若你通过 bun install -g 安装,Bun 默认会阻止本包 postinstall;因此这里会在首次执行 CLI 时再次提醒。`);
885
+ }
886
+
887
+ function maybeWarnLegacyCliAlias(options) {
888
+ if (options.quiet) {
889
+ return;
890
+ }
891
+ if (CURRENT_CLI_BASENAME !== LEGACY_CLI_NAME) {
892
+ return;
893
+ }
894
+
895
+ console.log(`[warn] \`${LEGACY_CLI_NAME}\` 已进入兼容模式,请改用 \`${PRIMARY_CLI_NAME}\`.`);
896
+ }
897
+
898
+ function normalizeTargets(rawTargets) {
899
+ const result = [];
900
+ const seen = new Set();
901
+
902
+ for (const raw of rawTargets || []) {
903
+ if (typeof raw !== "string") {
904
+ continue;
905
+ }
906
+ const parts = raw.split(",");
907
+ for (const part of parts) {
908
+ const target = part.trim().toLowerCase();
909
+ if (!target) {
910
+ continue;
911
+ }
912
+ if (!SUPPORTED_TARGETS.includes(target)) {
913
+ throw new Error(`不支持的目标: ${target}(可选: ${SUPPORTED_TARGETS.join(", ")})`);
914
+ }
915
+ if (!seen.has(target)) {
916
+ seen.add(target);
917
+ result.push(target);
918
+ }
919
+ }
920
+ }
921
+
922
+ return result;
923
+ }
924
+
925
+ function detectInstalledTargets(workspaceRoot) {
926
+ const targets = [];
927
+ if (fs.existsSync(path.join(workspaceRoot, ".agent"))) {
928
+ targets.push("gemini");
929
+ }
930
+ if (fs.existsSync(path.join(workspaceRoot, ".agents")) || fs.existsSync(path.join(workspaceRoot, ".codex"))) {
931
+ targets.push("codex");
932
+ }
933
+ return targets;
934
+ }
935
+
936
+ function isTargetInstalled(workspaceRoot, targetName) {
937
+ if (targetName === "gemini") {
938
+ return fs.existsSync(path.join(workspaceRoot, ".agent"));
939
+ }
940
+ if (targetName === "codex") {
941
+ return fs.existsSync(path.join(workspaceRoot, ".agents")) || fs.existsSync(path.join(workspaceRoot, ".codex"));
942
+ }
943
+ return false;
944
+ }
945
+
946
+ function setQuietStatusExitCode(state) {
947
+ process.exitCode = Object.prototype.hasOwnProperty.call(QUIET_STATUS_EXIT_CODES, state)
948
+ ? QUIET_STATUS_EXIT_CODES[state]
949
+ : 1;
950
+ }
951
+
952
+ function normalizeIntegrityState(result) {
953
+ if (!result || result.status === "missing") {
954
+ return "missing";
955
+ }
956
+ if (result.status === "ok") {
957
+ return "installed";
958
+ }
959
+ return "broken";
960
+ }
961
+
962
+ function evaluateWorkspaceState(workspaceRoot, options) {
963
+ const targets = detectInstalledTargets(workspaceRoot);
964
+ if (targets.length === 0) {
965
+ return {
966
+ state: "missing",
967
+ targets: [],
968
+ };
969
+ }
970
+
971
+ const targetStates = targets.map((targetName) => {
972
+ const adapter = createAdapter(targetName, workspaceRoot, {
973
+ ...options,
974
+ quiet: true,
975
+ });
976
+ const integrity = adapter.checkIntegrity();
977
+ return {
978
+ targetName,
979
+ state: normalizeIntegrityState(integrity),
980
+ integrity,
981
+ version: typeof adapter.getInstalledVersion === "function" ? adapter.getInstalledVersion() : null,
982
+ };
983
+ });
984
+
985
+ const hasIssue = targetStates.some((item) => item.state !== "installed");
986
+ return {
987
+ state: hasIssue ? "broken" : "installed",
988
+ targets: targetStates,
989
+ };
990
+ }
991
+
992
+ function evaluateGlobalState() {
993
+ const globalRoot = resolveGlobalRootDir();
994
+ const targetStates = listGlobalDestinations(globalRoot).map((destination) => {
995
+ const rootExists = fs.existsSync(destination.rootDir);
996
+ const skillsExists = fs.existsSync(destination.skillsRoot);
997
+ const skillsCount = skillsExists ? countSkillsRecursive(destination.skillsRoot) : 0;
998
+ let state = "missing";
999
+ const issues = [];
1000
+
1001
+ if (rootExists || skillsExists) {
1002
+ if (!skillsExists) {
1003
+ state = "broken";
1004
+ issues.push("Skills 根目录缺失");
1005
+ } else if (skillsCount === 0) {
1006
+ state = "broken";
1007
+ issues.push("未检测到任何 SKILL.md");
1008
+ } else {
1009
+ state = "installed";
1010
+ }
1011
+ }
1012
+
1013
+ return {
1014
+ targetName: destination.id,
1015
+ family: destination.targetName,
1016
+ state,
1017
+ rootDir: destination.rootDir,
1018
+ skillsRoot: destination.skillsRoot,
1019
+ skillsCount,
1020
+ issues,
1021
+ };
1022
+ }).filter((item) => item.state !== "missing");
1023
+
1024
+ if (targetStates.length === 0) {
1025
+ return {
1026
+ globalRoot,
1027
+ state: "missing",
1028
+ targets: [],
1029
+ };
1030
+ }
1031
+
1032
+ const hasIssue = targetStates.some((item) => item.state !== "installed");
1033
+ return {
1034
+ globalRoot,
1035
+ state: hasIssue ? "broken" : "installed",
1036
+ targets: targetStates,
1037
+ };
1038
+ }
1039
+
1040
+ function createAdapter(targetName, workspaceRoot, options) {
1041
+ if (targetName === "gemini") {
1042
+ return new GeminiAdapter(workspaceRoot, options);
1043
+ }
1044
+ if (targetName === "codex") {
1045
+ return new CodexAdapter(workspaceRoot, options);
1046
+ }
1047
+ throw new Error(`未知目标: ${targetName}`);
1048
+ }
1049
+
1050
+ async function resolveTargetsForInit(options) {
1051
+ let targets = normalizeTargets(options.targets);
1052
+
1053
+ if (targets.length > 0) {
1054
+ return targets;
1055
+ }
1056
+
1057
+ if (options.nonInteractive) {
1058
+ throw new Error("非交互模式下必须通过 --target 或 --targets 指定目标");
1059
+ }
1060
+
1061
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1062
+ throw new Error("当前环境不是交互终端,请通过 --target 或 --targets 指定目标");
1063
+ }
1064
+
1065
+ targets = normalizeTargets(await selectTargets(options));
1066
+ if (targets.length === 0) {
1067
+ throw new Error("必须选择至少一个目标");
1068
+ }
1069
+
1070
+ return targets;
1071
+ }
1072
+
1073
+ function resolveTargetsForUpdate(workspaceRoot, options) {
1074
+ const requested = normalizeTargets(options.targets);
1075
+ if (requested.length > 0) {
1076
+ return requested;
1077
+ }
1078
+ return detectInstalledTargets(workspaceRoot);
1079
+ }
1080
+
1081
+ function resolveTargetsForGlobalSync(options) {
1082
+ const requested = normalizeTargets(options.targets);
1083
+ if (requested.length > 0) {
1084
+ return requested;
1085
+ }
1086
+ // 保持 global sync 简洁:默认同步 codex + gemini;其中 gemini 会展开为 gemini-cli 与 antigravity。
1087
+ return ["codex", "gemini"];
1088
+ }
1089
+
1090
+ function resolveAgentInstallSource(options) {
1091
+ let agentDir = BUNDLED_AGENT_DIR;
1092
+ let cleanup = null;
1093
+ let sourceLabel = "bundled";
1094
+
1095
+ if (options.branch) {
1096
+ const remote = cloneBranchAgentDir(options.branch, {
1097
+ quiet: options.quiet,
1098
+ logger: log.bind(null, options),
1099
+ });
1100
+ agentDir = remote.agentDir;
1101
+ cleanup = remote.cleanup;
1102
+ sourceLabel = `branch:${options.branch}`;
1103
+ }
1104
+
1105
+ if (!fs.existsSync(agentDir) && !options.branch) {
1106
+ const legacyDir = path.resolve(__dirname, "../.agent");
1107
+ if (fs.existsSync(legacyDir)) {
1108
+ agentDir = legacyDir;
1109
+ sourceLabel = "bundled:legacy";
1110
+ }
1111
+ }
1112
+
1113
+ if (!fs.existsSync(agentDir)) {
1114
+ throw new Error(`未找到模板目录: ${agentDir}`);
1115
+ }
1116
+
1117
+ return { agentDir, cleanup, sourceLabel };
1118
+ }
1119
+
1120
+ function listSkillDirectories(skillsRoot) {
1121
+ if (!fs.existsSync(skillsRoot)) {
1122
+ return [];
1123
+ }
1124
+ return fs
1125
+ .readdirSync(skillsRoot, { withFileTypes: true })
1126
+ .filter((entry) => entry.isDirectory())
1127
+ .map((entry) => entry.name)
1128
+ .filter((name) => fs.existsSync(path.join(skillsRoot, name, "SKILL.md")));
1129
+ }
1130
+
1131
+ function backupSkillDirectory(targetName, skillName, sourceDir, timestamp, options) {
1132
+ const backupRoot = resolveGlobalBackupRoot(timestamp);
1133
+ const backupDir = path.join(backupRoot, targetName, skillName);
1134
+ fs.mkdirSync(path.dirname(backupDir), { recursive: true });
1135
+ copyDirRecursive(sourceDir, backupDir);
1136
+ log(options, `[backup] 已备份 ${targetName} 全局 Skill: ${skillName} -> ${backupDir}`);
1137
+ }
1138
+
1139
+ function syncSkillDirectory(destination, srcDir, destDir, timestamp, options) {
1140
+ const exists = fs.existsSync(destDir);
1141
+ if (exists) {
1142
+ if (areDirectoriesEqual(srcDir, destDir)) {
1143
+ log(options, `[skip] 全局 Skill 已最新,无需同步: ${destination.id}/${path.basename(destDir)}`);
1144
+ return { skipped: 1, synced: 0, backedUp: 0 };
1145
+ }
1146
+ }
1147
+
1148
+ if (options.dryRun) {
1149
+ log(options, `[dry-run] 将同步全局 Skill: ${destination.id}/${path.basename(destDir)}`);
1150
+ return { skipped: 0, synced: 0, backedUp: exists ? 1 : 0 };
1151
+ }
1152
+
1153
+ let backedUp = 0;
1154
+ if (exists) {
1155
+ backupSkillDirectory(destination.id, path.basename(destDir), destDir, timestamp, options);
1156
+ backedUp = 1;
1157
+ }
1158
+
1159
+ const logger = options.quiet ? (() => {}) : log.bind(null, options);
1160
+ AtomicWriter.atomicCopyDir(srcDir, destDir, { logger });
1161
+ log(options, `[ok] 已同步全局 Skill: ${destination.id}/${path.basename(destDir)}`);
1162
+
1163
+ return { skipped: 0, synced: 1, backedUp };
1164
+ }
1165
+
1166
+ function syncGlobalSkillsFromRoot(targetName, skillsRoot, timestamp, options) {
1167
+ const destinations = getGlobalDestinations(targetName);
1168
+ const skillNames = listSkillDirectories(skillsRoot);
1169
+ if (skillNames.length === 0) {
1170
+ throw new Error(`未检测到可同步的 Skills: ${skillsRoot}`);
1171
+ }
1172
+
1173
+ if (options.dryRun) {
1174
+ for (const destination of destinations) {
1175
+ log(options, `[dry-run] 将同步 ${skillNames.length} 个全局 Skills -> ${destination.skillsRoot}`);
1176
+ }
1177
+ }
1178
+
1179
+ let synced = 0;
1180
+ let skipped = 0;
1181
+ let backedUp = 0;
1182
+ const destinationResults = [];
1183
+
1184
+ for (const destination of destinations) {
1185
+ let destinationSynced = 0;
1186
+ let destinationSkipped = 0;
1187
+ let destinationBackedUp = 0;
1188
+
1189
+ for (const skillName of skillNames) {
1190
+ const srcDir = path.join(skillsRoot, skillName);
1191
+ const destDir = path.join(destination.skillsRoot, skillName);
1192
+ const result = syncSkillDirectory(destination, srcDir, destDir, timestamp, options);
1193
+ synced += result.synced;
1194
+ skipped += result.skipped;
1195
+ backedUp += result.backedUp;
1196
+ destinationSynced += result.synced;
1197
+ destinationSkipped += result.skipped;
1198
+ destinationBackedUp += result.backedUp;
1199
+ }
1200
+
1201
+ destinationResults.push({
1202
+ targetName: destination.id,
1203
+ family: destination.targetName,
1204
+ destRoot: destination.skillsRoot,
1205
+ total: skillNames.length,
1206
+ synced: destinationSynced,
1207
+ skipped: destinationSkipped,
1208
+ backedUp: destinationBackedUp,
1209
+ });
1210
+ }
1211
+
1212
+ return {
1213
+ total: skillNames.length * destinations.length,
1214
+ skillsPerDestination: skillNames.length,
1215
+ synced,
1216
+ skipped,
1217
+ backedUp,
1218
+ destinations: destinationResults,
1219
+ };
1220
+ }
1221
+
1222
+ function applyGlobalSync(targetName, agentDir, timestamp, options) {
1223
+ if (targetName === "codex") {
1224
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ag-kit-global-codex-"));
1225
+ const mockRoot = path.join(tempRoot, "source");
1226
+ const mockAgent = path.join(mockRoot, ".agents");
1227
+ const outputDir = path.join(tempRoot, "out");
1228
+
1229
+ try {
1230
+ copyDirRecursive(agentDir, mockAgent);
1231
+ CodexBuilder.build(mockRoot, outputDir);
1232
+ const skillsRoot = path.join(outputDir, "skills");
1233
+ return syncGlobalSkillsFromRoot(targetName, skillsRoot, timestamp, options);
1234
+ } finally {
1235
+ fs.rmSync(tempRoot, { recursive: true, force: true });
1236
+ }
1237
+ }
1238
+
1239
+ if (targetName === "gemini") {
1240
+ const skillsRoot = path.join(agentDir, "skills");
1241
+ return syncGlobalSkillsFromRoot(targetName, skillsRoot, timestamp, options);
1242
+ }
1243
+
1244
+ throw new Error(`未知目标: ${targetName}`);
1245
+ }
1246
+
1247
+ async function commandGlobalSync(options) {
1248
+ const targets = await resolveTargetsForGlobalSync(options);
1249
+ const { agentDir, cleanup, sourceLabel } = resolveAgentInstallSource(options);
1250
+ const timestamp = nowISO().replace(/[:.]/g, "-");
1251
+
1252
+ try {
1253
+ log(options, `[global] 全局同步源: ${sourceLabel}`);
1254
+ for (const target of targets) {
1255
+ log(options, `[sync] 正在同步全局目标 [${target}] ...`);
1256
+ const result = applyGlobalSync(target, agentDir, timestamp, options);
1257
+ if (!options.dryRun) {
1258
+ log(options, `[summary] 全局同步完成 [${target}]:总计 ${result.total},新增/覆盖 ${result.synced},跳过 ${result.skipped},备份 ${result.backedUp}`);
1259
+ for (const item of result.destinations) {
1260
+ log(options, ` - ${item.targetName}: ${item.destRoot}(每目标 ${item.total} 个 Skills)`);
1261
+ }
1262
+ }
1263
+ }
1264
+ } finally {
1265
+ if (cleanup) cleanup();
1266
+ }
1267
+ }
1268
+
1269
+ function commandGlobalStatus(options) {
1270
+ const summary = evaluateGlobalState();
1271
+
1272
+ if (summary.state === "missing") {
1273
+ if (options.quiet) {
1274
+ console.log("missing");
1275
+ }
1276
+ if (!options.quiet) {
1277
+ console.log("[error] 未检测到全局安装的 Skills");
1278
+ console.log(` 全局根目录: ${summary.globalRoot}`);
1279
+ }
1280
+ setQuietStatusExitCode("missing");
1281
+ return;
1282
+ }
1283
+
1284
+ if (options.quiet) {
1285
+ console.log(summary.state);
1286
+ setQuietStatusExitCode(summary.state);
1287
+ return;
1288
+ }
1289
+
1290
+ console.log(summary.state === "installed" ? "[ok] 全局 Skills 状态正常" : "[warn] 全局 Skills 存在问题");
1291
+ console.log(` 全局根目录: ${summary.globalRoot}`);
1292
+ console.log(` 总体状态: ${summary.state}`);
1293
+ console.log(` Targets: ${summary.targets.map((item) => item.targetName).join(", ")}`);
1294
+
1295
+ for (const item of summary.targets) {
1296
+ console.log(`\n[${item.targetName}:global]`);
1297
+ console.log(` 家族: ${item.family}`);
1298
+ console.log(` 状态: ${item.state}`);
1299
+ console.log(` 路径: ${item.skillsRoot}`);
1300
+ if (item.state === "installed") {
1301
+ console.log(` Skills: ${item.skillsCount}`);
1302
+ continue;
1303
+ }
1304
+ for (const issue of item.issues) {
1305
+ console.log(` Issue: ${issue}`);
1306
+ }
1307
+ }
1308
+
1309
+ setQuietStatusExitCode(summary.state);
1310
+ }
1311
+
1312
+ function commandGlobal(options) {
1313
+ const subcommand = String(options.subcommand || "status").toLowerCase();
1314
+ if (subcommand === "sync") {
1315
+ return commandGlobalSync(options);
1316
+ }
1317
+ if (subcommand === "status") {
1318
+ return commandGlobalStatus(options);
1319
+ }
1320
+ throw new Error(`未知 global 子命令: ${subcommand}`);
1321
+ }
1322
+
1323
+ function getSpecHomeDir() {
1324
+ return path.join(resolveGlobalRootDir(), ".ling", "spec");
1325
+ }
1326
+
1327
+ function getSpecStatePath() {
1328
+ return path.join(getSpecHomeDir(), "state.json");
1329
+ }
1330
+
1331
+ function resolveSpecBackupRoot(timestamp) {
1332
+ return path.join(resolveGlobalRootDir(), ".ling", "backups", "spec", timestamp, "before");
1333
+ }
1334
+
1335
+ function createEmptySpecState() {
1336
+ return {
1337
+ version: SPEC_STATE_VERSION,
1338
+ updatedAt: "",
1339
+ targets: {},
1340
+ assets: {},
1341
+ };
1342
+ }
1343
+
1344
+ function readSpecState() {
1345
+ const statePath = getSpecStatePath();
1346
+ if (!fs.existsSync(statePath)) {
1347
+ return { statePath, state: createEmptySpecState() };
1348
+ }
1349
+
1350
+ const raw = fs.readFileSync(statePath, "utf8").trim();
1351
+ if (!raw) {
1352
+ return { statePath, state: createEmptySpecState() };
1353
+ }
1354
+
1355
+ let parsed;
1356
+ try {
1357
+ parsed = JSON.parse(raw);
1358
+ } catch (err) {
1359
+ throw new Error(`Spec 状态文件解析失败: ${statePath}`);
1360
+ }
1361
+
1362
+ const state = createEmptySpecState();
1363
+ state.updatedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : "";
1364
+ state.targets = parsed && typeof parsed.targets === "object" && parsed.targets ? parsed.targets : {};
1365
+ state.assets = parsed && typeof parsed.assets === "object" && parsed.assets ? parsed.assets : {};
1366
+ return { statePath, state };
1367
+ }
1368
+
1369
+ function writeSpecState(statePath, state) {
1370
+ const payload = {
1371
+ version: SPEC_STATE_VERSION,
1372
+ updatedAt: state.updatedAt || nowISO(),
1373
+ targets: state.targets || {},
1374
+ assets: state.assets || {},
1375
+ };
1376
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
1377
+ fs.writeFileSync(statePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
1378
+ }
1379
+
1380
+ function removeSpecStateFile() {
1381
+ const statePath = getSpecStatePath();
1382
+ if (fs.existsSync(statePath)) {
1383
+ fs.rmSync(statePath, { force: true });
1384
+ }
1385
+ }
1386
+
1387
+ function resolveTargetsForSpec(options) {
1388
+ if (options.targets.length === 0) {
1389
+ return [...SUPPORTED_TARGETS];
1390
+ }
1391
+ return normalizeTargets(options.targets);
1392
+ }
1393
+
1394
+ function ensureBundledSpecResources() {
1395
+ for (const skillName of SPEC_SKILL_NAMES) {
1396
+ const skillDir = path.join(BUNDLED_SPEC_DIR, "skills", skillName);
1397
+ const skillFile = path.join(skillDir, "SKILL.md");
1398
+ if (!fs.existsSync(skillFile)) {
1399
+ throw new Error(`缺少 Spec Skill 资源: ${skillFile}`);
1400
+ }
1401
+ }
1402
+ }
1403
+
1404
+ function backupDirSnapshot(sourceDir, backupDir, options, label) {
1405
+ if (!fs.existsSync(sourceDir)) {
1406
+ return "";
1407
+ }
1408
+ if (options.dryRun) {
1409
+ log(options, `[dry-run] 将备份 ${label}: ${sourceDir} -> ${backupDir}`);
1410
+ return backupDir;
1411
+ }
1412
+ fs.mkdirSync(path.dirname(backupDir), { recursive: true });
1413
+ copyDirRecursive(sourceDir, backupDir);
1414
+ log(options, `[backup] 已备份 ${label}: ${sourceDir} -> ${backupDir}`);
1415
+ return backupDir;
1416
+ }
1417
+
1418
+ function applyDirSnapshot(srcDir, destDir, options, label) {
1419
+ if (options.dryRun) {
1420
+ log(options, `[dry-run] 将写入 ${label}: ${srcDir} -> ${destDir}`);
1421
+ return;
1422
+ }
1423
+ const logger = options.quiet ? (() => {}) : log.bind(null, options);
1424
+ AtomicWriter.atomicCopyDir(srcDir, destDir, { logger });
1425
+ log(options, `[ok] 已写入 ${label}: ${destDir}`);
1426
+ }
1427
+
1428
+ function removeDirIfExists(targetDir, options, label) {
1429
+ if (!fs.existsSync(targetDir)) {
1430
+ return;
1431
+ }
1432
+ if (options.dryRun) {
1433
+ log(options, `[dry-run] 将删除 ${label}: ${targetDir}`);
1434
+ return;
1435
+ }
1436
+ fs.rmSync(targetDir, { recursive: true, force: true });
1437
+ log(options, `[clean] 已删除 ${label}: ${targetDir}`);
1438
+ }
1439
+
1440
+ function ensureSpecAssetsInstalled(state, timestamp, options) {
1441
+ const specHome = getSpecHomeDir();
1442
+ const assets = {
1443
+ templates: {
1444
+ sourceDir: path.join(BUNDLED_SPEC_DIR, "templates"),
1445
+ destDir: path.join(specHome, "templates"),
1446
+ },
1447
+ references: {
1448
+ sourceDir: path.join(BUNDLED_SPEC_DIR, "references"),
1449
+ destDir: path.join(specHome, "references"),
1450
+ },
1451
+ };
1452
+
1453
+ for (const [assetName, config] of Object.entries(assets)) {
1454
+ if (state.assets[assetName] && state.assets[assetName].installedAt) {
1455
+ continue;
1456
+ }
1457
+
1458
+ const backupPath = backupDirSnapshot(
1459
+ config.destDir,
1460
+ path.join(resolveSpecBackupRoot(timestamp), "assets", assetName),
1461
+ options,
1462
+ `Spec ${assetName}`,
1463
+ );
1464
+ applyDirSnapshot(config.sourceDir, config.destDir, options, `Spec ${assetName}`);
1465
+ state.assets[assetName] = {
1466
+ destPath: config.destDir,
1467
+ backupPath,
1468
+ installedAt: nowISO(),
1469
+ };
1470
+ }
1471
+ }
1472
+
1473
+ function restoreSpecAsset(assetState, options, label) {
1474
+ if (!assetState || typeof assetState.destPath !== "string") {
1475
+ return;
1476
+ }
1477
+ if (assetState.backupPath && fs.existsSync(assetState.backupPath)) {
1478
+ applyDirSnapshot(assetState.backupPath, assetState.destPath, options, label);
1479
+ return;
1480
+ }
1481
+ removeDirIfExists(assetState.destPath, options, label);
1482
+ }
1483
+
1484
+ function enableSpecTarget(targetName, state, timestamp, options) {
1485
+ if (state.targets[targetName]) {
1486
+ log(options, `[skip] Spec 目标已启用: ${targetName}`);
1487
+ return;
1488
+ }
1489
+
1490
+ const destinations = getGlobalDestinations(targetName);
1491
+ const targetState = {
1492
+ enabledAt: nowISO(),
1493
+ consumers: {},
1494
+ };
1495
+
1496
+ for (const destination of destinations) {
1497
+ const consumerState = {
1498
+ skills: [],
1499
+ };
1500
+
1501
+ for (const skillName of SPEC_SKILL_NAMES) {
1502
+ const srcDir = path.join(BUNDLED_SPEC_DIR, "skills", skillName);
1503
+ const destDir = path.join(destination.skillsRoot, skillName);
1504
+ const backupPath = backupDirSnapshot(
1505
+ destDir,
1506
+ path.join(resolveSpecBackupRoot(timestamp), destination.id, "skills", skillName),
1507
+ options,
1508
+ `Spec Skill ${destination.id}/${skillName}`,
1509
+ );
1510
+ applyDirSnapshot(srcDir, destDir, options, `Spec Skill ${destination.id}/${skillName}`);
1511
+ consumerState.skills.push({
1512
+ name: skillName,
1513
+ destPath: destDir,
1514
+ backupPath,
1515
+ });
1516
+ }
1517
+
1518
+ targetState.consumers[destination.id] = consumerState;
1519
+ }
1520
+
1521
+ state.targets[targetName] = targetState;
1522
+ }
1523
+
1524
+ function disableSpecTarget(targetName, state, options) {
1525
+ const targetState = state.targets[targetName];
1526
+ if (!targetState) {
1527
+ log(options, `[skip] Spec 目标未启用: ${targetName}`);
1528
+ return;
1529
+ }
1530
+
1531
+ for (const [consumerId, consumerState] of Object.entries(targetState.consumers || {})) {
1532
+ for (const skill of consumerState.skills || []) {
1533
+ if (skill.backupPath && fs.existsSync(skill.backupPath)) {
1534
+ applyDirSnapshot(skill.backupPath, skill.destPath, options, `恢复 Spec Skill ${consumerId}/${skill.name}`);
1535
+ } else {
1536
+ removeDirIfExists(skill.destPath, options, `Spec Skill ${consumerId}/${skill.name}`);
1537
+ }
1538
+ }
1539
+ }
1540
+
1541
+ delete state.targets[targetName];
1542
+ }
1543
+
1544
+ function evaluateSpecState() {
1545
+ const { state } = readSpecState();
1546
+ const targetNames = Object.keys(state.targets || {});
1547
+ if (targetNames.length === 0) {
1548
+ return {
1549
+ state: "missing",
1550
+ targets: [],
1551
+ assets: state.assets || {},
1552
+ specHome: getSpecHomeDir(),
1553
+ };
1554
+ }
1555
+
1556
+ const issues = [];
1557
+ for (const targetName of targetNames) {
1558
+ const targetState = state.targets[targetName];
1559
+ for (const consumerState of Object.values(targetState.consumers || {})) {
1560
+ for (const skill of consumerState.skills || []) {
1561
+ if (!fs.existsSync(path.join(skill.destPath, "SKILL.md"))) {
1562
+ issues.push(`Missing spec skill: ${skill.destPath}`);
1563
+ }
1564
+ }
1565
+ }
1566
+ }
1567
+
1568
+ for (const assetName of ["templates", "references"]) {
1569
+ const asset = state.assets[assetName];
1570
+ if (!asset || !asset.destPath || !fs.existsSync(asset.destPath)) {
1571
+ issues.push(`Missing spec asset: ${assetName}`);
1572
+ }
1573
+ }
1574
+
1575
+ return {
1576
+ state: issues.length > 0 ? "broken" : "installed",
1577
+ targets: targetNames,
1578
+ issues,
1579
+ assets: state.assets || {},
1580
+ specHome: getSpecHomeDir(),
1581
+ };
1582
+ }
1583
+
1584
+ function commandSpecStatus(options) {
1585
+ const summary = evaluateSpecState();
1586
+ if (options.quiet) {
1587
+ console.log(summary.state);
1588
+ setQuietStatusExitCode(summary.state);
1589
+ return;
1590
+ }
1591
+
1592
+ if (summary.state === "missing") {
1593
+ console.log("[warn] Spec Profile 未启用");
1594
+ console.log(` Spec 目录: ${summary.specHome}`);
1595
+ setQuietStatusExitCode("missing");
1596
+ return;
1597
+ }
1598
+
1599
+ console.log(summary.state === "installed" ? "[ok] Spec Profile 状态正常" : "[warn] Spec Profile 存在问题");
1600
+ console.log(` Spec 目录: ${summary.specHome}`);
1601
+ console.log(` Targets: ${summary.targets.join(", ")}`);
1602
+ for (const assetName of Object.keys(summary.assets || {})) {
1603
+ console.log(` Asset: ${assetName} -> ${summary.assets[assetName].destPath}`);
1604
+ }
1605
+ for (const issue of summary.issues || []) {
1606
+ console.log(` Issue: ${issue}`);
1607
+ }
1608
+ setQuietStatusExitCode(summary.state);
1609
+ }
1610
+
1611
+ function commandSpecEnable(options) {
1612
+ ensureBundledSpecResources();
1613
+ const targets = resolveTargetsForSpec(options);
1614
+ const { statePath, state } = readSpecState();
1615
+ const timestamp = nowISO().replace(/[:.]/g, "-");
1616
+
1617
+ ensureSpecAssetsInstalled(state, timestamp, options);
1618
+ for (const targetName of targets) {
1619
+ enableSpecTarget(targetName, state, timestamp, options);
1620
+ }
1621
+
1622
+ state.updatedAt = nowISO();
1623
+ if (!options.dryRun) {
1624
+ writeSpecState(statePath, state);
1625
+ }
1626
+
1627
+ log(options, `[ok] Spec Profile 已启用 (Targets: ${targets.join(", ")})`);
1628
+ }
1629
+
1630
+ function commandSpecDisable(options) {
1631
+ const targets = resolveTargetsForSpec(options);
1632
+ const { statePath, state } = readSpecState();
1633
+
1634
+ for (const targetName of targets) {
1635
+ disableSpecTarget(targetName, state, options);
1636
+ }
1637
+
1638
+ const remainingTargets = Object.keys(state.targets || {});
1639
+ if (remainingTargets.length === 0) {
1640
+ restoreSpecAsset(state.assets.templates, options, "Spec templates");
1641
+ restoreSpecAsset(state.assets.references, options, "Spec references");
1642
+ state.assets = {};
1643
+ }
1644
+
1645
+ state.updatedAt = nowISO();
1646
+ if (!options.dryRun) {
1647
+ if (remainingTargets.length === 0) {
1648
+ removeSpecStateFile();
1649
+ } else {
1650
+ writeSpecState(statePath, state);
1651
+ }
1652
+ }
1653
+
1654
+ log(options, `[ok] Spec Profile 已停用 (Targets: ${targets.join(", ")})`);
1655
+ }
1656
+
1657
+ function commandSpec(options) {
1658
+ const subcommand = String(options.subcommand || "status").toLowerCase();
1659
+ if (subcommand === "status") {
1660
+ return commandSpecStatus(options);
1661
+ }
1662
+ if (subcommand === "enable") {
1663
+ return commandSpecEnable(options);
1664
+ }
1665
+ if (subcommand === "disable") {
1666
+ return commandSpecDisable(options);
1667
+ }
1668
+ throw new Error(`未知 spec 子命令: ${subcommand}`);
1669
+ }
1670
+
1671
+ async function commandInit(options) {
1672
+ const workspaceRoot = resolveWorkspaceRoot(options.path);
1673
+ const targets = await resolveTargetsForInit(options);
1674
+
1675
+ for (const target of targets) {
1676
+ const adapter = createAdapter(target, workspaceRoot, options);
1677
+ log(options, `[sync] 正在初始化目标 [${target}] ...`);
1678
+ adapter.install(BUNDLED_AGENT_DIR);
1679
+ registerWorkspaceTarget(workspaceRoot, target, options);
1680
+ }
1681
+
1682
+ if (targets.length > 0) {
1683
+ log(options, `[ok] 初始化完成 (Targets: ${targets.join(", ")})`);
1684
+ }
1685
+ }
1686
+
1687
+ async function commandUpdate(options) {
1688
+ const workspaceRoot = resolveWorkspaceRoot(options.path);
1689
+ const targets = resolveTargetsForUpdate(workspaceRoot, options);
1690
+
1691
+ if (targets.length === 0) {
1692
+ throw new Error(`此目录未检测到 ${PRIMARY_CLI_NAME} 安装,无法更新。请先执行 init。`);
1693
+ }
1694
+
1695
+ log(options, `[update] 正在更新 Ling (Targets: ${targets.join(", ")})...`);
1696
+
1697
+ let updatedAny = false;
1698
+ for (const target of targets) {
1699
+ if (!isTargetInstalled(workspaceRoot, target) && options.targets.length > 0) {
1700
+ throw new Error(`目标未安装: ${target}`);
1701
+ }
1702
+ if (!isTargetInstalled(workspaceRoot, target)) {
1703
+ log(options, `[skip] 目标未安装,跳过: ${target}`);
1704
+ continue;
1705
+ }
1706
+
1707
+ const runOptions = { ...options, force: true };
1708
+ const adapter = createAdapter(target, workspaceRoot, runOptions);
1709
+ log(options, `[sync] 更新 [${target}] ...`);
1710
+ adapter.update(BUNDLED_AGENT_DIR);
1711
+ registerWorkspaceTarget(workspaceRoot, target, runOptions);
1712
+ updatedAny = true;
1713
+ }
1714
+
1715
+ if (!updatedAny) {
1716
+ throw new Error("未找到可更新的目标");
1717
+ }
1718
+ }
1719
+
1720
+ function mergeUpdatedTargets(record, workspacePath, targetNames, timestamp) {
1721
+ const normalizedPath = normalizeAbsolutePath(workspacePath);
1722
+ const next = normalizeWorkspaceRecordV2(record || {}, normalizedPath);
1723
+
1724
+ for (const target of targetNames) {
1725
+ const prev = normalizeTargetState(next.targets[target]) || {
1726
+ version: "",
1727
+ installedAt: "",
1728
+ updatedAt: "",
1729
+ };
1730
+ next.targets[target] = {
1731
+ version: pkg.version,
1732
+ installedAt: prev.installedAt || timestamp,
1733
+ updatedAt: timestamp,
1734
+ };
1735
+ }
1736
+
1737
+ return next;
1738
+ }
1739
+
1740
+ async function commandUpdateAll(options) {
1741
+ if (options.path) {
1742
+ throw new Error(`update-all 不支持 --path,请直接执行 ${PRIMARY_CLI_NAME} update-all`);
1743
+ }
1744
+
1745
+ const requestedTargets = normalizeTargets(options.targets);
1746
+ const { indexPath, index } = readWorkspaceIndex();
1747
+ const records = index.workspaces || [];
1748
+
1749
+ if (records.length === 0) {
1750
+ log(options, "[info] 全局索引为空,没有可批量更新的工作区。");
1751
+ log(options, ` 先在项目中执行 ${PRIMARY_CLI_NAME} init 或 ${PRIMARY_CLI_NAME} update 建立索引。`);
1752
+ return;
1753
+ }
1754
+
1755
+ log(options, `[update] 开始批量更新工作区(共 ${records.length} 个)...`);
1756
+ log(options, `[index] 索引文件: ${indexPath}`);
1757
+
1758
+ let updated = 0;
1759
+ let skipped = 0;
1760
+ let failed = 0;
1761
+ let removedMissing = 0;
1762
+ let removedExcluded = 0;
1763
+ const timestamp = nowISO();
1764
+ const nextRecords = [];
1765
+ const removedRecordKeys = new Set();
1766
+
1767
+ for (let i = 0; i < records.length; i++) {
1768
+ const item = normalizeWorkspaceRecordV2(records[i], normalizeAbsolutePath(records[i].path));
1769
+ const workspacePath = normalizeAbsolutePath(item.path);
1770
+ const exclusion = evaluateWorkspaceExclusion(index, workspacePath);
1771
+
1772
+ if (exclusion.excluded) {
1773
+ removedExcluded += 1;
1774
+ removedRecordKeys.add(pathCompareKey(workspacePath));
1775
+ if (options.dryRun) {
1776
+ log(options, `[dry-run] [${i + 1}/${records.length}] 将从批量索引移除排除路径: ${workspacePath}(${exclusion.reason})`);
1777
+ } else {
1778
+ log(options, `[clean] [${i + 1}/${records.length}] 已从批量索引中移除排除路径: ${workspacePath}(${exclusion.reason})`);
1779
+ }
1780
+ continue;
1781
+ }
1782
+
1783
+ if (!fs.existsSync(workspacePath)) {
1784
+ if (options.pruneMissing) {
1785
+ removedMissing += 1;
1786
+ removedRecordKeys.add(pathCompareKey(workspacePath));
1787
+ log(options, `[clean] [${i + 1}/${records.length}] 已移除失效工作区索引: ${workspacePath}`);
1788
+ } else {
1789
+ skipped += 1;
1790
+ log(options, `[skip] [${i + 1}/${records.length}] 工作区不存在,已跳过: ${workspacePath}`);
1791
+ nextRecords.push(item);
1792
+ }
1793
+ continue;
1794
+ }
1795
+
1796
+ const installedTargets = detectInstalledTargets(workspacePath);
1797
+ let targets = [];
1798
+ if (requestedTargets.length > 0) {
1799
+ targets = installedTargets.filter((target) => requestedTargets.includes(target));
1800
+ } else {
1801
+ targets = [...Object.keys(item.targets || {}), ...installedTargets];
1802
+ }
1803
+ targets = normalizeTargets(targets);
1804
+
1805
+ if (targets.length === 0) {
1806
+ skipped += 1;
1807
+ log(options, `[skip] [${i + 1}/${records.length}] 未检测到可更新目标,已跳过: ${workspacePath}`);
1808
+ nextRecords.push(item);
1809
+ continue;
1810
+ }
1811
+
1812
+ log(options, `[sync] [${i + 1}/${records.length}] 更新: ${workspacePath} [${targets.join(", ")}]`);
1813
+
1814
+ const updatedTargets = [];
1815
+ for (const target of targets) {
1816
+ if (!isTargetInstalled(workspacePath, target)) {
1817
+ log(options, `[skip] [${i + 1}/${records.length}] 目标未安装,跳过: ${target}`);
1818
+ continue;
1819
+ }
1820
+
1821
+ try {
1822
+ const runOptions = {
1823
+ ...options,
1824
+ force: true,
1825
+ path: workspacePath,
1826
+ silentIndexLog: true,
1827
+ };
1828
+ const adapter = createAdapter(target, workspacePath, runOptions);
1829
+ adapter.update(BUNDLED_AGENT_DIR);
1830
+ updatedTargets.push(target);
1831
+ } catch (err) {
1832
+ failed += 1;
1833
+ if (!options.quiet) {
1834
+ console.error(`[error] 更新失败: ${workspacePath} [${target}]`);
1835
+ console.error(` ${err.message}`);
1836
+ }
1837
+ }
1838
+ }
1839
+
1840
+ if (updatedTargets.length > 0) {
1841
+ updated += 1;
1842
+ nextRecords.push(mergeUpdatedTargets(item, workspacePath, updatedTargets, timestamp));
1843
+ } else {
1844
+ skipped += 1;
1845
+ nextRecords.push(item);
1846
+ }
1847
+ }
1848
+
1849
+ if (!options.dryRun) {
1850
+ withWorkspaceIndexLock(indexPath, () => {
1851
+ const { index: latestIndex } = readWorkspaceIndex();
1852
+ const mergedMap = new Map();
1853
+
1854
+ for (const item of latestIndex.workspaces || []) {
1855
+ if (!item || typeof item.path !== "string") continue;
1856
+ mergedMap.set(pathCompareKey(item.path), normalizeWorkspaceRecordV2(item, normalizeAbsolutePath(item.path)));
1857
+ }
1858
+
1859
+ for (const removedKey of removedRecordKeys) {
1860
+ mergedMap.delete(removedKey);
1861
+ }
1862
+
1863
+ for (const item of nextRecords) {
1864
+ if (!item || typeof item.path !== "string") continue;
1865
+ mergedMap.set(pathCompareKey(item.path), normalizeWorkspaceRecordV2(item, normalizeAbsolutePath(item.path)));
1866
+ }
1867
+
1868
+ latestIndex.workspaces = Array.from(mergedMap.values()).sort((a, b) => a.path.localeCompare(b.path));
1869
+ latestIndex.updatedAt = timestamp;
1870
+ writeWorkspaceIndex(indexPath, latestIndex);
1871
+ });
1872
+ }
1873
+
1874
+ log(options, "[summary] 批量更新完成");
1875
+ log(options, ` 成功: ${updated}`);
1876
+ log(options, ` 跳过: ${skipped}`);
1877
+ log(options, ` 失败: ${failed}`);
1878
+ log(options, ` 清理排除路径: ${removedExcluded}`);
1879
+ if (options.pruneMissing) {
1880
+ log(options, ` 清理失效索引: ${removedMissing}`);
1881
+ }
1882
+
1883
+ if (failed > 0) {
1884
+ process.exitCode = 1;
1885
+ }
1886
+ }
1887
+
1888
+ async function commandDoctor(options) {
1889
+ const workspaceRoot = resolveWorkspaceRoot(options.path);
1890
+ let targets = normalizeTargets(options.targets);
1891
+ const out = (message) => {
1892
+ if (!options.quiet) {
1893
+ console.log(message);
1894
+ }
1895
+ };
1896
+
1897
+ if (targets.length === 0) {
1898
+ targets = detectInstalledTargets(workspaceRoot);
1899
+ }
1900
+
1901
+ if (targets.length === 0) {
1902
+ throw new Error("未检测到已安装的目标。请指定 --target 或先执行 init。");
1903
+ }
1904
+
1905
+ log(options, `[doctor] 开始诊断 (Targets: ${targets.join(", ")})...`);
1906
+
1907
+ let hasIssue = false;
1908
+ for (const target of targets) {
1909
+ const adapter = createAdapter(target, workspaceRoot, options);
1910
+ out(`\n[${target.toUpperCase()}] 检查完整性...`);
1911
+
1912
+ let result = adapter.checkIntegrity();
1913
+ if (result.status === "ok") {
1914
+ out(" [ok] 状态正常");
1915
+ continue;
1916
+ }
1917
+
1918
+ let targetHasIssue = true;
1919
+ out(` [error] 状态: ${result.status}`);
1920
+ for (const issue of result.issues || []) {
1921
+ out(` - ${issue}`);
1922
+ }
1923
+
1924
+ if (options.fix) {
1925
+ const fixRes = adapter.fixIntegrity();
1926
+ if (fixRes && fixRes.fixed) {
1927
+ out(` [fix] 已修复: ${fixRes.summary}`);
1928
+ result = adapter.checkIntegrity();
1929
+ if (result.status === "ok") {
1930
+ out(" [ok] 修复后状态正常");
1931
+ targetHasIssue = false;
1932
+ } else {
1933
+ out(` [warn] 修复后仍有问题: ${result.status}`);
1934
+ for (const issue of result.issues || []) {
1935
+ out(` - ${issue}`);
1936
+ }
1937
+ targetHasIssue = true;
1938
+ }
1939
+ } else {
1940
+ out(` [info] 自动修复未执行: ${fixRes ? fixRes.summary : "无可用修复"}`);
1941
+ }
1942
+ }
1943
+
1944
+ if (targetHasIssue) {
1945
+ hasIssue = true;
1946
+ }
1947
+ }
1948
+
1949
+ if (hasIssue) {
1950
+ process.exitCode = 1;
1951
+ }
1952
+ }
1953
+
1954
+ function requirePathOption(options, commandUsage) {
1955
+ if (!options.path) {
1956
+ throw new Error(`${commandUsage} 需要 --path <dir> 参数`);
1957
+ }
1958
+ return resolveWorkspaceRoot(options.path);
1959
+ }
1960
+
1961
+ function commandExcludeList(options) {
1962
+ const { indexPath, index } = readWorkspaceIndex();
1963
+ const excluded = Array.isArray(index.excludedPaths) ? index.excludedPaths : [];
1964
+
1965
+ if (options.quiet) {
1966
+ for (const item of excluded) {
1967
+ console.log(item);
1968
+ }
1969
+ return;
1970
+ }
1971
+
1972
+ console.log("[exclude] 工作区排除清单");
1973
+ console.log(` 索引文件: ${indexPath}`);
1974
+ console.log(" 默认规则: 自动排除灵轨工具包源码目录与系统临时目录(无需手动添加)");
1975
+
1976
+ if (excluded.length === 0) {
1977
+ console.log(" 当前无自定义排除路径。");
1978
+ return;
1979
+ }
1980
+
1981
+ console.log(` 自定义排除路径 (${excluded.length}):`);
1982
+ for (let i = 0; i < excluded.length; i++) {
1983
+ console.log(` ${i + 1}. ${excluded[i]}`);
1984
+ }
1985
+ }
1986
+
1987
+ function commandExcludeAdd(options) {
1988
+ const targetPath = requirePathOption(options, "exclude add");
1989
+ const indexPath = getWorkspaceIndexPath();
1990
+ const normalizedTarget = normalizeAbsolutePath(targetPath);
1991
+ const targetKey = pathCompareKey(normalizedTarget);
1992
+ let existed = false;
1993
+ let matchedCount = 0;
1994
+
1995
+ withWorkspaceIndexLock(indexPath, () => {
1996
+ const { index } = readWorkspaceIndex();
1997
+ existed = index.excludedPaths.some((item) => pathCompareKey(item) === targetKey);
1998
+ matchedCount = index.workspaces.filter((item) => isPathInOrUnder(normalizedTarget, item.path)).length;
1999
+ });
2000
+
2001
+ if (options.dryRun) {
2002
+ if (existed) {
2003
+ log(options, `[dry-run] 排除路径已存在: ${normalizedTarget}`);
2004
+ } else {
2005
+ log(options, `[dry-run] 将新增排除路径: ${normalizedTarget}`);
2006
+ }
2007
+ if (matchedCount > 0) {
2008
+ log(options, `[dry-run] 将移除 ${matchedCount} 条已登记工作区记录(位于该排除路径下)。`);
2009
+ }
2010
+ return;
2011
+ }
2012
+
2013
+ if (!existed) {
2014
+ withWorkspaceIndexLock(indexPath, () => {
2015
+ const { index } = readWorkspaceIndex();
2016
+ const hasTarget = index.excludedPaths.some((item) => pathCompareKey(item) === targetKey);
2017
+ if (!hasTarget) {
2018
+ index.excludedPaths.push(normalizedTarget);
2019
+ index.excludedPaths = normalizePathList(index.excludedPaths);
2020
+ }
2021
+ index.workspaces = index.workspaces.filter((item) => !isPathInOrUnder(normalizedTarget, item.path));
2022
+ index.updatedAt = nowISO();
2023
+ writeWorkspaceIndex(indexPath, index);
2024
+ });
2025
+ } else {
2026
+ withWorkspaceIndexLock(indexPath, () => {
2027
+ const { index } = readWorkspaceIndex();
2028
+ index.workspaces = index.workspaces.filter((item) => !isPathInOrUnder(normalizedTarget, item.path));
2029
+ index.updatedAt = nowISO();
2030
+ writeWorkspaceIndex(indexPath, index);
2031
+ });
2032
+ }
2033
+
2034
+ if (existed) {
2035
+ log(options, `[info] 排除路径已存在: ${normalizedTarget}`);
2036
+ } else {
2037
+ log(options, `[ok] 已新增排除路径: ${normalizedTarget}`);
2038
+ }
2039
+
2040
+ if (matchedCount > 0) {
2041
+ log(options, `[clean] 已移除 ${matchedCount} 条已登记工作区记录(位于排除路径下)。`);
2042
+ }
2043
+ log(options, `[index] 索引文件: ${indexPath}`);
2044
+ }
2045
+
2046
+ function commandExcludeRemove(options) {
2047
+ const targetPath = requirePathOption(options, "exclude remove");
2048
+ const indexPath = getWorkspaceIndexPath();
2049
+ const normalizedTarget = normalizeAbsolutePath(targetPath);
2050
+ const targetKey = pathCompareKey(normalizedTarget);
2051
+ let existed = false;
2052
+
2053
+ withWorkspaceIndexLock(indexPath, () => {
2054
+ const { index } = readWorkspaceIndex();
2055
+ existed = index.excludedPaths.some((item) => pathCompareKey(item) === targetKey);
2056
+ });
2057
+
2058
+ if (!existed) {
2059
+ log(options, `[info] 排除路径不存在: ${normalizedTarget}`);
2060
+ return;
2061
+ }
2062
+
2063
+ if (options.dryRun) {
2064
+ log(options, `[dry-run] 将移除排除路径: ${normalizedTarget}`);
2065
+ return;
2066
+ }
2067
+
2068
+ withWorkspaceIndexLock(indexPath, () => {
2069
+ const { index } = readWorkspaceIndex();
2070
+ index.excludedPaths = index.excludedPaths.filter((item) => pathCompareKey(item) !== targetKey);
2071
+ index.updatedAt = nowISO();
2072
+ writeWorkspaceIndex(indexPath, index);
2073
+ });
2074
+
2075
+ log(options, `[ok] 已移除排除路径: ${normalizedTarget}`);
2076
+ log(options, `[index] 索引文件: ${indexPath}`);
2077
+ }
2078
+
2079
+ function commandExclude(options) {
2080
+ const subcommand = (options.subcommand || "list").toLowerCase();
2081
+
2082
+ if (subcommand === "list") {
2083
+ commandExcludeList(options);
2084
+ return;
2085
+ }
2086
+ if (subcommand === "add") {
2087
+ commandExcludeAdd(options);
2088
+ return;
2089
+ }
2090
+ if (subcommand === "remove") {
2091
+ commandExcludeRemove(options);
2092
+ return;
2093
+ }
2094
+
2095
+ throw new Error(`未知 exclude 子命令: ${subcommand}`);
2096
+ }
2097
+
2098
+ function countFilesIfExists(dir, filterFn) {
2099
+ if (!fs.existsSync(dir)) return 0;
2100
+ return fs.readdirSync(dir).filter(filterFn).length;
2101
+ }
2102
+
2103
+ function countSkillsRecursive(dir) {
2104
+ if (!fs.existsSync(dir)) return 0;
2105
+ let count = 0;
2106
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
2107
+ for (const entry of entries) {
2108
+ const fullPath = path.join(dir, entry.name);
2109
+ if (entry.isDirectory()) {
2110
+ count += countSkillsRecursive(fullPath);
2111
+ } else if (entry.name === "SKILL.md") {
2112
+ count += 1;
2113
+ }
2114
+ }
2115
+ return count;
2116
+ }
2117
+
2118
+ function commandStatus(options) {
2119
+ const workspaceRoot = resolveWorkspaceRoot(options.path);
2120
+ const summary = evaluateWorkspaceState(workspaceRoot, options);
2121
+
2122
+ if (summary.state === "missing") {
2123
+ if (options.quiet) {
2124
+ console.log("missing");
2125
+ }
2126
+ if (!options.quiet) {
2127
+ console.log("[error] 未检测到 Ling 安装");
2128
+ console.log(` 目标目录: ${workspaceRoot}`);
2129
+ }
2130
+ setQuietStatusExitCode("missing");
2131
+ return;
2132
+ }
2133
+
2134
+ if (options.quiet) {
2135
+ console.log(summary.state);
2136
+ setQuietStatusExitCode(summary.state);
2137
+ return;
2138
+ }
2139
+
2140
+ console.log(summary.state === "installed" ? "[ok] Ling 状态正常" : "[warn] Ling 存在问题");
2141
+ console.log(` CLI 版本: ${getVersionTag()}`);
2142
+ console.log(` 工作区: ${workspaceRoot}`);
2143
+ console.log(` 总体状态: ${summary.state}`);
2144
+ console.log(` Targets: ${summary.targets.map((item) => item.targetName).join(", ")}`);
2145
+
2146
+ const geminiState = summary.targets.find((item) => item.targetName === "gemini");
2147
+ if (geminiState) {
2148
+ const agentDir = path.join(workspaceRoot, ".agent");
2149
+ const agentsCount = countFilesIfExists(path.join(agentDir, "agents"), (name) => name.endsWith(".md"));
2150
+ const workflowsCount = countFilesIfExists(path.join(agentDir, "workflows"), (name) => name.endsWith(".md"));
2151
+ const skillsCount = countSkillsRecursive(path.join(agentDir, "skills"));
2152
+ console.log("\n[gemini]");
2153
+ console.log(` 状态: ${geminiState.state}`);
2154
+ console.log(` 路径: ${agentDir}`);
2155
+ if (geminiState.state === "installed") {
2156
+ console.log(` Agents: ${agentsCount}`);
2157
+ console.log(` Skills: ${skillsCount}`);
2158
+ console.log(` Workflows: ${workflowsCount}`);
2159
+ } else {
2160
+ for (const issue of geminiState.integrity.issues || []) {
2161
+ console.log(` Issue: ${issue}`);
2162
+ }
2163
+ }
2164
+ }
2165
+
2166
+ const codexState = summary.targets.find((item) => item.targetName === "codex");
2167
+ if (codexState) {
2168
+ const managedDir = path.join(workspaceRoot, ".agents");
2169
+ const legacyDir = path.join(workspaceRoot, ".codex");
2170
+ const activeDir = fs.existsSync(managedDir) ? managedDir : legacyDir;
2171
+ const skillsCount = countSkillsRecursive(path.join(activeDir, "skills"));
2172
+ const hasManifest = fs.existsSync(path.join(activeDir, "manifest.json"));
2173
+ const legacyDetected = fs.existsSync(legacyDir);
2174
+ console.log("\n[codex]");
2175
+ console.log(` 状态: ${codexState.state}`);
2176
+ console.log(` 路径: ${activeDir}`);
2177
+ console.log(` Skills: ${skillsCount}`);
2178
+ console.log(` Manifest: ${hasManifest ? "yes" : "no"}`);
2179
+ if (legacyDetected) {
2180
+ console.log(` Legacy: 检测到 .codex(建议执行 ${PRIMARY_CLI_NAME} update 迁移清理)`);
2181
+ }
2182
+ if (codexState.state !== "installed") {
2183
+ for (const issue of codexState.integrity.issues || []) {
2184
+ console.log(` Issue: ${issue}`);
2185
+ }
2186
+ }
2187
+ }
2188
+
2189
+ setQuietStatusExitCode(summary.state);
2190
+ }
2191
+
2192
+ async function main() {
2193
+ try {
2194
+ const migration = migrateLegacyControlHomeDir();
2195
+ const { command, options, providedFlags } = parseArgs(process.argv.slice(2));
2196
+
2197
+ if (migration && !options.quiet) {
2198
+ console.log(`[migrate] 已迁移控制目录: ${migration.from} -> ${migration.to}`);
2199
+ }
2200
+ maybeWarnLegacyCliAlias(options);
2201
+
2202
+ if (!command || command === "--help" || command === "-h") {
2203
+ printUsage();
2204
+ if (!command || command === "--help" || command === "-h") {
2205
+ return;
2206
+ }
2207
+ }
2208
+
2209
+ if (command === "--version" || command === "-v") {
2210
+ printVersion();
2211
+ return;
2212
+ }
2213
+
2214
+ validateOptionScope(command, options, providedFlags);
2215
+ maybeWarnUpstreamGlobalConflict(command, options);
2216
+
2217
+ if (command === "init") {
2218
+ await commandInit(options);
2219
+ return;
2220
+ }
2221
+
2222
+ if (command === "update") {
2223
+ await commandUpdate(options);
2224
+ return;
2225
+ }
2226
+
2227
+ if (command === "update-all") {
2228
+ await commandUpdateAll(options);
2229
+ return;
2230
+ }
2231
+
2232
+ if (command === "doctor") {
2233
+ await commandDoctor(options);
2234
+ return;
2235
+ }
2236
+
2237
+ if (command === "global") {
2238
+ await commandGlobal(options);
2239
+ return;
2240
+ }
2241
+
2242
+ if (command === "spec") {
2243
+ commandSpec(options);
2244
+ return;
2245
+ }
2246
+
2247
+ if (command === "exclude") {
2248
+ commandExclude(options);
2249
+ return;
2250
+ }
2251
+
2252
+ if (command === "status") {
2253
+ commandStatus(options);
2254
+ return;
2255
+ }
2256
+
2257
+ console.error(`未知命令: ${command}`);
2258
+ printUsage();
2259
+ process.exitCode = 1;
2260
+ } catch (err) {
2261
+ console.error(`[error] ${err.message}`);
2262
+ process.exitCode = 1;
2263
+ }
2264
+ }
2265
+
2266
+ main();