@mison/ling 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/ling.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- require("./ag-kit.js");
3
+ require("./ling-cli.js");
@@ -10,6 +10,14 @@ function detectLineEnding(content) {
10
10
  }
11
11
 
12
12
  function buildMarkers(blockId) {
13
+ const id = String(blockId || "default").trim();
14
+ return {
15
+ begin: `<!-- BEGIN LING MANAGED BLOCK: ${id} -->`,
16
+ end: `<!-- END LING MANAGED BLOCK: ${id} -->`,
17
+ };
18
+ }
19
+
20
+ function buildLegacyMarkers(blockId) {
13
21
  const id = String(blockId || "default").trim();
14
22
  return {
15
23
  begin: `<!-- BEGIN AG-KIT MANAGED BLOCK: ${id} -->`,
@@ -29,10 +37,9 @@ function upsertManagedBlock(filePath, blockId, body, options = {}) {
29
37
  const lineEnding = detectLineEnding(original);
30
38
  const managedBlock = buildManagedBlock(blockId, body, lineEnding);
31
39
  const markers = buildMarkers(blockId);
32
- const blockRegex = new RegExp(
33
- `${escapeRegex(markers.begin)}[\\s\\S]*?${escapeRegex(markers.end)}`,
34
- "m",
35
- );
40
+ const legacyMarkers = buildLegacyMarkers(blockId);
41
+ const blockRegex = new RegExp(`${escapeRegex(markers.begin)}[\\s\\S]*?${escapeRegex(markers.end)}`, "m");
42
+ const legacyRegex = new RegExp(`${escapeRegex(legacyMarkers.begin)}[\\s\\S]*?${escapeRegex(legacyMarkers.end)}`, "m");
36
43
 
37
44
  let next = "";
38
45
  let action = "unchanged";
@@ -46,6 +53,12 @@ function upsertManagedBlock(filePath, blockId, body, options = {}) {
46
53
  if (next && !/\r?\n$/.test(next)) {
47
54
  next += lineEnding;
48
55
  }
56
+ } else if (legacyRegex.test(original)) {
57
+ next = original.replace(legacyRegex, managedBlock);
58
+ action = "updated";
59
+ if (next && !/\r?\n$/.test(next)) {
60
+ next += lineEnding;
61
+ }
49
62
  } else {
50
63
  next = `${original.replace(/\r?\n?$/, "")}${lineEnding}${lineEnding}${managedBlock}${lineEnding}`;
51
64
  action = "appended";
package/bin/utils.js CHANGED
@@ -49,7 +49,7 @@ function cloneBranchAgentDir(branch, options) {
49
49
  throw new Error(`非法分支名: ${branch}`);
50
50
  }
51
51
 
52
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ag-kit-"));
52
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ling-"));
53
53
  const logFn = options && options.logger ? options.logger : console.log;
54
54
 
55
55
  if (!options.quiet) logFn(`[download] 正在从 ${REPO_URL} 拉取分支 ${safeBranch} ...`);
package/docs/TECH.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  ## 快速验证(维护者)
4
4
  ```bash
5
- bun install
6
- bun run test
7
- bun run ci:verify
8
- bun run health-check
9
- cd web && bun install && bun run lint
5
+ npm install
6
+ npm test
7
+ npm run ci:verify
8
+ npm run health-check
9
+ cd web && npm install && npm run lint
10
10
  ```
11
11
 
12
12
  ## 核心目录与职责
@@ -20,6 +20,9 @@ cd web && bun install && bun run lint
20
20
  ### 项目级(功能最完整)
21
21
  - `gemini`:项目根目录 `.agent/`
22
22
  - `codex`:项目根目录 `.agents/`(受管)+ `.agents-backup/`(漂移覆盖备份)
23
+ - 项目级预备份(覆盖前快照):
24
+ - Gemini:`<project>/.agent-backup/<timestamp>/preflight/.agent/`
25
+ - Codex:`<project>/.agents-backup/<timestamp>/preflight/.agents/` 或 `<project>/.agents-backup/<timestamp>/preflight/.codex/`
23
26
 
24
27
  ### 全局级(仅同步 Skills)
25
28
  - `codex`:`$HOME/.codex/skills/`
@@ -31,9 +34,16 @@ cd web && bun install && bun run lint
31
34
  ## 端到端链路(简述)
32
35
  ### 项目安装 / 更新
33
36
  - `init`:选择目标 -> 适配器 `install()` -> 落盘目标目录(Gemini: `.agent/`;Codex: `.agents/`)->(Codex)注入托管区块到工作区 `AGENTS.md` 与 `ling.rules`
34
- - `update`:自动检测已安装目标(或通过 `--target/--targets` 指定)-> 适配器 `update()` ->(Codex)漂移检测与备份 -> 原子替换
37
+ - `update`:自动检测已安装目标(或通过 `--target/--targets` 指定)->(冲突时交互确认或默认预备份)-> 适配器 `update()` ->(Codex)漂移检测与备份 -> 原子替换
35
38
  - `doctor`:检查完整性;`--fix` 尝试修复(Codex 支持迁移 `.codex/` 与重写托管区块)
36
39
 
40
+ ### 已有资产冲突处理(项目级)
41
+ - 触发条件:
42
+ - `gemini`:`.agent/` 已存在且与内置模板不一致
43
+ - `codex`:`.agents/` 或 `.codex/` 已存在且存在漂移、缺失 `manifest.json` 或包含未知文件
44
+ - 交互终端会逐项询问处理方式:保留 / 备份后移除 / 直接移除,并支持按资产类别复用选择
45
+ - 非交互环境不会进入询问:需要覆盖时默认执行“备份后覆盖”;`init` 若检测到已有资产且未显式 `--force` 则报错
46
+
37
47
  ### Codex 构建(Workflow -> Skill)
38
48
  - 输入:`.agents/skills/` 与 `.agents/workflows/`
39
49
  - 规则:每个 Workflow `<name>.md` 会转换为一个 Skill:`workflow-<name>/SKILL.md`
@@ -161,7 +171,6 @@ cp -a "$HOME/.ling/backups/global/$ts/antigravity/$skill" "$HOME/.gemini/antigra
161
171
 
162
172
  ## 安装提示机制
163
173
  - npm 全局安装:`postinstall` 会尽力检测并提示上游英文版 `@vudovn/ag-kit` 冲突。
164
- - Bun 全局安装:Bun 默认会阻止本包 `postinstall`;因此冲突提示以内置 CLI 运行期检查为准,会在 `init` / `update` / `update-all` / `global sync` 时提示。
165
174
  - 冲突提示只负责提醒,不会自动修改当前安装状态;如需清理可执行 `npm uninstall -g @vudovn/ag-kit`。
166
175
 
167
176
  ## 常见故障
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mison/ling",
3
- "version": "1.0.2",
4
- "description": "AI Agent templates - Skills, Agents, and Workflows for enhanced coding assistance",
3
+ "version": "1.1.0",
4
+ "description": "面向 Gemini CLI、Antigravity Codex 的中文 AI Agent 模板工具包",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/MisonL/Ling.git"
@@ -18,7 +18,10 @@
18
18
  "skills",
19
19
  "templates"
20
20
  ],
21
- "author": "vudovn",
21
+ "author": "Mison",
22
+ "contributors": [
23
+ "vudovn"
24
+ ],
22
25
  "license": "MIT",
23
26
  "scripts": {
24
27
  "clean": "node scripts/clean.js",
@@ -9,6 +9,7 @@ const REPO_ROOT = path.resolve(__dirname, "..");
9
9
  const CLI_PATH = path.join(REPO_ROOT, "bin", "ling.js");
10
10
 
11
11
  function runCli(args, options = {}) {
12
+ const allowedStatuses = options.allowedStatuses || [0];
12
13
  const env = {
13
14
  ...process.env,
14
15
  LING_SKIP_UPSTREAM_CHECK: "1",
@@ -21,9 +22,11 @@ function runCli(args, options = {}) {
21
22
  encoding: "utf8",
22
23
  });
23
24
 
24
- if (result.status !== 0) {
25
+ if (!allowedStatuses.includes(result.status)) {
25
26
  const message = result.stderr || result.stdout || "";
26
- throw new Error(`命令失败: ling ${args.join(" ")}\n${message}`);
27
+ throw new Error(
28
+ `命令失败: ling ${args.join(" ")}\n(exit=${result.status})\n${message}`.trim(),
29
+ );
27
30
  }
28
31
 
29
32
  return result.stdout || "";
@@ -82,7 +85,10 @@ function main() {
82
85
  throw new Error(`spec status 结果异常: ${specStatus}`);
83
86
  }
84
87
  runCli(["spec", "disable", "--target", "codex", "--quiet"], { env });
85
- const specStatusAfterDisable = runCli(["spec", "status", "--quiet"], { env }).trim();
88
+ const specStatusAfterDisable = runCli(["spec", "status", "--quiet"], {
89
+ env,
90
+ allowedStatuses: [0, 2],
91
+ }).trim();
86
92
  if (specStatusAfterDisable !== "missing") {
87
93
  throw new Error(`spec disable 后状态异常: ${specStatusAfterDisable}`);
88
94
  }
@@ -58,7 +58,7 @@ async function main() {
58
58
  return;
59
59
  }
60
60
 
61
- if (process.env.LING_SKIP_UPSTREAM_CHECK === "1" || process.env.AG_KIT_SKIP_UPSTREAM_CHECK === "1") {
61
+ if (process.env.LING_SKIP_UPSTREAM_CHECK === "1") {
62
62
  return;
63
63
  }
64
64
 
@@ -72,13 +72,12 @@ async function main() {
72
72
  }
73
73
 
74
74
  console.warn(`\n[warn] 检测到全局已安装上游英文版 ${UPSTREAM_GLOBAL_PACKAGE}`);
75
- console.warn("[warn] 上游英文版与当前版本共用 `ag-kit` 兼容命令名,后安装者会覆盖该入口。");
76
- console.warn("[warn] 为避免后续混淆,建议仅保留一个来源。\n");
75
+ console.warn("[warn] 为避免 Skills/模板来源混用导致的行为差异,建议仅保留一个来源。\n");
77
76
 
78
77
  if (!canPromptUser()) {
79
78
  console.warn("[info] 当前环境不是交互式终端,无法确认是否自动卸载。");
80
79
  console.warn(`[hint] 如需卸载,请手动执行: npm uninstall -g ${UPSTREAM_GLOBAL_PACKAGE}`);
81
- console.warn("[info] 本次将继续安装;安装完成后正式命令请使用 `ling`。\n");
80
+ console.warn("[info] 本次将继续安装;安装完成后请使用 `ling`。\n");
82
81
  return;
83
82
  }
84
83
 
@@ -86,7 +85,7 @@ async function main() {
86
85
 
87
86
  if (!shouldUninstall) {
88
87
  console.warn(`[info] 已保留 ${UPSTREAM_GLOBAL_PACKAGE},继续安装当前版本。`);
89
- console.warn("[info] 结果说明:正式命令请使用 `ling`,旧 `ag-kit` 仅保留兼容入口。\n");
88
+ console.warn("[info] 结果说明:安装完成后请使用 `ling`。\n");
90
89
  return;
91
90
  }
92
91
 
@@ -101,7 +100,7 @@ async function main() {
101
100
  console.warn(`[error] 自动卸载 ${UPSTREAM_GLOBAL_PACKAGE} 失败,将继续安装当前版本。`);
102
101
  console.warn("[info] 若需手动处理,请执行:");
103
102
  console.warn(` npm uninstall -g ${UPSTREAM_GLOBAL_PACKAGE}`);
104
- console.warn("[info] 安装完成后,正式命令请使用 `ling`;旧 `ag-kit` 仅保留兼容入口。\n");
103
+ console.warn("[info] 安装完成后请使用 `ling`。\n");
105
104
  }
106
105
 
107
106
  main().catch((err) => {
@@ -9,7 +9,7 @@ describe("Clean Script", () => {
9
9
  let tempRoot;
10
10
 
11
11
  beforeEach(() => {
12
- tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ag-kit-clean-test-"));
12
+ tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ling-clean-test-"));
13
13
  });
14
14
 
15
15
  afterEach(() => {
@@ -27,6 +27,37 @@ describe("CLI Smoke", () => {
27
27
  let workspaceDir;
28
28
  let indexPath;
29
29
 
30
+ function findFirstTimestampDir(backupRoot) {
31
+ if (!fs.existsSync(backupRoot)) {
32
+ return "";
33
+ }
34
+ const entries = fs
35
+ .readdirSync(backupRoot, { withFileTypes: true })
36
+ .filter((entry) => entry.isDirectory())
37
+ .map((entry) => entry.name)
38
+ .sort();
39
+ return entries.length > 0 ? path.join(backupRoot, entries[entries.length - 1]) : "";
40
+ }
41
+
42
+ function findTimestampDirContaining(backupRoot, relPath) {
43
+ if (!fs.existsSync(backupRoot)) {
44
+ return "";
45
+ }
46
+ const entries = fs
47
+ .readdirSync(backupRoot, { withFileTypes: true })
48
+ .filter((entry) => entry.isDirectory())
49
+ .map((entry) => entry.name)
50
+ .sort();
51
+
52
+ for (const name of entries) {
53
+ const candidate = path.join(backupRoot, name, relPath);
54
+ if (fs.existsSync(candidate)) {
55
+ return path.join(backupRoot, name);
56
+ }
57
+ }
58
+ return "";
59
+ }
60
+
30
61
  beforeEach(() => {
31
62
  tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ling-cli-test-"));
32
63
  workspaceDir = path.join(tempDir, "workspace");
@@ -273,6 +304,60 @@ describe("CLI Smoke", () => {
273
304
  assert.strictEqual(result.status, 0, result.stderr || result.stdout);
274
305
  });
275
306
 
307
+ test("init --force should create preflight backup for existing gemini .agent", () => {
308
+ const agentDir = path.join(workspaceDir, ".agent");
309
+ fs.mkdirSync(agentDir, { recursive: true });
310
+ fs.writeFileSync(path.join(agentDir, "custom.txt"), "custom", "utf8");
311
+
312
+ const result = runCli(
313
+ ["init", "--target", "gemini", "--path", workspaceDir, "--force", "--quiet"],
314
+ { env: { LING_INDEX_PATH: indexPath } },
315
+ );
316
+ assert.strictEqual(result.status, 0, result.stderr || result.stdout);
317
+
318
+ const backupRoot = path.join(workspaceDir, ".agent-backup");
319
+ const tsDir = findFirstTimestampDir(backupRoot);
320
+ assert.ok(tsDir, "expected .agent-backup timestamp directory to exist");
321
+ assert.ok(fs.existsSync(path.join(tsDir, "preflight", ".agent", "custom.txt")));
322
+ });
323
+
324
+ test("update should create preflight backup for gemini when overwriting in non-interactive mode", () => {
325
+ const agentDir = path.join(workspaceDir, ".agent");
326
+ fs.mkdirSync(agentDir, { recursive: true });
327
+ fs.writeFileSync(path.join(agentDir, "custom.txt"), "custom", "utf8");
328
+
329
+ const result = runCli(
330
+ ["update", "--target", "gemini", "--path", workspaceDir, "--quiet"],
331
+ { env: { LING_INDEX_PATH: indexPath } },
332
+ );
333
+ assert.strictEqual(result.status, 0, result.stderr || result.stdout);
334
+
335
+ const backupRoot = path.join(workspaceDir, ".agent-backup");
336
+ const tsDir = findFirstTimestampDir(backupRoot);
337
+ assert.ok(tsDir, "expected .agent-backup timestamp directory to exist");
338
+ assert.ok(fs.existsSync(path.join(tsDir, "preflight", ".agent", "custom.txt")));
339
+ });
340
+
341
+ test("update should create preflight backup for codex when unknown files exist in managed dir", () => {
342
+ const initResult = runCli(
343
+ ["init", "--target", "codex", "--path", workspaceDir, "--quiet"],
344
+ { env: { LING_INDEX_PATH: indexPath } },
345
+ );
346
+ assert.strictEqual(initResult.status, 0, initResult.stderr || initResult.stdout);
347
+
348
+ fs.writeFileSync(path.join(workspaceDir, ".agents", "unknown.txt"), "unknown", "utf8");
349
+
350
+ const updateResult = runCli(
351
+ ["update", "--target", "codex", "--path", workspaceDir, "--quiet"],
352
+ { env: { LING_INDEX_PATH: indexPath } },
353
+ );
354
+ assert.strictEqual(updateResult.status, 0, updateResult.stderr || updateResult.stdout);
355
+
356
+ const backupRoot = path.join(workspaceDir, ".agents-backup");
357
+ const tsDir = findTimestampDirContaining(backupRoot, path.join("preflight", ".agents", "unknown.txt"));
358
+ assert.ok(tsDir, "expected .agents-backup preflight backup to exist");
359
+ });
360
+
276
361
  test("status --quiet should report missing with exit code 2 when nothing is installed", () => {
277
362
  const result = runCli(
278
363
  ["status", "--path", workspaceDir, "--quiet"],
@@ -25,7 +25,7 @@ describe("ManagedBlock", () => {
25
25
  assert.strictEqual(result.action, "appended");
26
26
  const content = fs.readFileSync(targetFile, "utf8");
27
27
  assert.ok(content.includes("User Content"));
28
- assert.ok(content.includes("BEGIN AG-KIT MANAGED BLOCK: codex-core-rules"));
28
+ assert.ok(content.includes("BEGIN LING MANAGED BLOCK: codex-core-rules"));
29
29
  assert.ok(content.includes("managed line"));
30
30
  });
31
31
 
@@ -38,4 +38,21 @@ describe("ManagedBlock", () => {
38
38
  assert.ok(content.includes("v2"));
39
39
  assert.ok(!content.includes("v1"));
40
40
  });
41
+
42
+ test("should migrate legacy AG-KIT markers when updating managed block", () => {
43
+ fs.writeFileSync(
44
+ targetFile,
45
+ `# User Content\n\n<!-- BEGIN AG-KIT MANAGED BLOCK: codex-core-rules -->\nlegacy\n<!-- END AG-KIT MANAGED BLOCK: codex-core-rules -->\n`,
46
+ "utf8",
47
+ );
48
+
49
+ const result = upsertManagedBlock(targetFile, "codex-core-rules", "v2");
50
+
51
+ assert.strictEqual(result.action, "updated");
52
+ const content = fs.readFileSync(targetFile, "utf8");
53
+ assert.ok(content.includes("BEGIN LING MANAGED BLOCK: codex-core-rules"));
54
+ assert.ok(!content.includes("BEGIN AG-KIT MANAGED BLOCK: codex-core-rules"));
55
+ assert.ok(content.includes("v2"));
56
+ assert.ok(!content.includes("legacy"));
57
+ });
41
58
  });
@@ -66,9 +66,9 @@ describe('Phase C Integration', () => {
66
66
  // Verification 4: Workspace managed block injection
67
67
  const workspaceAgents = fs.readFileSync(path.join(workDir, 'AGENTS.md'), 'utf8');
68
68
  const workspaceRules = fs.readFileSync(path.join(workDir, 'ling.rules'), 'utf8');
69
- assert.ok(workspaceAgents.includes('BEGIN AG-KIT MANAGED BLOCK: codex-core-rules'));
69
+ assert.ok(workspaceAgents.includes('BEGIN LING MANAGED BLOCK: codex-core-rules'));
70
70
  assert.ok(workspaceAgents.includes('test-skill'));
71
- assert.ok(workspaceRules.includes('BEGIN AG-KIT MANAGED BLOCK: codex-risk-controls'));
71
+ assert.ok(workspaceRules.includes('BEGIN LING MANAGED BLOCK: codex-risk-controls'));
72
72
 
73
73
  const codexJson = JSON.parse(fs.readFileSync(path.join(codexDir, 'codex.json'), 'utf8'));
74
74
  assert.strictEqual(codexJson.version, pkg.version);
@@ -79,7 +79,7 @@ describe('Standards Compliance', () => {
79
79
 
80
80
  test('critical mechanism tokens should remain aligned with reference snapshot', { skip: !fs.existsSync(REF_ROOT) }, () => {
81
81
  const refRoot = REF_ROOT;
82
- const tokenRegex = /(ag-kit|ling|python3?|checklist\.py|verify_all\.py|security_scan\.py|ux_audit\.py|accessibility_checker\.py|schema_validator\.py|lint_runner\.py|type_coverage\.py|playwright_runner\.py|lighthouse_audit\.py|api_validator\.py|mobile_audit\.py|seo_checker\.py|geo_checker\.py|i18n_checker\.py|\.agent\/|\.agents\/|\.agents-backup\/|\.codex\/|AGENTS\.md|antigravity\.rules|ling\.rules|manifest\.json|--target|--targets|--fix|--path|--no-index|--dry-run|--quiet|--force)/g;
82
+ const tokenRegex = /(ling|python3?|checklist\.py|verify_all\.py|security_scan\.py|ux_audit\.py|accessibility_checker\.py|schema_validator\.py|lint_runner\.py|type_coverage\.py|playwright_runner\.py|lighthouse_audit\.py|api_validator\.py|mobile_audit\.py|seo_checker\.py|geo_checker\.py|i18n_checker\.py|\.agent\/|\.agents\/|\.agents-backup\/|\.codex\/|AGENTS\.md|antigravity\.rules|ling\.rules|manifest\.json|--target|--targets|--fix|--path|--no-index|--dry-run|--quiet|--force)/g;
83
83
  const slashCommandRegex = /(?:^|[\s`"'(\[])(\/(?:brainstorm|create|debug|deploy|enhance|orchestrate|plan|preview|status|test|ui-ux-pro-max))(?=$|[\s`"',。,.::)\]])/gm;
84
84
 
85
85
  const markdownFiles = [];
@@ -9,7 +9,7 @@ describe("ResourceTransformer", () => {
9
9
  let tempDir;
10
10
 
11
11
  beforeEach(() => {
12
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ag-kit-transformer-test-"));
12
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ling-transformer-test-"));
13
13
  });
14
14
 
15
15
  afterEach(() => {