@nick848/sf-cli 1.0.4 → 1.0.7
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/CHANGELOG.md +29 -0
- package/dist/cli/index.js +250 -12
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +250 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +250 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -4090,6 +4090,92 @@ var WorkflowEngine = class {
|
|
|
4090
4090
|
getState() {
|
|
4091
4091
|
return this.state;
|
|
4092
4092
|
}
|
|
4093
|
+
/**
|
|
4094
|
+
* 获取所有活跃工作流
|
|
4095
|
+
*/
|
|
4096
|
+
async getAllActiveWorkflows() {
|
|
4097
|
+
const workflows = [];
|
|
4098
|
+
const changesDir = path5.join(this.openspecPath, "changes");
|
|
4099
|
+
try {
|
|
4100
|
+
const files = await fs4.readdir(changesDir);
|
|
4101
|
+
for (const file of files) {
|
|
4102
|
+
if (!file.endsWith(".md") || file.includes("-spec.md")) continue;
|
|
4103
|
+
const filePath = path5.join(changesDir, file);
|
|
4104
|
+
const content = await fs4.readFile(filePath, "utf-8");
|
|
4105
|
+
const state = this.parseChangeRecord(content);
|
|
4106
|
+
if (state && state.status === "running") {
|
|
4107
|
+
workflows.push(state);
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
} catch {
|
|
4111
|
+
}
|
|
4112
|
+
if (this.state && !workflows.find((w) => w.id === this.state?.id)) {
|
|
4113
|
+
workflows.unshift(this.state);
|
|
4114
|
+
}
|
|
4115
|
+
return workflows;
|
|
4116
|
+
}
|
|
4117
|
+
/**
|
|
4118
|
+
* 解析变更记录
|
|
4119
|
+
*/
|
|
4120
|
+
parseChangeRecord(content) {
|
|
4121
|
+
try {
|
|
4122
|
+
const idMatch = content.match(/^id:\s*(.+)$/m);
|
|
4123
|
+
const titleMatch = content.match(/^title:\s*(.+)$/m);
|
|
4124
|
+
const statusMatch = content.match(/^status:\s*(.+)$/m);
|
|
4125
|
+
const complexityMatch = content.match(/^complexity:\s*(\d+)/m);
|
|
4126
|
+
const workflowMatch = content.match(/^workflow:\s*(.+)$/m);
|
|
4127
|
+
const requirementMatch = content.match(/## 变更概述\s*\n+([\s\S]+?)(?=\n##|$)/);
|
|
4128
|
+
if (!idMatch || !titleMatch) return null;
|
|
4129
|
+
return {
|
|
4130
|
+
id: idMatch[1].trim(),
|
|
4131
|
+
title: titleMatch[1].trim(),
|
|
4132
|
+
status: statusMatch?.[1].trim() || "running",
|
|
4133
|
+
requirement: requirementMatch?.[1].trim() || "",
|
|
4134
|
+
complexity: parseInt(complexityMatch?.[1] || "5", 10),
|
|
4135
|
+
type: workflowMatch?.[1].trim() || "simple",
|
|
4136
|
+
currentStep: "propose",
|
|
4137
|
+
// 默认值,实际值需要从状态文件读取
|
|
4138
|
+
steps: [],
|
|
4139
|
+
artifacts: [],
|
|
4140
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
4141
|
+
};
|
|
4142
|
+
} catch {
|
|
4143
|
+
return null;
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
/**
|
|
4147
|
+
* 切换到指定工作流
|
|
4148
|
+
*/
|
|
4149
|
+
async switchTo(changeId) {
|
|
4150
|
+
if (this.state) {
|
|
4151
|
+
await this.saveState();
|
|
4152
|
+
}
|
|
4153
|
+
const statePath = path5.join(this.openspecPath, ".workflow-states", `${changeId}.json`);
|
|
4154
|
+
try {
|
|
4155
|
+
const content = await fs4.readFile(statePath, "utf-8");
|
|
4156
|
+
this.state = JSON.parse(content, (key, value) => {
|
|
4157
|
+
if (key.endsWith("At") && typeof value === "string") {
|
|
4158
|
+
return new Date(value);
|
|
4159
|
+
}
|
|
4160
|
+
return value;
|
|
4161
|
+
});
|
|
4162
|
+
await this.restoreSnapshots();
|
|
4163
|
+
return true;
|
|
4164
|
+
} catch {
|
|
4165
|
+
const changesDir = path5.join(this.openspecPath, "changes");
|
|
4166
|
+
const changeFile = path5.join(changesDir, `${changeId}.md`);
|
|
4167
|
+
try {
|
|
4168
|
+
const content = await fs4.readFile(changeFile, "utf-8");
|
|
4169
|
+
const parsed = this.parseChangeRecord(content);
|
|
4170
|
+
if (parsed && parsed.status === "running") {
|
|
4171
|
+
this.state = parsed;
|
|
4172
|
+
return true;
|
|
4173
|
+
}
|
|
4174
|
+
} catch {
|
|
4175
|
+
}
|
|
4176
|
+
return false;
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4093
4179
|
/**
|
|
4094
4180
|
* 获取允许的下一步
|
|
4095
4181
|
*/
|
|
@@ -4170,32 +4256,64 @@ var WorkflowEngine = class {
|
|
|
4170
4256
|
const changesDir = path5.join(this.openspecPath, "changes");
|
|
4171
4257
|
const archiveDir = path5.join(changesDir, "archive");
|
|
4172
4258
|
const specDir = path5.join(this.openspecPath, "spec");
|
|
4259
|
+
const statesDir = path5.join(this.openspecPath, ".workflow-states");
|
|
4173
4260
|
await fs4.mkdir(archiveDir, { recursive: true });
|
|
4174
4261
|
await fs4.mkdir(specDir, { recursive: true });
|
|
4262
|
+
await fs4.mkdir(statesDir, { recursive: true });
|
|
4175
4263
|
}
|
|
4176
4264
|
async restoreState() {
|
|
4177
|
-
const
|
|
4265
|
+
const activePath = path5.join(this.openspecPath, ".workflow-active.json");
|
|
4266
|
+
let activeId = null;
|
|
4178
4267
|
try {
|
|
4179
|
-
const
|
|
4268
|
+
const activeContent = await fs4.readFile(activePath, "utf-8");
|
|
4269
|
+
const activeData = JSON.parse(activeContent);
|
|
4270
|
+
activeId = activeData.activeId;
|
|
4271
|
+
} catch {
|
|
4272
|
+
}
|
|
4273
|
+
if (activeId) {
|
|
4274
|
+
const statePath = path5.join(this.openspecPath, ".workflow-states", `${activeId}.json`);
|
|
4275
|
+
try {
|
|
4276
|
+
const content = await fs4.readFile(statePath, "utf-8");
|
|
4277
|
+
this.state = JSON.parse(content, (key, value) => {
|
|
4278
|
+
if (key.endsWith("At") && typeof value === "string") {
|
|
4279
|
+
return new Date(value);
|
|
4280
|
+
}
|
|
4281
|
+
return value;
|
|
4282
|
+
});
|
|
4283
|
+
return;
|
|
4284
|
+
} catch {
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
const oldStatePath = path5.join(this.openspecPath, ".workflow-state.json");
|
|
4288
|
+
try {
|
|
4289
|
+
const content = await fs4.readFile(oldStatePath, "utf-8");
|
|
4180
4290
|
this.state = JSON.parse(content, (key, value) => {
|
|
4181
4291
|
if (key.endsWith("At") && typeof value === "string") {
|
|
4182
4292
|
return new Date(value);
|
|
4183
4293
|
}
|
|
4184
4294
|
return value;
|
|
4185
4295
|
});
|
|
4296
|
+
if (this.state) {
|
|
4297
|
+
await this.saveState();
|
|
4298
|
+
await fs4.unlink(oldStatePath).catch(() => {
|
|
4299
|
+
});
|
|
4300
|
+
}
|
|
4186
4301
|
} catch (e) {
|
|
4187
4302
|
const err = e;
|
|
4188
4303
|
if (err.code !== "ENOENT") {
|
|
4189
4304
|
console.warn("\u8B66\u544A: \u5DE5\u4F5C\u6D41\u72B6\u6001\u6587\u4EF6\u5DF2\u635F\u574F\uFF0C\u5C06\u91CD\u65B0\u5F00\u59CB");
|
|
4190
|
-
await fs4.unlink(statePath).catch(() => {
|
|
4191
|
-
});
|
|
4192
4305
|
}
|
|
4193
4306
|
this.state = null;
|
|
4194
4307
|
}
|
|
4195
4308
|
}
|
|
4196
4309
|
async saveState() {
|
|
4197
|
-
|
|
4310
|
+
if (!this.state) return;
|
|
4311
|
+
const statesDir = path5.join(this.openspecPath, ".workflow-states");
|
|
4312
|
+
await fs4.mkdir(statesDir, { recursive: true });
|
|
4313
|
+
const statePath = path5.join(statesDir, `${this.state.id}.json`);
|
|
4198
4314
|
await fs4.writeFile(statePath, JSON.stringify(this.state, null, 2));
|
|
4315
|
+
const activePath = path5.join(this.openspecPath, ".workflow-active.json");
|
|
4316
|
+
await fs4.writeFile(activePath, JSON.stringify({ activeId: this.state.id }, null, 2));
|
|
4199
4317
|
}
|
|
4200
4318
|
async restoreSnapshots() {
|
|
4201
4319
|
const snapshotsPath = path5.join(this.openspecPath, ".workflow-snapshots.json");
|
|
@@ -5566,18 +5684,38 @@ var CommandType = /* @__PURE__ */ ((CommandType3) => {
|
|
|
5566
5684
|
var CommandParser = class {
|
|
5567
5685
|
slashCommands = [
|
|
5568
5686
|
"help",
|
|
5687
|
+
"h",
|
|
5688
|
+
"?",
|
|
5569
5689
|
"init",
|
|
5690
|
+
"i",
|
|
5570
5691
|
"new",
|
|
5692
|
+
"n",
|
|
5571
5693
|
"model",
|
|
5694
|
+
"m",
|
|
5572
5695
|
"update",
|
|
5696
|
+
"u",
|
|
5573
5697
|
"clear",
|
|
5698
|
+
"c",
|
|
5574
5699
|
"exit",
|
|
5700
|
+
"e",
|
|
5701
|
+
"q",
|
|
5702
|
+
"quit",
|
|
5703
|
+
"version",
|
|
5704
|
+
"v",
|
|
5705
|
+
// OpenSpec 工作流命令
|
|
5575
5706
|
"opsx:explore",
|
|
5576
5707
|
"opsx:new",
|
|
5577
5708
|
"opsx:continue",
|
|
5578
5709
|
"opsx:apply",
|
|
5579
5710
|
"opsx:archive",
|
|
5580
|
-
"opsx:propose"
|
|
5711
|
+
"opsx:propose",
|
|
5712
|
+
"opsx:status",
|
|
5713
|
+
"opsx:cancel",
|
|
5714
|
+
"opsx:rollback",
|
|
5715
|
+
"opsx:confirm",
|
|
5716
|
+
"opsx:next",
|
|
5717
|
+
"opsx:auto",
|
|
5718
|
+
"opsx:test"
|
|
5581
5719
|
];
|
|
5582
5720
|
builtInAgents = [
|
|
5583
5721
|
"frontend-dev",
|
|
@@ -5626,8 +5764,19 @@ var CommandParser = class {
|
|
|
5626
5764
|
if (!command) {
|
|
5627
5765
|
return { success: false, error: "\u65E0\u6548\u7684\u547D\u4EE4\u683C\u5F0F" };
|
|
5628
5766
|
}
|
|
5767
|
+
if (command.startsWith("opsx:")) {
|
|
5768
|
+
return {
|
|
5769
|
+
success: true,
|
|
5770
|
+
command: {
|
|
5771
|
+
type: "slash" /* SLASH */,
|
|
5772
|
+
raw: input,
|
|
5773
|
+
command,
|
|
5774
|
+
args
|
|
5775
|
+
}
|
|
5776
|
+
};
|
|
5777
|
+
}
|
|
5629
5778
|
const isValidCommand = this.slashCommands.some(
|
|
5630
|
-
(cmd) => cmd === command
|
|
5779
|
+
(cmd) => cmd === command
|
|
5631
5780
|
);
|
|
5632
5781
|
if (!isValidCommand) {
|
|
5633
5782
|
return { success: false, error: `\u672A\u77E5\u547D\u4EE4: /${command}` };
|
|
@@ -6745,10 +6894,15 @@ async function handleNew(args, ctx) {
|
|
|
6745
6894
|
}
|
|
6746
6895
|
}
|
|
6747
6896
|
return {
|
|
6748
|
-
output: chalk9.yellow("\u5F53\u524D\u5DF2\u6709\u6D3B\u8DC3\u7684\u5DE5\u4F5C\u6D41") + chalk9.
|
|
6897
|
+
output: chalk9.yellow("\u5F53\u524D\u5DF2\u6709\u6D3B\u8DC3\u7684\u5DE5\u4F5C\u6D41") + chalk9.white(`
|
|
6749
6898
|
|
|
6750
|
-
\
|
|
6751
|
-
\
|
|
6899
|
+
\u{1F4CB} ${existingState.title || existingState.id}`) + chalk9.gray(`
|
|
6900
|
+
\u7C7B\u578B: ${existingState.type} | \u590D\u6742\u5EA6: ${existingState.complexity}/10`) + chalk9.cyan(`
|
|
6901
|
+
|
|
6902
|
+
\u8FDB\u5EA6: ${existingState.steps.map((s) => {
|
|
6903
|
+
const icon = s.status === "completed" ? "\u2713" : s.status === "running" ? "\u25CF" : "\u25CB";
|
|
6904
|
+
return `${icon} ${s.step}`;
|
|
6905
|
+
}).join(" \u2192 ")}`) + chalk9.yellow("\n\n\u53EF\u7528\u547D\u4EE4:") + chalk9.white("\n /opsx:status - \u67E5\u770B\u5DE5\u4F5C\u6D41\u8BE6\u60C5") + chalk9.white("\n /opsx:cancel - \u53D6\u6D88\u5F53\u524D\u5DE5\u4F5C\u6D41")
|
|
6752
6906
|
};
|
|
6753
6907
|
}
|
|
6754
6908
|
}
|
|
@@ -7405,9 +7559,13 @@ async function handleOpsx(command, args, ctx) {
|
|
|
7405
7559
|
return handleAutoSchedule(args);
|
|
7406
7560
|
case "test":
|
|
7407
7561
|
return handleRegressionTest(ctx);
|
|
7562
|
+
case "list":
|
|
7563
|
+
return handleList(workflow);
|
|
7564
|
+
case "switch":
|
|
7565
|
+
return handleSwitch(workflow, args);
|
|
7408
7566
|
default:
|
|
7409
7567
|
return {
|
|
7410
|
-
output: chalk9.red(`\u672A\u77E5\u7684OpenSpec\u547D\u4EE4: /${command}`)
|
|
7568
|
+
output: chalk9.red(`\u672A\u77E5\u7684OpenSpec\u547D\u4EE4: /${command}`) + chalk9.gray("\n\u53EF\u7528\u547D\u4EE4: /opsx:list, /opsx:status, /opsx:confirm, /opsx:next, /opsx:archive")
|
|
7411
7569
|
};
|
|
7412
7570
|
}
|
|
7413
7571
|
}
|
|
@@ -7880,6 +8038,76 @@ ${generateConfirmationPrompt(e.point)}`) + chalk9.cyan(`
|
|
|
7880
8038
|
throw e;
|
|
7881
8039
|
}
|
|
7882
8040
|
}
|
|
8041
|
+
async function handleList(workflow, ctx) {
|
|
8042
|
+
const lines = [];
|
|
8043
|
+
const activeWorkflows = await workflow.getAllActiveWorkflows();
|
|
8044
|
+
const currentState = workflow.getState();
|
|
8045
|
+
if (activeWorkflows.length === 0) {
|
|
8046
|
+
return {
|
|
8047
|
+
output: chalk9.gray("\u5F53\u524D\u6CA1\u6709\u6D3B\u8DC3\u7684\u5DE5\u4F5C\u6D41") + chalk9.yellow("\n\n\u4F7F\u7528 /new <\u9700\u6C42\u63CF\u8FF0> \u542F\u52A8\u65B0\u5DE5\u4F5C\u6D41")
|
|
8048
|
+
};
|
|
8049
|
+
}
|
|
8050
|
+
lines.push(chalk9.cyan.bold(`\u{1F4CB} \u6D3B\u8DC3\u5DE5\u4F5C\u6D41 (${activeWorkflows.length})`));
|
|
8051
|
+
lines.push("");
|
|
8052
|
+
for (const wf of activeWorkflows) {
|
|
8053
|
+
const isCurrent = currentState?.id === wf.id;
|
|
8054
|
+
const prefix = isCurrent ? chalk9.green("\u25B6 ") : " ";
|
|
8055
|
+
const title = isCurrent ? chalk9.white(wf.title) : chalk9.gray(wf.title);
|
|
8056
|
+
const stepIcon = wf.currentStep === "propose" || wf.currentStep === "explore" ? "\u23F3" : wf.currentStep === "apply" ? "\u{1F527}" : wf.currentStep === "archive" ? "\u{1F4E6}" : "\u{1F4DD}";
|
|
8057
|
+
lines.push(`${prefix}${title}`);
|
|
8058
|
+
lines.push(chalk9.gray(` ID: ${wf.id}`));
|
|
8059
|
+
lines.push(chalk9.gray(` \u9636\u6BB5: ${stepIcon} ${wf.currentStep} | \u590D\u6742\u5EA6: ${wf.complexity}/10`));
|
|
8060
|
+
if (isCurrent) {
|
|
8061
|
+
lines.push(chalk9.green(" (\u5F53\u524D)"));
|
|
8062
|
+
}
|
|
8063
|
+
lines.push("");
|
|
8064
|
+
}
|
|
8065
|
+
if (activeWorkflows.length > 1) {
|
|
8066
|
+
lines.push(chalk9.yellow("\u5207\u6362\u5DE5\u4F5C\u6D41:"));
|
|
8067
|
+
lines.push(chalk9.gray(" /opsx:switch <\u53D8\u66F4ID>"));
|
|
8068
|
+
}
|
|
8069
|
+
lines.push(chalk9.yellow("\n\u64CD\u4F5C\u547D\u4EE4:"));
|
|
8070
|
+
lines.push(chalk9.gray(" /opsx:status - \u67E5\u770B\u5F53\u524D\u5DE5\u4F5C\u6D41\u8BE6\u60C5"));
|
|
8071
|
+
lines.push(chalk9.gray(" /opsx:confirm - \u786E\u8BA4\u89C4\u683C"));
|
|
8072
|
+
lines.push(chalk9.gray(" /opsx:next - \u8FDB\u5165\u4E0B\u4E00\u9636\u6BB5"));
|
|
8073
|
+
lines.push(chalk9.gray(" /opsx:cancel - \u53D6\u6D88\u5F53\u524D\u5DE5\u4F5C\u6D41"));
|
|
8074
|
+
return { output: lines.join("\n") };
|
|
8075
|
+
}
|
|
8076
|
+
async function handleSwitch(workflow, args, ctx) {
|
|
8077
|
+
const targetId = args[0];
|
|
8078
|
+
if (!targetId) {
|
|
8079
|
+
const activeWorkflows = await workflow.getAllActiveWorkflows();
|
|
8080
|
+
if (activeWorkflows.length === 0) {
|
|
8081
|
+
return {
|
|
8082
|
+
output: chalk9.gray("\u6CA1\u6709\u53EF\u5207\u6362\u7684\u5DE5\u4F5C\u6D41")
|
|
8083
|
+
};
|
|
8084
|
+
}
|
|
8085
|
+
const lines = [
|
|
8086
|
+
chalk9.cyan("\u53EF\u7528\u5DE5\u4F5C\u6D41:"),
|
|
8087
|
+
""
|
|
8088
|
+
];
|
|
8089
|
+
for (const wf of activeWorkflows) {
|
|
8090
|
+
lines.push(chalk9.white(` ${wf.id}`) + chalk9.gray(` - ${wf.title.slice(0, 30)}...`));
|
|
8091
|
+
}
|
|
8092
|
+
lines.push("");
|
|
8093
|
+
lines.push(chalk9.gray("\u7528\u6CD5: /opsx:switch <\u53D8\u66F4ID>"));
|
|
8094
|
+
return { output: lines.join("\n") };
|
|
8095
|
+
}
|
|
8096
|
+
const success = await workflow.switchTo(targetId);
|
|
8097
|
+
if (success) {
|
|
8098
|
+
const state = workflow.getState();
|
|
8099
|
+
return {
|
|
8100
|
+
output: chalk9.green(`\u2713 \u5DF2\u5207\u6362\u5230\u5DE5\u4F5C\u6D41: ${targetId}`) + chalk9.gray(`
|
|
8101
|
+
|
|
8102
|
+
\u9700\u6C42: ${state?.title}`) + chalk9.cyan(`
|
|
8103
|
+
\u5F53\u524D\u9636\u6BB5: ${state?.currentStep}`) + chalk9.yellow("\n\n\u4F7F\u7528 /opsx:status \u67E5\u770B\u8BE6\u60C5")
|
|
8104
|
+
};
|
|
8105
|
+
} else {
|
|
8106
|
+
return {
|
|
8107
|
+
output: chalk9.red(`\u5207\u6362\u5931\u8D25: \u627E\u4E0D\u5230\u5DE5\u4F5C\u6D41 ${targetId}`) + chalk9.gray("\n\u4F7F\u7528 /opsx:list \u67E5\u770B\u6240\u6709\u6D3B\u8DC3\u5DE5\u4F5C\u6D41")
|
|
8108
|
+
};
|
|
8109
|
+
}
|
|
8110
|
+
}
|
|
7883
8111
|
|
|
7884
8112
|
// src/commands/runner.ts
|
|
7885
8113
|
function getVersion() {
|
|
@@ -8062,7 +8290,11 @@ var ALLOWED_COMMANDS_WITHOUT_WORKFLOW = [
|
|
|
8062
8290
|
"update",
|
|
8063
8291
|
"u",
|
|
8064
8292
|
"version",
|
|
8065
|
-
"v"
|
|
8293
|
+
"v",
|
|
8294
|
+
// OpenSpec 工作流管理命令(始终允许)
|
|
8295
|
+
"opsx:status",
|
|
8296
|
+
"opsx:cancel",
|
|
8297
|
+
"opsx:rollback"
|
|
8066
8298
|
];
|
|
8067
8299
|
var STAGE_PERMISSIONS = {
|
|
8068
8300
|
"explore": {
|
|
@@ -8135,6 +8367,12 @@ var CommandExecutor = class {
|
|
|
8135
8367
|
checkWorkflowPermission(command, ctx) {
|
|
8136
8368
|
const workflowEngine = ctx.workflowEngine;
|
|
8137
8369
|
const workflowState = workflowEngine?.getState();
|
|
8370
|
+
if (command.type === "slash" /* SLASH */) {
|
|
8371
|
+
const cmd = command.command?.toLowerCase();
|
|
8372
|
+
if (cmd?.startsWith("opsx:")) {
|
|
8373
|
+
return { allowed: true };
|
|
8374
|
+
}
|
|
8375
|
+
}
|
|
8138
8376
|
if (!workflowState) {
|
|
8139
8377
|
if (command.type === "slash" /* SLASH */) {
|
|
8140
8378
|
const cmd = command.command?.toLowerCase();
|