@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 CHANGED
@@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## v1.0.7 (2026-03-22)
9
+
10
+ **新增功能**
11
+
12
+ - ✨ 多工作流管理 - 支持同时管理多个活跃变更
13
+ - 📋 `/opsx:list` - 列出所有活跃工作流
14
+ - 🔄 `/opsx:switch <变更ID>` - 切换工作流
15
+
16
+ **改进**
17
+
18
+ - 🔧 状态文件独立存储 - `.workflow-states/{changeId}.json`
19
+ - 🔧 自动迁移旧格式状态文件
20
+ - 🔧 简化工作流切换逻辑
21
+
22
+ ## v1.0.6 (2026-03-22)
23
+
24
+ **修复问题**
25
+
26
+ - 🐛 修复 `/opsx:*` 命令无法识别的问题
27
+ - 🔧 `opsx:*` 命令直接通过解析器验证,无需预定义列表
28
+ - 🔧 `opsx:*` 命令始终允许执行(工作流管理命令)
29
+
30
+ ## v1.0.5 (2026-03-22)
31
+
32
+ **修复问题**
33
+
34
+ - 🐛 修复交互问题 - 移除编号选项,改为直接显示可用命令
35
+ - 🎨 改进工作流状态显示 - 显示进度条和详细信息
36
+
8
37
  ## v1.0.4 (2026-03-22)
9
38
 
10
39
  **新增功能**
