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.
@@ -0,0 +1,2068 @@
1
+ import { exec } from 'node:child_process';
2
+ import { homedir } from 'node:os';
3
+ import { promisify } from 'node:util';
4
+ import ansis from 'ansis';
5
+ import fs from 'fs-extra';
6
+ import inquirer from 'inquirer';
7
+ import ora from 'ora';
8
+ import { join, dirname, basename } from 'pathe';
9
+ import { parse, stringify } from 'smol-toml';
10
+ import i18next from 'i18next';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const version = "0.2.0";
14
+
15
+ const i18n = i18next;
16
+ const zhCN = {
17
+ common: {
18
+ yes: "\u662F",
19
+ no: "\u5426",
20
+ preflight: {
21
+ cliMissing: "\u672A\u68C0\u6D4B\u5230 openspec CLI\u2014\u2014OpenSpec \u751F\u547D\u5468\u671F\u547D\u4EE4\u4F9D\u8D56\u5B83\uFF08@fission-ai/openspec\uFF09",
22
+ installAsk: "\u662F\u5426\u73B0\u5728\u5168\u5C40\u5B89\u88C5 openspec\uFF1F(npm install -g @fission-ai/openspec@latest)",
23
+ installSuccessWithSkills: "openspec \u5DF2\u5B89\u88C5\uFF0COpenSpec \u547D\u4EE4\u6280\u80FD\u5DF2\u5C31\u7EEA\uFF0C\u5168\u90E8 @lyx-* skills \u53EF\u7528",
24
+ installSuccessNeedInit: "openspec \u5DF2\u5B89\u88C5\uFF1B\u8FD0\u884C @lyx-init \u5B8C\u6210 openspec init \u540E\uFF0COpenSpec \u751F\u547D\u5468\u671F\u547D\u4EE4\u53EF\u7528",
25
+ installFailed: "openspec \u5B89\u88C5\u5931\u8D25\uFF0C\u53EF\u7A0D\u540E\u624B\u52A8\u5B89\u88C5\uFF1Anpm install -g @fission-ai/openspec@latest",
26
+ installNotInPath: "openspec \u5DF2\u5B89\u88C5\u4F46\u5F53\u524D PATH \u4E2D\u672A\u68C0\u6D4B\u5230\u2014\u2014\u8BF7\u91CD\u5F00\u7EC8\u7AEF\uFF0C\u6216\u68C0\u67E5 npm \u5168\u5C40 bin \u76EE\u5F55\u662F\u5426\u5728 PATH \u4E2D",
27
+ unavailableList: "\u4EE5\u4E0B\u547D\u4EE4\u4F9D\u8D56 openspec\uFF0C\u5F53\u524D\u4E0D\u53EF\u7528\uFF1A{{list}}",
28
+ unaffectedNote: "@lyx-apply\u3001@lyx-review-code \u4EC5\u4F9D\u8D56\u9879\u76EE\u5185 change \u76EE\u5F55\u7ED3\u6784\uFF1BGit \u5DE5\u5177\u94FE\u4E0E\u8D28\u91CF\u5173\u5361\u6280\u80FD\u4E0D\u53D7\u5F71\u54CD",
29
+ skillsMissing: "\u68C0\u6D4B\u5230 openspec CLI \u5DF2\u5B89\u88C5\uFF0C\u4F46 OpenSpec \u547D\u4EE4\u6280\u80FD\u672A\u521D\u59CB\u5316\u2014\u2014\u5B8C\u6210\u540E\u8FD0\u884C @lyx-init \u521D\u59CB\u5316",
30
+ skillsMissingCodex: "codex \u5BBF\u4E3B\u7F3A\u5C11 openspec-* skills\u2014\u2014\u8FD0\u884C openspec init \u5B89\u88C5\u5230 ~/.agents/skills/ \u6216\u9879\u76EE .agents/skills/"
31
+ },
32
+ doctor: {
33
+ openspecCliMissing: "\u672A\u5B89\u88C5 (npm install -g @fission-ai/openspec)",
34
+ skillsInitialized: "\u5DF2\u521D\u59CB\u5316",
35
+ skillsMissing: "\u672A\u521D\u59CB\u5316\uFF08\u8FD0\u884C @lyx-init\uFF09"
36
+ },
37
+ confirm: "\u786E\u8BA4",
38
+ cancel: "\u53D6\u6D88",
39
+ back: "\u8FD4\u56DE",
40
+ exit: "\u9000\u51FA",
41
+ success: "\u6210\u529F",
42
+ error: "\u9519\u8BEF",
43
+ warning: "\u8B66\u544A",
44
+ info: "\u4FE1\u606F",
45
+ loading: "\u52A0\u8F7D\u4E2D...",
46
+ processing: "\u5904\u7406\u4E2D...",
47
+ completed: "\u5DF2\u5B8C\u6210",
48
+ failed: "\u5931\u8D25",
49
+ goodbye: "\u518D\u89C1\uFF01",
50
+ pressEnterToReturn: "\u6309 Enter \u8FD4\u56DE\u4E3B\u83DC\u5355...",
51
+ cancelled: "\u5DF2\u53D6\u6D88",
52
+ configNotModified: "\u672A\u4FEE\u6539\u914D\u7F6E",
53
+ configSaved: "\u914D\u7F6E\u5DF2\u4FDD\u5B58",
54
+ configFile: "\u914D\u7F6E\u6587\u4EF6",
55
+ unknownSubcommand: "\u672A\u77E5\u5B50\u547D\u4EE4: {{subcommand}}",
56
+ availableSubcommands: "\u53EF\u7528\u5B50\u547D\u4EE4: {{list}}"
57
+ },
58
+ cli: {
59
+ help: {
60
+ commands: "\u547D\u4EE4",
61
+ commandDescriptions: {
62
+ showMenu: "\u663E\u793A\u4EA4\u4E92\u5F0F\u83DC\u5355\uFF08\u9ED8\u8BA4\uFF09",
63
+ initConfig: "\u521D\u59CB\u5316 ly-workflow-codex"
64
+ },
65
+ shortcuts: "\u5FEB\u6377\u65B9\u5F0F:",
66
+ shortcutDescriptions: {
67
+ quickInit: "\u5FEB\u901F\u521D\u59CB\u5316"
68
+ },
69
+ options: "\u9009\u9879",
70
+ optionDescriptions: {
71
+ displayLanguage: "\u663E\u793A\u8BED\u8A00",
72
+ forceOverwrite: "\u5F3A\u5236\u8986\u76D6\u73B0\u6709\u914D\u7F6E",
73
+ displayHelp: "\u663E\u793A\u5E2E\u52A9\u4FE1\u606F",
74
+ displayVersion: "\u663E\u793A\u7248\u672C\u53F7",
75
+ skipAllPrompts: "\u8DF3\u8FC7\u6240\u6709\u4EA4\u4E92\u5F0F\u63D0\u793A\uFF08\u975E\u4EA4\u4E92\u6A21\u5F0F\uFF09",
76
+ skipConfirmation: "\u8DF3\u8FC7\u5378\u8F7D\u786E\u8BA4",
77
+ workflows: "\u8981\u5B89\u88C5\u7684\u5DE5\u4F5C\u6D41",
78
+ installDir: "\u5B89\u88C5\u76EE\u5F55"
79
+ },
80
+ nonInteractiveMode: "\u975E\u4EA4\u4E92\u6A21\u5F0F:",
81
+ examples: "\u793A\u4F8B",
82
+ exampleDescriptions: {
83
+ showInteractiveMenu: "\u663E\u793A\u4EA4\u4E92\u5F0F\u83DC\u5355",
84
+ runFullInitialization: "\u8FD0\u884C\u5B8C\u6574\u521D\u59CB\u5316",
85
+ customModels: "\u975E\u4EA4\u4E92\u5FEB\u901F\u521D\u59CB\u5316"
86
+ }
87
+ }
88
+ },
89
+ init: {
90
+ selectLanguage: "\u9009\u62E9\u8BED\u8A00 / Select language",
91
+ host: {
92
+ reviewModelPrompt: "\u5BA1\u67E5 agent A \u6A21\u578B\uFF08\u7559\u7A7A\u56DE\u9000\u5F53\u524D\u4F1A\u8BDD\u6A21\u578B\uFF1Bagent B / coding \u6A21\u578B\u8BF7\u8FD0\u884C lycx init \u914D\u7F6E\uFF09",
93
+ reviewModelHint: "\u5E38\u89C1\u5982 gpt-5.1-codex / gpt-5.1 / o4-mini\uFF0C\u53EF\u81EA\u7531\u8F93\u5165\uFF1B\u7559\u7A7A\u56DE\u9000",
94
+ codexSummaryTitle: "Codex \u914D\u7F6E\u6458\u8981:",
95
+ reviewModelUnset: "\u672A\u914D\u7F6E\uFF08\u56DE\u9000\u5F53\u524D\u4F1A\u8BDD\u6A21\u578B\uFF09"
96
+ },
97
+ mode: {
98
+ providerSelect: "\u9009\u62E9 API \u63D0\u4F9B\u65B9",
99
+ providerExisting: "\u5DF2\u6709 provider\uFF1A{{name}}",
100
+ providerOfficial: "OpenAI \u5B98\u65B9",
101
+ providerCustom: "\u81EA\u5B9A\u4E49\uFF08\u8F93\u5165\u540D\u79F0 / base_url / API key\uFF09",
102
+ customNamePrompt: "provider \u540D\u79F0\uFF08\u5199\u5165 [model_providers.<name>]\uFF09",
103
+ nameInvalid: "\u540D\u79F0\u4EC5\u5141\u8BB8\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u3001\u8FDE\u5B57\u7B26",
104
+ customUrlPrompt: "base_url",
105
+ customKeyPrompt: "API key\uFF08\u4EC5\u7528\u4E8E\u62C9\u53D6\u6A21\u578B\u5217\u8868\uFF0C\u4E0D\u5199\u5165 config.toml\uFF09",
106
+ optional: "\u53EF\u9009",
107
+ providerExists: "provider {{name}} \u5DF2\u5B58\u5728\u4E8E ~/.codex/config.toml\uFF0C\u76F4\u63A5\u9009\u7528",
108
+ providerWritten: "\u5DF2\u589E\u91CF\u5199\u5165 ~/.codex/config.toml\uFF1Amodel_provider = {{name}}\uFF08\u539F\u6709\u6CE8\u91CA\u4FDD\u7559\uFF09",
109
+ providerWriteFailed: "\u5199\u5165 ~/.codex/config.toml \u5931\u8D25\uFF1A{{error}}\uFF08\u672C\u6B21\u4E0D\u6301\u4E45\u5316\uFF0C\u4EC5\u7528\u4E8E\u62C9\u53D6\u6A21\u578B\u5217\u8868\uFF09",
110
+ configParseFailed: "~/.codex/config.toml \u89E3\u6790\u5931\u8D25\uFF0C\u672A\u8BC6\u522B\u5230\u5DF2\u6709 provider\uFF08\u4E0D\u5F71\u54CD\u7EE7\u7EED\uFF09",
111
+ modelFetching: "\u6B63\u5728\u4ECE {{baseUrl}}/models \u62C9\u53D6\u6A21\u578B\u5217\u8868...",
112
+ modelFetchFailed: "\u62C9\u53D6\u6A21\u578B\u5217\u8868\u5931\u8D25\uFF1A{{reason}}",
113
+ envKeyMissingPrompt: "\u73AF\u5883\u53D8\u91CF {{envKey}} \u672A\u8BBE\u7F6E\uFF0C\u8F93\u5165\u8BE5 provider \u7684 API key\uFF08\u76F4\u63A5\u56DE\u8F66\u8DF3\u8FC7\u9274\u6743\u62C9\u53D6\uFF09"
114
+ },
115
+ promptsMigrated: "\u89D2\u8272\u8BCD\u5DF2\u8FC1\u79FB\u5230 ~/.ly/prompts/\uFF08codex \u5171\u4EAB\u4F4D\u7F6E\uFF09",
116
+ promptsMigrationFailed: "\u65E7\u89D2\u8272\u8BCD\u8FC1\u79FB\u5931\u8D25\uFF08\u4E0D\u963B\u65AD\u5B89\u88C5\uFF09: {{error}}",
117
+ selectWorkflows: "\u9009\u62E9\u8981\u5B89\u88C5\u7684\u5DE5\u4F5C\u6D41\uFF08\u53EF\u591A\u9009\uFF09",
118
+ confirmInstall: "\u786E\u8BA4\u5B89\u88C5\u4EE5\u4E0A\u914D\u7F6E\uFF1F",
119
+ installing: "\u6B63\u5728\u5B89\u88C5...",
120
+ installSuccess: "\u5B89\u88C5\u6210\u529F\uFF01",
121
+ installFailed: "\u5B89\u88C5\u5931\u8D25",
122
+ installCancelled: "\u5B89\u88C5\u5DF2\u53D6\u6D88",
123
+ installedCommands: "\u5DF2\u5B89\u88C5\u547D\u4EE4:",
124
+ installedPrompts: "\u5DF2\u5B89\u88C5\u89D2\u8272\u63D0\u793A\u8BCD:",
125
+ installationErrors: "\u5B89\u88C5\u8FC7\u7A0B\u4E2D\u51FA\u73B0\u9519\u8BEF:",
126
+ summary: {
127
+ title: "\u914D\u7F6E\u6458\u8981:",
128
+ host: "\u5B89\u88C5\u5BBF\u4E3B",
129
+ reviewModelA: "\u5BA1\u67E5 agent A \u6A21\u578B",
130
+ reviewModelB: "\u5BA1\u67E5 agent B \u6A21\u578B",
131
+ codingModel: "coding \u5B9E\u65BD\u6A21\u578B",
132
+ reviewModelCodex: "\u5BA1\u67E5\u6A21\u578B",
133
+ workflows: "\u5DE5\u4F5C\u6D41:",
134
+ commandCount: "\u547D\u4EE4\u6570\u91CF"
135
+ },
136
+ model: {
137
+ title: "\u5BA1\u67E5\u6A21\u578B\u914D\u7F6E",
138
+ currentRouting: "\u5F53\u524D\u5BA1\u67E5\u6A21\u578B",
139
+ routingUpdated: "\u5BA1\u67E5\u6A21\u578B\u5DF2\u66F4\u65B0",
140
+ reinstalling: "\u6B63\u5728\u91CD\u88C5\u547D\u4EE4\u6A21\u677F\u4EE5\u5E94\u7528\u65B0\u914D\u7F6E...",
141
+ reinstallDone: "\u6A21\u677F\u5DF2\u66F4\u65B0",
142
+ reinstallFailed: "\u6A21\u677F\u66F4\u65B0\u5931\u8D25",
143
+ trioTitle: "\u6A21\u578B\u4E09\u8FDE\uFF08\u5BA1\u67E5 agent A / \u5BA1\u67E5 agent B / coding \u5B9E\u65BD\uFF09",
144
+ reviewModelA: "\u5BA1\u67E5 agent A \u6A21\u578B\uFF08reviewModel\uFF09",
145
+ reviewModelB: "\u5BA1\u67E5 agent B \u6A21\u578B\uFF08reviewModelB\uFF0C\u53EF\u9009\uFF09",
146
+ codingModel: "coding \u5B9E\u65BD\u6A21\u578B\uFF08codingModel\uFF0C\u53EF\u9009\uFF09",
147
+ customChoice: "\u81EA\u5B9A\u4E49\u8F93\u5165\u2026",
148
+ customPrompt: "\u8F93\u5165\u6A21\u578B\u540D\u79F0\uFF08\u7559\u7A7A = \u4E0D\u8BBE\u7F6E\uFF0C\u56DE\u9000\u5F53\u524D\u4F1A\u8BDD\u6A21\u578B\uFF09",
149
+ unsetChoice: "\u4E0D\u8BBE\u7F6E\uFF08\u56DE\u9000\u5F53\u524D\u4F1A\u8BDD\u6A21\u578B\uFF09",
150
+ reviewModelAPrompt: "\u5BA1\u67E5 agent A \u6A21\u578B\u540D\u79F0\uFF08\u7559\u7A7A = \u4E0D\u8BBE\u7F6E\uFF0C\u56DE\u9000\u5F53\u524D\u4F1A\u8BDD\u6A21\u578B\uFF09",
151
+ reviewModelBPrompt: "\u5BA1\u67E5 agent B \u6A21\u578B\u540D\u79F0\uFF08\u7559\u7A7A = \u4E0D\u8BBE\u7F6E\uFF0C\u56DE\u9000\u5F53\u524D\u4F1A\u8BDD\u6A21\u578B\uFF09",
152
+ codingModelPrompt: "coding \u5B9E\u65BD\u6A21\u578B\u540D\u79F0\uFF08\u7559\u7A7A = \u4E0D\u8BBE\u7F6E\uFF0C\u56DE\u9000\u5F53\u524D\u4F1A\u8BDD\u6A21\u578B\uFF09"
153
+ }
154
+ },
155
+ update: {
156
+ checking: "\u68C0\u67E5\u66F4\u65B0...",
157
+ checkingLatest: "\u6B63\u5728\u68C0\u67E5\u6700\u65B0\u7248\u672C...",
158
+ cannotConnect: "\u65E0\u6CD5\u8FDE\u63A5\u5230 npm registry\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5",
159
+ currentVersion: "\u5F53\u524D\u7248\u672C",
160
+ latestVersion: "\u6700\u65B0\u7248\u672C",
161
+ localWorkflow: "\u672C\u5730\u5DE5\u4F5C\u6D41",
162
+ newVersionFound: "\u53D1\u73B0\u65B0\u7248\u672C v{{latest}} (\u5F53\u524D: v{{current}})\uFF0C\u662F\u5426\u66F4\u65B0\uFF1F",
163
+ localOutdated: "\u68C0\u6D4B\u5230\u672C\u5730\u5DE5\u4F5C\u6D41\u7248\u672C (v{{local}}) \u4F4E\u4E8E\u5F53\u524D\u7248\u672C (v{{current}})\uFF0C\u662F\u5426\u66F4\u65B0\uFF1F",
164
+ alreadyLatest: "\u5F53\u524D\u5DF2\u662F\u6700\u65B0\u7248\u672C (v{{current}})\u3002\u662F\u5426\u5F3A\u5236\u91CD\u65B0\u5B89\u88C5/\u4FEE\u590D\u5DE5\u4F5C\u6D41\uFF1F",
165
+ cancelled: "\u5DF2\u53D6\u6D88\u66F4\u65B0",
166
+ starting: "\u5F00\u59CB\u66F4\u65B0...",
167
+ globalDetected: "\u68C0\u6D4B\u5230\u4F60\u662F\u901A\u8FC7 npm \u5168\u5C40\u5B89\u88C5\u7684",
168
+ packageLatest: "\u5F53\u524D\u5305\u7248\u672C\u5DF2\u662F\u6700\u65B0",
169
+ workflowOnly: "\u4EC5\u9700\u66F4\u65B0\u5DE5\u4F5C\u6D41\u6587\u4EF6",
170
+ recommendNpm: "\u63A8\u8350\u7684\u66F4\u65B0\u65B9\u5F0F\uFF1A",
171
+ willUpdateBoth: "\u8FD9\u5C06\u540C\u65F6\u66F4\u65B0\u547D\u4EE4\u548C\u5DE5\u4F5C\u6D41\u6587\u4EF6",
172
+ useNpmUpdate: "\u6539\u7528 npm \u66F4\u65B0\uFF08\u63A8\u8350\uFF09\uFF1F",
173
+ runInNewTerminal: "\u8BF7\u5728\u65B0\u7684\u7EC8\u7AEF\u7A97\u53E3\u4E2D\u8FD0\u884C\uFF1A",
174
+ autoUpdateAfter: "\u8FD0\u884C\u5B8C\u6210\u540E\uFF0C\u5F53\u524D\u7248\u672C\u5C06\u81EA\u52A8\u66F4\u65B0",
175
+ continueBuiltin: "\u7EE7\u7EED\u4F7F\u7528\u5185\u7F6E\u66F4\u65B0\uFF08\u4EC5\u66F4\u65B0\u5DE5\u4F5C\u6D41\u6587\u4EF6\uFF09",
176
+ willNotUpdateCli: "\u6CE8\u610F\uFF1A\u8FD9\u4E0D\u4F1A\u66F4\u65B0 lycx \u547D\u4EE4\u672C\u8EAB",
177
+ downloading: "\u6B63\u5728\u4E0B\u8F7D\u6700\u65B0\u7248\u672C...",
178
+ downloadDone: "\u6700\u65B0\u7248\u672C\u4E0B\u8F7D\u5B8C\u6210",
179
+ downloadFailed: "\u4E0B\u8F7D\u6700\u65B0\u7248\u672C\u5931\u8D25",
180
+ clearingCache: "\u6B63\u5728\u6E05\u7406 npx \u7F13\u5B58...",
181
+ removingOld: "\u6B63\u5728\u5220\u9664\u65E7\u5DE5\u4F5C\u6D41...",
182
+ oldRemoved: "\u65E7\u5DE5\u4F5C\u6D41\u5DF2\u5220\u9664",
183
+ partialRemoveFailed: "\u90E8\u5206\u6587\u4EF6\u5220\u9664\u5931\u8D25\uFF0C\u7EE7\u7EED\u5B89\u88C5...",
184
+ removeFailed: "\u5220\u9664\u65E7\u5DE5\u4F5C\u6D41\u65F6\u51FA\u9519: {{error}}\uFF0C\u7EE7\u7EED\u5B89\u88C5...",
185
+ installingNew: "\u6B63\u5728\u5B89\u88C5\u65B0\u7248\u672C\u5DE5\u4F5C\u6D41\u548C\u4E8C\u8FDB\u5236...",
186
+ installDone: "\u65B0\u7248\u672C\u5B89\u88C5\u6210\u529F",
187
+ installFailed: "\u5B89\u88C5\u65B0\u7248\u672C\u5931\u8D25",
188
+ installed: "\u5DF2\u5B89\u88C5 {{count}} \u4E2A\u547D\u4EE4:",
189
+ manualRetry: "\u8BF7\u5C1D\u8BD5\u624B\u52A8\u8FD0\u884C:",
190
+ updateDone: "\u66F4\u65B0\u5B8C\u6210\uFF01",
191
+ upgradedFromTo: "\u4ECE v{{from}} \u5347\u7EA7\u5230 v{{to}}",
192
+ reinstalled: "\u91CD\u65B0\u5B89\u88C5\u4E86 v{{version}}",
193
+ error: "\u66F4\u65B0\u5931\u8D25: {{error}}"
194
+ },
195
+ menu: {
196
+ title: "\u4E3B\u83DC\u5355",
197
+ options: {
198
+ init: "\u521D\u59CB\u5316 ly-workflow-codex",
199
+ update: "\u66F4\u65B0\u5DE5\u4F5C\u6D41",
200
+ configReviewModel: "\u914D\u7F6E\u5BA1\u67E5\u6A21\u578B",
201
+ uninstall: "\u5378\u8F7D ly-workflow-codex",
202
+ help: "\u5E2E\u52A9",
203
+ exit: "\u9000\u51FA"
204
+ },
205
+ help: {
206
+ title: "\u5DF2\u5B89\u88C5\u547D\u4EE4:",
207
+ hint: "\u7528 @lyx-propose \u63CF\u8FF0\u9700\u6C42\u751F\u6210\u65B9\u6848\uFF0C@lyx-apply \u5B9E\u65BD\u3002\u66F4\u591A\u4FE1\u606F: lycx --help"
208
+ },
209
+ uninstall: {
210
+ globalDetected: "\u68C0\u6D4B\u5230\u4F60\u662F\u901A\u8FC7 npm \u5168\u5C40\u5B89\u88C5\u7684",
211
+ twoSteps: "\u5B8C\u6574\u5378\u8F7D\u9700\u8981\u4E24\u6B65\uFF1A",
212
+ step1: "\u79FB\u9664\u5DE5\u4F5C\u6D41\u6587\u4EF6",
213
+ step1Hint: "\u5373\u5C06\u6267\u884C",
214
+ step2: "\u5378\u8F7D npm \u5168\u5C40\u5305",
215
+ step2Hint: "\u9700\u8981\u624B\u52A8\u6267\u884C",
216
+ continuePrompt: "\u7EE7\u7EED\u5378\u8F7D\u5DE5\u4F5C\u6D41\u6587\u4EF6\uFF1F",
217
+ confirm: "\u786E\u5B9A\u8981\u5378\u8F7D ly-workflow-codex \u5417\uFF1F\u5C06\u79FB\u9664 ~/.agents/skills/lyx-*\uFF08\u542B\u65E7 ~/.agents/skills/ly-* \u4E0E ~/.codex/prompts/ly-*.md \u6B8B\u7559\uFF09\u4E0E\u5171\u4EAB\u89D2\u8272\u8BCD ~/.ly/prompts/codex/\uFF1B\u5171\u4EAB\u914D\u7F6E ~/.ly/config.toml \u4E0E ~/.ly/ \u5176\u4F59\u5185\u5BB9\uFF08\u542B worktrees\uFF09\u4FDD\u7559\u3002",
218
+ uninstalling: "\u6B63\u5728\u5378\u8F7D...",
219
+ success: "\u5DE5\u4F5C\u6D41\u6587\u4EF6\u5DF2\u79FB\u9664",
220
+ removedSkills: "\u5DF2\u79FB\u9664 lyx-* skills:",
221
+ removedLegacyPrompts: "\u5DF2\u6E05\u7406\u65E7\u5B89\u88C5\u4F4D\u6B8B\u7559 (~/.agents/skills/ly-*\u3001~/.codex/prompts/ly-*.md):",
222
+ removedSharedPrompts: "\u5DF2\u6E05\u7406\u5171\u4EAB\u89D2\u8272\u8BCD (~/.ly/prompts/codex/):",
223
+ configTomlKept: "\u5171\u4EAB\u914D\u7F6E (~/.ly/config.toml) \u5DF2\u4FDD\u7559\uFF1A~/.ly \u4E3A ly-workflow \u5171\u4EAB\u547D\u540D\u7A7A\u95F4\uFF08\u542B worktrees \u7B49\u771F\u5B9E\u6570\u636E\uFF09\uFF0C\u5982\u9700\u6E05\u9664\u8BF7\u4EBA\u5DE5\u786E\u8BA4\u3002",
224
+ cancelled: "\u5378\u8F7D\u5DF2\u53D6\u6D88",
225
+ failed: "\u5378\u8F7D\u5931\u8D25",
226
+ lastStep: "\u6700\u540E\u4E00\u6B65\uFF1A\u5378\u8F7D npm \u5168\u5C40\u5305",
227
+ runInNewTerminal: "\u8BF7\u5728\u65B0\u7684\u7EC8\u7AEF\u7A97\u53E3\u4E2D\u8FD0\u884C\uFF1A",
228
+ afterDone: "\u5B8C\u6210\u540E lycx \u547D\u4EE4\u5C06\u5F7B\u5E95\u79FB\u9664"
229
+ }
230
+ }
231
+ };
232
+ const en = {
233
+ common: {
234
+ yes: "Yes",
235
+ no: "No",
236
+ preflight: {
237
+ cliMissing: "openspec CLI not found \u2014 OpenSpec lifecycle commands depend on it (@fission-ai/openspec)",
238
+ installAsk: "Install openspec globally now? (npm install -g @fission-ai/openspec@latest)",
239
+ installSuccessWithSkills: "openspec installed and OpenSpec command skills ready \u2014 all @lyx-* skills available",
240
+ installSuccessNeedInit: "openspec installed; run @lyx-init to complete openspec init before using OpenSpec lifecycle commands",
241
+ installFailed: "openspec installation failed \u2014 you can install it later: npm install -g @fission-ai/openspec@latest",
242
+ installNotInPath: "openspec was installed but not found on the current PATH \u2014 reopen your terminal or add the npm global bin directory to PATH",
243
+ unavailableList: "These commands depend on openspec and are currently unavailable: {{list}}",
244
+ unaffectedNote: "@lyx-apply and @lyx-review-code only need the in-project change directory; Git tools and quality-gate skills are unaffected",
245
+ skillsMissing: "openspec CLI is installed but OpenSpec command skills are not initialized \u2014 run @lyx-init to initialize",
246
+ skillsMissingCodex: "codex host is missing the openspec-* skills \u2014 run openspec init to install them into ~/.agents/skills/ or the project .agents/skills/"
247
+ },
248
+ doctor: {
249
+ openspecCliMissing: "Not found (npm install -g @fission-ai/openspec)",
250
+ skillsInitialized: "initialized",
251
+ skillsMissing: "not initialized (run @lyx-init)"
252
+ },
253
+ confirm: "Confirm",
254
+ cancel: "Cancel",
255
+ back: "Back",
256
+ exit: "Exit",
257
+ success: "Success",
258
+ error: "Error",
259
+ warning: "Warning",
260
+ info: "Info",
261
+ loading: "Loading...",
262
+ processing: "Processing...",
263
+ completed: "Completed",
264
+ failed: "Failed",
265
+ goodbye: "Goodbye!",
266
+ pressEnterToReturn: "Press Enter to return to main menu...",
267
+ cancelled: "Cancelled",
268
+ configNotModified: "Configuration not modified",
269
+ configSaved: "Configuration saved",
270
+ configFile: "Config file",
271
+ unknownSubcommand: "Unknown subcommand: {{subcommand}}",
272
+ availableSubcommands: "Available subcommands: {{list}}"
273
+ },
274
+ cli: {
275
+ help: {
276
+ commands: "Commands",
277
+ commandDescriptions: {
278
+ showMenu: "Show interactive menu (default)",
279
+ initConfig: "Initialize ly-workflow-codex"
280
+ },
281
+ shortcuts: "Shortcuts:",
282
+ shortcutDescriptions: {
283
+ quickInit: "Quick init"
284
+ },
285
+ options: "Options",
286
+ optionDescriptions: {
287
+ displayLanguage: "Display language",
288
+ forceOverwrite: "Force overwrite existing configuration",
289
+ displayHelp: "Display help",
290
+ displayVersion: "Display version",
291
+ skipAllPrompts: "Skip all interactive prompts (non-interactive mode)",
292
+ skipConfirmation: "Skip uninstall confirmation",
293
+ workflows: "Workflows to install",
294
+ installDir: "Installation directory"
295
+ },
296
+ nonInteractiveMode: "Non-interactive mode:",
297
+ examples: "Examples",
298
+ exampleDescriptions: {
299
+ showInteractiveMenu: "Show interactive menu",
300
+ runFullInitialization: "Run full initialization",
301
+ customModels: "Non-interactive quick init"
302
+ }
303
+ }
304
+ },
305
+ init: {
306
+ selectLanguage: "Select language / \u9009\u62E9\u8BED\u8A00",
307
+ host: {
308
+ reviewModelPrompt: "Model for review agent A (empty = fall back to the current session model; agent B / coding models can be configured via lycx init)",
309
+ reviewModelHint: "e.g. gpt-5.1-codex / gpt-5.1 / o4-mini, free-form input; leave empty to fall back",
310
+ codexSummaryTitle: "Codex Configuration Summary:",
311
+ reviewModelUnset: "not configured (fall back to the current session model)"
312
+ },
313
+ mode: {
314
+ providerSelect: "Select API provider",
315
+ providerExisting: "Existing provider: {{name}}",
316
+ providerOfficial: "OpenAI official",
317
+ providerCustom: "Custom (name / base_url / API key)",
318
+ customNamePrompt: "Provider name (written to [model_providers.<name>])",
319
+ nameInvalid: "Name allows letters, digits, underscores and hyphens only",
320
+ customUrlPrompt: "base_url",
321
+ customKeyPrompt: "API key (used only to fetch the model list, not written to config.toml)",
322
+ optional: "optional",
323
+ providerExists: "Provider {{name}} already exists in ~/.codex/config.toml \u2014 selected as-is",
324
+ providerWritten: "Incrementally written to ~/.codex/config.toml: model_provider = {{name}} (existing comments preserved)",
325
+ providerWriteFailed: "Failed to write ~/.codex/config.toml: {{error}} (not persisted this run; used only to fetch the model list)",
326
+ configParseFailed: "Failed to parse ~/.codex/config.toml \u2014 no existing providers detected (non-blocking)",
327
+ modelFetching: "Fetching model list from {{baseUrl}}/models...",
328
+ modelFetchFailed: "Failed to fetch the model list: {{reason}}",
329
+ envKeyMissingPrompt: "Environment variable {{envKey}} is not set \u2014 enter the API key for this provider (press Enter to skip authenticated fetching)"
330
+ },
331
+ promptsMigrated: "Role prompts migrated to ~/.ly/prompts/ (codex shared location)",
332
+ promptsMigrationFailed: "Legacy role prompt migration failed (non-blocking): {{error}}",
333
+ selectWorkflows: "Select workflows to install (multi-select)",
334
+ confirmInstall: "Confirm installation with above configuration?",
335
+ installing: "Installing...",
336
+ installSuccess: "Installation successful!",
337
+ installFailed: "Installation failed",
338
+ installCancelled: "Installation cancelled",
339
+ installedCommands: "Installed commands:",
340
+ installedPrompts: "Installed role prompts:",
341
+ installationErrors: "Installation errors:",
342
+ summary: {
343
+ title: "Configuration summary:",
344
+ host: "Install host",
345
+ reviewModelA: "Review agent A model",
346
+ reviewModelB: "Review agent B model",
347
+ codingModel: "Coding implementation model",
348
+ reviewModelCodex: "Review model",
349
+ workflows: "Workflows:",
350
+ commandCount: "Command count"
351
+ },
352
+ model: {
353
+ title: "Review model configuration",
354
+ currentRouting: "Current review model",
355
+ routingUpdated: "Review model updated",
356
+ reinstalling: "Reinstalling command templates to apply changes...",
357
+ reinstallDone: "Templates updated",
358
+ reinstallFailed: "Template update failed",
359
+ trioTitle: "Model trio (review agent A / review agent B / coding implementation)",
360
+ reviewModelA: "Review agent A model (reviewModel)",
361
+ reviewModelB: "Review agent B model (reviewModelB, optional)",
362
+ codingModel: "Coding implementation model (codingModel, optional)",
363
+ customChoice: "Custom input\u2026",
364
+ customPrompt: "Enter a model name (empty = not set, fall back to the current session model)",
365
+ unsetChoice: "Do not set (fall back to the current session model)",
366
+ reviewModelAPrompt: "Model for review agent A (empty = not set, fall back to the current session model)",
367
+ reviewModelBPrompt: "Model for review agent B (empty = not set, fall back to the current session model)",
368
+ codingModelPrompt: "Model for coding implementation (empty = not set, fall back to the current session model)"
369
+ }
370
+ },
371
+ update: {
372
+ checking: "Checking for updates...",
373
+ checkingLatest: "Checking latest version...",
374
+ cannotConnect: "Cannot connect to npm registry, please check your network",
375
+ currentVersion: "Current version",
376
+ latestVersion: "Latest version",
377
+ localWorkflow: "Local workflow",
378
+ newVersionFound: "New version v{{latest}} found (current: v{{current}}). Update?",
379
+ localOutdated: "Local workflow version (v{{local}}) is older than the current version (v{{current}}). Update?",
380
+ alreadyLatest: "Already on the latest version (v{{current}}). Force-reinstall/fix workflow files?",
381
+ cancelled: "Update cancelled",
382
+ starting: "Starting update...",
383
+ globalDetected: "Detected npm global installation",
384
+ packageLatest: "Package is already latest",
385
+ workflowOnly: "Only workflow files need an update",
386
+ recommendNpm: "Recommended update method:",
387
+ willUpdateBoth: "This will update both commands and workflow files",
388
+ useNpmUpdate: "Use npm update instead (recommended)?",
389
+ runInNewTerminal: "Please run in a new terminal window:",
390
+ autoUpdateAfter: "After completion, the current version will be updated automatically",
391
+ continueBuiltin: "Continue with the built-in update (workflow files only)",
392
+ willNotUpdateCli: "Note: this will not update the lycx command itself",
393
+ downloading: "Downloading the latest version...",
394
+ downloadDone: "Latest version downloaded",
395
+ downloadFailed: "Failed to download the latest version",
396
+ clearingCache: "Clearing npx cache...",
397
+ removingOld: "Removing old workflows...",
398
+ oldRemoved: "Old workflows removed",
399
+ partialRemoveFailed: "Some files failed to be removed, continuing...",
400
+ removeFailed: "Error removing old workflows: {{error}}, continuing...",
401
+ installingNew: "Installing new workflows...",
402
+ installDone: "New version installed",
403
+ installFailed: "Failed to install the new version",
404
+ installed: "Installed {{count}} commands:",
405
+ manualRetry: "Please try running manually:",
406
+ updateDone: "Update complete!",
407
+ upgradedFromTo: "Upgraded from v{{from}} to v{{to}}",
408
+ reinstalled: "Reinstalled v{{version}}",
409
+ error: "Update failed: {{error}}"
410
+ },
411
+ menu: {
412
+ title: "Main Menu",
413
+ options: {
414
+ init: "Initialize ly-workflow-codex",
415
+ update: "Update workflows",
416
+ configReviewModel: "Configure review model",
417
+ uninstall: "Uninstall ly-workflow-codex",
418
+ help: "Help",
419
+ exit: "Exit"
420
+ },
421
+ help: {
422
+ title: "Installed commands:",
423
+ hint: "Use @lyx-propose to describe a requirement and @lyx-apply to implement. More info: lycx --help"
424
+ },
425
+ uninstall: {
426
+ globalDetected: "Detected npm global installation",
427
+ twoSteps: "Full uninstall requires two steps:",
428
+ step1: "Remove workflow files",
429
+ step1Hint: "about to execute",
430
+ step2: "Uninstall npm global package",
431
+ step2Hint: "requires manual execution",
432
+ continuePrompt: "Continue removing workflow files?",
433
+ confirm: "Uninstall ly-workflow-codex? This removes ~/.agents/skills/lyx-* (incl. legacy ~/.agents/skills/ly-* and ~/.codex/prompts/ly-*.md residue) and shared prompts (~/.ly/prompts/codex/); shared config ~/.ly/config.toml and the rest of ~/.ly/ (incl. worktrees) are kept.",
434
+ uninstalling: "Uninstalling...",
435
+ success: "Workflow files removed",
436
+ removedSkills: "Removed lyx-* skills:",
437
+ removedLegacyPrompts: "Cleaned legacy install residue (~/.agents/skills/ly-*, ~/.codex/prompts/ly-*.md):",
438
+ removedSharedPrompts: "Removed shared prompts (~/.ly/prompts/codex/):",
439
+ configTomlKept: "Shared config (~/.ly/config.toml) kept: ~/.ly is the ly-workflow shared namespace (incl. worktrees); remove manually only after confirmation.",
440
+ cancelled: "Uninstallation cancelled",
441
+ failed: "Uninstallation failed",
442
+ lastStep: "Last step: Uninstall npm global package",
443
+ runInNewTerminal: "Please run in a new terminal window:",
444
+ afterDone: "After completion, the lycx command will be fully removed"
445
+ }
446
+ }
447
+ };
448
+ async function initI18n(lang = "zh-CN") {
449
+ if (!i18n.isInitialized) {
450
+ await i18n.init({
451
+ lng: lang,
452
+ fallbackLng: "en",
453
+ resources: {
454
+ "zh-CN": { translation: zhCN, ...zhCN },
455
+ "en": { translation: en, ...en }
456
+ },
457
+ interpolation: {
458
+ escapeValue: false
459
+ }
460
+ });
461
+ } else if (i18n.language !== lang) {
462
+ await i18n.changeLanguage(lang);
463
+ }
464
+ }
465
+ async function changeLanguage(lang) {
466
+ await i18n.changeLanguage(lang);
467
+ }
468
+
469
+ const PACKAGE_NAME = "ly-workflow-codex";
470
+ const BIN_NAME = "lycx";
471
+ const CODE_PROMPTS_DIR = join(homedir(), ".codex", "prompts");
472
+ const AGENTS_SKILLS_DIR = join(homedir(), ".agents", "skills");
473
+ const LY_DIR = join(homedir(), ".ly");
474
+ const CONFIG_FILE = join(LY_DIR, "config.toml");
475
+ const PROMPTS_DIR = join(LY_DIR, "prompts");
476
+ const ISSUES_URL = "https://github.com/FE-runner/ly-workflow-codex/issues";
477
+
478
+ const LY_PROMPTS_DIR = PROMPTS_DIR;
479
+ function getLyPromptsDir() {
480
+ return LY_PROMPTS_DIR;
481
+ }
482
+ function getLyDir() {
483
+ return LY_DIR;
484
+ }
485
+ function getConfigPath() {
486
+ return CONFIG_FILE;
487
+ }
488
+ function isValidInstalledHost(value) {
489
+ return value === "codex" || value === "claude";
490
+ }
491
+ function sanitizeInstalledHosts(value) {
492
+ const arr = Array.isArray(value) ? value : [];
493
+ return [...new Set(arr.filter(isValidInstalledHost))];
494
+ }
495
+ async function ensureLyDir() {
496
+ await fs.ensureDir(LY_DIR);
497
+ }
498
+ async function hasCoexistingLegacyLyProducts(home = homedir()) {
499
+ try {
500
+ if (await fs.pathExists(join(home, ".claude", "commands", "ly")))
501
+ return true;
502
+ const legacyConfig = join(home, ".claude", ".ly", "config.toml");
503
+ if (await fs.pathExists(legacyConfig)) {
504
+ const content = await fs.readFile(legacyConfig, "utf-8");
505
+ return /installedHosts\s*=.*claude/i.test(content) || content.includes(".claude");
506
+ }
507
+ } catch {
508
+ }
509
+ return false;
510
+ }
511
+ async function migrateLegacyConfig() {
512
+ const legacyConfig = join(homedir(), ".claude", ".ly", "config.toml");
513
+ try {
514
+ if (!await fs.pathExists(legacyConfig))
515
+ return false;
516
+ if (await fs.pathExists(CONFIG_FILE))
517
+ return false;
518
+ if (await hasCoexistingLegacyLyProducts()) {
519
+ console.warn("[lycx] \u68C0\u6D4B\u5230 ~/.claude/.ly/ \u4ECD\u88AB ly-workflow \u4F7F\u7528\uFF08\u5171\u5B58\u573A\u666F\uFF09\uFF0C\u8DF3\u8FC7\u914D\u7F6E\u8FC1\u79FB\uFF0C\u4FDD\u7559\u4ED6\u65B9\u5728\u7528\u914D\u7F6E");
520
+ return false;
521
+ }
522
+ await fs.ensureDir(LY_DIR);
523
+ await fs.move(legacyConfig, CONFIG_FILE, { overwrite: false });
524
+ return true;
525
+ } catch {
526
+ return false;
527
+ }
528
+ }
529
+ async function readLyConfig() {
530
+ try {
531
+ await migrateLegacyConfig();
532
+ if (await fs.pathExists(CONFIG_FILE)) {
533
+ const content = await fs.readFile(CONFIG_FILE, "utf-8");
534
+ const parsed = parse(content);
535
+ delete parsed.routing;
536
+ delete parsed.performance;
537
+ return parsed;
538
+ }
539
+ } catch {
540
+ }
541
+ return null;
542
+ }
543
+ async function writeLyConfig(config) {
544
+ await migrateLegacyConfig();
545
+ await ensureLyDir();
546
+ const content = stringify(config);
547
+ await fs.writeFile(CONFIG_FILE, content, "utf-8");
548
+ }
549
+ function createDefaultConfig(options) {
550
+ const config = {
551
+ general: {
552
+ version: version,
553
+ language: options.language,
554
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
555
+ },
556
+ workflows: {
557
+ installed: options.installedWorkflows
558
+ },
559
+ installedHosts: sanitizeInstalledHosts(options.installedHosts).length > 0 ? sanitizeInstalledHosts(options.installedHosts) : ["codex"],
560
+ paths: {
561
+ commands: PROMPTS_DIR,
562
+ prompts: LY_PROMPTS_DIR,
563
+ backup: join(LY_DIR, "backup")
564
+ }
565
+ };
566
+ const reviewModel = sanitizeReviewModel(options.codexHost?.reviewModel);
567
+ const reviewModelB = sanitizeModelField(options.codexHost?.reviewModelB);
568
+ const codingModel = sanitizeModelField(options.codexHost?.codingModel);
569
+ if (reviewModel || reviewModelB || codingModel) {
570
+ config.codexHost = {
571
+ ...reviewModel ? { reviewModel } : {},
572
+ ...reviewModelB ? { reviewModelB } : {},
573
+ ...codingModel ? { codingModel } : {}
574
+ };
575
+ }
576
+ return config;
577
+ }
578
+ function sanitizeReviewModel(value) {
579
+ if (typeof value !== "string")
580
+ return void 0;
581
+ const cleaned = value.trim().replace(/[^\w.:/-]/g, "");
582
+ return cleaned === "" ? void 0 : cleaned;
583
+ }
584
+ function sanitizeModelField(value) {
585
+ if (typeof value !== "string")
586
+ return void 0;
587
+ const cleaned = value.trim();
588
+ return cleaned === "" ? void 0 : cleaned;
589
+ }
590
+
591
+ function codexConfigPath() {
592
+ return join(homedir(), ".codex", "config.toml");
593
+ }
594
+ function sanitizeProviderName(value) {
595
+ if (typeof value !== "string")
596
+ return void 0;
597
+ const cleaned = value.trim().replace(/[^\w-]/g, "");
598
+ return cleaned === "" ? void 0 : cleaned;
599
+ }
600
+ async function readCodexConfigToml(filePath = codexConfigPath()) {
601
+ try {
602
+ if (!await fs.pathExists(filePath))
603
+ return null;
604
+ const content = await fs.readFile(filePath, "utf-8");
605
+ return parse(content);
606
+ } catch {
607
+ return null;
608
+ }
609
+ }
610
+ async function listModelProviders(filePath = codexConfigPath()) {
611
+ const parsed = await readCodexConfigToml(filePath);
612
+ const table = parsed?.model_providers;
613
+ if (!table || typeof table !== "object")
614
+ return [];
615
+ const providers = [];
616
+ for (const [name, entry] of Object.entries(table)) {
617
+ if (!entry || typeof entry !== "object")
618
+ continue;
619
+ providers.push({
620
+ name,
621
+ baseUrl: typeof entry.base_url === "string" ? entry.base_url : void 0,
622
+ envKey: typeof entry.env_key === "string" ? entry.env_key : void 0
623
+ });
624
+ }
625
+ return providers;
626
+ }
627
+ async function upsertModelProvider(input, filePath = codexConfigPath()) {
628
+ const name = sanitizeProviderName(input.name);
629
+ if (!name)
630
+ return { status: "error", error: "invalid provider name" };
631
+ const baseUrl = input.baseUrl.trim();
632
+ if (baseUrl === "")
633
+ return { status: "error", error: "base_url is required" };
634
+ let original = "";
635
+ try {
636
+ if (await fs.pathExists(filePath))
637
+ original = await fs.readFile(filePath, "utf-8");
638
+ } catch (error) {
639
+ return { status: "error", error: error instanceof Error ? error.message : String(error) };
640
+ }
641
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
642
+ const blockRe = new RegExp(`^\\s*\\[model_providers\\.(?:${escaped}|"${escaped}")\\]\\s*(#.*)?$`, "m");
643
+ if (blockRe.test(original))
644
+ return { status: "exists", name };
645
+ const block = [
646
+ `[model_providers.${name}]`,
647
+ `name = "${name}"`,
648
+ `base_url = "${baseUrl.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
649
+ ];
650
+ let updated;
651
+ if (original === "") {
652
+ updated = `# Added by ly-workflow-codex init
653
+ model_provider = "${name}"
654
+
655
+ ${block.join("\n")}
656
+ `;
657
+ } else {
658
+ const lines = original.split("\n");
659
+ let firstSectionIdx = lines.findIndex((l) => /^\s*\[/.test(l));
660
+ if (firstSectionIdx === -1)
661
+ firstSectionIdx = lines.length;
662
+ const existingKeyIdx = lines.slice(0, firstSectionIdx).findIndex((l) => /^\s*model_provider\s*=/.test(l));
663
+ if (existingKeyIdx >= 0) {
664
+ lines[existingKeyIdx] = `model_provider = "${name}"`;
665
+ } else {
666
+ lines.unshift(`model_provider = "${name}"`);
667
+ }
668
+ while (lines.length > 0 && lines[lines.length - 1] === "")
669
+ lines.pop();
670
+ updated = `${lines.join("\n")}
671
+
672
+ ${block.join("\n")}
673
+ `;
674
+ }
675
+ try {
676
+ await fs.ensureDir(join(filePath, ".."));
677
+ parse(updated);
678
+ await fs.writeFile(filePath, updated, "utf-8");
679
+ } catch (error) {
680
+ return { status: "error", error: error instanceof Error ? error.message : String(error) };
681
+ }
682
+ return { status: "written", path: filePath };
683
+ }
684
+ async function fetchCodexModels(input) {
685
+ const base = input.baseUrl.trim().replace(/\/+$/, "");
686
+ const url = `${base}/models`;
687
+ const headers = {};
688
+ if (input.apiKey)
689
+ headers.Authorization = `Bearer ${input.apiKey}`;
690
+ let res;
691
+ try {
692
+ res = await fetch(url, {
693
+ method: "GET",
694
+ headers,
695
+ signal: AbortSignal.timeout(input.timeoutMs ?? 1e4)
696
+ });
697
+ } catch (error) {
698
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
699
+ }
700
+ if (!res.ok)
701
+ return { ok: false, error: `HTTP ${res.status}` };
702
+ let json;
703
+ try {
704
+ json = await res.json();
705
+ } catch {
706
+ return { ok: false, error: "response is not valid JSON" };
707
+ }
708
+ const arr = Array.isArray(json) ? json : Array.isArray(json?.data) ? json.data : null;
709
+ if (!arr)
710
+ return { ok: false, error: "unexpected response shape (missing data[])" };
711
+ const models = [];
712
+ for (const item of arr) {
713
+ const id = typeof item === "string" ? item : typeof item?.id === "string" ? item.id : "";
714
+ const trimmed = id.trim();
715
+ if (trimmed !== "" && !models.includes(trimmed))
716
+ models.push(trimmed);
717
+ }
718
+ if (models.length === 0)
719
+ return { ok: false, error: "empty model list" };
720
+ return { ok: true, models };
721
+ }
722
+
723
+ const __filename$2 = fileURLToPath(import.meta.url);
724
+ const __dirname$2 = dirname(__filename$2);
725
+ function findPackageRoot$1(startDir) {
726
+ let dir = startDir;
727
+ for (let i = 0; i < 10; i++) {
728
+ if (fs.existsSync(join(dir, "package.json"))) {
729
+ if (fs.existsSync(join(dir, "templates"))) {
730
+ return dir;
731
+ }
732
+ }
733
+ const parent = dirname(dir);
734
+ if (parent === dir)
735
+ break;
736
+ dir = parent;
737
+ }
738
+ console.error(
739
+ `[lycx] \u26A0 PACKAGE_ROOT resolution failed: could not find package.json with templates/ directory.
740
+ Start dir: ${startDir}
741
+ Last checked: ${dir}
742
+ This will cause commands/skills/prompts to not be installed.
743
+ Please report this issue at: ${ISSUES_URL}`
744
+ );
745
+ return startDir;
746
+ }
747
+ const PACKAGE_ROOT = findPackageRoot$1(__dirname$2);
748
+ function injectConfigVariables(content, _config) {
749
+ let processed = content;
750
+ processed = processed.replace(/\{\{REVIEWER_MODEL\}\}/g, "codex");
751
+ processed = processed.replace(/\{\{IMPLEMENTER_MODEL\}\}/g, "codex");
752
+ processed = processed.replace(/\n?<!--\s*LY:IF:IMPLEMENTER_EXTERNAL\s*-->[\s\S]*?<!--\s*LY:ENDIF\s*-->\n?/g, "");
753
+ processed = processed.replace(/\n?<!--\s*LY:IF:IMPLEMENTER_CLAUDE\s*-->[\s\S]*?<!--\s*LY:ENDIF\s*-->\n?/g, "");
754
+ processed = processed.replace(/\{\{LITE_MODE_FLAG\}\}/g, "");
755
+ return processed;
756
+ }
757
+ function replaceHomePathsInTemplate(content, _installDir) {
758
+ const toForwardSlash = (path) => path.replace(/\\/g, "/");
759
+ return content.replace(/~\//g, `${toForwardSlash(homedir())}/`);
760
+ }
761
+
762
+ function renderCodexTemplate(content, config) {
763
+ let processed = injectConfigVariables(content);
764
+ const model = config.reviewModel?.trim() || "";
765
+ if (model) {
766
+ processed = processed.replace(/\{\{REVIEW_MODEL\}\}/g, model);
767
+ } else {
768
+ processed = processed.replace(/ -m \{\{REVIEW_MODEL\}\}/g, "");
769
+ processed = processed.replace(/\{\{REVIEW_MODEL\}\}/g, "");
770
+ }
771
+ return processed;
772
+ }
773
+ const CODEX_ROLE_FILE_TARGETS = ["reviewer.md", "plan-reviewer.md"];
774
+ const codexAdapter = {
775
+ id: "codex",
776
+ promptsTarget: (ctx) => ({
777
+ sourceDir: join(ctx.templateDir, "skills-codex"),
778
+ targetDir: ctx.codexSkillsDir,
779
+ filePrefix: "lyx-"
780
+ }),
781
+ renderTemplate: (content, ctx) => renderCodexTemplate(content, ctx.config),
782
+ uninstallList: async (ctx) => {
783
+ try {
784
+ if (!await fs.pathExists(ctx.codexSkillsDir))
785
+ return [];
786
+ const entries = await fs.readdir(ctx.codexSkillsDir);
787
+ const dirs = [];
788
+ for (const entry of entries) {
789
+ if (!entry.startsWith("lyx-"))
790
+ continue;
791
+ const full = join(ctx.codexSkillsDir, entry);
792
+ if ((await fs.stat(full)).isDirectory())
793
+ dirs.push(full);
794
+ }
795
+ return dirs;
796
+ } catch {
797
+ return [];
798
+ }
799
+ },
800
+ verify: async (ctx) => {
801
+ for (const file of CODEX_ROLE_FILE_TARGETS) {
802
+ const target = join(ctx.promptsDir, "codex", file);
803
+ if (!await fs.pathExists(target)) {
804
+ ctx.result.errors.push(`codex ROLE_FILE target missing: ${target} (shared prompts not installed)`);
805
+ ctx.result.success = false;
806
+ }
807
+ }
808
+ }
809
+ };
810
+ const ADAPTERS = {
811
+ codex: codexAdapter
812
+ };
813
+
814
+ function cmd(id, order, category, name, nameEn, description, descriptionEn, cmdOverride) {
815
+ return {
816
+ id,
817
+ name,
818
+ nameEn,
819
+ category,
820
+ commands: [cmdOverride ?? id],
821
+ defaultSelected: true,
822
+ order,
823
+ description,
824
+ descriptionEn
825
+ };
826
+ }
827
+ const CORE_CONFIGS = [
828
+ // ── Independent Tools ────────────────────────────────
829
+ cmd("init-project", 0, "init", "\u9879\u76EE\u521D\u59CB\u5316", "Project Init", "\u751F\u6210 AGENTS.md\uFF0C\u521D\u59CB\u5316 OpenSpec \u76EE\u5F55\u7ED3\u6784", "Generate AGENTS.md, initialize OpenSpec directory structure", "init"),
830
+ // ── Git ──────────────────────────────────────────────
831
+ cmd("commit", 10, "git", "Git \u63D0\u4EA4", "Git Commit", "\u667A\u80FD\u751F\u6210 conventional commit \u4FE1\u606F", "Smart conventional commit message generation"),
832
+ cmd("rollback", 11, "git", "Git \u56DE\u6EDA", "Git Rollback", "\u4EA4\u4E92\u5F0F\u56DE\u6EDA\u5206\u652F\u5230\u5386\u53F2\u7248\u672C", "Interactive rollback to historical version"),
833
+ cmd("clean-branches", 12, "git", "Git \u6E05\u7406\u5206\u652F", "Git Clean Branches", "\u5B89\u5168\u6E05\u7406\u5DF2\u5408\u5E76\u6216\u8FC7\u671F\u5206\u652F", "Safely clean merged or stale branches"),
834
+ cmd("worktree", 13, "git", "Git Worktree", "Git Worktree", "\u7BA1\u7406 Git worktree", "Manage Git worktree"),
835
+ // ── OpenSpec lifecycle (thin delegation to opsx:* skills) ──
836
+ cmd("explore", 20, "opsx", "\u63A2\u7D22\u6A21\u5F0F", "Explore", "\u59D4\u6258 opsx:explore\uFF0C\u60F3\u6E05\u695A\u518D\u52A8\u624B", "Delegates to opsx:explore \u2014 think before you build"),
837
+ cmd("propose", 21, "opsx", "\u63D0\u51FA\u65B9\u6848", "Propose", "\u59D4\u6258 opsx:propose\uFF0C\u751F\u6210 proposal/design/tasks", "Delegates to opsx:propose \u2014 generates proposal/design/tasks"),
838
+ cmd("apply", 22, "opsx", "\u5B9E\u65BD\u65B9\u6848", "Apply", "coding subagent \u5B9E\u65BD tasks\uFF1A\u8BFB tasks.md \u9010\u4EFB\u52A1\u5B9E\u65BD+\u9A8C\u8BC1+\u52FE\u9009\uFF0C\u56DE\u4F20\u4E3B\u4F1A\u8BDD\u7EDF\u4E00 commit", "Implements tasks via a coding subagent: reads tasks.md, implements + verifies + checks, returns to the main session for commit"),
839
+ cmd("archive", 23, "opsx", "\u5F52\u6863\u65B9\u6848", "Archive", "\u59D4\u6258 opsx:archive\uFF0C\u5B8C\u6210\u540E\u5F52\u6863", "Delegates to opsx:archive \u2014 archives a completed change"),
840
+ // ── Review gates (Codex-backed) ──────────────────────
841
+ cmd("review-plan", 30, "review", "\u65B9\u6848\u5BA1\u67E5", "Review Plan", "\u53CC\u5BA1\u67E5 subagent \u5BA1 proposal/design/tasks\uFF0C\u5206\u6B67\u4E3B\u4F1A\u8BDD\u62CD\u677F", "Dual-reviewer subagents review plan artifacts; main session arbitrates disagreements"),
842
+ cmd("review-code", 31, "review", "\u4EE3\u7801\u5BA1\u67E5", "Review Code", "\u53CC\u5BA1\u67E5 subagent \u5BA1\u4EE3\u7801\u53D8\u66F4\uFF0C\u5206\u7EA7\u8F93\u51FA Critical/Warning/Info", "Dual-reviewer subagents review code changes with severity grading"),
843
+ // ── Release pipeline ───────────────────────────────────
844
+ cmd("release", 40, "release", "GitFlow \u53D1\u7248", "GitFlow Release", "GitFlow \u56DB\u573A\u666F\u53D1\u7248\u6D41\u7A0B\uFF0CSemVer \u81EA\u52A8\u63A8\u5BFC\u7248\u672C\u53F7\uFF0C\u4E0A\u7EBF\u5408\u5E76\u4E8C\u9009\u4E00 + \u4E3B\u5206\u652F\u540D\u68C0\u6D4B", "GitFlow branching workflow with SemVer auto-detection, dual merge options and master/main detection"),
845
+ cmd("changelog", 41, "release", "\u751F\u6210 Changelog", "Generate Changelog", "Keep a Changelog \u683C\u5F0F\u751F\u6210 CHANGELOG.md\uFF0C\u6309 commit \u524D\u7F00\u5206\u7EC4", "Generate Keep a Changelog format CHANGELOG.md from commit history"),
846
+ cmd("publish", 42, "release", "npm \u53D1\u5E03", "npm Publish", "npm \u5305\u53D1\u5E03\uFF1Abmc \u79C1\u57DF/GitHub/npmjs/CI \u56DB\u573A\u666F", "npm publish to bmc Nexus/GitHub/npmjs/CI targets")
847
+ ];
848
+ const WORKFLOW_CONFIGS = CORE_CONFIGS;
849
+ function getWorkflowConfigs() {
850
+ return WORKFLOW_CONFIGS.sort((a, b) => a.order - b.order);
851
+ }
852
+ function getWorkflowById(id) {
853
+ return WORKFLOW_CONFIGS.find((w) => w.id === id);
854
+ }
855
+ function getCoreCommandIds() {
856
+ return CORE_CONFIGS.map((w) => w.id);
857
+ }
858
+ ({
859
+ full: {
860
+ description: `\u5168\u90E8\u547D\u4EE4\uFF08${WORKFLOW_CONFIGS.length}\u4E2A\uFF09`,
861
+ descriptionEn: `All commands (${WORKFLOW_CONFIGS.length})`,
862
+ workflows: WORKFLOW_CONFIGS.map((w) => w.id)
863
+ }
864
+ });
865
+
866
+ async function copyMdTemplates(ctx, srcDir, destDir, options = {}) {
867
+ const installed = [];
868
+ if (!await fs.pathExists(srcDir)) {
869
+ console.error(`[lycx] Template source directory not found: ${srcDir}`);
870
+ return installed;
871
+ }
872
+ await fs.ensureDir(destDir);
873
+ const files = await fs.readdir(srcDir);
874
+ for (const file of files) {
875
+ if (!file.endsWith(".md"))
876
+ continue;
877
+ const destFile = join(destDir, file);
878
+ if (ctx.force || !await fs.pathExists(destFile)) {
879
+ let content = await fs.readFile(join(srcDir, file), "utf-8");
880
+ if (options.inject)
881
+ content = injectConfigVariables(content, ctx.config);
882
+ content = replaceHomePathsInTemplate(content, ctx.installDir);
883
+ await fs.writeFile(destFile, content, "utf-8");
884
+ installed.push(file.replace(".md", ""));
885
+ }
886
+ }
887
+ return installed;
888
+ }
889
+ async function installCommandFiles(ctx, workflowIds, adapter) {
890
+ const target = adapter.promptsTarget(ctx);
891
+ const filePrefix = target.filePrefix ?? "";
892
+ await fs.ensureDir(target.targetDir);
893
+ for (const workflowId of workflowIds) {
894
+ const workflow = getWorkflowById(workflowId);
895
+ if (!workflow) {
896
+ ctx.result.errors.push(`Unknown workflow: ${workflowId}`);
897
+ continue;
898
+ }
899
+ for (const cmd of workflow.commands) {
900
+ const srcFile = join(target.sourceDir, `${cmd}.md`);
901
+ const destDir = join(target.targetDir, `${filePrefix}${cmd}`);
902
+ const destFile = join(destDir, "SKILL.md");
903
+ try {
904
+ if (await fs.pathExists(srcFile)) {
905
+ if (ctx.force || !await fs.pathExists(destFile)) {
906
+ let content = await fs.readFile(srcFile, "utf-8");
907
+ content = adapter.renderTemplate(content, ctx);
908
+ content = replaceHomePathsInTemplate(content, ctx.installDir);
909
+ await fs.ensureDir(destDir);
910
+ await fs.writeFile(destFile, content, "utf-8");
911
+ ctx.result.installedCommands.push(cmd);
912
+ } else {
913
+ ctx.result.skippedCommands ??= [];
914
+ ctx.result.skippedCommands.push(cmd);
915
+ }
916
+ } else {
917
+ const placeholder = `---
918
+ name: ${filePrefix}${cmd}
919
+ description: "${workflow.descriptionEn}"
920
+ ---
921
+
922
+ # ${filePrefix}${cmd}
923
+
924
+ ${workflow.description}
925
+ `;
926
+ await fs.ensureDir(destDir);
927
+ await fs.writeFile(destFile, placeholder, "utf-8");
928
+ ctx.result.installedCommands.push(cmd);
929
+ }
930
+ } catch (error) {
931
+ ctx.result.errors.push(`Failed to install ${cmd}: ${error}`);
932
+ ctx.result.success = false;
933
+ }
934
+ }
935
+ }
936
+ }
937
+ async function installPromptFiles(ctx) {
938
+ const srcDir = join(ctx.templateDir, "prompts", "codex");
939
+ if (!await fs.pathExists(srcDir)) {
940
+ ctx.result.errors.push(`Prompts template directory not found: ${srcDir}`);
941
+ return;
942
+ }
943
+ try {
944
+ const installed = await copyMdTemplates(
945
+ ctx,
946
+ srcDir,
947
+ join(ctx.promptsDir, "codex")
948
+ );
949
+ for (const name of installed) {
950
+ ctx.result.installedPrompts.push(`codex/${name}`);
951
+ }
952
+ } catch (error) {
953
+ ctx.result.errors.push(`Failed to install codex prompts: ${error}`);
954
+ ctx.result.success = false;
955
+ }
956
+ }
957
+ async function migrateLegacyPrompts(options) {
958
+ const home = options?.homeDir ?? homedir();
959
+ const oldDir = join(home, ".claude", ".ly", "prompts");
960
+ const newDir = options?.promptsDir || LY_PROMPTS_DIR;
961
+ if (!await fs.pathExists(oldDir)) {
962
+ return { migrated: false };
963
+ }
964
+ try {
965
+ if (await hasCoexistingLegacyLyProducts(home)) {
966
+ console.warn(`[lycx] \u68C0\u6D4B\u5230 ${join(home, ".claude")} \u4ECD\u88AB ly-workflow \u4F7F\u7528\uFF08\u5171\u5B58\u573A\u666F\uFF09\uFF0C\u8DF3\u8FC7\u89D2\u8272\u8BCD\u8FC1\u79FB\uFF0C\u4FDD\u7559\u4ED6\u65B9\u5728\u7528\u6587\u4EF6`);
967
+ return { migrated: false, from: oldDir, to: newDir };
968
+ }
969
+ await fs.ensureDir(newDir);
970
+ const entries = await fs.readdir(oldDir);
971
+ for (const entry of entries) {
972
+ if (entry === ".DS_Store")
973
+ continue;
974
+ const src = join(oldDir, entry);
975
+ const dst = join(newDir, entry);
976
+ if (await fs.pathExists(dst))
977
+ continue;
978
+ await fs.move(src, dst, { overwrite: false });
979
+ }
980
+ await fs.remove(oldDir);
981
+ return { migrated: true, from: oldDir, to: newDir };
982
+ } catch (error) {
983
+ return { migrated: false, from: oldDir, to: newDir, error: String(error) };
984
+ }
985
+ }
986
+ async function installWorkflows(workflowIds, installDir, force = false, config = {}) {
987
+ const ctx = {
988
+ installDir,
989
+ force,
990
+ config: {
991
+ reviewModel: config.reviewModel
992
+ },
993
+ templateDir: join(PACKAGE_ROOT, "templates"),
994
+ promptsDir: config.promptsDir || LY_PROMPTS_DIR,
995
+ codexSkillsDir: config.codexSkillsDir || AGENTS_SKILLS_DIR,
996
+ result: {
997
+ success: true,
998
+ installedCommands: [],
999
+ installedPrompts: [],
1000
+ skippedCommands: [],
1001
+ errors: [],
1002
+ configPath: ""
1003
+ }
1004
+ };
1005
+ if (!await fs.pathExists(ctx.templateDir)) {
1006
+ const errorMsg = `Template directory not found: ${ctx.templateDir} (PACKAGE_ROOT=${PACKAGE_ROOT}). This usually means the npm package is incomplete or the cache is corrupted. Try: npm cache clean --force && npx lycx@latest`;
1007
+ ctx.result.errors.push(errorMsg);
1008
+ ctx.result.success = false;
1009
+ return ctx.result;
1010
+ }
1011
+ await fs.ensureDir(ctx.promptsDir);
1012
+ await installPromptFiles(ctx);
1013
+ const adapter = ADAPTERS.codex;
1014
+ await installCommandFiles(ctx, workflowIds, adapter);
1015
+ await adapter.installExtras?.(ctx);
1016
+ await adapter.verify?.(ctx);
1017
+ const skippedCount = ctx.result.skippedCommands?.length ?? 0;
1018
+ if (ctx.result.installedCommands.length === 0 && ctx.result.errors.length === 0) {
1019
+ if (skippedCount > 0) {
1020
+ console.warn(`[lycx] ${skippedCount} \u4E2A\u547D\u4EE4\u6587\u4EF6\u5DF2\u5B58\u5728\uFF08\u975E force \u8DF3\u8FC7\uFF0C\u7591\u4F3C ly-workflow \u65E7\u5B89\u88C5\u6B8B\u7559\uFF09\uFF0C\u672A\u8986\u76D6\u3002\u5982\u786E\u8BA4\u9700\u8981\u8986\u76D6\uFF0C\u8BF7\u7528 --force \u91CD\u88C5\uFF0C\u6216\u4EBA\u5DE5\u786E\u8BA4\u540E\u4FDD\u7559\u3002`);
1021
+ } else {
1022
+ ctx.result.errors.push(
1023
+ `No commands were installed (expected ${workflowIds.length}). Template dir: ${ctx.templateDir}. This may indicate a corrupted package or file permission issue.`
1024
+ );
1025
+ ctx.result.success = false;
1026
+ }
1027
+ }
1028
+ ctx.result.configPath = ctx.codexSkillsDir;
1029
+ return ctx.result;
1030
+ }
1031
+ async function uninstallWorkflows(installDir, options) {
1032
+ const result = {
1033
+ success: true,
1034
+ removedSkills: [],
1035
+ removedLegacyPrompts: [],
1036
+ removedSharedPrompts: false,
1037
+ configTomlKept: false,
1038
+ errors: []
1039
+ };
1040
+ const codexSkillsDir = options?.codexSkillsDir || AGENTS_SKILLS_DIR;
1041
+ const stubCtx = {
1042
+ installDir,
1043
+ force: false,
1044
+ templateDir: "",
1045
+ promptsDir: options?.lyPromptsDir || LY_PROMPTS_DIR,
1046
+ codexSkillsDir,
1047
+ config: {},
1048
+ result: {
1049
+ success: true,
1050
+ installedCommands: [],
1051
+ installedPrompts: [],
1052
+ errors: [],
1053
+ configPath: ""
1054
+ }
1055
+ };
1056
+ try {
1057
+ const files = await ADAPTERS.codex.uninstallList(stubCtx);
1058
+ for (const file of files) {
1059
+ await fs.remove(file);
1060
+ result.removedSkills.push(basename(file));
1061
+ }
1062
+ } catch (error) {
1063
+ result.errors.push(`Failed to remove codex skills: ${error}`);
1064
+ result.success = false;
1065
+ }
1066
+ try {
1067
+ if (await fs.pathExists(AGENTS_SKILLS_DIR)) {
1068
+ const entries = await fs.readdir(AGENTS_SKILLS_DIR);
1069
+ for (const entry of entries) {
1070
+ if (!entry.startsWith("ly-"))
1071
+ continue;
1072
+ const full = join(AGENTS_SKILLS_DIR, entry);
1073
+ if ((await fs.stat(full)).isDirectory()) {
1074
+ await fs.remove(full);
1075
+ result.removedLegacyPrompts.push(`${entry}/SKILL.md`);
1076
+ }
1077
+ }
1078
+ }
1079
+ } catch (error) {
1080
+ result.errors.push(`Failed to clean legacy ly-* skills: ${error}`);
1081
+ }
1082
+ try {
1083
+ if (await fs.pathExists(CODE_PROMPTS_DIR)) {
1084
+ const files = await fs.readdir(CODE_PROMPTS_DIR);
1085
+ for (const file of files) {
1086
+ if (!file.startsWith("ly-") || !file.endsWith(".md"))
1087
+ continue;
1088
+ const full = join(CODE_PROMPTS_DIR, file);
1089
+ if ((await fs.stat(full)).isFile()) {
1090
+ await fs.remove(full);
1091
+ result.removedLegacyPrompts.push(file);
1092
+ }
1093
+ }
1094
+ }
1095
+ } catch (error) {
1096
+ result.errors.push(`Failed to clean legacy codex prompts: ${error}`);
1097
+ }
1098
+ const sharedPromptsDir = options?.lyPromptsDir || LY_PROMPTS_DIR;
1099
+ const sharedCodexPromptsDir = join(sharedPromptsDir, "codex");
1100
+ if (await fs.pathExists(sharedCodexPromptsDir)) {
1101
+ try {
1102
+ await fs.remove(sharedCodexPromptsDir);
1103
+ result.removedSharedPrompts = true;
1104
+ } catch (error) {
1105
+ result.errors.push(`Failed to remove shared codex prompts directory: ${error}`);
1106
+ }
1107
+ }
1108
+ const lyDir = options?.lyDir || getLyDir();
1109
+ const configToml = join(lyDir, "config.toml");
1110
+ if (await fs.pathExists(configToml)) {
1111
+ result.configTomlKept = true;
1112
+ console.warn("[lycx] \u4FDD\u7559\u5171\u4EAB\u914D\u7F6E ~/.ly/config.toml\uFF1A~/.ly \u4E3A ly-workflow \u5171\u4EAB\u547D\u540D\u7A7A\u95F4\uFF08\u542B worktrees/ \u7B49\u771F\u5B9E\u6570\u636E\uFF09\uFF0C\u5378\u8F7D\u4EC5\u6E05\u7406\u672C\u5305\u5F52\u5C5E\u4EA7\u7269\u3002\u5982\u9700\u6E05\u9664\u8BE5\u914D\u7F6E\uFF0C\u8BF7\u4EBA\u5DE5\u786E\u8BA4\u540E\u81EA\u884C\u5904\u7406\u3002");
1113
+ }
1114
+ try {
1115
+ const { cleanupLegacyArtifacts, reportCleanupResult } = await import('../chunks/legacy-cleanup.mjs');
1116
+ reportCleanupResult(await cleanupLegacyArtifacts(options?.legacyCleanupDirs));
1117
+ } catch (error) {
1118
+ result.errors.push(`Legacy cleanup failed (non-blocking): ${error}`);
1119
+ }
1120
+ return result;
1121
+ }
1122
+
1123
+ const OPENAI_OFFICIAL_BASE_URL = "https://api.openai.com/v1";
1124
+ const MODEL_CHOICE_CUSTOM = "\0lyx:custom";
1125
+ const MODEL_CHOICE_UNSET = "\0lyx:unset";
1126
+ const MODEL_FIELDS = [
1127
+ { key: "reviewModel", labelKey: "init:model.reviewModelA", promptKey: "init:model.reviewModelAPrompt", sanitize: sanitizeReviewModel },
1128
+ { key: "reviewModelB", labelKey: "init:model.reviewModelB", promptKey: "init:model.reviewModelBPrompt", sanitize: sanitizeModelField },
1129
+ { key: "codingModel", labelKey: "init:model.codingModel", promptKey: "init:model.codingModelPrompt", sanitize: sanitizeModelField }
1130
+ ];
1131
+ async function pickModelField(input) {
1132
+ const { field, models, current } = input;
1133
+ const inList = current !== void 0 && models.includes(current);
1134
+ const choices = [
1135
+ ...models.map((id) => ({ name: id, value: id })),
1136
+ { name: ansis.cyan(`\u270F\uFE0F ${i18n.t("init:model.customChoice")}`), value: MODEL_CHOICE_CUSTOM },
1137
+ { name: ansis.gray(i18n.t("init:model.unsetChoice")), value: MODEL_CHOICE_UNSET }
1138
+ ];
1139
+ const defaultChoice = inList ? current : current !== void 0 ? MODEL_CHOICE_CUSTOM : MODEL_CHOICE_UNSET;
1140
+ const { pick } = await inquirer.prompt([{
1141
+ type: "list",
1142
+ name: "pick",
1143
+ message: i18n.t(field.labelKey),
1144
+ choices,
1145
+ default: defaultChoice,
1146
+ pageSize: 15
1147
+ }]);
1148
+ if (pick === MODEL_CHOICE_UNSET)
1149
+ return void 0;
1150
+ if (pick !== MODEL_CHOICE_CUSTOM)
1151
+ return pick;
1152
+ const { custom } = await inquirer.prompt([{
1153
+ type: "input",
1154
+ name: "custom",
1155
+ message: i18n.t("init:model.customPrompt"),
1156
+ default: current || ""
1157
+ }]);
1158
+ return custom?.trim() || void 0;
1159
+ }
1160
+ async function inputModelField(input) {
1161
+ const { field, current } = input;
1162
+ const { model } = await inquirer.prompt([{
1163
+ type: "input",
1164
+ name: "model",
1165
+ message: i18n.t(field.promptKey),
1166
+ default: current || ""
1167
+ }]);
1168
+ return model?.trim() || void 0;
1169
+ }
1170
+ async function collectCodexHostConfig(options) {
1171
+ console.log();
1172
+ console.log(ansis.cyan.bold(` \u{1F50C} ${i18n.t("init:mode.providerSelect")}`));
1173
+ console.log();
1174
+ const configExists = await fs.pathExists(codexConfigPath());
1175
+ const parsedConfig = await readCodexConfigToml();
1176
+ const existingProviders = await listModelProviders();
1177
+ if (configExists && parsedConfig === null)
1178
+ console.log(ansis.yellow(` \u26A0 ${i18n.t("init:mode.configParseFailed")}`));
1179
+ const { provider } = await inquirer.prompt([{
1180
+ type: "list",
1181
+ name: "provider",
1182
+ message: i18n.t("init:mode.providerSelect"),
1183
+ choices: [
1184
+ ...existingProviders.map((p) => ({
1185
+ name: `${i18n.t("init:mode.providerExisting", { name: p.name })}${p.baseUrl ? ansis.gray(` \u2014 ${p.baseUrl}`) : ""}`,
1186
+ value: { type: "existing", provider: p }
1187
+ })),
1188
+ { name: `${ansis.green("\u25CF")} ${i18n.t("init:mode.providerOfficial")}`, value: { type: "official" } },
1189
+ { name: `${ansis.cyan("\u25CF")} ${i18n.t("init:mode.providerCustom")}`, value: { type: "custom" } }
1190
+ ]
1191
+ }]);
1192
+ let baseUrl = OPENAI_OFFICIAL_BASE_URL;
1193
+ let apiKey = "";
1194
+ if (provider.type === "existing") {
1195
+ baseUrl = provider.provider.baseUrl?.trim() || OPENAI_OFFICIAL_BASE_URL;
1196
+ const envKey = provider.provider.envKey;
1197
+ if (envKey && !process.env[envKey]) {
1198
+ const { key } = await inquirer.prompt([{
1199
+ type: "password",
1200
+ name: "key",
1201
+ message: i18n.t("init:mode.envKeyMissingPrompt", { envKey }),
1202
+ mask: "*"
1203
+ }]);
1204
+ apiKey = key?.trim() || "";
1205
+ }
1206
+ } else if (provider.type === "custom") {
1207
+ console.log();
1208
+ const { name } = await inquirer.prompt([{
1209
+ type: "input",
1210
+ name: "name",
1211
+ message: i18n.t("init:mode.customNamePrompt"),
1212
+ validate: (v) => Boolean(sanitizeProviderName(v)) || i18n.t("init:mode.nameInvalid")
1213
+ }]);
1214
+ const providerName = sanitizeProviderName(name);
1215
+ const { url } = await inquirer.prompt([{
1216
+ type: "input",
1217
+ name: "url",
1218
+ message: i18n.t("init:mode.customUrlPrompt"),
1219
+ default: OPENAI_OFFICIAL_BASE_URL
1220
+ }]);
1221
+ const { key } = await inquirer.prompt([{
1222
+ type: "password",
1223
+ name: "key",
1224
+ message: `${i18n.t("init:mode.customKeyPrompt")} ${ansis.gray(`(${i18n.t("init:mode.optional")})`)}`,
1225
+ mask: "*"
1226
+ }]);
1227
+ baseUrl = (url || OPENAI_OFFICIAL_BASE_URL).trim();
1228
+ apiKey = key?.trim() || "";
1229
+ const writeResult = await upsertModelProvider({ name: providerName, baseUrl });
1230
+ if (writeResult.status === "written") {
1231
+ console.log(ansis.green(` \u2713 ${i18n.t("init:mode.providerWritten", { name: providerName })}`));
1232
+ } else if (writeResult.status === "exists") {
1233
+ console.log(ansis.gray(` ${i18n.t("init:mode.providerExists", { name: providerName })}`));
1234
+ } else {
1235
+ console.log(ansis.yellow(` \u26A0 ${i18n.t("init:mode.providerWriteFailed", { error: writeResult.error })}`));
1236
+ }
1237
+ }
1238
+ console.log();
1239
+ console.log(ansis.cyan.bold(` \u{1F9E0} ${i18n.t("init:model.trioTitle")}`));
1240
+ console.log();
1241
+ if (baseUrl) {
1242
+ console.log(ansis.gray(` ${i18n.t("init:mode.modelFetching", { baseUrl })}`));
1243
+ }
1244
+ const fetchResult = await fetchCodexModels({ baseUrl, apiKey });
1245
+ if (!fetchResult.ok) {
1246
+ console.log(ansis.yellow(` \u26A0 ${i18n.t("init:mode.modelFetchFailed", { reason: fetchResult.error })}`));
1247
+ }
1248
+ const collected = {};
1249
+ for (const field of MODEL_FIELDS) {
1250
+ const current = options.defaults[field.key]?.trim() || void 0;
1251
+ const raw = fetchResult.ok ? await pickModelField({ field, models: fetchResult.models, current }) : await inputModelField({ field, current });
1252
+ collected[field.key] = field.sanitize(raw);
1253
+ }
1254
+ return collected;
1255
+ }
1256
+ function printSummary(input) {
1257
+ const { models, commandCount } = input;
1258
+ console.log();
1259
+ console.log(ansis.yellow("\u2501".repeat(50)));
1260
+ console.log(ansis.bold(` ${i18n.t("init:summary.title")}`));
1261
+ console.log();
1262
+ console.log(` ${ansis.cyan(i18n.t("init:summary.host"))} ${ansis.green("codex")}`);
1263
+ console.log(` ${ansis.cyan(i18n.t("init:summary.reviewModelA"))} ${models.reviewModel ? ansis.green(models.reviewModel) : ansis.gray(i18n.t("init:host.reviewModelUnset"))}`);
1264
+ console.log(` ${ansis.cyan(i18n.t("init:summary.reviewModelB"))} ${models.reviewModelB ? ansis.green(models.reviewModelB) : ansis.gray(i18n.t("init:host.reviewModelUnset"))}`);
1265
+ console.log(` ${ansis.cyan(i18n.t("init:summary.codingModel"))} ${models.codingModel ? ansis.green(models.codingModel) : ansis.gray(i18n.t("init:host.reviewModelUnset"))}`);
1266
+ console.log(` ${ansis.cyan(i18n.t("init:summary.commandCount"))} ${ansis.yellow(commandCount.toString())}`);
1267
+ console.log(ansis.yellow("\u2501".repeat(50)));
1268
+ console.log();
1269
+ }
1270
+ async function init(options = {}) {
1271
+ console.log();
1272
+ console.log(ansis.cyan.bold(` ${PACKAGE_NAME} v${version}`));
1273
+ console.log(ansis.gray(` Codex \u5355 Agent \u5F00\u53D1\u5DE5\u4F5C\u6D41`));
1274
+ console.log();
1275
+ const existingConfig = await readLyConfig();
1276
+ const savedLang = existingConfig?.general?.language;
1277
+ let language = savedLang ?? "zh-CN";
1278
+ if (!options.skipPrompt) {
1279
+ if (savedLang) {
1280
+ language = savedLang;
1281
+ await initI18n(language);
1282
+ } else {
1283
+ const { selectedLang } = await inquirer.prompt([{
1284
+ type: "list",
1285
+ name: "selectedLang",
1286
+ message: "\u9009\u62E9\u8BED\u8A00 / Select language",
1287
+ choices: [
1288
+ { name: "\u7B80\u4F53\u4E2D\u6587", value: "zh-CN" },
1289
+ { name: "English", value: "en" }
1290
+ ],
1291
+ default: "zh-CN"
1292
+ }]);
1293
+ language = selectedLang;
1294
+ await initI18n(language);
1295
+ }
1296
+ } else if (options.lang) {
1297
+ language = options.lang;
1298
+ await initI18n(language);
1299
+ } else if (savedLang) {
1300
+ language = savedLang;
1301
+ await initI18n(language);
1302
+ }
1303
+ const selectedWorkflows = getCoreCommandIds();
1304
+ const defaultModels = {
1305
+ reviewModel: sanitizeReviewModel(existingConfig?.codexHost?.reviewModel),
1306
+ reviewModelB: sanitizeModelField(existingConfig?.codexHost?.reviewModelB),
1307
+ codingModel: sanitizeModelField(existingConfig?.codexHost?.codingModel)
1308
+ };
1309
+ let collectedModels = { ...defaultModels };
1310
+ if (!options.skipPrompt) {
1311
+ collectedModels = await collectCodexHostConfig({ defaults: defaultModels });
1312
+ printSummary({ models: collectedModels, commandCount: selectedWorkflows.length });
1313
+ const { confirm } = await inquirer.prompt([{
1314
+ type: "confirm",
1315
+ name: "confirm",
1316
+ message: i18n.t("init:confirmInstall"),
1317
+ default: true
1318
+ }]);
1319
+ if (!confirm) {
1320
+ console.log(ansis.yellow(i18n.t("init:installCancelled")));
1321
+ return;
1322
+ }
1323
+ } else {
1324
+ printSummary({ models: collectedModels, commandCount: selectedWorkflows.length });
1325
+ }
1326
+ const spinner = ora(i18n.t("init:installing")).start();
1327
+ try {
1328
+ await ensureLyDir();
1329
+ const config = createDefaultConfig({
1330
+ language,
1331
+ installedWorkflows: selectedWorkflows,
1332
+ codexHost: {
1333
+ reviewModel: collectedModels.reviewModel,
1334
+ reviewModelB: collectedModels.reviewModelB,
1335
+ codingModel: collectedModels.codingModel
1336
+ },
1337
+ installedHosts: ["codex"]
1338
+ });
1339
+ await writeLyConfig(config);
1340
+ try {
1341
+ const migration = await migrateLegacyPrompts();
1342
+ if (migration.migrated) {
1343
+ console.log();
1344
+ console.log(` ${ansis.green("\u2713")} ${i18n.t("init:promptsMigrated")}`);
1345
+ } else if (migration.error) {
1346
+ console.log();
1347
+ console.log(` ${ansis.yellow("\u26A0")} ${i18n.t("init:promptsMigrationFailed", { error: migration.error })}`);
1348
+ }
1349
+ } catch {
1350
+ }
1351
+ const result = await installWorkflows(selectedWorkflows, "", options.force, {
1352
+ reviewModel: collectedModels.reviewModel
1353
+ });
1354
+ spinner.succeed(ansis.green(i18n.t("init:installSuccess")));
1355
+ if (!result.success || result.errors.length > 0) {
1356
+ if (result.errors.length > 0) {
1357
+ result.errors.forEach((error) => {
1358
+ console.log(` ${ansis.red("\u2717")} ${error}`);
1359
+ });
1360
+ }
1361
+ if (!result.success) {
1362
+ console.log();
1363
+ console.log(ansis.yellow(" \u5C1D\u8BD5\u4FEE\u590D / Try to fix:"));
1364
+ console.log(ansis.cyan(` npx ${PACKAGE_NAME}@latest init --force`));
1365
+ console.log(ansis.gray(` If still failing, report an issue at ${"https://github.com/FE-runner/ly-workflow-codex/issues"}`));
1366
+ }
1367
+ }
1368
+ console.log();
1369
+ console.log(` ${ansis.cyan(i18n.t("init:installedCommands"))}`);
1370
+ for (const cmd of result.installedCommands) {
1371
+ console.log(` ${ansis.green("\u2713")} lyx-${cmd} ${ansis.gray("\u2192 ~/.agents/skills/")}`);
1372
+ }
1373
+ if (result.installedPrompts.length > 0) {
1374
+ console.log();
1375
+ console.log(` ${ansis.cyan(i18n.t("init:installedPrompts"))}`);
1376
+ for (const name of result.installedPrompts) {
1377
+ console.log(` ${ansis.green("\u2713")} ${name} ${ansis.gray("\u2192 ~/.ly/prompts/")}`);
1378
+ }
1379
+ }
1380
+ try {
1381
+ const { cleanupLegacyArtifacts, reportCleanupResult } = await import('../chunks/legacy-cleanup.mjs');
1382
+ reportCleanupResult(await cleanupLegacyArtifacts());
1383
+ } catch {
1384
+ }
1385
+ console.log();
1386
+ console.log(ansis.green(` \u2713 ${i18n.t("init:installSuccess")}`));
1387
+ console.log(ansis.gray(` Config: ~/.ly/config.toml`));
1388
+ console.log();
1389
+ } catch (error) {
1390
+ spinner.fail(ansis.red(i18n.t("init:installFailed")));
1391
+ console.error(error);
1392
+ }
1393
+ }
1394
+
1395
+ const execAsync$2 = promisify(exec);
1396
+ const __filename$1 = fileURLToPath(import.meta.url);
1397
+ const __dirname$1 = dirname(__filename$1);
1398
+ function findPackageRoot(startDir) {
1399
+ let dir = startDir;
1400
+ while (true) {
1401
+ if (fs.existsSync(join(dir, "package.json"))) {
1402
+ return dir;
1403
+ }
1404
+ const parentDir = dirname(dir);
1405
+ if (parentDir === dir) {
1406
+ return dir;
1407
+ }
1408
+ dir = parentDir;
1409
+ }
1410
+ }
1411
+ findPackageRoot(__dirname$1);
1412
+ async function getCurrentVersion() {
1413
+ {
1414
+ return version;
1415
+ }
1416
+ }
1417
+ async function getLatestVersion(packageName = PACKAGE_NAME) {
1418
+ try {
1419
+ const { stdout } = await execAsync$2(`npm view ${packageName} version`);
1420
+ return stdout.trim();
1421
+ } catch {
1422
+ return null;
1423
+ }
1424
+ }
1425
+ function compareVersions(v1, v2) {
1426
+ const parts1 = v1.split(".").map(Number);
1427
+ const parts2 = v2.split(".").map(Number);
1428
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
1429
+ const num1 = parts1[i] || 0;
1430
+ const num2 = parts2[i] || 0;
1431
+ if (num1 > num2)
1432
+ return 1;
1433
+ if (num1 < num2)
1434
+ return -1;
1435
+ }
1436
+ return 0;
1437
+ }
1438
+ async function checkForUpdates() {
1439
+ const currentVersion = await getCurrentVersion();
1440
+ const latestVersion = await getLatestVersion();
1441
+ if (!latestVersion) {
1442
+ return {
1443
+ hasUpdate: false,
1444
+ currentVersion,
1445
+ latestVersion: null
1446
+ };
1447
+ }
1448
+ const hasUpdate = compareVersions(latestVersion, currentVersion) > 0;
1449
+ return {
1450
+ hasUpdate,
1451
+ currentVersion,
1452
+ latestVersion
1453
+ };
1454
+ }
1455
+
1456
+ const execAsync$1 = promisify(exec);
1457
+ function buildInitArgs() {
1458
+ return "init --force --skip-prompt";
1459
+ }
1460
+ async function update() {
1461
+ console.log();
1462
+ console.log(ansis.cyan.bold(`\u{1F504} ${i18n.t("update:checking")}`));
1463
+ console.log();
1464
+ const spinner = ora(i18n.t("update:checkingLatest")).start();
1465
+ try {
1466
+ const { hasUpdate, currentVersion, latestVersion } = await checkForUpdates();
1467
+ const config = await readLyConfig();
1468
+ const localVersion = config?.general?.version || "0.0.0";
1469
+ const needsWorkflowUpdate = compareVersions(currentVersion, localVersion) > 0;
1470
+ spinner.stop();
1471
+ if (!latestVersion) {
1472
+ console.log(ansis.red(`\u274C ${i18n.t("update:cannotConnect")}`));
1473
+ return;
1474
+ }
1475
+ console.log(`${i18n.t("update:currentVersion")}: ${ansis.yellow(`v${currentVersion}`)}`);
1476
+ console.log(`${i18n.t("update:latestVersion")}: ${ansis.green(`v${latestVersion}`)}`);
1477
+ if (localVersion !== "0.0.0") {
1478
+ console.log(`${i18n.t("update:localWorkflow")}: ${ansis.gray(`v${localVersion}`)}`);
1479
+ }
1480
+ console.log();
1481
+ const effectiveNeedsUpdate = hasUpdate || needsWorkflowUpdate;
1482
+ let defaultConfirm = effectiveNeedsUpdate;
1483
+ let message;
1484
+ if (hasUpdate) {
1485
+ message = i18n.t("update:newVersionFound", { latest: latestVersion, current: currentVersion });
1486
+ defaultConfirm = true;
1487
+ } else if (needsWorkflowUpdate) {
1488
+ message = i18n.t("update:localOutdated", { local: localVersion, current: currentVersion });
1489
+ defaultConfirm = true;
1490
+ } else {
1491
+ message = i18n.t("update:alreadyLatest", { current: currentVersion });
1492
+ defaultConfirm = false;
1493
+ }
1494
+ const { confirmUpdate } = await inquirer.prompt([{
1495
+ type: "confirm",
1496
+ name: "confirmUpdate",
1497
+ message,
1498
+ default: defaultConfirm
1499
+ }]);
1500
+ if (!confirmUpdate) {
1501
+ console.log(ansis.gray(i18n.t("update:cancelled")));
1502
+ return;
1503
+ }
1504
+ const fromVersion = needsWorkflowUpdate ? localVersion : currentVersion;
1505
+ await performUpdate(fromVersion, latestVersion || currentVersion, hasUpdate);
1506
+ } catch (error) {
1507
+ spinner.stop();
1508
+ console.log(ansis.red(`\u274C ${i18n.t("update:error", { error: String(error) })}`));
1509
+ }
1510
+ }
1511
+ async function checkIfGlobalInstall$1() {
1512
+ try {
1513
+ const { stdout } = await execAsync$1(`npm list -g ${PACKAGE_NAME} --depth=0`, { timeout: 5e3 });
1514
+ return stdout.includes(`${PACKAGE_NAME}@`);
1515
+ } catch {
1516
+ return false;
1517
+ }
1518
+ }
1519
+ async function performUpdate(fromVersion, toVersion, isNewVersion) {
1520
+ console.log();
1521
+ console.log(ansis.yellow.bold(`\u2699\uFE0F ${i18n.t("update:starting")}`));
1522
+ console.log();
1523
+ const isGlobalInstall = await checkIfGlobalInstall$1();
1524
+ if (isGlobalInstall && !isNewVersion) {
1525
+ console.log(ansis.cyan(`\u2139\uFE0F ${i18n.t("update:globalDetected")}`));
1526
+ console.log();
1527
+ console.log(ansis.green(`\u2713 ${i18n.t("update:packageLatest")} (v${toVersion})`));
1528
+ console.log(ansis.yellow(`\u2699\uFE0F ${i18n.t("update:workflowOnly")}`));
1529
+ console.log();
1530
+ } else if (isGlobalInstall && isNewVersion) {
1531
+ console.log(ansis.yellow(`\u26A0\uFE0F ${i18n.t("update:globalDetected")}`));
1532
+ console.log();
1533
+ console.log(`${i18n.t("update:recommendNpm")}`);
1534
+ console.log();
1535
+ console.log(ansis.cyan(` npm install -g ${PACKAGE_NAME}@latest`));
1536
+ console.log();
1537
+ console.log(ansis.gray(i18n.t("update:willUpdateBoth")));
1538
+ console.log();
1539
+ const { useNpmUpdate } = await inquirer.prompt([{
1540
+ type: "confirm",
1541
+ name: "useNpmUpdate",
1542
+ message: i18n.t("update:useNpmUpdate"),
1543
+ default: true
1544
+ }]);
1545
+ if (useNpmUpdate) {
1546
+ console.log();
1547
+ console.log(ansis.cyan(i18n.t("update:runInNewTerminal")));
1548
+ console.log();
1549
+ console.log(ansis.cyan.bold(` npm install -g ${PACKAGE_NAME}@latest`));
1550
+ console.log();
1551
+ console.log(ansis.gray(`(${i18n.t("update:autoUpdateAfter")})`));
1552
+ console.log();
1553
+ return;
1554
+ }
1555
+ console.log();
1556
+ console.log(ansis.yellow(`\u26A0\uFE0F ${i18n.t("update:continueBuiltin")}`));
1557
+ console.log(ansis.gray(i18n.t("update:willNotUpdateCli")));
1558
+ console.log();
1559
+ }
1560
+ let spinner = ora(i18n.t("update:downloading")).start();
1561
+ try {
1562
+ if (process.platform === "win32") {
1563
+ spinner.text = i18n.t("update:clearingCache");
1564
+ try {
1565
+ await execAsync$1("npx clear-npx-cache", { timeout: 1e4 });
1566
+ } catch {
1567
+ const npxCachePath = join(homedir(), ".npm", "_npx");
1568
+ try {
1569
+ await fs.remove(npxCachePath);
1570
+ } catch {
1571
+ }
1572
+ }
1573
+ }
1574
+ spinner.text = i18n.t("update:downloading");
1575
+ await execAsync$1(`npx --yes ${PACKAGE_NAME}@latest --version`, { timeout: 6e4 });
1576
+ spinner.succeed(i18n.t("update:downloadDone"));
1577
+ } catch (error) {
1578
+ spinner.fail(i18n.t("update:downloadFailed"));
1579
+ console.log(ansis.red(`${i18n.t("common:error")}: ${error}`));
1580
+ return;
1581
+ }
1582
+ const BACKUP_SUFFIX = ".ly-update-bak";
1583
+ const codexSkillsDir = AGENTS_SKILLS_DIR;
1584
+ const legacyPromptsDir = CODE_PROMPTS_DIR;
1585
+ const backupDir = join(codexSkillsDir + BACKUP_SUFFIX);
1586
+ spinner = ora(i18n.t("update:removingOld")).start();
1587
+ const backedUp = [];
1588
+ try {
1589
+ if (await fs.pathExists(codexSkillsDir) || await fs.pathExists(legacyPromptsDir)) {
1590
+ if (await fs.pathExists(backupDir)) {
1591
+ await fs.remove(backupDir);
1592
+ }
1593
+ await fs.ensureDir(backupDir);
1594
+ if (await fs.pathExists(codexSkillsDir)) {
1595
+ const entries = await fs.readdir(codexSkillsDir);
1596
+ for (const entry of entries) {
1597
+ if (!entry.startsWith("lyx-") && !entry.startsWith("ly-"))
1598
+ continue;
1599
+ const full = join(codexSkillsDir, entry);
1600
+ if ((await fs.stat(full)).isDirectory()) {
1601
+ await fs.move(full, join(backupDir, entry));
1602
+ backedUp.push(full);
1603
+ }
1604
+ }
1605
+ }
1606
+ if (await fs.pathExists(legacyPromptsDir)) {
1607
+ const files = await fs.readdir(legacyPromptsDir);
1608
+ for (const f of files) {
1609
+ if (!f.startsWith("ly-") || !f.endsWith(".md"))
1610
+ continue;
1611
+ const full = join(legacyPromptsDir, f);
1612
+ if ((await fs.stat(full)).isFile()) {
1613
+ await fs.move(full, join(backupDir, f));
1614
+ backedUp.push(full);
1615
+ }
1616
+ }
1617
+ }
1618
+ }
1619
+ spinner.succeed(i18n.t("update:oldRemoved"));
1620
+ } catch (error) {
1621
+ spinner.warn(`Backup failed: ${error}`);
1622
+ for (const file of backedUp) {
1623
+ const backupPath = join(backupDir, basename(file));
1624
+ try {
1625
+ if (await fs.pathExists(backupPath)) {
1626
+ await fs.move(backupPath, file);
1627
+ }
1628
+ } catch {
1629
+ }
1630
+ }
1631
+ console.log(ansis.yellow(" \u65E7\u7248\u672C\u6587\u4EF6\u5DF2\u4FDD\u7559 / Old files preserved"));
1632
+ return;
1633
+ }
1634
+ spinner = ora(i18n.t("update:installingNew")).start();
1635
+ let installSuccess = false;
1636
+ try {
1637
+ await execAsync$1(`npx --yes ${PACKAGE_NAME}@latest ${buildInitArgs()}`, {
1638
+ timeout: 3e5,
1639
+ // 5min — install from npm registry may be slow (especially in China)
1640
+ env: {
1641
+ ...process.env,
1642
+ LY_UPDATE_MODE: "true"
1643
+ }
1644
+ });
1645
+ let hasInstalled = false;
1646
+ if (await fs.pathExists(codexSkillsDir)) {
1647
+ const entries = await fs.readdir(codexSkillsDir);
1648
+ for (const entry of entries) {
1649
+ if (!entry.startsWith("lyx-"))
1650
+ continue;
1651
+ try {
1652
+ if ((await fs.stat(join(codexSkillsDir, entry))).isDirectory()) {
1653
+ hasInstalled = true;
1654
+ break;
1655
+ }
1656
+ } catch {
1657
+ }
1658
+ }
1659
+ }
1660
+ if (hasInstalled) {
1661
+ installSuccess = true;
1662
+ spinner.succeed(i18n.t("update:installDone"));
1663
+ const config = await readLyConfig();
1664
+ if (config?.workflows?.installed) {
1665
+ console.log();
1666
+ console.log(ansis.cyan(i18n.t("update:installed", { count: config.workflows.installed.length })));
1667
+ for (const cmd of config.workflows.installed) {
1668
+ console.log(` ${ansis.gray("\u2022")} @lyx-${cmd}`);
1669
+ }
1670
+ }
1671
+ } else {
1672
+ spinner.fail(i18n.t("update:installFailed"));
1673
+ console.log(ansis.red(" Install subprocess completed but no command files were created"));
1674
+ }
1675
+ } catch (error) {
1676
+ spinner.fail(i18n.t("update:installFailed"));
1677
+ console.log(ansis.red(`${i18n.t("common:error")}: ${error}`));
1678
+ }
1679
+ if (installSuccess) {
1680
+ try {
1681
+ await fs.remove(backupDir);
1682
+ } catch {
1683
+ }
1684
+ try {
1685
+ const { cleanupLegacyArtifacts, reportCleanupResult } = await import('../chunks/legacy-cleanup.mjs');
1686
+ reportCleanupResult(await cleanupLegacyArtifacts());
1687
+ } catch {
1688
+ }
1689
+ } else {
1690
+ console.log();
1691
+ console.log(ansis.yellow.bold(" \u26A0 \u6B63\u5728\u6062\u590D\u65E7\u7248\u672C\u6587\u4EF6 / Restoring old version files..."));
1692
+ let restored = 0;
1693
+ for (const file of backedUp) {
1694
+ const backupPath = join(backupDir, basename(file));
1695
+ try {
1696
+ if (await fs.pathExists(file)) {
1697
+ await fs.remove(file);
1698
+ }
1699
+ if (await fs.pathExists(backupPath)) {
1700
+ await fs.move(backupPath, file);
1701
+ restored++;
1702
+ }
1703
+ } catch (restoreErr) {
1704
+ console.log(ansis.red(` Failed to restore ${file}: ${restoreErr}`));
1705
+ }
1706
+ }
1707
+ if (restored > 0) {
1708
+ console.log(ansis.green(` \u2713 \u5DF2\u6062\u590D ${restored} \u4E2A\u6587\u4EF6 / Restored ${restored} files`));
1709
+ console.log(ansis.gray(" \u65E7\u7248\u547D\u4EE4\u4ECD\u53EF\u6B63\u5E38\u4F7F\u7528 / Old commands still work"));
1710
+ }
1711
+ console.log();
1712
+ console.log(ansis.yellow(i18n.t("update:manualRetry")));
1713
+ console.log(ansis.cyan(` npx ${PACKAGE_NAME}@latest`));
1714
+ return;
1715
+ }
1716
+ console.log();
1717
+ console.log(ansis.green.bold(`\u2705 ${i18n.t("update:updateDone")}`));
1718
+ console.log();
1719
+ if (isNewVersion) {
1720
+ console.log(ansis.gray(i18n.t("update:upgradedFromTo", { from: fromVersion, to: toVersion })));
1721
+ } else {
1722
+ console.log(ansis.gray(i18n.t("update:reinstalled", { version: toVersion })));
1723
+ }
1724
+ console.log();
1725
+ }
1726
+
1727
+ const execAsync = promisify(exec);
1728
+ function visWidth(s) {
1729
+ const stripped = s.replace(/\x1B\[[0-9;]*m/g, "");
1730
+ let w = 0;
1731
+ for (const ch of stripped) {
1732
+ const code = ch.codePointAt(0) || 0;
1733
+ if (code >= 11904 && code <= 40959 || code >= 63744 && code <= 64255 || code >= 65072 && code <= 65103 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 127744 && code <= 129535 || code >= 131072 && code <= 195103) {
1734
+ w += 2;
1735
+ } else {
1736
+ w += 1;
1737
+ }
1738
+ }
1739
+ return w;
1740
+ }
1741
+ function pad(s, w) {
1742
+ const diff = w - visWidth(s);
1743
+ return diff > 0 ? s + " ".repeat(diff) : s;
1744
+ }
1745
+ const INNER_W = 60;
1746
+ function centerLine(s, w) {
1747
+ const vis = visWidth(s);
1748
+ const left = Math.max(0, Math.floor((w - vis) / 2));
1749
+ const right = Math.max(0, w - vis - left);
1750
+ return " ".repeat(left) + s + " ".repeat(right);
1751
+ }
1752
+ function boxRow(content) {
1753
+ const vis = visWidth(content);
1754
+ const gap = Math.max(0, INNER_W - vis);
1755
+ return ansis.cyan("\u2551") + content + " ".repeat(gap) + ansis.cyan("\u2551");
1756
+ }
1757
+ function wrapStatusLines(parts, width) {
1758
+ const sep = ansis.gray(" | ");
1759
+ const sepVis = visWidth(sep);
1760
+ const lines = [];
1761
+ let cur = "";
1762
+ for (const p of parts) {
1763
+ const pVis = visWidth(p);
1764
+ const wouldBe = cur ? visWidth(cur) + sepVis + pVis : pVis;
1765
+ if (cur && wouldBe > width) {
1766
+ lines.push(cur);
1767
+ cur = p;
1768
+ } else {
1769
+ cur = cur ? cur + sep + p : p;
1770
+ }
1771
+ }
1772
+ if (cur)
1773
+ lines.push(cur);
1774
+ return lines;
1775
+ }
1776
+ function drawHeader(statusParts) {
1777
+ const top = ansis.cyan(`\u2554${"\u2550".repeat(INNER_W)}\u2557`);
1778
+ const bot = ansis.cyan(`\u255A${"\u2550".repeat(INNER_W)}\u255D`);
1779
+ const empty = boxRow(" ".repeat(INNER_W));
1780
+ const logo = [
1781
+ "\u2588 \u2588\u2588 \u2588\u2588",
1782
+ "\u2588 \u2588\u2588 \u2588\u2588 ",
1783
+ "\u2588 \u2588\u2588\u2588 ",
1784
+ "\u2588 \u2588 ",
1785
+ "\u2588\u2588\u2588\u2588\u2588\u2588 \u2588 "
1786
+ ];
1787
+ console.log();
1788
+ console.log(top);
1789
+ console.log(empty);
1790
+ for (const line of logo) {
1791
+ console.log(boxRow(centerLine(ansis.bold.white(line), INNER_W)));
1792
+ }
1793
+ console.log(empty);
1794
+ console.log(boxRow(centerLine(ansis.gray("Codex Single-Agent + OpenSpec"), INNER_W)));
1795
+ console.log(boxRow(centerLine(ansis.gray("Review Gates / GitFlow Release"), INNER_W)));
1796
+ console.log(empty);
1797
+ if (statusParts.length > 0) {
1798
+ for (const line of wrapStatusLines(statusParts, INNER_W)) {
1799
+ console.log(boxRow(centerLine(line, INNER_W)));
1800
+ }
1801
+ console.log(empty);
1802
+ }
1803
+ console.log(bot);
1804
+ console.log();
1805
+ }
1806
+ function groupSep(label) {
1807
+ const w = 42;
1808
+ const labelW = visWidth(label);
1809
+ const remaining = Math.max(0, w - labelW - 2);
1810
+ const left = Math.floor(remaining / 2);
1811
+ const right = remaining - left;
1812
+ return new inquirer.Separator(ansis.gray(`${"\u2500".repeat(left)} ${label} ${"\u2500".repeat(right)}`));
1813
+ }
1814
+ function buildMainMenuChoices(isZh) {
1815
+ const item = (key, label, desc) => ({
1816
+ name: ` ${ansis.green(`${key}.`)} ${pad(label, 20)} ${ansis.gray(`- ${desc}`)}`,
1817
+ value: key
1818
+ });
1819
+ return [
1820
+ groupSep(isZh ? "\u5DE5\u4F5C\u6D41" : "Workflow"),
1821
+ item("1", i18n.t("menu:options.init"), isZh ? `\u5B89\u88C5 ${PACKAGE_NAME}` : `Install ${PACKAGE_NAME}`),
1822
+ item("2", i18n.t("menu:options.update"), isZh ? "\u66F4\u65B0\u5230\u6700\u65B0\u7248\u672C" : "Update to latest version"),
1823
+ item("3", i18n.t("menu:options.configReviewModel"), isZh ? "\u914D\u7F6E\u5BA1\u67E5\u6A21\u578B" : "Configure review model"),
1824
+ groupSep(isZh ? "\u5E2E\u52A9\u4E0E\u5378\u8F7D" : "Help & Uninstall"),
1825
+ item("H", i18n.t("menu:options.help"), isZh ? "\u67E5\u770B\u5168\u90E8\u659C\u6760\u547D\u4EE4" : "View all slash commands"),
1826
+ item("-", i18n.t("menu:options.uninstall"), isZh ? `\u79FB\u9664 ${PACKAGE_NAME} \u914D\u7F6E` : `Remove ${PACKAGE_NAME} config`),
1827
+ new inquirer.Separator(ansis.gray("\u2500".repeat(42))),
1828
+ { name: ` ${ansis.red("Q.")} ${i18n.t("menu:options.exit")}`, value: "Q" }
1829
+ ];
1830
+ }
1831
+ async function showMainMenu() {
1832
+ while (true) {
1833
+ const config = await readLyConfig();
1834
+ const cmdCount = config?.workflows?.installed?.length || 0;
1835
+ const lang = config?.general?.language || "zh-CN";
1836
+ const statusParts = [
1837
+ ansis.green(`v${version}`),
1838
+ ansis.white(`${cmdCount} commands`),
1839
+ ansis.yellow(lang)
1840
+ ];
1841
+ if (sanitizeReviewModel(config?.codexHost?.reviewModel)) {
1842
+ statusParts.push(ansis.green(`review model: ${config?.codexHost?.reviewModel}`));
1843
+ }
1844
+ drawHeader(statusParts);
1845
+ const isZh = lang === "zh-CN";
1846
+ const { action } = await inquirer.prompt([{
1847
+ type: "list",
1848
+ name: "action",
1849
+ message: i18n.t("menu:title"),
1850
+ pageSize: 20,
1851
+ choices: buildMainMenuChoices(isZh)
1852
+ }]);
1853
+ switch (action) {
1854
+ case "1":
1855
+ await init();
1856
+ break;
1857
+ case "2":
1858
+ await update();
1859
+ break;
1860
+ case "3":
1861
+ await configReviewModel();
1862
+ break;
1863
+ case "-":
1864
+ await uninstall();
1865
+ break;
1866
+ case "H":
1867
+ showHelp();
1868
+ break;
1869
+ case "Q":
1870
+ console.log();
1871
+ console.log(ansis.gray(` ${i18n.t("common:goodbye")}`));
1872
+ console.log();
1873
+ return;
1874
+ }
1875
+ console.log();
1876
+ await inquirer.prompt([{
1877
+ type: "input",
1878
+ name: "continue",
1879
+ message: ansis.gray(i18n.t("common:pressEnterToReturn"))
1880
+ }]);
1881
+ }
1882
+ }
1883
+ function showHelp() {
1884
+ const config = readLyConfigSync();
1885
+ const isZh = (config?.general?.language || "zh-CN") === "zh-CN";
1886
+ console.log();
1887
+ console.log(ansis.cyan.bold(` ${i18n.t("menu:help.title")}`));
1888
+ console.log();
1889
+ const col1 = 22;
1890
+ const section = (title) => console.log(ansis.yellow.bold(` ${title}`));
1891
+ const cmd = (name, desc) => console.log(` ${ansis.green(name.padEnd(col1))} ${ansis.gray(desc)}`);
1892
+ const commandsDir = AGENTS_SKILLS_DIR;
1893
+ let installedFiles = [];
1894
+ try {
1895
+ installedFiles = fs.readdirSync(commandsDir).filter((f) => {
1896
+ if (!f.startsWith("lyx-"))
1897
+ return false;
1898
+ try {
1899
+ return fs.statSync(join(commandsDir, f)).isDirectory();
1900
+ } catch {
1901
+ return false;
1902
+ }
1903
+ });
1904
+ } catch {
1905
+ console.log(ansis.yellow(` ${isZh ? "\u672A\u627E\u5230\u5DF2\u5B89\u88C5\u7684\u547D\u4EE4\u3002" : "No installed commands found."}`));
1906
+ console.log(ansis.gray(` ${isZh ? "\u8FD0\u884C `lycx init` \u540E\u67E5\u770B\u5DF2\u5B89\u88C5\u547D\u4EE4" : "Run `lycx init` then check installed commands"}`));
1907
+ console.log();
1908
+ return;
1909
+ }
1910
+ const coreConfigs = getWorkflowConfigs();
1911
+ const coreCommandNames = new Set(coreConfigs.flatMap((w) => w.commands));
1912
+ const coreFiles = installedFiles.map((f) => f.replace(/^lyx-/, ""));
1913
+ section(isZh ? "\u6838\u5FC3\u547D\u4EE4" : "Core commands");
1914
+ for (const config_ of coreConfigs) {
1915
+ for (const cmdName of config_.commands) {
1916
+ if (coreFiles.includes(cmdName) && coreCommandNames.has(cmdName)) {
1917
+ cmd(`@lyx-${cmdName}`, (isZh ? config_.description : config_.descriptionEn) || "");
1918
+ }
1919
+ }
1920
+ }
1921
+ console.log();
1922
+ console.log(ansis.gray(` ${i18n.t("menu:help.hint")}`));
1923
+ console.log();
1924
+ }
1925
+ function readLyConfigSync() {
1926
+ try {
1927
+ const configPath = getConfigPath();
1928
+ if (fs.pathExistsSync(configPath)) {
1929
+ return parse(fs.readFileSync(configPath, "utf-8"));
1930
+ }
1931
+ } catch {
1932
+ }
1933
+ return null;
1934
+ }
1935
+ async function configReviewModel() {
1936
+ const config = await readLyConfig();
1937
+ const currentReviewModel = sanitizeReviewModel(config?.codexHost?.reviewModel);
1938
+ console.log();
1939
+ console.log(ansis.cyan.bold(` ${i18n.t("init:model.title")}`));
1940
+ console.log();
1941
+ console.log(ansis.gray(` ${i18n.t("init:host.reviewModelHint")}`));
1942
+ const { model } = await inquirer.prompt([{
1943
+ type: "input",
1944
+ name: "model",
1945
+ message: i18n.t("init:host.reviewModelPrompt"),
1946
+ default: currentReviewModel || ""
1947
+ }]);
1948
+ const next = sanitizeReviewModel(model);
1949
+ if (next === currentReviewModel) {
1950
+ console.log(ansis.gray(` ${i18n.t("common:configNotModified")}`));
1951
+ return;
1952
+ }
1953
+ const fresh = await readLyConfig();
1954
+ if (!fresh) {
1955
+ console.log(` ${ansis.yellow("\u26A0")} ${PACKAGE_NAME} config not initialized`);
1956
+ return;
1957
+ }
1958
+ const existingB = sanitizeModelField(fresh.codexHost?.reviewModelB);
1959
+ const existingCoding = sanitizeModelField(fresh.codexHost?.codingModel);
1960
+ if (next || existingB || existingCoding) {
1961
+ fresh.codexHost = {
1962
+ ...next ? { reviewModel: next } : {},
1963
+ ...existingB ? { reviewModelB: existingB } : {},
1964
+ ...existingCoding ? { codingModel: existingCoding } : {}
1965
+ };
1966
+ } else {
1967
+ fresh.codexHost = void 0;
1968
+ }
1969
+ await writeLyConfig(fresh);
1970
+ console.log();
1971
+ console.log(ansis.green(` \u2713 ${i18n.t("init:model.routingUpdated")}`));
1972
+ console.log(` ${ansis.cyan(i18n.t("init:summary.reviewModelCodex"))} ${next || i18n.t("init:host.reviewModelUnset")}`);
1973
+ await reinstallTemplates();
1974
+ }
1975
+ async function reinstallTemplates() {
1976
+ const spinner = ora(i18n.t("init:model.reinstalling")).start();
1977
+ try {
1978
+ const config = await readLyConfig();
1979
+ const reviewModel = sanitizeReviewModel(config?.codexHost?.reviewModel);
1980
+ const result = await installWorkflows(getCoreCommandIds(), "", true, { reviewModel });
1981
+ if (result.success) {
1982
+ spinner.succeed(i18n.t("init:model.reinstallDone"));
1983
+ } else {
1984
+ spinner.fail(i18n.t("init:model.reinstallFailed"));
1985
+ }
1986
+ for (const error of result.errors) {
1987
+ console.log(ansis.yellow(` \u26A0 ${error}`));
1988
+ }
1989
+ } catch {
1990
+ spinner.fail(i18n.t("init:model.reinstallFailed"));
1991
+ }
1992
+ }
1993
+ async function checkIfGlobalInstall() {
1994
+ try {
1995
+ const { stdout } = await execAsync(`npm list -g ${PACKAGE_NAME} --depth=0`, { timeout: 5e3 });
1996
+ return stdout.includes(`${PACKAGE_NAME}@`);
1997
+ } catch {
1998
+ return false;
1999
+ }
2000
+ }
2001
+ async function uninstall() {
2002
+ console.log();
2003
+ const isGlobalInstall = await checkIfGlobalInstall();
2004
+ if (isGlobalInstall) {
2005
+ console.log(ansis.yellow(` \u26A0\uFE0F ${i18n.t("menu:uninstall.globalDetected")}`));
2006
+ console.log();
2007
+ console.log(` ${i18n.t("menu:uninstall.twoSteps")}`);
2008
+ console.log(` ${ansis.cyan(`1. ${i18n.t("menu:uninstall.step1")}`)} ${ansis.gray(`(${i18n.t("menu:uninstall.step1Hint")})`)}`);
2009
+ console.log(` ${ansis.cyan(`2. ${i18n.t("menu:uninstall.step2")}`)} ${ansis.gray(`(${i18n.t("menu:uninstall.step2Hint")})`)}`);
2010
+ console.log();
2011
+ }
2012
+ const { confirm } = await inquirer.prompt([{
2013
+ type: "confirm",
2014
+ name: "confirm",
2015
+ message: isGlobalInstall ? i18n.t("menu:uninstall.continuePrompt") : i18n.t("menu:uninstall.confirm"),
2016
+ default: false
2017
+ }]);
2018
+ if (!confirm) {
2019
+ console.log(ansis.gray(` ${i18n.t("menu:uninstall.cancelled")}`));
2020
+ return;
2021
+ }
2022
+ console.log();
2023
+ console.log(ansis.yellow(` ${i18n.t("menu:uninstall.uninstalling")}`));
2024
+ const result = await uninstallWorkflows(join(homedir(), ".codex"));
2025
+ if (result.success) {
2026
+ console.log(ansis.green(` \u2705 ${i18n.t("menu:uninstall.success")}`));
2027
+ if (result.removedSkills.length > 0) {
2028
+ console.log();
2029
+ console.log(ansis.cyan(` ${i18n.t("menu:uninstall.removedSkills")}`));
2030
+ for (const file of result.removedSkills) {
2031
+ console.log(` ${ansis.gray("\u2022")} ${file}`);
2032
+ }
2033
+ }
2034
+ if (result.removedLegacyPrompts.length > 0) {
2035
+ console.log();
2036
+ console.log(ansis.cyan(` ${i18n.t("menu:uninstall.removedLegacyPrompts")}`));
2037
+ for (const file of result.removedLegacyPrompts) {
2038
+ console.log(` ${ansis.gray("\u2022")} ${file}`);
2039
+ }
2040
+ }
2041
+ if (result.removedSharedPrompts) {
2042
+ console.log();
2043
+ console.log(ansis.cyan(` ${i18n.t("menu:uninstall.removedSharedPrompts")}`));
2044
+ }
2045
+ if (result.configTomlKept) {
2046
+ console.log();
2047
+ console.log(ansis.cyan(` ${i18n.t("menu:uninstall.configTomlKept")}`));
2048
+ }
2049
+ if (isGlobalInstall) {
2050
+ console.log();
2051
+ console.log(ansis.yellow.bold(` \u{1F538} ${i18n.t("menu:uninstall.lastStep")}`));
2052
+ console.log();
2053
+ console.log(` ${i18n.t("menu:uninstall.runInNewTerminal")}`);
2054
+ console.log();
2055
+ console.log(ansis.cyan.bold(` npm uninstall -g ${PACKAGE_NAME}`));
2056
+ console.log();
2057
+ console.log(ansis.gray(` (${i18n.t("menu:uninstall.afterDone")})`));
2058
+ }
2059
+ } else {
2060
+ console.log(ansis.red(` ${i18n.t("menu:uninstall.failed")}`));
2061
+ for (const error of result.errors) {
2062
+ console.log(ansis.red(` ${error}`));
2063
+ }
2064
+ }
2065
+ console.log();
2066
+ }
2067
+
2068
+ export { ADAPTERS as A, BIN_NAME as B, LY_PROMPTS_DIR as L, PACKAGE_NAME as P, i18n as a, initI18n as b, changeLanguage as c, createDefaultConfig as d, getLyDir as e, getLyPromptsDir as f, getConfigPath as g, getWorkflowById as h, init as i, getWorkflowConfigs as j, installWorkflows as k, uninstallWorkflows as l, migrateLegacyPrompts as m, checkForUpdates as n, compareVersions as o, getCurrentVersion as p, getLatestVersion as q, readLyConfig as r, showMainMenu as s, AGENTS_SKILLS_DIR as t, update as u, version as v, writeLyConfig as w };