@kafka0102/onespec 0.1.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.
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@kafka0102/onespec",
3
+ "version": "0.1.2",
4
+ "description": "OpenSpec + Superpowers workflow skill installer for Codex",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/kafka0102/onespec.git"
8
+ },
9
+ "keywords": [
10
+ "openspec",
11
+ "superpowers",
12
+ "skills",
13
+ "codex",
14
+ "workflow"
15
+ ],
16
+ "type": "module",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "bin": {
21
+ "onespec": "./bin/onespec.js"
22
+ },
23
+ "files": [
24
+ "assets",
25
+ "bin",
26
+ "src",
27
+ "scripts/postinstall.js"
28
+ ],
29
+ "scripts": {
30
+ "test": "node --test test/*.test.js",
31
+ "test:shell": "node --test test/*.shell.test.js",
32
+ "postinstall": "node scripts/postinstall.js"
33
+ },
34
+ "engines": {
35
+ "node": ">=20"
36
+ },
37
+ "license": "MIT"
38
+ }
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+
3
+ function shouldSkipHint() {
4
+ if (process.env.CI === 'true' || process.env.CI === '1') {
5
+ return true;
6
+ }
7
+ if (process.env.ONESPEC_NO_HINTS === '1') {
8
+ return true;
9
+ }
10
+ return false;
11
+ }
12
+
13
+ function main() {
14
+ if (shouldSkipHint()) {
15
+ return;
16
+ }
17
+
18
+ console.log('\nOneSpec installed. Next run:');
19
+ console.log(' onespec init --scope global --yes');
20
+ console.log('or for the current project:');
21
+ console.log(' onespec init . --scope project --yes\n');
22
+ }
23
+
24
+ try {
25
+ main();
26
+ } catch {
27
+ // Never break npm install because of an install hint.
28
+ }
package/src/cli.js ADDED
@@ -0,0 +1,244 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import readline from 'node:readline/promises';
4
+ import { stdin as input, stdout as output } from 'node:process';
5
+
6
+ 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';
11
+
12
+ function parseArgs(argv) {
13
+ const args = [...argv];
14
+ const command = args.shift() ?? 'help';
15
+ const options = {
16
+ targetPath: process.cwd(),
17
+ platform: 'codex',
18
+ scope: undefined,
19
+ language: undefined,
20
+ yes: false,
21
+ overwrite: false,
22
+ json: false,
23
+ };
24
+
25
+ while (args.length > 0) {
26
+ const arg = args.shift();
27
+ switch (arg) {
28
+ case '--yes':
29
+ case '-y':
30
+ options.yes = true;
31
+ break;
32
+ case '--overwrite':
33
+ options.overwrite = true;
34
+ break;
35
+ case '--json':
36
+ options.json = true;
37
+ break;
38
+ case '--scope':
39
+ options.scope = args.shift();
40
+ break;
41
+ case '--language':
42
+ case '--lang':
43
+ options.language = args.shift();
44
+ break;
45
+ case '--platform':
46
+ options.platform = args.shift() ?? 'codex';
47
+ break;
48
+ default:
49
+ if (arg?.startsWith('-')) {
50
+ throw new Error(`Unknown option: ${arg}`);
51
+ }
52
+ options.targetPath = arg ?? options.targetPath;
53
+ break;
54
+ }
55
+ }
56
+
57
+ return { command, options };
58
+ }
59
+
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
+ }
68
+ }
69
+
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
+ }
81
+
82
+ const preflight = await doctorProject(options.targetPath, {
83
+ platform: options.platform,
84
+ scope: options.scope ?? 'project',
85
+ });
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
+
116
+ 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()),
124
+ };
125
+ } finally {
126
+ rl.close();
127
+ }
128
+ }
129
+
130
+ function runCommand(command, args, cwd = process.cwd()) {
131
+ execFileSync(command, args, {
132
+ cwd,
133
+ stdio: 'inherit',
134
+ shell: process.platform === 'win32',
135
+ });
136
+ }
137
+
138
+ function getNpxExecutable() {
139
+ return process.platform === 'win32' ? 'npx.cmd' : 'npx';
140
+ }
141
+
142
+ async function ensureRequestedDependencies(targetPath, options, preflight) {
143
+ if (options.installOpenSpecCli && !preflight.openspecCli.available) {
144
+ runCommand('npm', ['install', '-g', OPEN_SPEC_CLI_PACKAGE]);
145
+ }
146
+
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);
153
+ }
154
+
155
+ if (options.initOpenSpecProject && !preflight.hasOpenSpecProject) {
156
+ runCommand('openspec', ['init', targetPath], targetPath);
157
+ }
158
+ }
159
+
160
+ function printHelp() {
161
+ console.log(`OneSpec Skill Installer
162
+
163
+ 用法:
164
+ onespec init [path] [--yes] [--overwrite] [--scope project|global] [--language zh|en]
165
+ onespec doctor [path] [--scope project|global]
166
+
167
+ 说明:
168
+ 当前提供中英文 Skill bundle,暂仅支持 Codex 平台。
169
+ `);
170
+ }
171
+
172
+ function printSummary(result) {
173
+ console.log('\nOneSpec 初始化完成\n');
174
+ console.log(`安装位置:${result.skillPath}`);
175
+ console.log(`安装范围:${result.scope}`);
176
+ 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');
185
+ }
186
+
187
+ function printDoctor(report) {
188
+ console.log('\nOneSpec 环境检查\n');
189
+ console.log(`OneSpec Skill:${report.onespec.installed ? '已安装' : '未安装'}`);
190
+ console.log(`OneSpec 子 Skills:${report.onespec.installedSkills.join(', ') || '无'}`);
191
+ console.log(`缺少 OneSpec 子 Skills:${report.onespec.missingSkills.join(', ') || '无'}`);
192
+ console.log(`Skill 语言:${report.onespec.language}`);
193
+ console.log(`OpenSpec CLI:${report.openspecCli.available ? '已找到' : '未找到'}`);
194
+ console.log(`OpenSpec 项目:${report.hasOpenSpecProject ? '已初始化' : '未初始化'}`);
195
+ console.log(
196
+ `Superpowers:${report.superpowers.available ? '关键 Skills 已找到' : `缺少 ${report.superpowers.missing.join(', ')}`}`,
197
+ );
198
+ console.log('\n下一步:');
199
+ for (const step of report.nextSteps) {
200
+ console.log(`- ${step}`);
201
+ }
202
+ console.log('');
203
+ }
204
+
205
+ export async function main(argv = process.argv.slice(2)) {
206
+ const { command, options } = parseArgs(argv);
207
+
208
+ if (command === 'help' || command === '--help' || command === '-h') {
209
+ printHelp();
210
+ return;
211
+ }
212
+ if (command !== 'init' && command !== 'doctor') {
213
+ throw new Error(`Unknown command: ${command}`);
214
+ }
215
+
216
+ if (command === 'doctor') {
217
+ const report = await doctorProject(options.targetPath, {
218
+ platform: options.platform,
219
+ scope: options.scope ?? 'project',
220
+ });
221
+ if (options.json) {
222
+ console.log(JSON.stringify(report, null, 2));
223
+ } else {
224
+ printDoctor(report);
225
+ }
226
+ return;
227
+ }
228
+
229
+ const initOptions = await askInitOptions(options);
230
+ if (!SUPPORTED_LANGUAGES[initOptions.language]) {
231
+ throw new Error(`Unsupported language: ${initOptions.language}`);
232
+ }
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);
239
+ if (initOptions.json) {
240
+ console.log(JSON.stringify(result, null, 2));
241
+ } else {
242
+ printSummary(result);
243
+ }
244
+ }
package/src/doctor.js ADDED
@@ -0,0 +1,172 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { access, readFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ import { BUNDLED_ONESPEC_SKILLS } from './init.js';
7
+ import { getSkillDir, PLATFORMS } from './platforms.js';
8
+
9
+ const REQUIRED_SUPERPOWERS = [
10
+ 'brainstorming',
11
+ 'writing-plans',
12
+ 'using-git-worktrees',
13
+ 'subagent-driven-development',
14
+ 'executing-plans',
15
+ 'test-driven-development',
16
+ ];
17
+
18
+ async function exists(filePath) {
19
+ try {
20
+ await access(filePath);
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ async function hasOpenSpecProject(projectPath) {
28
+ return exists(path.join(projectPath, 'openspec'));
29
+ }
30
+
31
+ function defaultCommandChecker(command) {
32
+ try {
33
+ const checker = process.platform === 'win32' ? 'where' : 'which';
34
+ execFileSync(checker, [command], { stdio: 'ignore' });
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ function defaultSkillRoots(projectPath, scope, platform) {
42
+ return [
43
+ getSkillDir(projectPath, scope, platform),
44
+ path.join(os.homedir(), '.codex', 'skills'),
45
+ path.join(os.homedir(), '.codex', 'superpowers', 'skills'),
46
+ path.join(os.homedir(), '.agents', 'skills'),
47
+ ];
48
+ }
49
+
50
+ async function skillInstalledInRoots(roots, name) {
51
+ for (const root of roots) {
52
+ if (await exists(path.join(root, name, 'SKILL.md'))) {
53
+ return true;
54
+ }
55
+ }
56
+ return false;
57
+ }
58
+
59
+ async function isChineseOneSpec(projectPath, scope, platform) {
60
+ const skillsDir = getSkillDir(projectPath, scope, platform);
61
+ const installedSkills = [];
62
+ const missingSkills = [];
63
+ const skillPaths = {};
64
+
65
+ for (const skillName of BUNDLED_ONESPEC_SKILLS) {
66
+ const skillPath = path.join(skillsDir, skillName, 'SKILL.md');
67
+ skillPaths[skillName] = skillPath;
68
+ if (await exists(skillPath)) {
69
+ installedSkills.push(skillName);
70
+ } else {
71
+ missingSkills.push(skillName);
72
+ }
73
+ }
74
+
75
+ const routerPath = skillPaths.onespec;
76
+ if (!(await exists(routerPath))) {
77
+ return {
78
+ installed: false,
79
+ skillPath: routerPath,
80
+ skillPaths,
81
+ installedSkills,
82
+ missingSkills,
83
+ chinese: false,
84
+ };
85
+ }
86
+
87
+ const content = await readFile(routerPath, 'utf8');
88
+ const chinese = content.includes('OneSpec 工作流');
89
+ const english = content.includes('# OneSpec Workflow');
90
+ return {
91
+ installed: missingSkills.length === 0 && (chinese || english),
92
+ skillPath: routerPath,
93
+ skillPaths,
94
+ installedSkills,
95
+ missingSkills,
96
+ chinese,
97
+ english,
98
+ language: chinese ? 'zh' : english ? 'en' : 'unknown',
99
+ };
100
+ }
101
+
102
+ export async function doctorProject(projectPath, options = {}) {
103
+ const resolvedProject = path.resolve(projectPath);
104
+ const platform = options.platform ?? 'codex';
105
+ const scope = options.scope ?? 'project';
106
+ const commandChecker = options.commandChecker ?? defaultCommandChecker;
107
+
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);
113
+ const skillRoots =
114
+ options.skillRoots ??
115
+ [
116
+ ...new Set([
117
+ ...defaultSkillRoots(resolvedProject, scope, platform),
118
+ ...(options.extraSkillRoots ?? []),
119
+ ]),
120
+ ];
121
+ const missing = [];
122
+ for (const skill of REQUIRED_SUPERPOWERS) {
123
+ if (!(await skillInstalledInRoots(skillRoots, skill))) {
124
+ missing.push(skill);
125
+ }
126
+ }
127
+
128
+ const openspecCli = {
129
+ available: commandChecker('openspec'),
130
+ };
131
+ const openSpecProjectInstalled = await hasOpenSpecProject(resolvedProject);
132
+ const superpowers = {
133
+ available: missing.length === 0,
134
+ required: REQUIRED_SUPERPOWERS,
135
+ missing,
136
+ searchedRoots: skillRoots,
137
+ };
138
+
139
+ const nextSteps = [];
140
+ if (onespec.missingSkills.length > 0) {
141
+ nextSteps.push(
142
+ `缺少 OneSpec Skills:${onespec.missingSkills.join(', ')}。运行 \`onespec init --overwrite\` 补齐 OneSpec Skill bundle。`,
143
+ );
144
+ } else if (!onespec.installed) {
145
+ nextSteps.push('运行 `onespec init --yes` 安装 OneSpec Skill。');
146
+ } else if (!onespec.chinese && !onespec.english) {
147
+ nextSteps.push('当前 OneSpec Skill 无法识别语言版本,运行 `onespec init --overwrite` 覆盖安装。');
148
+ }
149
+ if (!openspecCli.available) {
150
+ nextSteps.push('未找到 OpenSpec CLI,请安装 OpenSpec CLI 并在目标项目运行 `openspec init`。');
151
+ } else if (scope === 'project' && !openSpecProjectInstalled) {
152
+ nextSteps.push('当前项目尚未初始化 OpenSpec,请先运行 `openspec init`。');
153
+ }
154
+ if (!superpowers.available) {
155
+ nextSteps.push(`缺少 Superpowers Skills:${missing.join(', ')}。请先安装 Superpowers。`);
156
+ }
157
+ if (nextSteps.length === 0) {
158
+ nextSteps.push('环境检查通过。可以在 Codex 中使用 `onespec` 工作流。');
159
+ }
160
+
161
+ return {
162
+ projectPath: resolvedProject,
163
+ platform,
164
+ platformName: PLATFORMS[platform].name,
165
+ scope,
166
+ onespec,
167
+ openspecCli,
168
+ hasOpenSpecProject: openSpecProjectInstalled,
169
+ superpowers,
170
+ nextSteps,
171
+ };
172
+ }
package/src/init.js ADDED
@@ -0,0 +1,136 @@
1
+ import { access, chmod, cp, mkdir, rm, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { getSkillDir, PLATFORMS } from './platforms.js';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ export const BUNDLED_ONESPEC_SKILLS = [
11
+ 'onespec',
12
+ 'onespec-design',
13
+ 'onespec-execute',
14
+ 'onespec-archive',
15
+ ];
16
+
17
+ export const SUPPORTED_LANGUAGES = {
18
+ zh: {
19
+ id: 'zh',
20
+ name: '中文',
21
+ },
22
+ en: {
23
+ id: 'en',
24
+ name: 'English',
25
+ },
26
+ };
27
+
28
+ function assetsSkillsDir() {
29
+ return path.resolve(__dirname, '..', 'assets', 'skills');
30
+ }
31
+
32
+ function localizedSkillsDir(language) {
33
+ return path.resolve(__dirname, '..', 'assets', `skills-${language}`);
34
+ }
35
+
36
+ async function exists(filePath) {
37
+ try {
38
+ await access(filePath);
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ async function chmodExecutable(filePath) {
46
+ const current = await stat(filePath);
47
+ await chmod(filePath, current.mode | 0o111);
48
+ }
49
+
50
+ async function makeBundledScriptsExecutable(skillPath) {
51
+ const scriptsDir = path.join(skillPath, 'scripts');
52
+ if (!(await exists(scriptsDir))) {
53
+ return;
54
+ }
55
+ await chmodExecutable(path.join(scriptsDir, 'onespec-env.sh'));
56
+ await chmodExecutable(path.join(scriptsDir, 'onespec-state.sh'));
57
+ await chmodExecutable(path.join(scriptsDir, 'onespec-handoff.sh'));
58
+ await chmodExecutable(path.join(scriptsDir, 'onespec-commit.sh'));
59
+ await chmodExecutable(path.join(scriptsDir, 'onespec-closeout.sh'));
60
+ }
61
+
62
+ async function createWorkingDirs(projectPath) {
63
+ await mkdir(path.join(projectPath, 'docs', 'superpowers', 'specs'), { recursive: true });
64
+ await mkdir(path.join(projectPath, 'docs', 'superpowers', 'plans'), { recursive: true });
65
+ }
66
+
67
+ export async function initProject(projectPath, options = {}) {
68
+ const resolvedProject = path.resolve(projectPath);
69
+ const platform = options.platform ?? 'codex';
70
+ const scope = options.scope ?? 'project';
71
+ const overwrite = Boolean(options.overwrite);
72
+ const language = options.language ?? 'zh';
73
+
74
+ if (!PLATFORMS[platform]) {
75
+ throw new Error(`Unsupported platform "${platform}". Currently only "codex" is supported.`);
76
+ }
77
+ if (!['project', 'global'].includes(scope)) {
78
+ throw new Error(`Unsupported scope "${scope}". Use "project" or "global".`);
79
+ }
80
+ if (!SUPPORTED_LANGUAGES[language]) {
81
+ throw new Error(
82
+ `Unsupported language "${language}". Use one of: ${Object.keys(SUPPORTED_LANGUAGES).join(', ')}.`,
83
+ );
84
+ }
85
+
86
+ const sourceRoot = assetsSkillsDir();
87
+ const localizedRoot = localizedSkillsDir(language);
88
+ const skillsDir = getSkillDir(resolvedProject, scope, platform);
89
+ const destination = path.join(skillsDir, 'onespec');
90
+ const installedSkills = [];
91
+ const skippedSkills = [];
92
+
93
+ await mkdir(skillsDir, { recursive: true });
94
+
95
+ for (const skillName of BUNDLED_ONESPEC_SKILLS) {
96
+ const source = path.join(sourceRoot, skillName);
97
+ const target = path.join(skillsDir, skillName);
98
+ const hadExisting = await exists(target);
99
+
100
+ if (hadExisting && !overwrite) {
101
+ skippedSkills.push(skillName);
102
+ continue;
103
+ }
104
+
105
+ if (hadExisting) {
106
+ await rm(target, { recursive: true, force: true });
107
+ }
108
+ await cp(source, target, { recursive: true });
109
+ if (language !== 'zh') {
110
+ const localizedSkill = path.join(localizedRoot, skillName, 'SKILL.md');
111
+ if (await exists(localizedSkill)) {
112
+ await cp(localizedSkill, path.join(target, 'SKILL.md'));
113
+ }
114
+ }
115
+ await makeBundledScriptsExecutable(target);
116
+ installedSkills.push(skillName);
117
+ }
118
+
119
+ if (scope === 'project') {
120
+ await createWorkingDirs(resolvedProject);
121
+ }
122
+
123
+ return {
124
+ projectPath: resolvedProject,
125
+ platform,
126
+ platformName: PLATFORMS[platform].name,
127
+ scope,
128
+ language,
129
+ languageName: SUPPORTED_LANGUAGES[language].name,
130
+ skillPath: destination,
131
+ installedSkill: installedSkills.length > 0,
132
+ skippedExisting: installedSkills.length === 0 && skippedSkills.length > 0,
133
+ installedSkills,
134
+ skippedSkills,
135
+ };
136
+ }
@@ -0,0 +1,23 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+
4
+ export const PLATFORMS = {
5
+ codex: {
6
+ id: 'codex',
7
+ name: 'Codex',
8
+ skillsDir: '.codex',
9
+ openspecToolId: 'codex',
10
+ },
11
+ };
12
+
13
+ export function getInstallBase(projectPath, scope) {
14
+ return scope === 'global' ? os.homedir() : projectPath;
15
+ }
16
+
17
+ export function getSkillDir(projectPath, scope, platformId = 'codex') {
18
+ const platform = PLATFORMS[platformId];
19
+ if (!platform) {
20
+ throw new Error(`Unsupported platform: ${platformId}`);
21
+ }
22
+ return path.join(getInstallBase(projectPath, scope), platform.skillsDir, 'skills');
23
+ }