ly-workflow-codex 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -16
- package/dist/cli.mjs +410 -70
- package/dist/index.d.mts +27 -31
- package/dist/index.d.ts +27 -31
- package/dist/index.mjs +1 -1
- package/dist/shared/{ly-workflow-codex.K-E7PMp3.mjs → ly-workflow-codex.DZD52_5A.mjs} +494 -356
- package/package.json +1 -1
- package/templates/skills-codex/apply.md +46 -15
- package/templates/skills-codex/archive.md +15 -2
- package/templates/skills-codex/init.md +10 -13
- package/templates/skills-codex/propose.md +39 -19
- package/templates/skills-codex/review-code.md +72 -48
- package/templates/skills-codex/review-plan.md +64 -43
package/dist/cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import ansis from 'ansis';
|
|
5
5
|
import inquirer from 'inquirer';
|
|
6
6
|
import { join } from 'pathe';
|
|
7
|
-
import { a as i18n,
|
|
7
|
+
import { a as i18n, q as AGENTS_SKILLS_DIR, r as readLyConfig, L as LY_PROMPTS_DIR, t as readCodexCurrentModel, v as version, P as PACKAGE_NAME, x as sanitizeSpawnableModels, y as sanitizeReasoningEffort, z as sanitizeReviewModel, B as sanitizeModelField, C as sanitizeExecutor, b as initI18n, s as showMainMenu, i as init, l as uninstallWorkflows, D as BIN_NAME } from './shared/ly-workflow-codex.DZD52_5A.mjs';
|
|
8
8
|
import { execFile, spawn, execSync } from 'node:child_process';
|
|
9
9
|
import fs from 'fs-extra';
|
|
10
10
|
import { existsSync } from 'node:fs';
|
|
@@ -17,20 +17,28 @@ import 'node:url';
|
|
|
17
17
|
const DEPENDENT_COMMANDS = ["@lyx-init", "@lyx-explore", "@lyx-propose", "@lyx-review-plan", "@lyx-archive"];
|
|
18
18
|
const INSTALL_CMD = ["npm", "install", "-g", "@fission-ai/openspec@latest"];
|
|
19
19
|
const SHELL_OPT = process.platform === "win32" ? { shell: true } : {};
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
"openspec-
|
|
23
|
-
"openspec-
|
|
24
|
-
"openspec-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
const DEFAULT_WORKFLOWS = ["propose", "explore", "apply", "update", "sync", "archive"];
|
|
21
|
+
const WORKFLOW_TO_SKILL = {
|
|
22
|
+
explore: "openspec-explore",
|
|
23
|
+
new: "openspec-new-change",
|
|
24
|
+
continue: "openspec-continue-change",
|
|
25
|
+
apply: "openspec-apply-change",
|
|
26
|
+
update: "openspec-update-change",
|
|
27
|
+
ff: "openspec-ff-change",
|
|
28
|
+
sync: "openspec-sync-specs",
|
|
29
|
+
archive: "openspec-archive-change",
|
|
30
|
+
"bulk-archive": "openspec-bulk-archive-change",
|
|
31
|
+
verify: "openspec-verify-change",
|
|
32
|
+
onboard: "openspec-onboard",
|
|
33
|
+
propose: "openspec-propose"
|
|
34
|
+
};
|
|
35
|
+
function getOpenspecSkillRoots(cwd = process.cwd()) {
|
|
36
|
+
return [
|
|
37
|
+
{ scope: "project", path: join(cwd, ".agents", "skills") },
|
|
38
|
+
{ scope: "project", path: join(cwd, ".codex", "skills") },
|
|
39
|
+
{ scope: "global", path: AGENTS_SKILLS_DIR },
|
|
40
|
+
{ scope: "global", path: join(homedir(), ".codex", "skills") }
|
|
30
41
|
];
|
|
31
|
-
return OPENSPEC_SKILL_NAMES.some(
|
|
32
|
-
(name) => roots.some((root) => existsSync(join(root, name, "SKILL.md")))
|
|
33
|
-
);
|
|
34
42
|
}
|
|
35
43
|
function detectOpenspecCli() {
|
|
36
44
|
return new Promise((resolve) => {
|
|
@@ -47,6 +55,126 @@ function detectOpenspecCli() {
|
|
|
47
55
|
});
|
|
48
56
|
});
|
|
49
57
|
}
|
|
58
|
+
function execFileText(cmd, args, options = {}) {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
execFile(cmd, args, { timeout: 5e3, ...SHELL_OPT, ...options }, (err, stdout, stderr) => {
|
|
61
|
+
resolve({
|
|
62
|
+
ok: !err,
|
|
63
|
+
stdout: stdout?.toString() ?? "",
|
|
64
|
+
stderr: stderr?.toString() ?? "",
|
|
65
|
+
error: err
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function resolveWorkflowDependencies(workflows) {
|
|
71
|
+
const resolved = [...workflows];
|
|
72
|
+
if ((resolved.includes("archive") || resolved.includes("bulk-archive")) && !resolved.includes("sync"))
|
|
73
|
+
resolved.splice(Math.max(resolved.findIndex((w) => w === "archive" || w === "bulk-archive"), 0), 0, "sync");
|
|
74
|
+
return resolved;
|
|
75
|
+
}
|
|
76
|
+
async function readOpenspecProfile(cwd) {
|
|
77
|
+
const result = await execFileText("openspec", ["config", "list", "--json"], { cwd });
|
|
78
|
+
if (!result.ok)
|
|
79
|
+
return { workflows: DEFAULT_WORKFLOWS, source: "fallback" };
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(result.stdout);
|
|
82
|
+
if (!Array.isArray(parsed.workflows) || parsed.workflows.length === 0)
|
|
83
|
+
return { workflows: DEFAULT_WORKFLOWS, source: "fallback" };
|
|
84
|
+
const workflows = parsed.workflows.filter((w) => typeof w === "string" && w.length > 0);
|
|
85
|
+
return { workflows: resolveWorkflowDependencies(workflows), source: "openspec-config" };
|
|
86
|
+
} catch {
|
|
87
|
+
return { workflows: DEFAULT_WORKFLOWS, source: "fallback" };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function mapWorkflowsToSkills(workflows) {
|
|
91
|
+
return [...new Set(workflows.map((w) => WORKFLOW_TO_SKILL[w]).filter((s) => Boolean(s)))];
|
|
92
|
+
}
|
|
93
|
+
function inspectOpenspecSkills(required, cwd, profileSource) {
|
|
94
|
+
const roots = getOpenspecSkillRoots(cwd);
|
|
95
|
+
const resolved = {};
|
|
96
|
+
const projectHits = /* @__PURE__ */ new Set();
|
|
97
|
+
const missing = [];
|
|
98
|
+
for (const skill of required) {
|
|
99
|
+
for (const root of roots) {
|
|
100
|
+
const skillFile = join(root.path, skill, "SKILL.md");
|
|
101
|
+
if (existsSync(skillFile)) {
|
|
102
|
+
resolved[skill] = skillFile;
|
|
103
|
+
if (root.scope === "project")
|
|
104
|
+
projectHits.add(skill);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!resolved[skill])
|
|
109
|
+
missing.push(skill);
|
|
110
|
+
}
|
|
111
|
+
let status;
|
|
112
|
+
if (missing.length > 0)
|
|
113
|
+
status = "missing";
|
|
114
|
+
else if (profileSource === "fallback")
|
|
115
|
+
status = "unknown";
|
|
116
|
+
else if (projectHits.size !== required.length)
|
|
117
|
+
status = "global-only";
|
|
118
|
+
else
|
|
119
|
+
status = "project-ready";
|
|
120
|
+
return { status, required, resolved, missing, roots, profileSource };
|
|
121
|
+
}
|
|
122
|
+
async function inspectOpenspecRoot(cwd, cli) {
|
|
123
|
+
if (!cli.installed)
|
|
124
|
+
return { status: "not-checked" };
|
|
125
|
+
const result = await execFileText("openspec", ["doctor", "--json"], { cwd });
|
|
126
|
+
let doctor;
|
|
127
|
+
try {
|
|
128
|
+
doctor = result.stdout ? JSON.parse(result.stdout) : void 0;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return { status: "unhealthy", error: error instanceof Error ? error.message : String(error) };
|
|
131
|
+
}
|
|
132
|
+
const statuses = doctor?.status ?? [];
|
|
133
|
+
if (!result.ok) {
|
|
134
|
+
if (statuses.some((s) => s.code === "no_openspec_root"))
|
|
135
|
+
return { status: "missing", doctor };
|
|
136
|
+
return {
|
|
137
|
+
status: "unhealthy",
|
|
138
|
+
doctor,
|
|
139
|
+
error: result.stderr || "openspec doctor --json failed"
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const rootHealthy = doctor?.root?.healthy;
|
|
143
|
+
return rootHealthy === true ? { status: "healthy", doctor } : { status: "unhealthy", doctor };
|
|
144
|
+
}
|
|
145
|
+
async function inspectOpenspec(options) {
|
|
146
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
147
|
+
const cli = await detectOpenspecCli();
|
|
148
|
+
const cliStatus = !cli.installed ? "missing" : cli.version === "unknown" ? "unhealthy" : "ok";
|
|
149
|
+
const profile = cli.installed && cliStatus !== "unhealthy" ? await readOpenspecProfile(cwd) : { workflows: DEFAULT_WORKFLOWS, source: "fallback" };
|
|
150
|
+
const required = mapWorkflowsToSkills(profile.workflows);
|
|
151
|
+
const skills = inspectOpenspecSkills(required, cwd, profile.source);
|
|
152
|
+
const root = await inspectOpenspecRoot(cwd, cli);
|
|
153
|
+
const actions = [];
|
|
154
|
+
if (cliStatus === "missing")
|
|
155
|
+
actions.push({ kind: "install-cli" });
|
|
156
|
+
if (skills.status === "global-only")
|
|
157
|
+
actions.push({ kind: "warn-global-only" });
|
|
158
|
+
if (skills.status === "missing") {
|
|
159
|
+
if (root.status === "missing")
|
|
160
|
+
actions.push({ kind: "init-root" });
|
|
161
|
+
else
|
|
162
|
+
actions.push({ kind: "repair-skills", strategy: "update" });
|
|
163
|
+
}
|
|
164
|
+
if (root.status === "missing" && !actions.some((a) => a.kind === "init-root"))
|
|
165
|
+
actions.push({ kind: "init-root" });
|
|
166
|
+
if (root.status === "unhealthy")
|
|
167
|
+
actions.push({ kind: "report-root", doctor: root.doctor });
|
|
168
|
+
return {
|
|
169
|
+
cli: { status: cliStatus, version: cli.version },
|
|
170
|
+
skills,
|
|
171
|
+
root,
|
|
172
|
+
actions
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function isNonInteractive(skipPrompt) {
|
|
176
|
+
return Boolean(skipPrompt || process.env.CI || !process.stdin.isTTY || !process.stdout.isTTY);
|
|
177
|
+
}
|
|
50
178
|
function printUnavailable() {
|
|
51
179
|
console.log(ansis.yellow(` ${i18n.t("common:preflight.unavailableList", { list: DEPENDENT_COMMANDS.join(" ") })}`));
|
|
52
180
|
console.log(ansis.gray(` ${i18n.t("common:preflight.unaffectedNote")}`));
|
|
@@ -59,49 +187,118 @@ async function runNpmInstall() {
|
|
|
59
187
|
child.on("close", (code) => resolve(code === 0));
|
|
60
188
|
});
|
|
61
189
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
190
|
+
async function confirmOpenspecCliInstall(skipPrompt) {
|
|
191
|
+
if (isNonInteractive(skipPrompt)) {
|
|
192
|
+
printUnavailable();
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
const { confirmed } = await inquirer.prompt([{
|
|
196
|
+
type: "confirm",
|
|
197
|
+
name: "confirmed",
|
|
198
|
+
message: i18n.t("common:preflight.installAsk"),
|
|
199
|
+
default: true
|
|
200
|
+
}]);
|
|
201
|
+
if (!confirmed) {
|
|
202
|
+
printUnavailable();
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
return true;
|
|
64
206
|
}
|
|
65
|
-
async function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
207
|
+
async function installCli() {
|
|
208
|
+
const ok = await runNpmInstall();
|
|
209
|
+
if (!ok) {
|
|
210
|
+
console.error(ansis.red(`\u2717 ${i18n.t("common:preflight.installFailed")}`));
|
|
211
|
+
printUnavailable();
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const recheck = await detectOpenspecCli();
|
|
215
|
+
if (!recheck.installed) {
|
|
216
|
+
console.log(ansis.yellow(`\u26A0 ${i18n.t("common:preflight.installNotInPath")}`));
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
function printOpenspecInspection(inspection) {
|
|
222
|
+
if (inspection.cli.status === "missing") {
|
|
74
223
|
console.log(ansis.yellow(`\u26A0 ${i18n.t("common:preflight.cliMissing")}`));
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
224
|
+
printUnavailable();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (inspection.cli.status === "unhealthy") {
|
|
228
|
+
console.log(ansis.yellow(`\u26A0 ${i18n.t("common:preflight.cliUnhealthy")}`));
|
|
229
|
+
}
|
|
230
|
+
if (inspection.skills.status === "global-only") {
|
|
231
|
+
console.log(ansis.yellow(`\u26A0 ${i18n.t("common:preflight.skillsGlobalOnly")}`));
|
|
232
|
+
} else if (inspection.skills.status === "missing") {
|
|
233
|
+
console.log(ansis.yellow(`\u26A0 ${i18n.t("common:preflight.skillsMissing", { list: inspection.skills.missing.join(", ") })}`));
|
|
234
|
+
} else if (inspection.skills.status === "unknown") {
|
|
235
|
+
console.log(ansis.yellow(`\u26A0 ${i18n.t("common:preflight.skillsUnknown")}`));
|
|
236
|
+
}
|
|
237
|
+
if (inspection.root.status === "missing") {
|
|
238
|
+
console.log(ansis.yellow(`\u26A0 ${i18n.t("common:preflight.rootMissing")}`));
|
|
239
|
+
} else if (inspection.root.status === "unhealthy") {
|
|
240
|
+
console.error(ansis.red(`\u2717 ${i18n.t("common:preflight.rootUnhealthy")}`));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function ensureOpenspec(options) {
|
|
244
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
245
|
+
const executed = [];
|
|
246
|
+
let inspection = await inspectOpenspec({ cwd });
|
|
247
|
+
if (inspection.cli.status === "missing") {
|
|
248
|
+
const confirmed = options?.yes || (options?.confirmInstall ? await options.confirmInstall() : false);
|
|
249
|
+
if (!confirmed)
|
|
250
|
+
return { inspection, executed };
|
|
251
|
+
if (await installCli()) {
|
|
252
|
+
executed.push("install-cli");
|
|
253
|
+
inspection = await inspectOpenspec({ cwd });
|
|
254
|
+
} else {
|
|
255
|
+
return { inspection, executed };
|
|
88
256
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
257
|
+
}
|
|
258
|
+
if (inspection.cli.status !== "ok")
|
|
259
|
+
return { inspection, executed };
|
|
260
|
+
if (inspection.root.status === "unhealthy")
|
|
261
|
+
return { inspection, executed };
|
|
262
|
+
if (inspection.root.status === "missing") {
|
|
263
|
+
const initResult = await execFileText("openspec", ["init", "--tools", "codex", "--no-animation"], { cwd });
|
|
264
|
+
executed.push("init-root");
|
|
265
|
+
inspection = await inspectOpenspec({ cwd });
|
|
266
|
+
if (!initResult.ok && inspection.root.status === "missing")
|
|
267
|
+
return { inspection, executed };
|
|
268
|
+
}
|
|
269
|
+
if (inspection.skills.status === "missing") {
|
|
270
|
+
const updateResult = await execFileText("openspec", ["update", "--force"], { cwd });
|
|
271
|
+
executed.push("repair-skills:update");
|
|
272
|
+
inspection = await inspectOpenspec({ cwd });
|
|
273
|
+
if (inspection.skills.status === "missing") {
|
|
274
|
+
const initResult = await execFileText("openspec", ["init", "--tools", "codex", "--no-animation"], { cwd });
|
|
275
|
+
executed.push("repair-skills:init");
|
|
276
|
+
inspection = await inspectOpenspec({ cwd });
|
|
277
|
+
if (!updateResult.ok && !initResult.ok && inspection.skills.status === "missing")
|
|
278
|
+
return { inspection, executed };
|
|
94
279
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
280
|
+
}
|
|
281
|
+
return { inspection, executed };
|
|
282
|
+
}
|
|
283
|
+
async function checkExternalDeps(options) {
|
|
284
|
+
try {
|
|
285
|
+
if (options?.initOpenspec) {
|
|
286
|
+
const result = await ensureOpenspec({
|
|
287
|
+
yes: options.skipPrompt,
|
|
288
|
+
confirmInstall: () => confirmOpenspecCliInstall(options.skipPrompt)
|
|
289
|
+
});
|
|
290
|
+
printOpenspecInspection(result.inspection);
|
|
98
291
|
return;
|
|
99
292
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
293
|
+
let inspection = await inspectOpenspec();
|
|
294
|
+
if (inspection.cli.status === "missing") {
|
|
295
|
+
if (!await confirmOpenspecCliInstall(options?.skipPrompt))
|
|
296
|
+
return;
|
|
297
|
+
if (!await installCli())
|
|
298
|
+
return;
|
|
299
|
+
inspection = await inspectOpenspec();
|
|
104
300
|
}
|
|
301
|
+
printOpenspecInspection(inspection);
|
|
105
302
|
} catch {
|
|
106
303
|
}
|
|
107
304
|
}
|
|
@@ -124,6 +321,113 @@ function execSafe(cmd) {
|
|
|
124
321
|
return null;
|
|
125
322
|
}
|
|
126
323
|
}
|
|
324
|
+
function buildOpenspecSkillsDetail(status2, missing) {
|
|
325
|
+
if (status2 === "project-ready")
|
|
326
|
+
return i18n.t("common:doctor.skillsInitialized");
|
|
327
|
+
if (status2 === "global-only")
|
|
328
|
+
return i18n.t("common:doctor.skillsGlobalOnly");
|
|
329
|
+
if (status2 === "unknown")
|
|
330
|
+
return i18n.t("common:doctor.skillsUnknown");
|
|
331
|
+
return i18n.t("common:doctor.skillsMissing", { list: missing.join(", ") || "unknown" });
|
|
332
|
+
}
|
|
333
|
+
function buildOpenspecRootDetail(status2) {
|
|
334
|
+
if (status2 === "healthy")
|
|
335
|
+
return i18n.t("common:doctor.rootHealthy");
|
|
336
|
+
if (status2 === "missing")
|
|
337
|
+
return i18n.t("common:doctor.rootMissing");
|
|
338
|
+
if (status2 === "unhealthy")
|
|
339
|
+
return i18n.t("common:doctor.rootUnhealthy");
|
|
340
|
+
return i18n.t("common:doctor.rootNotChecked");
|
|
341
|
+
}
|
|
342
|
+
function resolveExecutor(value) {
|
|
343
|
+
const cleaned = sanitizeExecutor(value);
|
|
344
|
+
if (cleaned)
|
|
345
|
+
return { kind: cleaned, invalid: false };
|
|
346
|
+
const invalid = typeof value === "string" && value.trim() !== "";
|
|
347
|
+
return { kind: "main", invalid };
|
|
348
|
+
}
|
|
349
|
+
function assessSubagentModelConfig(codexHost) {
|
|
350
|
+
const spawn = sanitizeSpawnableModels(codexHost?.spawnableModels);
|
|
351
|
+
const reviewExecutor = resolveExecutor(codexHost?.reviewExecutor);
|
|
352
|
+
const codingExecutor = resolveExecutor(codexHost?.codingExecutor);
|
|
353
|
+
const buildField = (input) => {
|
|
354
|
+
const ineffective = input.executorKind === "main";
|
|
355
|
+
const hasValue = Boolean(input.value || input.reasoningEffort);
|
|
356
|
+
return {
|
|
357
|
+
key: input.key,
|
|
358
|
+
value: input.value,
|
|
359
|
+
status: ineffective && hasValue ? "warn" : "ok",
|
|
360
|
+
okKind: hasValue ? ineffective ? "ineffective" : "configured" : "unset",
|
|
361
|
+
reasoningEffortKey: input.reasoningEffortKey,
|
|
362
|
+
reasoningEffort: input.reasoningEffort
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
const fields = [
|
|
366
|
+
buildField({
|
|
367
|
+
key: "reviewModel",
|
|
368
|
+
value: sanitizeReviewModel(codexHost?.reviewModel),
|
|
369
|
+
reasoningEffortKey: "reviewReasoningEffort",
|
|
370
|
+
reasoningEffort: sanitizeReasoningEffort(codexHost?.reviewReasoningEffort),
|
|
371
|
+
executorKind: reviewExecutor.kind
|
|
372
|
+
}),
|
|
373
|
+
buildField({
|
|
374
|
+
key: "codingModel",
|
|
375
|
+
value: sanitizeModelField(codexHost?.codingModel),
|
|
376
|
+
reasoningEffortKey: "codingReasoningEffort",
|
|
377
|
+
reasoningEffort: sanitizeReasoningEffort(codexHost?.codingReasoningEffort),
|
|
378
|
+
executorKind: codingExecutor.kind
|
|
379
|
+
})
|
|
380
|
+
];
|
|
381
|
+
const executors = [
|
|
382
|
+
{
|
|
383
|
+
key: "reviewExecutor",
|
|
384
|
+
kind: reviewExecutor.kind,
|
|
385
|
+
invalid: reviewExecutor.invalid,
|
|
386
|
+
rawValue: typeof codexHost?.reviewExecutor === "string" ? codexHost.reviewExecutor : void 0
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
key: "codingExecutor",
|
|
390
|
+
kind: codingExecutor.kind,
|
|
391
|
+
invalid: codingExecutor.invalid,
|
|
392
|
+
rawValue: typeof codexHost?.codingExecutor === "string" ? codexHost.codingExecutor : void 0
|
|
393
|
+
}
|
|
394
|
+
];
|
|
395
|
+
const raw = codexHost;
|
|
396
|
+
const removedFields = ["reviewModelB", "reviewReasoningEffortB"].filter((key) => raw?.[key] !== void 0);
|
|
397
|
+
const spawnWarn = spawn.state === "empty" || spawn.state === "invalid";
|
|
398
|
+
const fieldWarn = fields.some((f) => f.status === "warn");
|
|
399
|
+
const executorWarn = executors.some((e) => e.invalid);
|
|
400
|
+
return {
|
|
401
|
+
status: spawnWarn || fieldWarn || executorWarn ? "warn" : "ok",
|
|
402
|
+
executors,
|
|
403
|
+
fields,
|
|
404
|
+
spawnState: spawn.state,
|
|
405
|
+
removedFields
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function buildSubagentModelCheckDetail(result) {
|
|
409
|
+
const parts = [];
|
|
410
|
+
for (const e of result.executors) {
|
|
411
|
+
parts.push(e.invalid ? i18n.t("doctor:modelConfig.warnInvalidExecutor", { key: e.key, value: e.rawValue ?? "" }) : i18n.t(e.kind === "main" ? "doctor:modelConfig.executorMain" : "doctor:modelConfig.executorSubagent", { key: e.key }));
|
|
412
|
+
}
|
|
413
|
+
for (const f of result.fields) {
|
|
414
|
+
const modelPart = f.value ? f.okKind === "ineffective" ? i18n.t("doctor:modelConfig.warnIneffective", { key: f.key, model: f.value }) : i18n.t("doctor:modelConfig.okConfigured", { key: f.key, model: f.value }) : i18n.t("doctor:modelConfig.okUnset", { key: f.key });
|
|
415
|
+
const reasoningPart = f.reasoningEffort ? i18n.t("doctor:modelConfig.okReasoningConfigured", { key: f.reasoningEffortKey, effort: f.reasoningEffort }) : i18n.t("doctor:modelConfig.okReasoningUnset", { key: f.reasoningEffortKey });
|
|
416
|
+
parts.push(modelPart, reasoningPart);
|
|
417
|
+
}
|
|
418
|
+
for (const key of result.removedFields)
|
|
419
|
+
parts.push(i18n.t("doctor:modelConfig.removedNote", { key }));
|
|
420
|
+
if (result.spawnState === "empty" || result.spawnState === "invalid")
|
|
421
|
+
parts.push(i18n.t("doctor:modelConfig.warnInvalid"));
|
|
422
|
+
return parts.join("; ");
|
|
423
|
+
}
|
|
424
|
+
function buildSubagentModelHintLines(currentModel) {
|
|
425
|
+
const lines = [i18n.t("doctor:modelConfig.agentNeedConfig")];
|
|
426
|
+
if (currentModel)
|
|
427
|
+
lines.push(i18n.t("doctor:modelConfig.inheritNote", { model: currentModel }));
|
|
428
|
+
lines.push(i18n.t("doctor:modelConfig.verifyHint"));
|
|
429
|
+
return lines;
|
|
430
|
+
}
|
|
127
431
|
async function doctor() {
|
|
128
432
|
const checks = [];
|
|
129
433
|
const nodeVer = process.version;
|
|
@@ -137,7 +441,7 @@ async function doctor() {
|
|
|
137
441
|
checks.push({
|
|
138
442
|
label: "config",
|
|
139
443
|
status: config ? OK : WARN,
|
|
140
|
-
detail: config ? `v${config.general?.version || "?"}, lang=${config.general?.language || "?"}` : "Not found (~/.
|
|
444
|
+
detail: config ? `v${config.general?.version || "?"}, lang=${config.general?.language || "?"}` : "Not found (~/.codex/lyx/config.toml)"
|
|
141
445
|
});
|
|
142
446
|
const skillsEntries = await dirFiles(AGENTS_SKILLS_DIR);
|
|
143
447
|
const cmdCount = skillsEntries.filter((f) => f.startsWith("lyx-")).length;
|
|
@@ -151,19 +455,30 @@ async function doctor() {
|
|
|
151
455
|
checks.push({
|
|
152
456
|
label: "Roles",
|
|
153
457
|
status: roleFiles.length >= 2 ? OK : roleFiles.length > 0 ? WARN : FAIL,
|
|
154
|
-
detail: roleFiles.length > 0 ? roleFiles.join(", ") : "None (~/.
|
|
458
|
+
detail: roleFiles.length > 0 ? roleFiles.join(", ") : "None (~/.codex/lyx/prompts/codex/)"
|
|
155
459
|
});
|
|
156
|
-
const
|
|
460
|
+
const openspec = await inspectOpenspec();
|
|
157
461
|
checks.push({
|
|
158
462
|
label: "OpenSpec CLI",
|
|
159
|
-
status:
|
|
160
|
-
detail:
|
|
463
|
+
status: openspec.cli.status === "ok" ? OK : WARN,
|
|
464
|
+
detail: openspec.cli.status === "ok" ? `v${openspec.cli.version}` : openspec.cli.status === "unhealthy" ? i18n.t("common:doctor.openspecCliUnhealthy") : i18n.t("common:doctor.openspecCliMissing")
|
|
161
465
|
});
|
|
162
|
-
const hasOpenspecSkills = detectOpenspecSkills();
|
|
163
466
|
checks.push({
|
|
164
467
|
label: "OpenSpec skills",
|
|
165
|
-
status:
|
|
166
|
-
detail:
|
|
468
|
+
status: openspec.skills.status === "project-ready" ? OK : WARN,
|
|
469
|
+
detail: buildOpenspecSkillsDetail(openspec.skills.status, openspec.skills.missing)
|
|
470
|
+
});
|
|
471
|
+
checks.push({
|
|
472
|
+
label: "OpenSpec root",
|
|
473
|
+
status: openspec.root.status === "healthy" ? OK : WARN,
|
|
474
|
+
detail: buildOpenspecRootDetail(openspec.root.status)
|
|
475
|
+
});
|
|
476
|
+
const currentModel = await readCodexCurrentModel();
|
|
477
|
+
const modelCheck = assessSubagentModelConfig(config?.codexHost);
|
|
478
|
+
checks.push({
|
|
479
|
+
label: i18n.t("doctor:modelConfig.label"),
|
|
480
|
+
status: modelCheck.status === "warn" ? WARN : OK,
|
|
481
|
+
detail: buildSubagentModelCheckDetail(modelCheck)
|
|
167
482
|
});
|
|
168
483
|
console.log();
|
|
169
484
|
console.log(ansis.cyan.bold(` ly-workflow-codex Doctor v${version}`));
|
|
@@ -171,6 +486,8 @@ async function doctor() {
|
|
|
171
486
|
for (const { label, status: status2, detail } of checks) {
|
|
172
487
|
console.log(` ${status2} ${ansis.bold(label.padEnd(20))} ${ansis.gray(detail)}`);
|
|
173
488
|
}
|
|
489
|
+
for (const line of buildSubagentModelHintLines(currentModel))
|
|
490
|
+
console.log(ansis.gray(` ${line}`));
|
|
174
491
|
const failures = checks.filter((c) => c.status === FAIL);
|
|
175
492
|
console.log();
|
|
176
493
|
if (failures.length === 0) {
|
|
@@ -205,16 +522,16 @@ async function status() {
|
|
|
205
522
|
}
|
|
206
523
|
}
|
|
207
524
|
}
|
|
208
|
-
const
|
|
209
|
-
const hasOpenspecSkills = detectOpenspecSkills();
|
|
525
|
+
const openspec = await inspectOpenspec();
|
|
210
526
|
console.log();
|
|
211
527
|
console.log(ansis.cyan.bold(" ly-workflow-codex Status"));
|
|
212
528
|
console.log();
|
|
213
529
|
console.log(` ${ansis.bold("Version")} ${installedVer}${installedVer !== latestVer ? ansis.yellow(` (latest: ${latestVer})`) : ansis.green(" (up to date)")}`);
|
|
214
530
|
console.log(` ${ansis.bold("Commands")} ${cmds.length}`);
|
|
215
531
|
console.log(` ${ansis.bold("Review model")} ${reviewModel}`);
|
|
216
|
-
console.log(` ${ansis.bold("OpenSpec CLI")} ${
|
|
217
|
-
console.log(` ${ansis.bold("OpenSpec skills")}${
|
|
532
|
+
console.log(` ${ansis.bold("OpenSpec CLI")} ${openspec.cli.status === "ok" ? `v${openspec.cli.version}` : ansis.yellow(i18n.t(openspec.cli.status === "unhealthy" ? "common:doctor.openspecCliUnhealthy" : "common:doctor.openspecCliMissing"))}`);
|
|
533
|
+
console.log(` ${ansis.bold("OpenSpec skills")} ${openspec.skills.status === "project-ready" ? i18n.t("common:doctor.skillsInitialized") : ansis.yellow(buildOpenspecSkillsDetail(openspec.skills.status, openspec.skills.missing))}`);
|
|
534
|
+
console.log(` ${ansis.bold("OpenSpec root")} ${openspec.root.status === "healthy" ? i18n.t("common:doctor.rootHealthy") : ansis.yellow(buildOpenspecRootDetail(openspec.root.status))}`);
|
|
218
535
|
console.log(` ${ansis.bold("Active tasks")} ${activeTasks > 0 ? ansis.yellow(String(activeTasks)) : "0"}`);
|
|
219
536
|
console.log();
|
|
220
537
|
}
|
|
@@ -231,6 +548,8 @@ function customizeHelp(sections) {
|
|
|
231
548
|
` ${ansis.cyan(`${BIN_NAME} init`)} | ${ansis.cyan("i")} ${i18n.t("cli:help.commandDescriptions.initConfig")}`,
|
|
232
549
|
` ${ansis.cyan(`${BIN_NAME} doctor`)} Check installation health`,
|
|
233
550
|
` ${ansis.cyan(`${BIN_NAME} status`)} Show installation overview`,
|
|
551
|
+
` ${ansis.cyan(`${BIN_NAME} openspec inspect`)} Inspect OpenSpec dependency state`,
|
|
552
|
+
` ${ansis.cyan(`${BIN_NAME} openspec ensure`)} Ensure OpenSpec CLI/skills/root`,
|
|
234
553
|
` ${ansis.cyan(`${BIN_NAME} uninstall`)} Uninstall ${PACKAGE_NAME} (non-interactive)`,
|
|
235
554
|
"",
|
|
236
555
|
ansis.gray(` ${i18n.t("cli:help.shortcuts")}`),
|
|
@@ -284,26 +603,49 @@ async function setupCommands(cli) {
|
|
|
284
603
|
await checkExternalDeps();
|
|
285
604
|
await showMainMenu();
|
|
286
605
|
});
|
|
287
|
-
cli.command("init", i18n.t("cli:help.commandDescriptions.initConfig")).alias("i").option("--lang, -l <lang>", `${i18n.t("cli:help.optionDescriptions.displayLanguage")} (zh-CN, en)`).option("--force, -f", i18n.t("cli:help.optionDescriptions.forceOverwrite")).option("--skip-prompt, -s", i18n.t("cli:help.optionDescriptions.skipAllPrompts")).option("--workflows, -w <workflows>", i18n.t("cli:help.optionDescriptions.workflows")).option("--install-dir, -d <path>", i18n.t("cli:help.optionDescriptions.installDir")).action(async (options) => {
|
|
606
|
+
cli.command("init", i18n.t("cli:help.commandDescriptions.initConfig")).alias("i").option("--lang, -l <lang>", `${i18n.t("cli:help.optionDescriptions.displayLanguage")} (zh-CN, en)`).option("--force, -f", i18n.t("cli:help.optionDescriptions.forceOverwrite")).option("--skip-prompt, -s", i18n.t("cli:help.optionDescriptions.skipAllPrompts")).option("--workflows, -w <workflows>", i18n.t("cli:help.optionDescriptions.workflows")).option("--install-dir, -d <path>", i18n.t("cli:help.optionDescriptions.installDir")).option("--init-openspec", i18n.t("cli:help.optionDescriptions.initOpenspec")).action(async (options) => {
|
|
288
607
|
if (options.lang) {
|
|
289
608
|
await initI18n(options.lang);
|
|
290
609
|
}
|
|
291
|
-
await checkExternalDeps({ skipPrompt: options.skipPrompt });
|
|
610
|
+
await checkExternalDeps({ skipPrompt: options.skipPrompt, initOpenspec: options.initOpenspec });
|
|
292
611
|
await init(options);
|
|
293
612
|
});
|
|
613
|
+
cli.command("openspec <action>", "Inspect or ensure OpenSpec dependencies").option("--json", "Output JSON").option("--yes, -y", "Skip confirmation").action(async (action, options) => {
|
|
614
|
+
if (action === "inspect") {
|
|
615
|
+
const result = await inspectOpenspec();
|
|
616
|
+
if (options.json)
|
|
617
|
+
console.log(JSON.stringify(result, null, 2));
|
|
618
|
+
else
|
|
619
|
+
printOpenspecInspection(result);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
if (action === "ensure") {
|
|
623
|
+
const result = await ensureOpenspec({
|
|
624
|
+
yes: options.yes,
|
|
625
|
+
confirmInstall: () => confirmOpenspecCliInstall()
|
|
626
|
+
});
|
|
627
|
+
if (options.json)
|
|
628
|
+
console.log(JSON.stringify(result, null, 2));
|
|
629
|
+
else
|
|
630
|
+
printOpenspecInspection(result.inspection);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
console.error(ansis.red(`\u672A\u77E5 openspec \u5B50\u547D\u4EE4: ${action}`));
|
|
634
|
+
process.exitCode = 1;
|
|
635
|
+
});
|
|
294
636
|
cli.command("doctor", "Check ly-workflow-codex installation health").action(async () => {
|
|
295
637
|
await doctor();
|
|
296
638
|
});
|
|
297
639
|
cli.command("status", "Show ly-workflow-codex installation status").action(async () => {
|
|
298
640
|
await status();
|
|
299
641
|
});
|
|
300
|
-
cli.command("uninstall", "Uninstall ly-workflow-codex workflows (~/.agents/skills/lyx-*, ~/.
|
|
642
|
+
cli.command("uninstall", "Uninstall ly-workflow-codex workflows (~/.agents/skills/lyx-*, ~/.codex/lyx/)").option("--yes, -y", "Skip confirmation").action(async (options) => {
|
|
301
643
|
const installDir = join(homedir(), ".codex");
|
|
302
644
|
if (!options.yes) {
|
|
303
645
|
const { confirm } = await inquirer.prompt([{
|
|
304
646
|
type: "confirm",
|
|
305
647
|
name: "confirm",
|
|
306
|
-
message: `\u786E\u5B9A\u8981\u5378\u8F7D ${PACKAGE_NAME} \u5417\uFF1F\u5C06\u79FB\u9664 ~/.agents/skills/lyx-*\uFF08\u542B\u65E7 ~/.agents/skills/ly-* \u4E0E ~/.codex/prompts/ly-*.md \u6B8B\u7559\uFF09\u4E0E ~/.
|
|
648
|
+
message: `\u786E\u5B9A\u8981\u5378\u8F7D ${PACKAGE_NAME} \u5417\uFF1F\u5C06\u79FB\u9664 ~/.agents/skills/lyx-*\uFF08\u542B\u65E7 ~/.agents/skills/ly-* \u4E0E ~/.codex/prompts/ly-*.md \u6B8B\u7559\uFF09\u4E0E ~/.codex/lyx/\uFF08\u914D\u7F6E config.toml\u3001\u89D2\u8272\u8BCD prompts/\u3001worktrees\uFF09\uFF1B\u82E5 ~/.codex/lyx/worktrees/ \u4E0B\u5B58\u5728\u672A\u6E05\u7406\u7684\u5B9E\u9645 git worktree\uFF0C\u8BE5\u5B50\u76EE\u5F55\u4F1A\u88AB\u4FDD\u7559\u5E76\u63D0\u793A\u9700\u5148\u624B\u52A8\u6E05\u7406\u3002`,
|
|
307
649
|
default: false
|
|
308
650
|
}]);
|
|
309
651
|
if (!confirm) {
|
|
@@ -318,10 +660,8 @@ async function setupCommands(cli) {
|
|
|
318
660
|
console.log(ansis.gray(` lyx-* skills: ${result.removedSkills.length} removed`));
|
|
319
661
|
if (result.removedLegacyPrompts.length > 0)
|
|
320
662
|
console.log(ansis.gray(` Legacy ~/.codex/prompts residue: ${result.removedLegacyPrompts.length} removed`));
|
|
321
|
-
if (result.
|
|
322
|
-
console.log(ansis.gray("
|
|
323
|
-
if (result.configTomlKept)
|
|
324
|
-
console.log(ansis.gray(" Shared config (~/.ly/config.toml): kept (~/.ly is the ly-workflow shared namespace)"));
|
|
663
|
+
if (result.removedPrompts)
|
|
664
|
+
console.log(ansis.gray(" Prompts (~/.codex/lyx/prompts/codex/): removed"));
|
|
325
665
|
} else {
|
|
326
666
|
console.error(ansis.red("\u2717 Uninstall failed"));
|
|
327
667
|
for (const err of result.errors) console.error(ansis.gray(` ${err}`));
|