@kafka0102/onespec 0.1.2 → 0.2.2

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.
Files changed (29) hide show
  1. package/README.md +45 -48
  2. package/assets/skills/onespec/SKILL.md +22 -14
  3. package/assets/skills/onespec/references/archive.md +214 -0
  4. package/assets/skills/{onespec-design/SKILL.md → onespec/references/design.md} +55 -51
  5. package/assets/skills/onespec/references/execute.md +291 -0
  6. package/assets/skills/onespec/references/fast.md +110 -0
  7. package/assets/skills/onespec/scripts/onespec-closeout.sh +238 -77
  8. package/assets/skills/onespec/scripts/onespec-commit.sh +191 -11
  9. package/assets/skills/onespec/scripts/onespec-handoff.sh +19 -6
  10. package/assets/skills/onespec/scripts/onespec-state.sh +157 -18
  11. package/assets/skills/onespec-fast/SKILL.md +22 -0
  12. package/assets/skills/onespec-fast/agents/openai.yaml +4 -0
  13. package/assets/skills-en/onespec/SKILL.md +22 -13
  14. package/assets/skills-en/onespec/references/archive.md +213 -0
  15. package/assets/skills-en/{onespec-design/SKILL.md → onespec/references/design.md} +58 -43
  16. package/assets/skills-en/onespec/references/execute.md +291 -0
  17. package/assets/skills-en/onespec/references/fast.md +110 -0
  18. package/assets/skills-en/onespec-fast/SKILL.md +22 -0
  19. package/package.json +10 -3
  20. package/scripts/postinstall.js +3 -3
  21. package/src/cli.js +120 -110
  22. package/src/doctor.js +46 -20
  23. package/src/init.js +24 -10
  24. package/src/platforms.js +88 -8
  25. package/src/setup.js +211 -0
  26. package/assets/skills/onespec-archive/SKILL.md +0 -202
  27. package/assets/skills/onespec-execute/SKILL.md +0 -219
  28. package/assets/skills-en/onespec-archive/SKILL.md +0 -199
  29. package/assets/skills-en/onespec-execute/SKILL.md +0 -219
package/src/cli.js CHANGED
@@ -1,20 +1,24 @@
1
- import { execFileSync } from 'node:child_process';
2
1
  import path from 'node:path';
3
- import readline from 'node:readline/promises';
4
- import { stdin as input, stdout as output } from 'node:process';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { checkbox, select, confirm } from '@inquirer/prompts';
5
4
 
6
5
  import { doctorProject } from './doctor.js';
7
- import { initProject, SUPPORTED_LANGUAGES } from './init.js';
8
-
9
- const OPEN_SPEC_CLI_PACKAGE = '@fission-ai/openspec@latest';
10
- const SUPERPOWERS_PACKAGE = 'obra/superpowers';
6
+ import { SUPPORTED_LANGUAGES } from './init.js';
7
+ import { getPlatform, PLATFORMS } from './platforms.js';
8
+ import {
9
+ detectExistingOneSpecPlatforms,
10
+ detectPlatforms,
11
+ initWorkspace,
12
+ parsePlatformList,
13
+ SUPPORTED_PLATFORM_IDS,
14
+ } from './setup.js';
11
15
 
