@peterxiaoyang/superspec 0.1.17-alpha → 0.1.20

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 CHANGED
@@ -232,13 +232,13 @@ openspec instructions proposal --change <change>
232
232
  superspec status
233
233
  ```
234
234
 
235
- 更新当前 beta CLI:
235
+ 同步当前项目的 SuperSpec 工作流入口:
236
236
 
237
237
  ```bash
238
238
  superspec update
239
239
  ```
240
240
 
241
- 这条命令只检查当前 `.superspec/changes` 运行时是否已经是新引擎布局;真正升级全局 npm 包仍需使用 `npm install -g @peterxiaoyang/superspec@<version>`。
241
+ 这条命令会补齐 `.superspec/changes` 运行时目录,并把当前 CLI 内置的 `.codex/skills/superspec-*`、`.codex/prompts/*.md`、`.codex/agents/*.toml` 同步到项目里。真正升级全局 npm 包仍需使用 `npm install -g @peterxiaoyang/superspec@<version>`。
242
242
 
243
243
  ## 进阶信息
244
244
 
package/dist/cli.js CHANGED
@@ -9,6 +9,7 @@ import { proposeReady, commitTransition, transitionInit, transitionExplore, star
9
9
  import { recordJobSubmit, recordUserDecision, jobsList, jobsPacket } from "./record.js";
10
10
  import { recordTestRun } from "./task.js";
11
11
  import { probeOpenSpec, openspecStatus, changeRoot } from "./openspec.js";
12
+ import { SUPERSPEC_VERSION } from "./version.js";
12
13
  // ===== 参数解析 =====
13
14
  function parseArgs(argv) {
14
15
  const [command, subcommand, ...rest] = argv;
@@ -44,7 +45,7 @@ function parseFlags(args) {
44
45
  async function main(argv) {
45
46
  // --help / 无参数 → 打印用法
46
47
  if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
47
- console.log(`SuperSpec 流程引擎 0.1.16-alpha
48
+ console.log(`SuperSpec 流程引擎 ${SUPERSPEC_VERSION}
48
49
 
49
50
  用法:superspec <命令> [选项]
50
51
 
@@ -55,7 +56,7 @@ async function main(argv) {
55
56
  jobs <子命令> --change <C> 工作项管理(见下)
56
57
  install 安装项目工作流入口
57
58
  init --scope project install 的兼容别名
58
- update 更新 SuperSpec
59
+ update 同步项目工作流模板
59
60
  version 版本号
60
61
 
61
62
  transition 子命令:
@@ -75,7 +76,7 @@ jobs 子命令:
75
76
  }
76
77
  // version
77
78
  if (argv[0] === "version" || argv[0] === "--version" || argv[0] === "-v") {
78
- console.log("SuperSpec 0.1.16-alpha");
79
+ console.log(`SuperSpec ${SUPERSPEC_VERSION}`);
79
80
  return 0;
80
81
  }
81
82
  const { command, subcommand, opts } = parseArgs(argv);
@@ -102,20 +103,21 @@ jobs 子命令:
102
103
  }
103
104
  }
104
105
  if (command === "update") {
105
- // 检测是否从老版 update 过来
106
- const { existsSync } = await import("node:fs");
107
- const isLegacyUpdate = !existsSync(join(projectRoot, ".superspec", "changes"));
108
- if (isLegacyUpdate) {
106
+ try {
107
+ const result = installProject(projectRoot, { allowLegacyState: true });
108
+ console.log(JSON.stringify({
109
+ ...result,
110
+ message: `SuperSpec ${SUPERSPEC_VERSION} 已更新项目工作流`,
111
+ }));
112
+ return 0;
113
+ }
114
+ catch (err) {
109
115
  console.log(JSON.stringify({
110
116
  ok: false,
111
- message: "SuperSpec 0.1.16-alpha 是全新引擎,不能从 0.x 直接 update。\n" +
112
- "请用 npm install -g @peterxiaoyang/superspec@0.1.16-alpha 手动安装。\n" +
113
- "现有 change 请先用 0.1.x 完成归档。",
117
+ message: err.message,
114
118
  }));
115
119
  return 1;
116
120
  }
117
- console.log(JSON.stringify({ ok: true, message: "已是最新版本 0.1.16-alpha" }));
118
- return 0;
119
121
  }
120
122
  const change = opts.change;
121
123
  if (!change && command !== "status") {
package/dist/install.d.ts CHANGED
@@ -15,5 +15,6 @@ export interface InstallResult {
15
15
  }
16
16
  export interface InstallOptions {
17
17
  templateRoot?: string;
18
+ allowLegacyState?: boolean;
18
19
  }
19
20
  export declare function installProject(projectRoot: string, options?: InstallOptions): InstallResult;
package/dist/install.js CHANGED
@@ -1,5 +1,6 @@
1
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { SUPERSPEC_VERSION } from "./version.js";
3
4
  export const WORKFLOW_SKILLS = [
4
5
  "superspec-explore",
5
6
  "superspec-propose",
@@ -58,13 +59,23 @@ function assertWorkflowTemplates(templateRoot) {
58
59
  throw new Error(`workflow templates missing: ${missing.join(", ")}`);
59
60
  }
60
61
  }
62
+ function writeBundledFile(src, dest) {
63
+ mkdirSync(dirname(dest), { recursive: true });
64
+ const next = readFileSync(src, "utf8");
65
+ if (existsSync(dest)) {
66
+ const current = readFileSync(dest, "utf8");
67
+ if (current === next)
68
+ return;
69
+ writeFileSync(`${dest}.bak`, current);
70
+ }
71
+ writeFileSync(dest, next);
72
+ }
61
73
  function copySkills(templateRoot, projectRoot) {
62
74
  const skillsDest = join(projectRoot, ".codex", "skills");
63
75
  const installedSkills = [];
64
76
  for (const skill of WORKFLOW_SKILLS) {
65
77
  const src = join(templateRoot, "skills", skill, "SKILL.md");
66
- mkdirSync(join(skillsDest, skill), { recursive: true });
67
- copyFileSync(src, join(skillsDest, skill, "SKILL.md"));
78
+ writeBundledFile(src, join(skillsDest, skill, "SKILL.md"));
68
79
  installedSkills.push(skill);
69
80
  }
70
81
  return installedSkills;
@@ -74,8 +85,7 @@ function copyPrompts(templateRoot, projectRoot) {
74
85
  const installedPrompts = [];
75
86
  for (const prompt of WORKFLOW_PROMPTS) {
76
87
  const src = join(templateRoot, "prompts", prompt);
77
- mkdirSync(promptsDest, { recursive: true });
78
- copyFileSync(src, join(promptsDest, prompt));
88
+ writeBundledFile(src, join(promptsDest, prompt));
79
89
  installedPrompts.push(prompt);
80
90
  }
81
91
  return installedPrompts;
@@ -85,12 +95,88 @@ function copyAgents(templateRoot, projectRoot) {
85
95
  const installedAgents = [];
86
96
  for (const agent of WORKFLOW_AGENTS) {
87
97
  const src = join(templateRoot, "agents", agent);
88
- mkdirSync(agentsDest, { recursive: true });
89
- copyFileSync(src, join(agentsDest, agent));
98
+ writeBundledFile(src, join(agentsDest, agent));
90
99
  installedAgents.push(agent);
91
100
  }
92
101
  return installedAgents;
93
102
  }
103
+ function isRecord(value) {
104
+ return typeof value === "object" && value !== null && !Array.isArray(value);
105
+ }
106
+ function isLegacySuperspecHook(value) {
107
+ return isRecord(value)
108
+ && typeof value.command === "string"
109
+ && /\bsuperspec-hook\b/.test(value.command);
110
+ }
111
+ function removeLegacySuperspecHookCommands(hooks) {
112
+ if (!isRecord(hooks))
113
+ return { hooks, removed: 0 };
114
+ let removed = 0;
115
+ const nextHooks = { ...hooks };
116
+ for (const [eventName, eventValue] of Object.entries(hooks)) {
117
+ if (!Array.isArray(eventValue))
118
+ continue;
119
+ const nextEvent = [];
120
+ for (const entry of eventValue) {
121
+ if (!isRecord(entry) || !Array.isArray(entry.hooks)) {
122
+ nextEvent.push(entry);
123
+ continue;
124
+ }
125
+ const keptCommands = entry.hooks.filter(command => {
126
+ if (!isLegacySuperspecHook(command))
127
+ return true;
128
+ removed += 1;
129
+ return false;
130
+ });
131
+ if (keptCommands.length > 0) {
132
+ nextEvent.push({ ...entry, hooks: keptCommands });
133
+ }
134
+ }
135
+ if (nextEvent.length > 0)
136
+ nextHooks[eventName] = nextEvent;
137
+ else
138
+ delete nextHooks[eventName];
139
+ }
140
+ return { hooks: nextHooks, removed };
141
+ }
142
+ function hasUserHookContent(config) {
143
+ return Object.entries(config).some(([key, value]) => {
144
+ if (key === "superspec")
145
+ return false;
146
+ if (key === "hooks" && isRecord(value) && Object.keys(value).length === 0)
147
+ return false;
148
+ return true;
149
+ });
150
+ }
151
+ function migrateLegacyManagedHooks(projectRoot) {
152
+ const hooksPath = join(projectRoot, ".codex", "hooks.json");
153
+ if (!existsSync(hooksPath))
154
+ return;
155
+ const current = readFileSync(hooksPath, "utf8");
156
+ let parsed;
157
+ try {
158
+ parsed = JSON.parse(current);
159
+ }
160
+ catch {
161
+ return;
162
+ }
163
+ if (!isRecord(parsed) || !isRecord(parsed.superspec) || parsed.superspec.managed !== true)
164
+ return;
165
+ const migrated = removeLegacySuperspecHookCommands(parsed.hooks);
166
+ if (migrated.removed === 0)
167
+ return;
168
+ writeFileSync(`${hooksPath}.bak`, current);
169
+ const nextConfig = { ...parsed, hooks: migrated.hooks };
170
+ delete nextConfig.superspec;
171
+ if (isRecord(nextConfig.hooks) && Object.keys(nextConfig.hooks).length === 0) {
172
+ delete nextConfig.hooks;
173
+ }
174
+ if (!hasUserHookContent(nextConfig)) {
175
+ rmSync(hooksPath);
176
+ return;
177
+ }
178
+ writeFileSync(hooksPath, `${JSON.stringify(nextConfig, null, 2)}\n`);
179
+ }
94
180
  function insertMissingTableEntries(content, table, entries) {
95
181
  const tablePattern = new RegExp(`^\\[${table}\\]\\s*$`, "m");
96
182
  const tableMatch = tablePattern.exec(content);
@@ -157,10 +243,10 @@ function ensureOpenSpecChineseContext(projectRoot) {
157
243
  return OPENSPEC_CONFIG_PATH;
158
244
  }
159
245
  export function installProject(projectRoot, options = {}) {
160
- if (legacyStateFound(projectRoot)) {
246
+ if (!options.allowLegacyState && legacyStateFound(projectRoot)) {
161
247
  throw new Error("检测到老版 SuperSpec (0.x) 的状态文件。\n" +
162
- "SuperSpec 0.1.16-alpha 是全新引擎,不兼容 0.x 的状态格式。\n" +
163
- "请先用老版(0.1.x)完成或归档现有 change,再安装 0.1.16-alpha。\n" +
248
+ `SuperSpec ${SUPERSPEC_VERSION} 是全新引擎,不兼容 0.x 的状态格式。\n` +
249
+ `请先用老版(0.1.x)完成或归档现有 change,再安装 ${SUPERSPEC_VERSION}。\n` +
164
250
  "或在全新项目目录中安装。");
165
251
  }
166
252
  const templateRoot = options.templateRoot ?? defaultTemplateRoot();
@@ -170,9 +256,10 @@ export function installProject(projectRoot, options = {}) {
170
256
  const gitignorePath = join(engineDir, ".gitignore");
171
257
  if (!existsSync(gitignorePath))
172
258
  writeFileSync(gitignorePath, "changes/\n*.log\n*.tmp\n");
259
+ migrateLegacyManagedHooks(projectRoot);
173
260
  return {
174
261
  ok: true,
175
- message: "SuperSpec 0.1.16-alpha 已安装",
262
+ message: `SuperSpec ${SUPERSPEC_VERSION} 已安装`,
176
263
  installed: {
177
264
  engine_dir: ".superspec/",
178
265
  skills: copySkills(templateRoot, projectRoot),
@@ -0,0 +1 @@
1
+ export declare const SUPERSPEC_VERSION: string;
@@ -0,0 +1,9 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+ function readPackageVersion() {
4
+ const pkg = require("../package.json");
5
+ if (!pkg.version)
6
+ throw new Error("package.json 缺少 version 字段");
7
+ return pkg.version;
8
+ }
9
+ export const SUPERSPEC_VERSION = readPackageVersion();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peterxiaoyang/superspec",
3
- "version": "0.1.17-alpha",
3
+ "version": "0.1.20",
4
4
  "description": "SuperSpec 流程引擎 — transition engine with lightweight fact-sync",
5
5
  "type": "module",
6
6
  "engines": { "node": ">=20.19.0" },