package/dist/cli/index.js CHANGED
@@ -48,18 +48,38 @@ var os__namespace = /*#__PURE__*/_interopNamespace(os);
48
48
  var CommandParser = class {
49
49
  slashCommands = [
50
50
  "help",
51
+ "h",
52
+ "?",
51
53
  "init",
54
+ "i",
52
55
  "new",
56
+ "n",
53
57
  "model",
58
+ "m",
54
59
  "update",
60
+ "u",
55
61
  "clear",
62
+ "c",
56
63
  "exit",
64
+ "e",
65
+ "q",
66
+ "quit",
67
+ "version",
68
+ "v",
69
+ // OpenSpec 工作流命令
57
70
  "opsx:explore",
58
71
  "opsx:new",
59
72
  "opsx:continue",
60
73
  "opsx:apply",
61
74
  "opsx:archive",
62
- "opsx:propose"
75
+ "opsx:propose",
76
+ "opsx:status",
77
+ "opsx:cancel",
78
+ "opsx:rollback",
79
+ "opsx:confirm",
80
+ "opsx:next",
81
+ "opsx:auto",
82
+ "opsx:test"
63
83
  ];
64
84
  builtInAgents = [
65
85
  "frontend-dev",
@@ -108,8 +128,19 @@ var CommandParser = class {
108
128
  if (!command) {
109
129
  return { success: false, error: "\u65E0\u6548\u7684\u547D\u4EE4\u683C\u5F0F" };
110
130
  }
131
+ if (command.startsWith("opsx:")) {
132
+ return {
133
+ success: true,
134
+ command: {
135
+ type: "slash" /* SLASH */,
136
+ raw: input,
137
+ command,
138
+ args
139
+ }
140
+ };
141
+ }
111
142
  const isValidCommand = this.slashCommands.some(
112
- (cmd) => cmd === command || cmd.startsWith(command)
143
+ (cmd) => cmd === command
113
144
  );
114
145
  if (!isValidCommand) {
115
146
  return { success: false, error: `\u672A\u77E5\u547D\u4EE4: /${command}` };
@@ -3799,6 +3830,92 @@ var WorkflowEngine = class {
3799
3830
  getState() {
3800
3831
  return this.state;
3801
3832
  }
3833
+ /**
3834
+ * 获取所有活跃工作流
3835
+ */
3836
+ async getAllActiveWorkflows() {
3837
+ const workflows = [];
3838
+ const changesDir = path6__namespace.join(this.openspecPath, "changes");
3839
+ try {
3840
+ const files = await fs6__namespace.readdir(changesDir);
3841
+ for (const file of files) {
3842
+ if (!file.endsWith(".md") || file.includes("-spec.md")) continue;
3843
+ const filePath = path6__namespace.join(changesDir, file);
3844
+ const content = await fs6__namespace.readFile(filePath, "utf-8");
3845
+ const state = this.parseChangeRecord(content);
3846
+ if (state && state.status === "running") {
3847
+ workflows.push(state);
3848
+ }
3849
+ }
3850
+ } catch {
3851
+ }
3852
+ if (this.state && !workflows.find((w) => w.id === this.state?.id)) {
3853
+ workflows.unshift(this.state);
3854
+ }
3855
+ return workflows;
3856
+ }
3857
+ /**
3858
+ * 解析变更记录
3859
+ */
3860
+ parseChangeRecord(content) {
3861
+ try {
3862
+ const idMatch = content.match(/^id:\s*(.+)$/m);
3863
+ const titleMatch = content.match(/^title:\s*(.+)$/m);
3864
+ const statusMatch = content.match(/^status:\s*(.+)$/m);
3865
+ const complexityMatch = content.match(/^complexity:\s*(\d+)/m);
3866
+ const workflowMatch = content.match(/^workflow:\s*(.+)$/m);
3867
+ const requirementMatch = content.match(/## 变更概述\s*\n+([\s\S]+?)(?=\n##|$)/);
3868
+ if (!idMatch || !titleMatch) return null;
3869
+ return {
3870
+ id: idMatch[1].trim(),
3871
+ title: titleMatch[1].trim(),
3872
+ status: statusMatch?.[1].trim() || "running",
3873
+ requirement: requirementMatch?.[1].trim() || "",
3874
+ complexity: parseInt(complexityMatch?.[1] || "5", 10),
3875
+ type: workflowMatch?.[1].trim() || "simple",
3876
+ currentStep: "propose",
3877
+ // 默认值,实际值需要从状态文件读取
3878
+ steps: [],
3879
+ artifacts: [],
3880
+ createdAt: /* @__PURE__ */ new Date()
3881
+ };
3882
+ } catch {
3883
+ return null;
3884
+ }
3885
+ }
3886
+ /**
3887
+ * 切换到指定工作流
3888
+ */
3889
+ async switchTo(changeId) {
3890
+ if (this.state) {
3891
+ await this.saveState();
3892
+ }
3893
+ const statePath = path6__namespace.join(this.openspecPath, ".workflow-states", `${changeId}.json`);
3894
+ try {
3895
+ const content = await fs6__namespace.readFile(statePath, "utf-8");
3896
+ this.state = JSON.parse(content, (key, value) => {
3897
+ if (key.endsWith("At") && typeof value === "string") {
3898
+ return new Date(value);
3899
+ }
3900
+ return value;
3901
+ });
3902
+ await this.restoreSnapshots();
3903
+ return true;
3904
+ } catch {
3905
+ const changesDir = path6__namespace.join(this.openspecPath, "changes");
3906
+ const changeFile = path6__namespace.join(changesDir, `${changeId}.md`);
3907
+ try {
3908
+ const content = await fs6__namespace.readFile(changeFile, "utf-8");
3909
+ const parsed = this.parseChangeRecord(content);
3910
+ if (parsed && parsed.status === "running") {
3911
+ this.state = parsed;
3912
+ return true;
3913
+ }
3914
+ } catch {
3915
+ }
3916
+ return false;
3917
+ }
3918
+ }
3802
3919
  /**
3803
3920
  * 获取允许的下一步
3804
3921
  */
@@ -3879,32 +3996,64 @@ var WorkflowEngine = class {
3879
3996
  const changesDir = path6__namespace.join(this.openspecPath, "changes");
3880
3997
  const archiveDir = path6__namespace.join(changesDir, "archive");
3881
3998
  const specDir = path6__namespace.join(this.openspecPath, "spec");
3999
+ const statesDir = path6__namespace.join(this.openspecPath, ".workflow-states");
3882
4000
  await fs6__namespace.mkdir(archiveDir, { recursive: true });
3883
4001
  await fs6__namespace.mkdir(specDir, { recursive: true });
4002
+ await fs6__namespace.mkdir(statesDir, { recursive: true });
3884
4003
  }
3885
4004
  async restoreState() {
3886
- const statePath = path6__namespace.join(this.openspecPath, ".workflow-state.json");
4005
+ const activePath = path6__namespace.join(this.openspecPath, ".workflow-active.json");
4006
+ let activeId = null;
3887
4007
  try {
3888
- const content = await fs6__namespace.readFile(statePath, "utf-8");
4008
+ const activeContent = await fs6__namespace.readFile(activePath, "utf-8");
4009
+ const activeData = JSON.parse(activeContent);
4010
+ activeId = activeData.activeId;
4011
+ } catch {
4012
+ }
4013
+ if (activeId) {
4014
+ const statePath = path6__namespace.join(this.openspecPath, ".workflow-states", `${activeId}.json`);
4015
+ try {
4016
+ const content = await fs6__namespace.readFile(statePath, "utf-8");
4017
+ this.state = JSON.parse(content, (key, value) => {
4018
+ if (key.endsWith("At") && typeof value === "string") {
4019
+ return new Date(value);
4020
+ }
4021
+ return value;
4022
+ });
4023
+ return;
4024
+ } catch {
4025
+ }
4026
+ }
4027
+ const oldStatePath = path6__namespace.join(this.openspecPath, ".workflow-state.json");
4028
+ try {
4029
+ const content = await fs6__namespace.readFile(oldStatePath, "utf-8");
3889
4030
  this.state = JSON.parse(content, (key, value) => {
3890
4031
  if (key.endsWith("At") && typeof value === "string") {
3891
4032
  return new Date(value);
3892
4033
  }
3893
4034
  return value;
3894
4035
  });
4036
+ if (this.state) {
4037
+ await this.saveState();
4038
+ await fs6__namespace.unlink(oldStatePath).catch(() => {
4039
+ });
4040
+ }
3895
4041
  } catch (e) {
3896
4042
  const err = e;
3897
4043
  if (err.code !== "ENOENT") {
3898
4044
  console.warn("\u8B66\u544A: \u5DE5\u4F5C\u6D41\u72B6\u6001\u6587\u4EF6\u5DF2\u635F\u574F\uFF0C\u5C06\u91CD\u65B0\u5F00\u59CB");
3899
- await fs6__namespace.unlink(statePath).catch(() => {
3900
- });
3901
4045
  }
3902
4046
  this.state = null;
3903
4047
  }
3904
4048
  }
3905
4049
  async saveState() {
3906
- const statePath = path6__namespace.join(this.openspecPath, ".workflow-state.json");
4050
+ if (!this.state) return;
4051
+ const statesDir = path6__namespace.join(this.openspecPath, ".workflow-states");
4052
+ await fs6__namespace.mkdir(statesDir, { recursive: true });
4053
+ const statePath = path6__namespace.join(statesDir, `${this.state.id}.json`);
3907
4054
  await fs6__namespace.writeFile(statePath, JSON.stringify(this.state, null, 2));
4055
+ const activePath = path6__namespace.join(this.openspecPath, ".workflow-active.json");
4056
+ await fs6__namespace.writeFile(activePath, JSON.stringify({ activeId: this.state.id }, null, 2));
3908
4057
  }
3909
4058
  async restoreSnapshots() {
3910
4059
  const snapshotsPath = path6__namespace.join(this.openspecPath, ".workflow-snapshots.json");
@@ -4052,10 +4201,15 @@ async function handleNew(args, ctx) {
4052
4201
  }
4053
4202
  }
4054
4203
  return {
4055
- output: chalk9__default.default.yellow("\u5F53\u524D\u5DF2\u6709\u6D3B\u8DC3\u7684\u5DE5\u4F5C\u6D41") + chalk9__default.default.gray(`
4204
+ output: chalk9__default.default.yellow("\u5F53\u524D\u5DF2\u6709\u6D3B\u8DC3\u7684\u5DE5\u4F5C\u6D41") + chalk9__default.default.white(`
4056
4205
 
4057
- \u5DE5\u4F5C\u6D41: ${existingState.title}`) + chalk9__default.default.gray(`
4058
- \u5F53\u524D\u9636\u6BB5: ${existingState.currentStep}`) + chalk9__default.default.gray("\n\n\u9009\u9879:") + chalk9__default.default.gray("\n 1. \u7EE7\u7EED\u5F53\u524D\u5DE5\u4F5C\u6D41: /opsx:status") + chalk9__default.default.gray("\n 2. \u53D6\u6D88\u5F53\u524D\u5DE5\u4F5C\u6D41: /opsx:cancel")
4206
+ \u{1F4CB} ${existingState.title || existingState.id}`) + chalk9__default.default.gray(`
4207
+ \u7C7B\u578B: ${existingState.type} | \u590D\u6742\u5EA6: ${existingState.complexity}/10`) + chalk9__default.default.cyan(`
4208
+
4209
+ \u8FDB\u5EA6: ${existingState.steps.map((s) => {
4210
+ const icon = s.status === "completed" ? "\u2713" : s.status === "running" ? "\u25CF" : "\u25CB";
4211
+ return `${icon} ${s.step}`;
4212
+ }).join(" \u2192 ")}`) + chalk9__default.default.yellow("\n\n\u53EF\u7528\u547D\u4EE4:") + chalk9__default.default.white("\n /opsx:status - \u67E5\u770B\u5DE5\u4F5C\u6D41\u8BE6\u60C5") + chalk9__default.default.white("\n /opsx:cancel - \u53D6\u6D88\u5F53\u524D\u5DE5\u4F5C\u6D41")
4059
4213
  };
4060
4214
  }
4061
4215
  }
@@ -5354,9 +5508,13 @@ async function handleOpsx(command, args, ctx) {
5354
5508
  return handleAutoSchedule(args);
5355
5509
  case "test":
5356
5510
  return handleRegressionTest(ctx);
5511
+ case "list":
5512
+ return handleList(workflow);
5513
+ case "switch":
5514
+ return handleSwitch(workflow, args);
5357
5515
  default:
5358
5516
  return {
5359
- output: chalk9__default.default.red(`\u672A\u77E5\u7684OpenSpec\u547D\u4EE4: /${command}`)
5517
+ output: chalk9__default.default.red(`\u672A\u77E5\u7684OpenSpec\u547D\u4EE4: /${command}`) + chalk9__default.default.gray("\n\u53EF\u7528\u547D\u4EE4: /opsx:list, /opsx:status, /opsx:confirm, /opsx:next, /opsx:archive")
5360
5518
  };
5361
5519
  }
5362
5520
  }
@@ -5829,6 +5987,76 @@ ${generateConfirmationPrompt(e.point)}`) + chalk9__default.default.cyan(`
5829
5987
  throw e;
5830
5988
  }
5831
5989
  }
5990
+ async function handleList(workflow, ctx) {
5991
+ const lines = [];
5992
+ const activeWorkflows = await workflow.getAllActiveWorkflows();
5993
+ const currentState = workflow.getState();
5994
+ if (activeWorkflows.length === 0) {
5995
+ return {
5996
+ output: chalk9__default.default.gray("\u5F53\u524D\u6CA1\u6709\u6D3B\u8DC3\u7684\u5DE5\u4F5C\u6D41") + chalk9__default.default.yellow("\n\n\u4F7F\u7528 /new <\u9700\u6C42\u63CF\u8FF0> \u542F\u52A8\u65B0\u5DE5\u4F5C\u6D41")
5997
+ };
5998
+ }
5999
+ lines.push(chalk9__default.default.cyan.bold(`\u{1F4CB} \u6D3B\u8DC3\u5DE5\u4F5C\u6D41 (${activeWorkflows.length})`));
6000
+ lines.push("");
6001
+ for (const wf of activeWorkflows) {
6002
+ const isCurrent = currentState?.id === wf.id;
6003
+ const prefix = isCurrent ? chalk9__default.default.green("\u25B6 ") : " ";
6004
+ const title = isCurrent ? chalk9__default.default.white(wf.title) : chalk9__default.default.gray(wf.title);
6005
+ const stepIcon = wf.currentStep === "propose" || wf.currentStep === "explore" ? "\u23F3" : wf.currentStep === "apply" ? "\u{1F527}" : wf.currentStep === "archive" ? "\u{1F4E6}" : "\u{1F4DD}";
6006
+ lines.push(`${prefix}${title}`);
6007
+ lines.push(chalk9__default.default.gray(` ID: ${wf.id}`));
6008
+ lines.push(chalk9__default.default.gray(` \u9636\u6BB5: ${stepIcon} ${wf.currentStep} | \u590D\u6742\u5EA6: ${wf.complexity}/10`));
6009
+ if (isCurrent) {
6010
+ lines.push(chalk9__default.default.green(" (\u5F53\u524D)"));
6011
+ }
6012
+ lines.push("");
6013
+ }
6014
+ if (activeWorkflows.length > 1) {
6015
+ lines.push(chalk9__default.default.yellow("\u5207\u6362\u5DE5\u4F5C\u6D41:"));
6016
+ lines.push(chalk9__default.default.gray(" /opsx:switch <\u53D8\u66F4ID>"));
6017
+ }
6018
+ lines.push(chalk9__default.default.yellow("\n\u64CD\u4F5C\u547D\u4EE4:"));
6019
+ lines.push(chalk9__default.default.gray(" /opsx:status - \u67E5\u770B\u5F53\u524D\u5DE5\u4F5C\u6D41\u8BE6\u60C5"));
6020
+ lines.push(chalk9__default.default.gray(" /opsx:confirm - \u786E\u8BA4\u89C4\u683C"));
6021
+ lines.push(chalk9__default.default.gray(" /opsx:next - \u8FDB\u5165\u4E0B\u4E00\u9636\u6BB5"));
6022
+ lines.push(chalk9__default.default.gray(" /opsx:cancel - \u53D6\u6D88\u5F53\u524D\u5DE5\u4F5C\u6D41"));
6023
+ return { output: lines.join("\n") };
6024
+ }
6025
+ async function handleSwitch(workflow, args, ctx) {
6026
+ const targetId = args[0];
6027
+ if (!targetId) {
6028
+ const activeWorkflows = await workflow.getAllActiveWorkflows();
6029
+ if (activeWorkflows.length === 0) {
6030
+ return {
6031
+ output: chalk9__default.default.gray("\u6CA1\u6709\u53EF\u5207\u6362\u7684\u5DE5\u4F5C\u6D41")
6032
+ };
6033
+ }
6034
+ const lines = [
6035
+ chalk9__default.default.cyan("\u53EF\u7528\u5DE5\u4F5C\u6D41:"),
6036
+ ""
6037
+ ];
6038
+ for (const wf of activeWorkflows) {
6039
+ lines.push(chalk9__default.default.white(` ${wf.id}`) + chalk9__default.default.gray(` - ${wf.title.slice(0, 30)}...`));
6040
+ }
6041
+ lines.push("");
6042
+ lines.push(chalk9__default.default.gray("\u7528\u6CD5: /opsx:switch <\u53D8\u66F4ID>"));
6043
+ return { output: lines.join("\n") };
6044
+ }
6045
+ const success = await workflow.switchTo(targetId);
6046
+ if (success) {
6047
+ const state = workflow.getState();
6048
+ return {
6049
+ output: chalk9__default.default.green(`\u2713 \u5DF2\u5207\u6362\u5230\u5DE5\u4F5C\u6D41: ${targetId}`) + chalk9__default.default.gray(`
6050
+
6051
+ \u9700\u6C42: ${state?.title}`) + chalk9__default.default.cyan(`
6052
+ \u5F53\u524D\u9636\u6BB5: ${state?.currentStep}`) + chalk9__default.default.yellow("\n\n\u4F7F\u7528 /opsx:status \u67E5\u770B\u8BE6\u60C5")
6053
+ };
6054
+ } else {
6055
+ return {
6056
+ output: chalk9__default.default.red(`\u5207\u6362\u5931\u8D25: \u627E\u4E0D\u5230\u5DE5\u4F5C\u6D41 ${targetId}`) + chalk9__default.default.gray("\n\u4F7F\u7528 /opsx:list \u67E5\u770B\u6240\u6709\u6D3B\u8DC3\u5DE5\u4F5C\u6D41")
6057
+ };
6058
+ }
6059
+ }
5832
6060
 
5833
6061
  // src/commands/runner.ts
5834
6062
  function getVersion() {
@@ -6011,7 +6239,11 @@ var ALLOWED_COMMANDS_WITHOUT_WORKFLOW = [
6011
6239
  "update",
6012
6240
  "u",
6013
6241
  "version",
6014
- "v"
6242
+ "v",
6243
+ // OpenSpec 工作流管理命令(始终允许)
6244
+ "opsx:status",
6245
+ "opsx:cancel",
6246
+ "opsx:rollback"
6015
6247
  ];
6016
6248
  var STAGE_PERMISSIONS = {
6017
6249
  "explore": {
@@ -6084,6 +6316,12 @@ var CommandExecutor = class {
6084
6316
  checkWorkflowPermission(command, ctx) {
6085
6317
  const workflowEngine = ctx.workflowEngine;
6086
6318
  const workflowState = workflowEngine?.getState();
6319
+ if (command.type === "slash" /* SLASH */) {
6320
+ const cmd = command.command?.toLowerCase();
6321
+ if (cmd?.startsWith("opsx:")) {
6322
+ return { allowed: true };
6323
+ }
6324
+ }
6087
6325
  if (!workflowState) {
6088
6326
  if (command.type === "slash" /* SLASH */) {
6089
6327
  const cmd = command.command?.toLowerCase();