ly-workflow-codex 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 fengshao1227
4
+ Copyright (c) 2025 yang.li12
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # ly-workflow-codex
2
+
3
+ > Codex 单 Agent 工作流:同一 Codex 会话内自己完成探索 / 方案 / 实施 / 审查编排;方案审查与代码审查两个关卡由**双审查 subagent**(fork 当前会话上下文 + 范围点名 + 独立审 → 交换 → 共识)执行,实施由 **coding subagent** 执行。最大化复用 OpenSpec 原生工作流。
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+ [![Node](https://img.shields.io/badge/Node.js-%3E%3D20-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
7
+
8
+ ## 安装
9
+
10
+ ```bash
11
+ npx ly-workflow-codex # 交互式菜单(含 openspec 依赖 preflight 检查)
12
+ npx ly-workflow-codex init # 全量初始化(生成 AGENTS.md + openspec init)
13
+ ```
14
+
15
+ - CLI 二进制名 `lycx`(子命令:`init`/`doctor`/`status`/`uninstall`,裸命令进菜单;`update` 在菜单内)
16
+ - 安装产物:14 个 `@lyx-*` skills(`SKILL.md`)→ `~/.agents/skills/lyx-*/`;8 个角色提示词 → `~/.ly/prompts/codex/`
17
+ - 配置:`~/.ly/config.toml`——首次读取/写入时自动从旧 `~/.claude/.ly/config.toml` 迁移(新位置已有配置则不覆盖)
18
+ - 初始化向导采集流程:语言 → API 提供方(`~/.codex/config.toml` 现有 `[model_providers.*]` / OpenAI 官方 / 自定义)→ 模型三连(共用 provider 模型列表逐个选择:`codexHost.reviewModel` 审查 agent A / `reviewModelB` 审查 agent B / `codingModel` coding subagent,可"自定义输入…"或"不设置(回退当前会话模型)",拉取失败回退自由输入)→ 配置摘要(三模型各自状态)
19
+
20
+ ## 命令(14 个 `@lyx-*` skills)
21
+
22
+ | 命令 | 用途 |
23
+ |------|------|
24
+ | `@lyx-init` | 生成项目 AGENTS.md + 初始化 OpenSpec 目录结构 + 自动 commit |
25
+ | `@lyx-explore` | 想清楚再动手(委托 `@openspec-explore skill`),收敛到方案时提示转 `@lyx-propose` |
26
+ | `@lyx-propose` | 创建方案前问一次隔离方式(三选一:隔离 worktree【从当前分支 HEAD 切出、同会话 cd 续跑,续接命令为异常兜底】/ 本项目切新分支【仅分支隔离,脏改动三选处置】/ 留在当前分支)+ 问"全自动/手动" → 委托 `@openspec-propose skill` → 方案自审(四项检查 + 逐项结论清单;机械断链直接修、业务判断类问用户)→ commit `propose: <change>`;全自动 = review-plan → apply → review-code 流水线,手动 = 逐步确认 |
27
+ | `@lyx-apply` | **coding subagent 实施**:spawn coding subagent(fork 当前上下文 + 只实施 change 范围,模型按 `codingModel` 指定)读 tasks.md 逐任务实施 + 验证 + 勾 checkbox → 回传主会话,主会话确认后统一 commit `apply: <change>`;环境级不可用回退当前会话,业务失败转人工 |
28
+ | `@lyx-archive` | 归档完成的 change(委托 `@openspec-archive-change skill`)+ 自动 commit |
29
+ | `@lyx-review-plan` | 审方案:**双审查 subagent**(角色词 `plan-reviewer.md`,fork 上下文 + 范围点名,模型按 `reviewModel`/`reviewModelB` 分别指定)分级审查,独立审→交换→共识,分歧主会话拍板;审查-修复循环直到 Critical 清零或触发终止条件(全局轮数上限 5,清零优先),清零统一提交修复 |
30
+ | `@lyx-review-code` | 审代码:同上(角色词 `reviewer.md`),Critical/Warning/Info 分级 |
31
+ | `@lyx-release` | GitFlow 四场景发版(feature/release/hotfix/dev-offline),SemVer 自动推导版本号;上线合并二选一(远端 PR 默认 / 本地直接合并)+ 主分支名检测(master/main) |
32
+ | `@lyx-changelog` | 按 commit 前缀分组生成/更新 Keep a Changelog 格式的 CHANGELOG.md |
33
+ | `@lyx-publish` | npm 包发布四场景(bmc 私域 Nexus / GitHub Packages / npmjs + GitHub Release / CI 自动发布),前置检查→版本号推导→构建→发布→验证 |
34
+ | `@lyx-commit` `@lyx-rollback` `@lyx-clean-branches` `@lyx-worktree` | Git 工具(worktree 只留 `add`/`list`/`remove`/`prune`/`migrate`,`switch` 已移除——隔离切换统一由 `@lyx-propose` 触发) |
35
+
36
+ 审查-修复循环细节(增量传递、沿用同一批审查 subagent、终止条件、提交时机)内联在各 skill 模板(`@lyx-review-plan` / `@lyx-review-code`);[docs/codex-exec-contract.md](./docs/codex-exec-contract.md) 已 DEPRECATED,仅作历史参考。
37
+
38
+ ## 典型工作流
39
+
40
+ ```
41
+ @lyx-init
42
+ @lyx-propose "要做什么" # 隔离三选一 + 全自动/手动
43
+ @lyx-review-plan # 审方案(双审查 subagent)
44
+ @lyx-apply # coding subagent 实施 + 主会话统一 commit
45
+ @lyx-review-code # 审代码(双审查 subagent)
46
+ @lyx-archive
47
+ ```
48
+
49
+ ## 架构
50
+
51
+ - **codex 单 Agent 编排**:当前 Codex 会话完成探索 → 方案 → 自审 → 实施 → 修复的全部编排;无 wrapper、无 Web UI、无 routing/implementer 后端选择
52
+ - **审查关卡 = 双审查 subagent**:每关 spawn 2 个审查 subagent(fork 当前会话上下文 + 任务点名"只审 change 范围"),各自独立审 → 交换结论 → 达成共识;意见分歧 → 主会话拍板并显式提示用户,不能确认 → 判定 Critical;模型按 `codexHost.reviewModel`/`reviewModelB` 分别指定(未配置回退当前会话模型);角色词绝对路径 `~/.ly/prompts/codex/` 为行为契约
53
+ - **apply = coding subagent 实施**:spawn 一个 coding subagent(fork 当前上下文 + 只实施 change 范围),模型 = `codexHost.codingModel`;coding subagent 不自行 commit,结果回传主会话,由主会话确认后统一提交 `apply: <change-name>`;环境级不可用回退当前会话直接实施,业务失败原样呈报转人工
54
+ - **发布**:打 tag `v*.*.*` push 触发 GitHub Actions 自动发 npm 包;无独立二进制构建步骤
55
+ - **生命周期**:直接委托 OpenSpec 原生 skills(`@openspec-explore` / `@openspec-propose` / `@openspec-apply-change` / `@openspec-archive-change`),安装器只负责 preflight(openspec CLI + openspec-* skills 检测)与安装
56
+
57
+ ## 与 ly-workflow 的关系
58
+
59
+ - **ly-workflow**(上游原仓库)是双宿主工具(claude + codex);**ly-workflow-codex** 是它的 codex 单宿主独立版:砍掉 claude 宿主、wrapper、Web UI、routing 概念,审查/实施改为 **subagent 多 Agent 模式**(双审查 subagent + coding subagent,执行约定内联在各 skill 模板)
60
+ - **安装位与旧包(v0.2.0 前)不再冲突**:本包命令以 `@lyx-*` skills 安装在 `~/.agents/skills/lyx-*/`(Codex 官方 skill 发现目录),与 ly-workflow 的 codex 分支产物(`~/.codex/prompts/ly-*.md`)互不覆盖;`~/.codex/prompts/ly-*.md` 旧残留由本包 init/uninstall 自动清理
61
+ - **迁移路径**:先卸载旧包(`npx ly-workflow` 的 uninstall)再安装本包;或直接 `npx ly-workflow-codex init --force` 覆盖安装(同时清理旧安装位残留)。配置 `~/.ly/config.toml` 由本包自动从旧 `~/.claude/.ly/config.toml` 迁移
62
+
63
+ ## License
64
+
65
+ MIT
package/bin/lycx.mjs ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/cli.mjs'
@@ -0,0 +1,143 @@
1
+ import { readFile, writeFile, readdir } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import fs from 'fs-extra';
5
+
6
+ async function removePath(target) {
7
+ await fs.remove(target);
8
+ }
9
+ async function cleanupCodexAgentsMd(result, dirs) {
10
+ const filePath = join(dirs.codexDir, "AGENTS.md");
11
+ if (!await fs.pathExists(filePath)) {
12
+ result.skipped.push("~/.codex/AGENTS.md (not found)");
13
+ return;
14
+ }
15
+ try {
16
+ const content = await readFile(filePath, "utf-8");
17
+ const stripped = content.replace(/<!-- LY:START[\s\S]*?-- LY:END -->\n?/g, "");
18
+ if (stripped !== content) {
19
+ const tmpPath = `${filePath}.tmp`;
20
+ await writeFile(tmpPath, stripped);
21
+ await fs.move(tmpPath, filePath, { overwrite: true });
22
+ result.cleaned.push("~/.codex/AGENTS.md (LY blocks stripped)");
23
+ } else {
24
+ result.skipped.push("~/.codex/AGENTS.md (no LY blocks)");
25
+ }
26
+ } catch (error) {
27
+ result.failed.push(`~/.codex/AGENTS.md: ${error instanceof Error ? error.message : String(error)}`);
28
+ }
29
+ }
30
+ async function cleanupCodexConfigTomlLyBlocks(result, dirs) {
31
+ const filePath = join(dirs.codexDir, "config.toml");
32
+ if (!await fs.pathExists(filePath)) {
33
+ result.skipped.push("~/.codex/config.toml (not found)");
34
+ return;
35
+ }
36
+ try {
37
+ const content = await readFile(filePath, "utf-8");
38
+ const hasLyMarker = /^\s*#\s*(?:ly-workflow\b|Installed by: npx ly-workflow\b|Added by ly-workflow-codex\b)/m.test(content);
39
+ if (!hasLyMarker) {
40
+ result.skipped.push("~/.codex/config.toml (no ly-workflow write markers)");
41
+ return;
42
+ }
43
+ const lines = content.split("\n");
44
+ const out = [];
45
+ let removed = false;
46
+ let inLyTable = false;
47
+ let headerZone = true;
48
+ for (const line of lines) {
49
+ const trimmed = line.trim();
50
+ if (headerZone && trimmed.startsWith("#")) {
51
+ if (/^#\s*ly-workflow/.test(trimmed) || /^#\s*Installed by: npx ly-workflow/.test(trimmed) || /^#\s*Added by ly-workflow-codex/.test(trimmed)) {
52
+ removed = true;
53
+ continue;
54
+ }
55
+ out.push(line);
56
+ continue;
57
+ }
58
+ if (trimmed !== "")
59
+ headerZone = false;
60
+ if (inLyTable) {
61
+ if (trimmed.startsWith("[")) {
62
+ inLyTable = false;
63
+ } else {
64
+ removed = true;
65
+ continue;
66
+ }
67
+ }
68
+ if (trimmed.startsWith("[features.multi_agent_v2]")) {
69
+ removed = true;
70
+ inLyTable = true;
71
+ continue;
72
+ }
73
+ out.push(line);
74
+ }
75
+ if (removed) {
76
+ const tmpPath = `${filePath}.tmp`;
77
+ await writeFile(tmpPath, out.join("\n"));
78
+ await fs.move(tmpPath, filePath, { overwrite: true });
79
+ result.cleaned.push("~/.codex/config.toml (LY blocks stripped)");
80
+ } else {
81
+ result.skipped.push("~/.codex/config.toml (no LY blocks)");
82
+ }
83
+ } catch (error) {
84
+ result.failed.push(`~/.codex/config.toml: ${error instanceof Error ? error.message : String(error)}`);
85
+ }
86
+ }
87
+ async function cleanupCodexAgents(result, dirs) {
88
+ const agentsDir = join(dirs.codexDir, "agents");
89
+ if (!await fs.pathExists(agentsDir)) {
90
+ result.skipped.push("~/.codex/agents (not found)");
91
+ return;
92
+ }
93
+ try {
94
+ let lyFiles = 0;
95
+ for (const file of await readdir(agentsDir)) {
96
+ if (!(file.startsWith("ly-") && file.endsWith(".toml")))
97
+ continue;
98
+ lyFiles++;
99
+ const filePath = join(agentsDir, file);
100
+ const content = await readFile(filePath, "utf-8");
101
+ if (!/ly-workflow|Installed by: npx ly-workflow|Added by ly-workflow-codex/.test(content)) {
102
+ result.skipped.push(`~/.codex/agents/${file} (no ly-workflow write marker)`);
103
+ continue;
104
+ }
105
+ try {
106
+ await removePath(join(agentsDir, file));
107
+ result.cleaned.push(`~/.codex/agents/${file}`);
108
+ } catch (error) {
109
+ result.failed.push(`~/.codex/agents/${file}: ${error instanceof Error ? error.message : String(error)}`);
110
+ }
111
+ }
112
+ if (lyFiles === 0)
113
+ result.skipped.push("~/.codex/agents (no ly-*.toml)");
114
+ } catch (error) {
115
+ result.failed.push(`~/.codex/agents: ${error instanceof Error ? error.message : String(error)}`);
116
+ }
117
+ }
118
+ async function cleanupLegacyArtifacts(options) {
119
+ const result = { cleaned: [], skipped: [], failed: [] };
120
+ const dirs = {
121
+ codexDir: options?.codexDir ?? join(homedir(), ".codex")
122
+ };
123
+ await cleanupCodexAgentsMd(result, dirs);
124
+ await cleanupCodexConfigTomlLyBlocks(result, dirs);
125
+ await cleanupCodexAgents(result, dirs);
126
+ return result;
127
+ }
128
+ function reportCleanupResult(result) {
129
+ if (result.cleaned.length > 0) {
130
+ console.log(` \u2713 \u6B8B\u7559\u6E05\u7406 cleaned: ${result.cleaned.length} \u9879`);
131
+ for (const item of result.cleaned) console.log(` - ${item}`);
132
+ }
133
+ if (result.skipped.length > 0) {
134
+ console.log(` - \u6B8B\u7559\u6E05\u7406 skipped: ${result.skipped.length} \u9879\uFF08\u4E0D\u5B58\u5728\u6216\u65E0\u9700\u5904\u7406\uFF09`);
135
+ for (const item of result.skipped) console.log(` - ${item}`);
136
+ }
137
+ if (result.failed.length > 0) {
138
+ console.log(" \u26A0 \u6B8B\u7559\u6E05\u7406\u5931\u8D25\uFF08\u4E0D\u963B\u65AD\uFF09:");
139
+ for (const item of result.failed) console.log(` - ${item}`);
140
+ }
141
+ }
142
+
143
+ export { cleanupLegacyArtifacts, reportCleanupResult };
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+
package/dist/cli.mjs ADDED
@@ -0,0 +1,340 @@
1
+ #!/usr/bin/env node
2
+ import cac from 'cac';
3
+ import { homedir } from 'node:os';
4
+ import ansis from 'ansis';
5
+ import inquirer from 'inquirer';
6
+ import { join } from 'pathe';
7
+ import { a as i18n, t as AGENTS_SKILLS_DIR, r as readLyConfig, L as LY_PROMPTS_DIR, v as version, P as PACKAGE_NAME, b as initI18n, s as showMainMenu, i as init, l as uninstallWorkflows, B as BIN_NAME } from './shared/ly-workflow-codex.K-E7PMp3.mjs';
8
+ import { execFile, spawn, execSync } from 'node:child_process';
9
+ import fs from 'fs-extra';
10
+ import { existsSync } from 'node:fs';
11
+ import 'node:util';
12
+ import 'ora';
13
+ import 'smol-toml';
14
+ import 'i18next';
15
+ import 'node:url';
16
+
17
+ const DEPENDENT_COMMANDS = ["@lyx-init", "@lyx-explore", "@lyx-propose", "@lyx-review-plan", "@lyx-archive"];
18
+ const INSTALL_CMD = ["npm", "install", "-g", "@fission-ai/openspec@latest"];
19
+ const SHELL_OPT = process.platform === "win32" ? { shell: true } : {};
20
+ const OPENSPEC_SKILL_NAMES = [
21
+ "openspec-explore",
22
+ "openspec-propose",
23
+ "openspec-apply-change",
24
+ "openspec-archive-change"
25
+ ];
26
+ function detectOpenspecSkills() {
27
+ const roots = [
28
+ AGENTS_SKILLS_DIR,
29
+ join(process.cwd(), ".agents", "skills")
30
+ ];
31
+ return OPENSPEC_SKILL_NAMES.some(
32
+ (name) => roots.some((root) => existsSync(join(root, name, "SKILL.md")))
33
+ );
34
+ }
35
+ function detectOpenspecCli() {
36
+ return new Promise((resolve) => {
37
+ execFile("openspec", ["--version"], { timeout: 5e3, ...SHELL_OPT }, (err, stdout) => {
38
+ if (err) {
39
+ if ("killed" in err && err.killed || err.code === "ETIMEDOUT") {
40
+ resolve({ installed: true, version: "unknown" });
41
+ return;
42
+ }
43
+ resolve({ installed: false });
44
+ return;
45
+ }
46
+ resolve({ installed: true, version: stdout.toString().trim() || "unknown" });
47
+ });
48
+ });
49
+ }
50
+ function printUnavailable() {
51
+ console.log(ansis.yellow(` ${i18n.t("common:preflight.unavailableList", { list: DEPENDENT_COMMANDS.join(" ") })}`));
52
+ console.log(ansis.gray(` ${i18n.t("common:preflight.unaffectedNote")}`));
53
+ }
54
+ async function runNpmInstall() {
55
+ return new Promise((resolve) => {
56
+ const [cmd, ...args] = INSTALL_CMD;
57
+ const child = spawn(cmd, args, { stdio: "inherit", ...SHELL_OPT });
58
+ child.on("error", () => resolve(false));
59
+ child.on("close", (code) => resolve(code === 0));
60
+ });
61
+ }
62
+ function isNonInteractive(skipPrompt) {
63
+ return Boolean(skipPrompt || process.env.CI || !process.stdin.isTTY || !process.stdout.isTTY);
64
+ }
65
+ async function checkExternalDeps(options) {
66
+ try {
67
+ const cli = await detectOpenspecCli();
68
+ if (cli.installed) {
69
+ if (!detectOpenspecSkills()) {
70
+ console.log(ansis.yellow(`\u26A0 ${i18n.t("common:preflight.skillsMissingCodex")}`));
71
+ }
72
+ return;
73
+ }
74
+ console.log(ansis.yellow(`\u26A0 ${i18n.t("common:preflight.cliMissing")}`));
75
+ if (isNonInteractive(options?.skipPrompt)) {
76
+ printUnavailable();
77
+ return;
78
+ }
79
+ const { confirmed } = await inquirer.prompt([{
80
+ type: "confirm",
81
+ name: "confirmed",
82
+ message: i18n.t("common:preflight.installAsk"),
83
+ default: true
84
+ }]);
85
+ if (!confirmed) {
86
+ printUnavailable();
87
+ return;
88
+ }
89
+ const ok = await runNpmInstall();
90
+ if (!ok) {
91
+ console.error(ansis.red(`\u2717 ${i18n.t("common:preflight.installFailed")}`));
92
+ printUnavailable();
93
+ return;
94
+ }
95
+ const recheck = await detectOpenspecCli();
96
+ if (!recheck.installed) {
97
+ console.log(ansis.yellow(`\u26A0 ${i18n.t("common:preflight.installNotInPath")}`));
98
+ return;
99
+ }
100
+ if (detectOpenspecSkills()) {
101
+ console.log(ansis.green(`\u2713 ${i18n.t("common:preflight.installSuccessWithSkills")}`));
102
+ } else {
103
+ console.log(ansis.green(`\u2713 ${i18n.t("common:preflight.installSuccessNeedInit")}`));
104
+ }
105
+ } catch {
106
+ }
107
+ }
108
+
109
+ const OK = ansis.green("\u2713");
110
+ const WARN = ansis.yellow("\u26A0");
111
+ const FAIL = ansis.red("\u2717");
112
+ async function fileExists(p) {
113
+ return fs.pathExists(p);
114
+ }
115
+ async function dirFiles(p) {
116
+ if (!await fs.pathExists(p))
117
+ return [];
118
+ return (await fs.readdir(p)).filter((f) => !f.startsWith("."));
119
+ }
120
+ function execSafe(cmd) {
121
+ try {
122
+ return execSync(cmd, { stdio: "pipe", timeout: 1e4 }).toString().trim();
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+ async function doctor() {
128
+ const checks = [];
129
+ const nodeVer = process.version;
130
+ const major = Number.parseInt(nodeVer.slice(1));
131
+ checks.push({
132
+ label: "Node.js",
133
+ status: major >= 20 ? OK : FAIL,
134
+ detail: `${nodeVer}${major < 20 ? " (requires >=20)" : ""}`
135
+ });
136
+ const config = await readLyConfig();
137
+ checks.push({
138
+ label: "config",
139
+ status: config ? OK : WARN,
140
+ detail: config ? `v${config.general?.version || "?"}, lang=${config.general?.language || "?"}` : "Not found (~/.ly/config.toml)"
141
+ });
142
+ const skillsEntries = await dirFiles(AGENTS_SKILLS_DIR);
143
+ const cmdCount = skillsEntries.filter((f) => f.startsWith("lyx-")).length;
144
+ checks.push({
145
+ label: "Commands",
146
+ status: cmdCount > 0 ? OK : FAIL,
147
+ detail: `${cmdCount} installed (~/.agents/skills/lyx-*/)`
148
+ });
149
+ const roleDir = join(LY_PROMPTS_DIR, "codex");
150
+ const roleFiles = (await dirFiles(roleDir)).filter((f) => f.endsWith(".md"));
151
+ checks.push({
152
+ label: "Roles",
153
+ status: roleFiles.length >= 2 ? OK : roleFiles.length > 0 ? WARN : FAIL,
154
+ detail: roleFiles.length > 0 ? roleFiles.join(", ") : "None (~/.ly/prompts/codex/)"
155
+ });
156
+ const openspecCli = await detectOpenspecCli();
157
+ checks.push({
158
+ label: "OpenSpec CLI",
159
+ status: openspecCli.installed ? OK : WARN,
160
+ detail: openspecCli.installed ? `v${openspecCli.version}` : i18n.t("common:doctor.openspecCliMissing")
161
+ });
162
+ const hasOpenspecSkills = detectOpenspecSkills();
163
+ checks.push({
164
+ label: "OpenSpec skills",
165
+ status: hasOpenspecSkills ? OK : WARN,
166
+ detail: hasOpenspecSkills ? i18n.t("common:doctor.skillsInitialized") : i18n.t("common:doctor.skillsMissing")
167
+ });
168
+ console.log();
169
+ console.log(ansis.cyan.bold(` ly-workflow-codex Doctor v${version}`));
170
+ console.log();
171
+ for (const { label, status: status2, detail } of checks) {
172
+ console.log(` ${status2} ${ansis.bold(label.padEnd(20))} ${ansis.gray(detail)}`);
173
+ }
174
+ const failures = checks.filter((c) => c.status === FAIL);
175
+ console.log();
176
+ if (failures.length === 0) {
177
+ console.log(ansis.green(" All checks passed."));
178
+ } else {
179
+ console.log(ansis.red(` ${failures.length} issue(s) found. Run ${ansis.cyan(`npx ${PACKAGE_NAME}`)} to reinstall.`));
180
+ }
181
+ console.log();
182
+ }
183
+ async function status() {
184
+ const config = await readLyConfig();
185
+ const installedVer = config?.general?.version || "unknown";
186
+ const latestVer = execSafe(`npm view ${PACKAGE_NAME} version`) || "unknown";
187
+ const cmds = (await dirFiles(AGENTS_SKILLS_DIR)).filter((f) => f.startsWith("lyx-"));
188
+ const reviewModel = config?.codexHost?.reviewModel || "\u672A\u914D\u7F6E\uFF08\u56DE\u9000\u5F53\u524D\u4F1A\u8BDD\u6A21\u578B\uFF09";
189
+ let activeTasks = 0;
190
+ const tasksDir = join(process.cwd(), ".ly", "tasks");
191
+ if (await fileExists(tasksDir)) {
192
+ for (const d of await fs.readdir(tasksDir)) {
193
+ if (d === "archive")
194
+ continue;
195
+ const taskJson = join(tasksDir, d, "task.json");
196
+ if (await fileExists(taskJson)) {
197
+ try {
198
+ const t = await fs.readJSON(taskJson);
199
+ const s = String(t.status || "").toLowerCase();
200
+ if (!["completed", "complete", "done", "finished", "archived", "cancelled", "closed"].includes(s)) {
201
+ activeTasks++;
202
+ }
203
+ } catch {
204
+ }
205
+ }
206
+ }
207
+ }
208
+ const openspecCli = await detectOpenspecCli();
209
+ const hasOpenspecSkills = detectOpenspecSkills();
210
+ console.log();
211
+ console.log(ansis.cyan.bold(" ly-workflow-codex Status"));
212
+ console.log();
213
+ console.log(` ${ansis.bold("Version")} ${installedVer}${installedVer !== latestVer ? ansis.yellow(` (latest: ${latestVer})`) : ansis.green(" (up to date)")}`);
214
+ console.log(` ${ansis.bold("Commands")} ${cmds.length}`);
215
+ console.log(` ${ansis.bold("Review model")} ${reviewModel}`);
216
+ console.log(` ${ansis.bold("OpenSpec CLI")} ${openspecCli.installed ? `v${openspecCli.version}` : ansis.yellow(i18n.t("common:doctor.openspecCliMissing"))}`);
217
+ console.log(` ${ansis.bold("OpenSpec skills")}${hasOpenspecSkills ? ` ${i18n.t("common:doctor.skillsInitialized")}` : ansis.yellow(` ${i18n.t("common:doctor.skillsMissing")}`)}`);
218
+ console.log(` ${ansis.bold("Active tasks")} ${activeTasks > 0 ? ansis.yellow(String(activeTasks)) : "0"}`);
219
+ console.log();
220
+ }
221
+
222
+ function customizeHelp(sections) {
223
+ sections.unshift({
224
+ title: "",
225
+ body: ansis.cyan.bold(`ly-workflow-codex \u2014 Codex \u5355 Agent \u5DE5\u4F5C\u6D41 + OpenSpec \u53CC\u5BA1\u67E5\u5173\u5361 v${version}`)
226
+ });
227
+ sections.push({
228
+ title: ansis.yellow(i18n.t("cli:help.commands")),
229
+ body: [
230
+ ` ${ansis.cyan(BIN_NAME)} ${i18n.t("cli:help.commandDescriptions.showMenu")}`,
231
+ ` ${ansis.cyan(`${BIN_NAME} init`)} | ${ansis.cyan("i")} ${i18n.t("cli:help.commandDescriptions.initConfig")}`,
232
+ ` ${ansis.cyan(`${BIN_NAME} doctor`)} Check installation health`,
233
+ ` ${ansis.cyan(`${BIN_NAME} status`)} Show installation overview`,
234
+ ` ${ansis.cyan(`${BIN_NAME} uninstall`)} Uninstall ${PACKAGE_NAME} (non-interactive)`,
235
+ "",
236
+ ansis.gray(` ${i18n.t("cli:help.shortcuts")}`),
237
+ ` ${ansis.cyan(`${BIN_NAME} i`)} ${i18n.t("cli:help.shortcutDescriptions.quickInit")}`
238
+ ].join("\n")
239
+ });
240
+ sections.push({
241
+ title: ansis.yellow(i18n.t("cli:help.options")),
242
+ body: [
243
+ ` ${ansis.green("--lang, -l")} <lang> ${i18n.t("cli:help.optionDescriptions.displayLanguage")} (zh-CN, en)`,
244
+ ` ${ansis.green("--force, -f")} ${i18n.t("cli:help.optionDescriptions.forceOverwrite")}`,
245
+ ` ${ansis.green("--help, -h")} ${i18n.t("cli:help.optionDescriptions.displayHelp")}`,
246
+ ` ${ansis.green("--version, -v")} ${i18n.t("cli:help.optionDescriptions.displayVersion")}`,
247
+ ` ${ansis.green("--yes, -y")} ${i18n.t("cli:help.optionDescriptions.skipConfirmation")}`,
248
+ "",
249
+ ansis.gray(` ${i18n.t("cli:help.nonInteractiveMode")}`),
250
+ ` ${ansis.green("--skip-prompt, -s")} ${i18n.t("cli:help.optionDescriptions.skipAllPrompts")}`,
251
+ ` ${ansis.green("--workflows, -w")} <list> ${i18n.t("cli:help.optionDescriptions.workflows")}`,
252
+ ` ${ansis.green("--install-dir, -d")} <path> ${i18n.t("cli:help.optionDescriptions.installDir")}`
253
+ ].join("\n")
254
+ });
255
+ sections.push({
256
+ title: ansis.yellow(i18n.t("cli:help.examples")),
257
+ body: [
258
+ ansis.gray(` # ${i18n.t("cli:help.exampleDescriptions.showInteractiveMenu")}`),
259
+ ` ${ansis.cyan(`npx ${PACKAGE_NAME}`)}`,
260
+ "",
261
+ ansis.gray(` # ${i18n.t("cli:help.exampleDescriptions.runFullInitialization")}`),
262
+ ` ${ansis.cyan(`npx ${PACKAGE_NAME} init`)}`,
263
+ ` ${ansis.cyan(`npx ${PACKAGE_NAME} i`)}`,
264
+ "",
265
+ ansis.gray(` # ${i18n.t("cli:help.exampleDescriptions.customModels")}`),
266
+ ` ${ansis.cyan(`npx ${PACKAGE_NAME} init -s`)}`,
267
+ ""
268
+ ].join("\n")
269
+ });
270
+ return sections;
271
+ }
272
+ async function setupCommands(cli) {
273
+ try {
274
+ const config = await readLyConfig();
275
+ const defaultLang = config?.general?.language || "zh-CN";
276
+ await initI18n(defaultLang);
277
+ } catch {
278
+ await initI18n("zh-CN");
279
+ }
280
+ cli.command("", i18n.t("cli:help.commandDescriptions.showMenu")).option("--lang, -l <lang>", `${i18n.t("cli:help.optionDescriptions.displayLanguage")} (zh-CN, en)`).action(async (options) => {
281
+ if (options.lang) {
282
+ await initI18n(options.lang);
283
+ }
284
+ await checkExternalDeps();
285
+ await showMainMenu();
286
+ });
287
+ cli.command("init", i18n.t("cli:help.commandDescriptions.initConfig")).alias("i").option("--lang, -l <lang>", `${i18n.t("cli:help.optionDescriptions.displayLanguage")} (zh-CN, en)`).option("--force, -f", i18n.t("cli:help.optionDescriptions.forceOverwrite")).option("--skip-prompt, -s", i18n.t("cli:help.optionDescriptions.skipAllPrompts")).option("--workflows, -w <workflows>", i18n.t("cli:help.optionDescriptions.workflows")).option("--install-dir, -d <path>", i18n.t("cli:help.optionDescriptions.installDir")).action(async (options) => {
288
+ if (options.lang) {
289
+ await initI18n(options.lang);
290
+ }
291
+ await checkExternalDeps({ skipPrompt: options.skipPrompt });
292
+ await init(options);
293
+ });
294
+ cli.command("doctor", "Check ly-workflow-codex installation health").action(async () => {
295
+ await doctor();
296
+ });
297
+ cli.command("status", "Show ly-workflow-codex installation status").action(async () => {
298
+ await status();
299
+ });
300
+ cli.command("uninstall", "Uninstall ly-workflow-codex workflows (~/.agents/skills/lyx-*, ~/.ly/prompts/codex/)").option("--yes, -y", "Skip confirmation").action(async (options) => {
301
+ const installDir = join(homedir(), ".codex");
302
+ if (!options.yes) {
303
+ const { confirm } = await inquirer.prompt([{
304
+ type: "confirm",
305
+ name: "confirm",
306
+ message: `\u786E\u5B9A\u8981\u5378\u8F7D ${PACKAGE_NAME} \u5417\uFF1F\u5C06\u79FB\u9664 ~/.agents/skills/lyx-*\uFF08\u542B\u65E7 ~/.agents/skills/ly-* \u4E0E ~/.codex/prompts/ly-*.md \u6B8B\u7559\uFF09\u4E0E ~/.ly/prompts/codex/\uFF1B\u5171\u4EAB\u914D\u7F6E ~/.ly/config.toml \u4E0E ~/.ly/ \u5176\u4F59\u5185\u5BB9\uFF08\u542B worktrees\uFF09\u4FDD\u7559\u3002`,
307
+ default: false
308
+ }]);
309
+ if (!confirm) {
310
+ console.log(ansis.gray("\u5378\u8F7D\u5DF2\u53D6\u6D88"));
311
+ return;
312
+ }
313
+ }
314
+ const result = await uninstallWorkflows(installDir);
315
+ if (result.success) {
316
+ console.log(ansis.green(`\u2713 ${PACKAGE_NAME} uninstalled`));
317
+ if (result.removedSkills.length > 0)
318
+ console.log(ansis.gray(` lyx-* skills: ${result.removedSkills.length} removed`));
319
+ if (result.removedLegacyPrompts.length > 0)
320
+ console.log(ansis.gray(` Legacy ~/.codex/prompts residue: ${result.removedLegacyPrompts.length} removed`));
321
+ if (result.removedSharedPrompts)
322
+ console.log(ansis.gray(" Shared prompts (~/.ly/prompts/codex/): removed"));
323
+ if (result.configTomlKept)
324
+ console.log(ansis.gray(" Shared config (~/.ly/config.toml): kept (~/.ly is the ly-workflow shared namespace)"));
325
+ } else {
326
+ console.error(ansis.red("\u2717 Uninstall failed"));
327
+ for (const err of result.errors) console.error(ansis.gray(` ${err}`));
328
+ process.exitCode = 1;
329
+ }
330
+ });
331
+ cli.help((sections) => customizeHelp(sections));
332
+ cli.version(version);
333
+ }
334
+
335
+ async function main() {
336
+ const cli = cac(BIN_NAME);
337
+ await setupCommands(cli);
338
+ cli.parse();
339
+ }
340
+ main().catch(console.error);