12
16
  function parseArgs(argv) {
13
17
  const args = [...argv];
14
18
  const command = args.shift() ?? 'help';
15
19
  const options = {
16
20
  targetPath: process.cwd(),
17
- platform: 'codex',
21
+ platforms: [],
18
22
  scope: undefined,
19
23
  language: undefined,
20
24
  yes: false,
@@ -43,7 +47,7 @@ function parseArgs(argv) {
43
47
  options.language = args.shift();
44
48
  break;
45
49
  case '--platform':
46
- options.platform = args.shift() ?? 'codex';
50
+ options.platforms.push(args.shift() ?? '');
47
51
  break;
48
52
  default:
49
53
  if (arg?.startsWith('-')) {
@@ -57,135 +61,141 @@ function parseArgs(argv) {
57
61
  return { command, options };
58
62
  }
59
63
 
60
- function commandExists(command) {
61
- try {
62
- const checker = process.platform === 'win32' ? 'where' : 'which';
63
- execFileSync(checker, [command], { stdio: 'ignore' });
64
- return true;
65
- } catch {
66
- return false;
67
- }
64
+ async function selectScope(options) {
65
+ if (options.scope) return options.scope;
66
+ if (options.yes) return 'project';
67
+
68
+ return select({
69
+ message: 'Install scope:',
70
+ choices: [
71
+ { name: 'Project (current directory)', value: 'project' },
72
+ { name: 'Global (home directory)', value: 'global' },
73
+ ],
74
+ });
68
75
  }
69
76
 
70
- async function askInitOptions(options) {
71
- if (options.yes) {
72
- return {
73
- ...options,
74
- scope: options.scope ?? 'project',
75
- language: options.language ?? 'zh',
76
- installOpenSpecCli: false,
77
- initOpenSpecProject: false,
78
- installSuperpowers: false,
79
- };
80
- }
77
+ async function selectLanguage(options) {
78
+ if (options.yes) return 'zh';
81
79
 
82
- const preflight = await doctorProject(options.targetPath, {
83
- platform: options.platform,
84
- scope: options.scope ?? 'project',
80
+ const langId = await select({
81
+ message: 'Language for OneSpec skills:',
82
+ choices: Object.entries(SUPPORTED_LANGUAGES).map(([id, lang]) => ({ name: lang.name, value: id })),
85
83
  });
86
- const rl = readline.createInterface({ input, output });
87
- try {
88
- const scopeAnswer =
89
- options.scope ??
90
- (await rl.question('安装范围?输入 project 或 global(默认 project):'));
91
- const resolvedScope = scopeAnswer.trim() || 'project';
92
- const languageAnswer =
93
- options.language ??
94
- (await rl.question('Skill 语言?输入 zh 或 en(默认 zh):'));
95
- const overwriteAnswer = options.overwrite
96
- ? 'yes'
97
- : await rl.question('如果 OneSpec skill 已存在,是否覆盖?输入 yes 或 no(默认 no):');
98
- const installOpenSpecCliAnswer =
99
- preflight.openspecCli.available
100
- ? 'no'
101
- : await rl.question(
102
- `未检测到 OpenSpec CLI。是否现在执行 npm install -g ${OPEN_SPEC_CLI_PACKAGE} ?输入 yes 或 no(默认 no):`,
103
- );
104
- const initOpenSpecProjectAnswer =
105
- resolvedScope === 'project' && !preflight.hasOpenSpecProject
106
- ? await rl.question(
107
- '当前项目未初始化 OpenSpec。是否在安装 OneSpec 后执行 openspec init?输入 yes 或 no(默认 no):',
108
- )
109
- : 'no';
110
- const installSuperpowersAnswer = preflight.superpowers.available
111
- ? 'no'
112
- : await rl.question(
113
- `未检测到 Superpowers。是否现在执行 npx skills add ${SUPERPOWERS_PACKAGE} -a codex${resolvedScope === 'global' ? ' -g' : ''} -y ?输入 yes 或 no(默认 no):`,
114
- );
115
84
 
85
+ return langId;
86
+ }
87
+
88
+ async function selectPlatforms(detected, options) {
89
+ const choices = SUPPORTED_PLATFORM_IDS.map((platformId) => {
90
+ const platform = getPlatform(platformId);
116
91
  return {
117
- ...options,
118
- scope: resolvedScope,
119
- language: languageAnswer.trim() || 'zh',
120
- overwrite: options.overwrite || ['y', 'yes', '是', '覆盖'].includes(overwriteAnswer.trim()),
121
- installOpenSpecCli: ['y', 'yes', '是'].includes(installOpenSpecCliAnswer.trim()),
122
- initOpenSpecProject: ['y', 'yes', '是'].includes(initOpenSpecProjectAnswer.trim()),
123
- installSuperpowers: ['y', 'yes', '是'].includes(installSuperpowersAnswer.trim()),
92
+ name: `${platform.name}${detected.has(platformId) ? ' (detected)' : ''}`,
93
+ value: platformId,
94
+ checked: detected.has(platformId),
124
95
  };
125
- } finally {
126
- rl.close();
96
+ });
97
+
98
+ if (options.yes) {
99
+ const selected = [...detected];
100
+ return selected.length > 0 ? selected : ['codex'];
127
101
  }
128
- }
129
102
 
130
- function runCommand(command, args, cwd = process.cwd()) {
131
- execFileSync(command, args, {
132
- cwd,
133
- stdio: 'inherit',
134
- shell: process.platform === 'win32',
135
- });
103
+ return checkbox({ message: 'Select platforms to set up:', choices, required: true });
136
104
  }
137
105
 
138
- function getNpxExecutable() {
139
- return process.platform === 'win32' ? 'npx.cmd' : 'npx';
106
+ async function askOverwrite(existingPlatforms, options) {
107
+ if (options.overwrite || existingPlatforms.length === 0) {
108
+ return options.overwrite;
109
+ }
110
+
111
+ return confirm({
112
+ message: `OneSpec skills already exist for ${existingPlatforms.join(', ')}. Overwrite existing items?`,
113
+ default: false,
114
+ });
140
115
  }
141
116
 
142
- async function ensureRequestedDependencies(targetPath, options, preflight) {
143
- if (options.installOpenSpecCli && !preflight.openspecCli.available) {
144
- runCommand('npm', ['install', '-g', OPEN_SPEC_CLI_PACKAGE]);
145
- }
117
+ async function askInitOptions(options) {
118
+ const explicitPlatforms = parsePlatformList(options.platforms);
119
+ const detectedPlatforms = await detectPlatforms(options.targetPath);
120
+ const defaultPlatforms = explicitPlatforms.length > 0 ? explicitPlatforms : detectedPlatforms.length > 0 ? detectedPlatforms : ['codex'];
146
121
 
147
- if (options.installSuperpowers && !preflight.superpowers.available) {
148
- const args = ['skills', 'add', SUPERPOWERS_PACKAGE, '-a', 'codex', '-y'];
149
- if (options.scope === 'global') {
150
- args.push('-g');
151
- }
152
- runCommand(getNpxExecutable(), args, targetPath);
122
+ if (options.yes) {
123
+ return {
124
+ ...options,
125
+ scope: options.scope ?? 'project',
126
+ language: options.language ?? 'zh',
127
+ platforms: defaultPlatforms,
128
+ };
153
129
  }
154
130
 
155
- if (options.initOpenSpecProject && !preflight.hasOpenSpecProject) {
156
- runCommand('openspec', ['init', targetPath], targetPath);
157
- }
131
+ const detectedSet = new Set(detectedPlatforms);
132
+ const selectedPlatforms = explicitPlatforms.length > 0 ? explicitPlatforms : await selectPlatforms(detectedSet, options);
133
+ const resolvedScope = await selectScope(options);
134
+ const language = await selectLanguage(options);
135
+ const existingPlatforms = await detectExistingOneSpecPlatforms(
136
+ options.targetPath,
137
+ resolvedScope,
138
+ selectedPlatforms,
139
+ );
140
+ const overwrite = await askOverwrite(existingPlatforms, options);
141
+
142
+ return {
143
+ ...options,
144
+ platforms: selectedPlatforms,
145
+ scope: resolvedScope,
146
+ language,
147
+ overwrite,
148
+ };
158
149
  }
159
150
 
160
151
  function printHelp() {
161
152
  console.log(`OneSpec Skill Installer
162
153
 
163
154
  用法:
164
- onespec init [path] [--yes] [--overwrite] [--scope project|global] [--language zh|en]
165
- onespec doctor [path] [--scope project|global]
155
+ onespec init [path] [--yes] [--overwrite] [--scope project|global] [--language zh|en] [--platform ${SUPPORTED_PLATFORM_IDS.join('|')}[,...]]
156
+ onespec doctor [path] [--scope project|global] [--platform ${SUPPORTED_PLATFORM_IDS.join('|')}]
166
157
 
167
158
  说明:
168
- 当前提供中英文 Skill bundle,暂仅支持 Codex 平台。
159
+ 当前提供中英文 Skill bundle,官方支持 ${SUPPORTED_PLATFORM_IDS.join(' / ')}。
160
+ init 会引导选择 agent,并自动安装 OpenSpec / Superpowers / OneSpec。
169
161
  `);
170
162
  }
171
163
 
164
+ let cachedVersion;
165
+
166
+ async function getPackageVersion() {
167
+ if (cachedVersion) {
168
+ return cachedVersion;
169
+ }
170
+
171
+ const packageJsonPath = new URL('../package.json', import.meta.url);
172
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
173
+ cachedVersion = packageJson.version;
174
+ return cachedVersion;
175
+ }
176
+
172
177
  function printSummary(result) {
173
178
  console.log('\nOneSpec 初始化完成\n');
174
- console.log(`安装位置:${result.skillPath}`);
179
+ console.log(`目标平台:${result.platformNames.join(', ')}`);
175
180
  console.log(`安装范围:${result.scope}`);
176
181
  console.log(`Skill 语言:${result.languageName} (${result.language})`);
177
- console.log(`Skill 状态:${result.installedSkill ? '已安装/已覆盖' : '已存在,已跳过'}`);
178
- console.log(`已安装 Skills:${result.installedSkills.join(', ') || '无'}`);
179
- console.log(`已跳过 Skills:${result.skippedSkills.join(', ') || '无'}`);
180
- console.log(`工作目录:${path.join(result.projectPath, 'docs', 'superpowers')}`);
181
- console.log('\n环境检查:');
182
- console.log(`OpenSpec CLI:${commandExists('openspec') ? '已找到' : '未找到,请先安装或运行 openspec init'}`);
183
- console.log('Superpowers:请确认 Codex 可发现 brainstorming / writing-plans / using-git-worktrees 等 skills');
184
- console.log('\n开始使用:在 Codex 中输入 “使用 onespec:<你的任务描述>”。\n');
182
+ console.log(`OpenSpec CLI:${result.openspecCli.status === 'installed' ? '已自动安装' : '已存在'}`);
183
+ console.log(`OpenSpec Tools:${result.openspec.toolIds.join(', ')}`);
184
+ console.log(`Superpowers Agents:${result.superpowers.agents.join(', ')}`);
185
+ for (const platformResult of result.results) {
186
+ const platformLabel = `${platformResult.platformName} (${platformResult.platform})`;
187
+ const skillStatus = platformResult.installedSkill ? '已安装/已覆盖' : '已存在,已跳过';
188
+ console.log(`- ${platformLabel}:${skillStatus} -> ${platformResult.skillPath}`);
189
+ }
190
+ if (result.scope === 'project') {
191
+ console.log(`工作目录:${path.join(result.projectPath, 'docs', 'superpowers')}`);
192
+ }
193
+ console.log('\n开始使用:重启对应 agent 会话后,直接输入 “使用 onespec:<你的任务描述>”。\n');
185
194
  }
186
195
 
187
196
  function printDoctor(report) {
188
197
  console.log('\nOneSpec 环境检查\n');
198
+ console.log(`目标平台:${report.platformName} (${report.platform})`);
189
199
  console.log(`OneSpec Skill:${report.onespec.installed ? '已安装' : '未安装'}`);
190
200
  console.log(`OneSpec 子 Skills:${report.onespec.installedSkills.join(', ') || '无'}`);
191
201
  console.log(`缺少 OneSpec 子 Skills:${report.onespec.missingSkills.join(', ') || '无'}`);
@@ -205,6 +215,10 @@ function printDoctor(report) {
205
215
  export async function main(argv = process.argv.slice(2)) {
206
216
  const { command, options } = parseArgs(argv);
207
217
 
218
+ if (command === 'version' || command === '--version' || command === '-v') {
219
+ console.log(await getPackageVersion());
220
+ return;
221
+ }
208
222
  if (command === 'help' || command === '--help' || command === '-h') {
209
223
  printHelp();
210
224
  return;
@@ -214,8 +228,9 @@ export async function main(argv = process.argv.slice(2)) {
214
228
  }
215
229
 
216
230
  if (command === 'doctor') {
231
+ const doctorPlatforms = parsePlatformList(options.platforms);
217
232
  const report = await doctorProject(options.targetPath, {
218
- platform: options.platform,
233
+ platform: doctorPlatforms[0],
219
234
  scope: options.scope ?? 'project',
220
235
  });
221
236
  if (options.json) {
@@ -230,12 +245,7 @@ export async function main(argv = process.argv.slice(2)) {
230
245
  if (!SUPPORTED_LANGUAGES[initOptions.language]) {
231
246
  throw new Error(`Unsupported language: ${initOptions.language}`);
232
247
  }
233
- const preflight = await doctorProject(initOptions.targetPath, {
234
- platform: initOptions.platform,
235
- scope: initOptions.scope ?? 'project',
236
- });
237
- await ensureRequestedDependencies(initOptions.targetPath, initOptions, preflight);
238
- const result = await initProject(initOptions.targetPath, initOptions);
248
+ const result = await initWorkspace(initOptions.targetPath, initOptions);
239
249
  if (initOptions.json) {
240
250
  console.log(JSON.stringify(result, null, 2));
241
251
  } else {
package/src/doctor.js CHANGED
@@ -3,8 +3,8 @@ import { access, readFile } from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
 
6
- import { BUNDLED_ONESPEC_SKILLS } from './init.js';
7
- import { getSkillDir, PLATFORMS } from './platforms.js';
6
+ import { BUNDLED_ONESPEC_REFERENCE_FILES, BUNDLED_ONESPEC_SKILLS } from './init.js';
7
+ import { getDiscoveryRoots, getPlatform, getSkillDir } from './platforms.js';
8
8
 
9
9
  const REQUIRED_SUPERPOWERS = [
10
10
  'brainstorming',
@@ -41,8 +41,12 @@ function defaultCommandChecker(command) {
41
41
  function defaultSkillRoots(projectPath, scope, platform) {
42
42
  return [
43
43
  getSkillDir(projectPath, scope, platform),
44
- path.join(os.homedir(), '.codex', 'skills'),
44
+ ...getDiscoveryRoots(projectPath, platform),
45
+ path.join(os.homedir(), '.claude', 'skills'),
45
46
  path.join(os.homedir(), '.codex', 'superpowers', 'skills'),
47
+ path.join(os.homedir(), '.cursor', 'skills'),
48
+ path.join(os.homedir(), '.gemini', 'skills'),
49
+ path.join(os.homedir(), '.copilot', 'skills'),
46
50
  path.join(os.homedir(), '.agents', 'skills'),
47
51
  ];
48
52
  }
@@ -60,7 +64,9 @@ async function isChineseOneSpec(projectPath, scope, platform) {
60
64
  const skillsDir = getSkillDir(projectPath, scope, platform);
61
65
  const installedSkills = [];
62
66
  const missingSkills = [];
67
+ const missingFiles = [];
63
68
  const skillPaths = {};
69
+ const referencePaths = {};
64
70
 
65
71
  for (const skillName of BUNDLED_ONESPEC_SKILLS) {
66
72
  const skillPath = path.join(skillsDir, skillName, 'SKILL.md');
@@ -72,14 +78,24 @@ async function isChineseOneSpec(projectPath, scope, platform) {
72
78
  }
73
79
  }
74
80
 
81
+ for (const referenceFile of BUNDLED_ONESPEC_REFERENCE_FILES) {
82
+ const referencePath = path.join(skillsDir, 'onespec', referenceFile);
83
+ referencePaths[referenceFile] = referencePath;
84
+ if (!(await exists(referencePath))) {
85
+ missingFiles.push(path.join('onespec', referenceFile));
86
+ }
87
+ }
88
+
75
89
  const routerPath = skillPaths.onespec;
76
90
  if (!(await exists(routerPath))) {
77
91
  return {
78
92
  installed: false,
79
93
  skillPath: routerPath,
80
94
  skillPaths,
95
+ referencePaths,
81
96
  installedSkills,
82
97
  missingSkills,
98
+ missingFiles,
83
99
  chinese: false,
84
100
  };
85
101
  }
@@ -88,11 +104,13 @@ async function isChineseOneSpec(projectPath, scope, platform) {
88
104
  const chinese = content.includes('OneSpec 工作流');
89
105
  const english = content.includes('# OneSpec Workflow');
90
106
  return {
91
- installed: missingSkills.length === 0 && (chinese || english),
107
+ installed: missingSkills.length === 0 && missingFiles.length === 0 && (chinese || english),
92
108
  skillPath: routerPath,
93
109
  skillPaths,
110
+ referencePaths,
94
111
  installedSkills,
95
112
  missingSkills,
113
+ missingFiles,
96
114
  chinese,
97
115
  english,
98
116
  language: chinese ? 'zh' : english ? 'en' : 'unknown',
@@ -101,20 +119,16 @@ async function isChineseOneSpec(projectPath, scope, platform) {
101
119
 
102
120
  export async function doctorProject(projectPath, options = {}) {
103
121
  const resolvedProject = path.resolve(projectPath);
104
- const platform = options.platform ?? 'codex';
122
+ const platform = getPlatform(options.platform ?? 'codex');
105
123
  const scope = options.scope ?? 'project';
106
124
  const commandChecker = options.commandChecker ?? defaultCommandChecker;
107
125
 
108
- if (!PLATFORMS[platform]) {
109
- throw new Error(`Unsupported platform "${platform}". Currently only "codex" is supported.`);
110
- }
111
-
112
- const onespec = await isChineseOneSpec(resolvedProject, scope, platform);
126
+ const onespec = await isChineseOneSpec(resolvedProject, scope, platform.id);
113
127
  const skillRoots =
114
128
  options.skillRoots ??
115
129
  [
116
130
  ...new Set([
117
- ...defaultSkillRoots(resolvedProject, scope, platform),
131
+ ...defaultSkillRoots(resolvedProject, scope, platform.id),
118
132
  ...(options.extraSkillRoots ?? []),
119
133
  ]),
120
134
  ];
@@ -139,29 +153,41 @@ export async function doctorProject(projectPath, options = {}) {
139
153
  const nextSteps = [];
140
154
  if (onespec.missingSkills.length > 0) {
141
155
  nextSteps.push(
142
- `缺少 OneSpec Skills:${onespec.missingSkills.join(', ')}。运行 \`onespec init --overwrite\` 补齐 OneSpec Skill bundle。`,
156
+ `缺少 OneSpec Skills:${onespec.missingSkills.join(', ')}。运行 \`onespec init --platform ${platform.id} --overwrite\` 补齐 OneSpec Skill bundle。`,
157
+ );
158
+ } else if (onespec.missingFiles.length > 0) {
159
+ nextSteps.push(
160
+ `缺少 OneSpec references:${onespec.missingFiles.join(', ')}。运行 \`onespec init --platform ${platform.id} --overwrite\` 补齐 OneSpec Skill bundle。`,
143
161
  );
144
162
  } else if (!onespec.installed) {
145
- nextSteps.push('运行 `onespec init --yes` 安装 OneSpec Skill。');
163
+ nextSteps.push(`运行 \`onespec init --platform ${platform.id} --yes\` 安装 OneSpec Skill。`);
146
164
  } else if (!onespec.chinese && !onespec.english) {
147
- nextSteps.push('当前 OneSpec Skill 无法识别语言版本,运行 `onespec init --overwrite` 覆盖安装。');
165
+ nextSteps.push(
166
+ `当前 OneSpec Skill 无法识别语言版本,运行 \`onespec init --platform ${platform.id} --overwrite\` 覆盖安装。`,
167
+ );
148
168
  }
149
169
  if (!openspecCli.available) {
150
- nextSteps.push('未找到 OpenSpec CLI,请安装 OpenSpec CLI 并在目标项目运行 `openspec init`。');
170
+ nextSteps.push(
171
+ `未找到 OpenSpec CLI。运行 \`onespec init --platform ${platform.id} --scope ${scope}\` 让 OneSpec 自动安装并初始化 OpenSpec。`,
172
+ );
151
173
  } else if (scope === 'project' && !openSpecProjectInstalled) {
152
- nextSteps.push('当前项目尚未初始化 OpenSpec,请先运行 `openspec init`。');
174
+ nextSteps.push(
175
+ `当前项目尚未初始化 OpenSpec。请重新运行 \`onespec init --platform ${platform.id} --scope project\` 让 OneSpec 自动补齐。`,
176
+ );
153
177
  }
154
178
  if (!superpowers.available) {
155
- nextSteps.push(`缺少 Superpowers Skills:${missing.join(', ')}。请先安装 Superpowers。`);
179
+ nextSteps.push(
180
+ `缺少 Superpowers Skills:${missing.join(', ')}。运行 \`onespec init --platform ${platform.id} --scope ${scope}\` 让 OneSpec 自动补齐。`,
181
+ );
156
182
  }
157
183
  if (nextSteps.length === 0) {
158
- nextSteps.push('环境检查通过。可以在 Codex 中使用 `onespec` 工作流。');
184
+ nextSteps.push(`环境检查通过。可以在 ${platform.name} 中使用 \`onespec\` 工作流。`);
159
185
  }
160
186
 
161
187
  return {
162
188
  projectPath: resolvedProject,
163
- platform,
164
- platformName: PLATFORMS[platform].name,
189
+ platform: platform.id,
190
+ platformName: platform.name,
165
191
  scope,
166
192
  onespec,
167
193
  openspecCli,
package/src/init.js CHANGED
@@ -2,18 +2,29 @@ import { access, chmod, cp, mkdir, rm, stat } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
 
5
- import { getSkillDir, PLATFORMS } from './platforms.js';
5
+ import { getPlatform, getSkillDir } from './platforms.js';
6
6
 
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = path.dirname(__filename);
9
9
 
10
10
  export const BUNDLED_ONESPEC_SKILLS = [
11
11
  'onespec',
12
+ 'onespec-fast',
13
+ ];
14
+
15
+ export const LEGACY_ONESPEC_CHILD_SKILLS = [
12
16
  'onespec-design',
13
17
  'onespec-execute',
14
18
  'onespec-archive',
15
19
  ];
16
20
 
21
+ export const BUNDLED_ONESPEC_REFERENCE_FILES = [
22
+ 'references/design.md',
23
+ 'references/execute.md',
24
+ 'references/archive.md',
25
+ 'references/fast.md',
26
+ ];
27
+
17
28
  export const SUPPORTED_LANGUAGES = {
18
29
  zh: {
19
30
  id: 'zh',
@@ -66,14 +77,11 @@ async function createWorkingDirs(projectPath) {
66
77
 
67
78
  export async function initProject(projectPath, options = {}) {
68
79
  const resolvedProject = path.resolve(projectPath);
69
- const platform = options.platform ?? 'codex';
80
+ const platform = getPlatform(options.platform ?? 'codex');
70
81
  const scope = options.scope ?? 'project';
71
82
  const overwrite = Boolean(options.overwrite);
72
83
  const language = options.language ?? 'zh';
73
84
 
74
- if (!PLATFORMS[platform]) {
75
- throw new Error(`Unsupported platform "${platform}". Currently only "codex" is supported.`);
76
- }
77
85
  if (!['project', 'global'].includes(scope)) {
78
86
  throw new Error(`Unsupported scope "${scope}". Use "project" or "global".`);
79
87
  }
@@ -85,13 +93,19 @@ export async function initProject(projectPath, options = {}) {
85
93
 
86
94
  const sourceRoot = assetsSkillsDir();
87
95
  const localizedRoot = localizedSkillsDir(language);
88
- const skillsDir = getSkillDir(resolvedProject, scope, platform);
96
+ const skillsDir = getSkillDir(resolvedProject, scope, platform.id);
89
97
  const destination = path.join(skillsDir, 'onespec');
90
98
  const installedSkills = [];
91
99
  const skippedSkills = [];
92
100
 
93
101
  await mkdir(skillsDir, { recursive: true });
94
102
 
103
+ if (overwrite) {
104
+ for (const skillName of LEGACY_ONESPEC_CHILD_SKILLS) {
105
+ await rm(path.join(skillsDir, skillName), { recursive: true, force: true });
106
+ }
107
+ }
108
+
95
109
  for (const skillName of BUNDLED_ONESPEC_SKILLS) {
96
110
  const source = path.join(sourceRoot, skillName);
97
111
  const target = path.join(skillsDir, skillName);
@@ -107,9 +121,9 @@ export async function initProject(projectPath, options = {}) {
107
121
  }
108
122
  await cp(source, target, { recursive: true });
109
123
  if (language !== 'zh') {
110
- const localizedSkill = path.join(localizedRoot, skillName, 'SKILL.md');
124
+ const localizedSkill = path.join(localizedRoot, skillName);
111
125
  if (await exists(localizedSkill)) {
112
- await cp(localizedSkill, path.join(target, 'SKILL.md'));
126
+ await cp(localizedSkill, target, { recursive: true, force: true });
113
127
  }
114
128
  }
115
129
  await makeBundledScriptsExecutable(target);
@@ -122,8 +136,8 @@ export async function initProject(projectPath, options = {}) {
122
136
 
123
137
  return {
124
138
  projectPath: resolvedProject,
125
- platform,
126
- platformName: PLATFORMS[platform].name,
139
+ platform: platform.id,
140
+ platformName: platform.name,
127
141
  scope,
128
142
  language,
129
143
  languageName: SUPPORTED_LANGUAGES[language].name,
package/src/platforms.js CHANGED
@@ -1,23 +1,103 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
3
 
4
+ const home = os.homedir();
5
+ const codexHome = process.env.CODEX_HOME?.trim() || path.join(home, '.codex');
6
+ const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || path.join(home, '.claude');
7
+
4
8
  export const PLATFORMS = {
5
9
  codex: {
6
10
  id: 'codex',
7
11
  name: 'Codex',
8
- skillsDir: '.codex',
12
+ projectSkillsDir: path.join('.agents', 'skills'),
13
+ globalSkillsDir: path.join(codexHome, 'skills'),
14
+ legacyProjectSkillDirs: [path.join('.codex', 'skills')],
15
+ discoveryRoots: [path.join(home, '.agents', 'skills'), '/etc/codex/skills'],
9
16
  openspecToolId: 'codex',
10
17
  },
18
+ 'claude-code': {
19
+ id: 'claude-code',
20
+ name: 'Claude Code',
21
+ projectSkillsDir: path.join('.claude', 'skills'),
22
+ globalSkillsDir: path.join(claudeHome, 'skills'),
23
+ discoveryRoots: [],
24
+ openspecToolId: 'claude',
25
+ },
26
+ cursor: {
27
+ id: 'cursor',
28
+ name: 'Cursor',
29
+ projectSkillsDir: path.join('.agents', 'skills'),
30
+ globalSkillsDir: path.join(home, '.cursor', 'skills'),
31
+ discoveryRoots: [path.join(home, '.agents', 'skills')],
32
+ openspecToolId: 'cursor',
33
+ },
34
+ 'gemini-cli': {
35
+ id: 'gemini-cli',
36
+ name: 'Gemini CLI',
37
+ projectSkillsDir: path.join('.agents', 'skills'),
38
+ globalSkillsDir: path.join(home, '.gemini', 'skills'),
39
+ discoveryRoots: [path.join(home, '.agents', 'skills')],
40
+ openspecToolId: 'gemini',
41
+ },
42
+ 'github-copilot': {
43
+ id: 'github-copilot',
44
+ name: 'GitHub Copilot',
45
+ projectSkillsDir: path.join('.agents', 'skills'),
46
+ globalSkillsDir: path.join(home, '.copilot', 'skills'),
47
+ discoveryRoots: [path.join(home, '.agents', 'skills')],
48
+ openspecToolId: 'github-copilot',
49
+ },
11
50
  };
12
51
 
13
- export function getInstallBase(projectPath, scope) {
14
- return scope === 'global' ? os.homedir() : projectPath;
15
- }
52
+ const PLATFORM_ALIASES = {
53
+ codex: 'codex',
54
+ claude: 'claude-code',
55
+ 'claude-code': 'claude-code',
56
+ claude_code: 'claude-code',
57
+ cursor: 'cursor',
58
+ gemini: 'gemini-cli',
59
+ 'gemini-cli': 'gemini-cli',
60
+ gemini_cli: 'gemini-cli',
61
+ copilot: 'github-copilot',
62
+ 'github-copilot': 'github-copilot',
63
+ github_copilot: 'github-copilot',
64
+ 'github-copilot-cli': 'github-copilot',
65
+ };
16
66
 
17
- export function getSkillDir(projectPath, scope, platformId = 'codex') {
18
- const platform = PLATFORMS[platformId];
19
- if (!platform) {
67
+ export function resolvePlatformId(platformId = 'codex') {
68
+ const resolved = PLATFORM_ALIASES[platformId];
69
+ if (!resolved) {
20
70
  throw new Error(`Unsupported platform: ${platformId}`);
21
71
  }
22
- return path.join(getInstallBase(projectPath, scope), platform.skillsDir, 'skills');
72
+ return resolved;
73
+ }
74
+
75
+ export function getPlatform(platformId = 'codex') {
76
+ return PLATFORMS[resolvePlatformId(platformId)];
77
+ }
78
+
79
+ export function getProjectSkillDir(projectPath, platformId = 'codex') {
80
+ const platform = getPlatform(platformId);
81
+ return path.join(projectPath, platform.projectSkillsDir);
82
+ }
83
+
84
+ export function getGlobalSkillDir(platformId = 'codex') {
85
+ const platform = getPlatform(platformId);
86
+ return platform.globalSkillsDir;
87
+ }
88
+
89
+ export function getSkillDir(projectPath, scope, platformId = 'codex') {
90
+ return scope === 'global'
91
+ ? getGlobalSkillDir(platformId)
92
+ : getProjectSkillDir(projectPath, platformId);
93
+ }
94
+
95
+ export function getDiscoveryRoots(projectPath, platformId = 'codex') {
96
+ const platform = getPlatform(platformId);
97
+ return [
98
+ getProjectSkillDir(projectPath, platformId),
99
+ ...(platform.legacyProjectSkillDirs ?? []).map((relativeDir) => path.join(projectPath, relativeDir)),
100
+ getGlobalSkillDir(platformId),
101
+ ...(platform.discoveryRoots ?? []),
102
+ ];
23
103
  }