@supatent/skills 0.4.0 → 0.5.0

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
@@ -1,10 +1,10 @@
1
1
  # @supatent/skills
2
2
 
3
- Claude Code content authoring skills for [Supatent CMS](https://supatent.ai).
3
+ Claude Code and Codex CLI content authoring skills for [Supatent CMS](https://supatent.ai).
4
4
 
5
5
  ## What this does
6
6
 
7
- Installs AI-powered content authoring skills into your Claude Code environment. Once installed, Claude Code can create and manage CMS content through natural conversation — blog posts, landing pages, and more.
7
+ Installs AI-powered content authoring skills for Claude Code and Codex CLI. Once installed, agents can create and manage CMS content through natural conversation — blog posts, landing pages, and more.
8
8
 
9
9
  ## Quick start
10
10
 
@@ -12,15 +12,40 @@ Installs AI-powered content authoring skills into your Claude Code environment.
12
12
  npx @supatent/skills
13
13
  ```
14
14
 
15
- This installs skill files to `.claude/skills/` in your project. Then use `/supatent:core` in Claude Code to get started.
15
+ In interactive terminals, the installer prompts for a target:
16
+
17
+ - `claude`
18
+ - `codex`
19
+ - `both`
20
+
21
+ In non-interactive environments (CI), the default is `claude`.
22
+
23
+ ### Explicit target selection
24
+
25
+ ```bash
26
+ # Claude Code
27
+ npx @supatent/skills --target claude
28
+
29
+ # Codex CLI
30
+ npx @supatent/skills --target codex
31
+
32
+ # Both
33
+ npx @supatent/skills --target both
34
+ ```
35
+
36
+ Use `--codex-home` to override Codex install location (default: `$CODEX_HOME` or `~/.codex`):
37
+
38
+ ```bash
39
+ npx @supatent/skills --target codex --codex-home /path/to/codex-home
40
+ ```
16
41
 
17
42
  ## Included skills
18
43
 
19
- | Skill | Command | Description |
20
- |-------|---------|-------------|
21
- | Core | `/supatent:core` | Schema management, content operations, validation |
22
- | Blog | `/supatent:blog` | Create and edit blog posts with structured fields |
23
- | Landing | `/supatent:landing` | Build landing pages with configurable sections |
44
+ | Skill | Claude Code | Codex CLI | Description |
45
+ |-------|-------------|-----------|-------------|
46
+ | Core | `/supatent:core` | `supatent-core` | Schema management, content operations, validation |
47
+ | Blog | `/supatent:content-blog` | `supatent-content-blog` | Create and edit blog posts with structured fields |
48
+ | Landing | `/supatent:content-landing` | `supatent-content-landing` | Build landing pages with configurable sections |
24
49
 
25
50
  ## Updating
26
51
 
@@ -36,7 +61,7 @@ npx @supatent/skills --force
36
61
 
37
62
  - Node.js >= 20
38
63
  - A Supatent CMS project with `@supatent/cli` configured
39
- - Claude Code
64
+ - Claude Code and/or Codex CLI
40
65
 
41
66
  ## License
42
67
 
package/bin/install.mjs CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // bin/install.mjs - Self-contained zero-dependency installer for @supatent/skills
4
- // Copies bundled skill files to .claude/skills/ with manifest tracking.
4
+ // Copies bundled skill files to Claude Code and/or Codex CLI skill directories.
5
5
 
6
6
  import { readFile, writeFile, mkdir, copyFile, readdir, stat } from 'node:fs/promises';
7
7
  import { realpathSync } from 'node:fs';
8
8
  import { createHash } from 'node:crypto';
9
9
  import { join, dirname, relative, resolve } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
+ import { homedir } from 'node:os';
11
12
  import { createInterface } from 'node:readline/promises';
12
13
 
13
14
  // ---------------------------------------------------------------------------
@@ -16,8 +17,10 @@ import { createInterface } from 'node:readline/promises';
16
17
 
17
18
  const __filename = fileURLToPath(import.meta.url);
18
19
  const __dirname = dirname(__filename);
19
- const SKILLS_SOURCE_DIR = join(__dirname, '..', 'skills');
20
- const TARGET_SUBDIR = join('.claude', 'skills');
20
+ const CLAUDE_SKILLS_SOURCE_DIR = join(__dirname, '..', 'skills');
21
+ const CODEX_SKILLS_SOURCE_DIR = join(__dirname, '..', 'skills-codex');
22
+ const CLAUDE_TARGET_SUBDIR = join('.claude', 'skills');
23
+ const VALID_TARGETS = new Set(['claude', 'codex', 'both']);
21
24
 
22
25
  // ---------------------------------------------------------------------------
23
26
  // Exported helpers (for testability)
@@ -65,6 +68,109 @@ export async function confirm(message, { force = false } = {}) {
65
68
  }
66
69
  }
67
70
 
71
+ /**
72
+ * Prompt install target when not explicitly provided.
73
+ * Defaults to "claude" when stdin is non-interactive.
74
+ */
75
+ export async function promptInstallTarget({ force = false } = {}) {
76
+ if (force || !process.stdin.isTTY) return 'claude';
77
+
78
+ const rl = createInterface({
79
+ input: process.stdin,
80
+ output: process.stdout,
81
+ });
82
+
83
+ try {
84
+ console.log('Select installation target:');
85
+ console.log(' 1) Claude Code');
86
+ console.log(' 2) Codex CLI');
87
+ console.log(' 3) Both');
88
+
89
+ while (true) {
90
+ const answer = (await rl.question('Target [1/2/3] (default: 1): ')).trim().toLowerCase();
91
+
92
+ if (answer === '' || answer === '1' || answer === 'claude') {
93
+ return 'claude';
94
+ }
95
+ if (answer === '2' || answer === 'codex') {
96
+ return 'codex';
97
+ }
98
+ if (answer === '3' || answer === 'both') {
99
+ return 'both';
100
+ }
101
+
102
+ console.log('Invalid choice. Enter 1, 2, 3, claude, codex, or both.');
103
+ }
104
+ } finally {
105
+ rl.close();
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Parse command line args.
111
+ */
112
+ export function parseArgs(argv) {
113
+ const parsed = {
114
+ force: false,
115
+ help: false,
116
+ target: null,
117
+ codexHome: null,
118
+ };
119
+
120
+ for (let i = 0; i < argv.length; i += 1) {
121
+ const arg = argv[i];
122
+
123
+ if (arg === '--force' || arg === '-f' || arg === '--yes' || arg === '-y') {
124
+ parsed.force = true;
125
+ continue;
126
+ }
127
+
128
+ if (arg === '--help' || arg === '-h') {
129
+ parsed.help = true;
130
+ continue;
131
+ }
132
+
133
+ if (arg === '--target') {
134
+ i += 1;
135
+ if (!argv[i]) {
136
+ throw new Error('Missing value for --target. Use: claude, codex, or both.');
137
+ }
138
+ parsed.target = argv[i];
139
+ continue;
140
+ }
141
+
142
+ if (arg.startsWith('--target=')) {
143
+ parsed.target = arg.slice('--target='.length);
144
+ continue;
145
+ }
146
+
147
+ if (arg === '--codex-home') {
148
+ i += 1;
149
+ if (!argv[i]) {
150
+ throw new Error('Missing value for --codex-home.');
151
+ }
152
+ parsed.codexHome = argv[i];
153
+ continue;
154
+ }
155
+
156
+ if (arg.startsWith('--codex-home=')) {
157
+ parsed.codexHome = arg.slice('--codex-home='.length);
158
+ continue;
159
+ }
160
+
161
+ throw new Error(`Unknown argument: ${arg}`);
162
+ }
163
+
164
+ if (parsed.target) {
165
+ parsed.target = parsed.target.toLowerCase();
166
+ if (!VALID_TARGETS.has(parsed.target)) {
167
+ throw new Error(`Invalid --target value: ${parsed.target}. Use: claude, codex, or both.`);
168
+ }
169
+ }
170
+
171
+ return parsed;
172
+ }
173
+
68
174
  /**
69
175
  * Read and parse .manifest.json. Returns null if missing or invalid.
70
176
  */
@@ -161,60 +267,76 @@ export async function getVersion() {
161
267
  return pkg.version;
162
268
  }
163
269
 
164
- // ---------------------------------------------------------------------------
165
- // Main installer flow
166
- // ---------------------------------------------------------------------------
167
-
168
- async function main() {
169
- const args = process.argv.slice(2);
170
- const force = args.includes('--force');
270
+ /**
271
+ * Resolve default Codex home.
272
+ */
273
+ export function getDefaultCodexHome() {
274
+ return process.env.CODEX_HOME ? resolve(process.env.CODEX_HOME) : join(homedir(), '.codex');
275
+ }
171
276
 
172
- const version = await getVersion();
173
- const cwd = process.cwd();
174
- const claudeDir = join(cwd, '.claude');
175
- const targetDir = join(cwd, TARGET_SUBDIR);
176
- const manifestPath = join(targetDir, 'supatent-core', '.manifest.json');
277
+ function printHelp() {
278
+ console.log('Usage: npx @supatent/skills [options]');
279
+ console.log('');
280
+ console.log('Options:');
281
+ console.log(' --target <claude|codex|both> Installation target (prompts when omitted in TTY)');
282
+ console.log(' --codex-home <path> Override Codex home (default: $CODEX_HOME or ~/.codex)');
283
+ console.log(' -f, --force Auto-confirm prompts and overwrite user-modified files');
284
+ console.log(' -y, --yes Alias for --force');
285
+ console.log(' -h, --help Show this help');
286
+ }
177
287
 
178
- // -----------------------------------------------------------------------
179
- // Step a: Check .claude/ directory
180
- // -----------------------------------------------------------------------
181
- let claudeExists = false;
288
+ async function ensureDirectoryExists(dirPath, missingMessage, { force }) {
289
+ let exists = false;
182
290
  try {
183
- const s = await stat(claudeDir);
184
- claudeExists = s.isDirectory();
291
+ const s = await stat(dirPath);
292
+ exists = s.isDirectory();
185
293
  } catch {
186
294
  // does not exist
187
295
  }
188
296
 
189
- if (!claudeExists) {
190
- console.log('\x1b[33m!\x1b[0m No .claude/ directory found. This installer creates skill files for Claude Code.');
191
- const ok = await confirm('Create .claude/ directory and continue?', { force });
192
- if (!ok) {
193
- console.log('Aborted.');
194
- process.exitCode = 1;
195
- return;
196
- }
197
- await mkdir(claudeDir, { recursive: true });
297
+ if (exists) return true;
298
+
299
+ if (missingMessage) {
300
+ console.log(`\x1b[33m!\x1b[0m ${missingMessage}`);
198
301
  }
199
302
 
303
+ const ok = await confirm(`Create ${dirPath} and continue?`, { force });
304
+ if (!ok) {
305
+ console.log('Aborted.');
306
+ return false;
307
+ }
308
+
309
+ await mkdir(dirPath, { recursive: true });
310
+ return true;
311
+ }
312
+
313
+ async function installSkillBundle({
314
+ sourceDir,
315
+ targetDir,
316
+ manifestPath,
317
+ targetLabel,
318
+ startHint,
319
+ force,
320
+ version,
321
+ }) {
200
322
  // -----------------------------------------------------------------------
201
- // Step b: Walk source skills/ directory and build source map
323
+ // Step a: Build source map
202
324
  // -----------------------------------------------------------------------
203
- const sourceMap = await buildSourceMap(SKILLS_SOURCE_DIR);
325
+ const sourceMap = await buildSourceMap(sourceDir);
204
326
  const sourceFiles = Object.keys(sourceMap);
205
327
 
206
328
  // -----------------------------------------------------------------------
207
- // Step c: Read existing manifest
329
+ // Step b: Read existing manifest
208
330
  // -----------------------------------------------------------------------
209
331
  const manifest = await readManifest(manifestPath);
210
332
 
211
333
  // -----------------------------------------------------------------------
212
- // Step d: Compute diff
334
+ // Step c: Compute diff
213
335
  // -----------------------------------------------------------------------
214
336
  const diff = await computeDiff(sourceMap, manifest, targetDir);
215
337
 
216
338
  // -----------------------------------------------------------------------
217
- // Step e: Handle user-modified files
339
+ // Step d: Handle user-modified files
218
340
  // -----------------------------------------------------------------------
219
341
  const filesToCopy = [...diff.newFiles, ...diff.changed];
220
342
  const skippedFiles = [];
@@ -232,14 +354,14 @@ async function main() {
232
354
  // Check if anything to do
233
355
  // -----------------------------------------------------------------------
234
356
  if (filesToCopy.length === 0 && skippedFiles.length === 0) {
235
- console.log(`\x1b[32m\u2713\x1b[0m Already up to date (v${version})`);
357
+ console.log(`\x1b[32m\u2713\x1b[0m Already up to date (${targetLabel}, v${version})`);
236
358
  console.log('');
237
- console.log(' Run \x1b[36m/supatent:core\x1b[0m to get started');
359
+ console.log(` ${startHint}`);
238
360
  return;
239
361
  }
240
362
 
241
363
  // -----------------------------------------------------------------------
242
- // Step f: Copy files
364
+ // Step e: Copy files
243
365
  // -----------------------------------------------------------------------
244
366
  const isFreshInstall = !manifest;
245
367
 
@@ -263,7 +385,7 @@ async function main() {
263
385
  }
264
386
 
265
387
  // -----------------------------------------------------------------------
266
- // Step g: Write manifest
388
+ // Step f: Write manifest
267
389
  // -----------------------------------------------------------------------
268
390
  const manifestFiles = {};
269
391
 
@@ -293,16 +415,103 @@ async function main() {
293
415
  await writeFile(manifestPath, JSON.stringify(manifestData, null, 2) + '\n');
294
416
 
295
417
  // -----------------------------------------------------------------------
296
- // Step h: Print summary
418
+ // Step g: Print summary
297
419
  // -----------------------------------------------------------------------
298
420
  console.log('');
299
421
  if (isFreshInstall) {
300
- console.log(`\x1b[32m\u2713\x1b[0m Installed ${filesToCopy.length} files to ${TARGET_SUBDIR}/ (v${version})`);
422
+ console.log(`\x1b[32m\u2713\x1b[0m Installed ${filesToCopy.length} files to ${targetDir} (${targetLabel}, v${version})`);
301
423
  } else {
302
- console.log(`\x1b[32m\u2713\x1b[0m Updated ${filesToCopy.length} file${filesToCopy.length === 1 ? '' : 's'} in ${TARGET_SUBDIR}/ (v${version})`);
424
+ console.log(`\x1b[32m\u2713\x1b[0m Updated ${filesToCopy.length} file${filesToCopy.length === 1 ? '' : 's'} in ${targetDir} (${targetLabel}, v${version})`);
303
425
  }
304
426
  console.log('');
305
- console.log(' Run \x1b[36m/supatent:core\x1b[0m to get started');
427
+ console.log(` ${startHint}`);
428
+ }
429
+
430
+ async function installClaude({ cwd, force, version }) {
431
+ const claudeDir = join(cwd, '.claude');
432
+ const targetDir = join(cwd, CLAUDE_TARGET_SUBDIR);
433
+ const manifestPath = join(targetDir, 'supatent-core', '.manifest.json');
434
+
435
+ const ok = await ensureDirectoryExists(
436
+ claudeDir,
437
+ 'No .claude/ directory found. This installer creates skill files for Claude Code.',
438
+ { force },
439
+ );
440
+ if (!ok) {
441
+ process.exitCode = 1;
442
+ return;
443
+ }
444
+
445
+ await installSkillBundle({
446
+ sourceDir: CLAUDE_SKILLS_SOURCE_DIR,
447
+ targetDir,
448
+ manifestPath,
449
+ targetLabel: 'Claude Code',
450
+ startHint: 'Run \x1b[36m/supatent:core\x1b[0m to get started',
451
+ force,
452
+ version,
453
+ });
454
+ }
455
+
456
+ async function installCodex({ force, version, codexHomeOverride = null }) {
457
+ const codexHome = codexHomeOverride ? resolve(codexHomeOverride) : getDefaultCodexHome();
458
+ const targetDir = join(codexHome, 'skills');
459
+ const manifestPath = join(targetDir, 'supatent-core', '.manifest.json');
460
+
461
+ const ok = await ensureDirectoryExists(
462
+ codexHome,
463
+ `No Codex home directory found at ${codexHome}.`,
464
+ { force },
465
+ );
466
+ if (!ok) {
467
+ process.exitCode = 1;
468
+ return;
469
+ }
470
+
471
+ await installSkillBundle({
472
+ sourceDir: CODEX_SKILLS_SOURCE_DIR,
473
+ targetDir,
474
+ manifestPath,
475
+ targetLabel: 'Codex CLI',
476
+ startHint: 'Restart Codex CLI to pick up new skills',
477
+ force,
478
+ version,
479
+ });
480
+ }
481
+
482
+ // ---------------------------------------------------------------------------
483
+ // Main installer flow
484
+ // ---------------------------------------------------------------------------
485
+
486
+ async function main() {
487
+ const args = parseArgs(process.argv.slice(2));
488
+
489
+ if (args.help) {
490
+ printHelp();
491
+ return;
492
+ }
493
+
494
+ const version = await getVersion();
495
+ const cwd = process.cwd();
496
+ const force = args.force;
497
+
498
+ const target = args.target || await promptInstallTarget({ force });
499
+
500
+ if (target === 'both') {
501
+ console.log('Installing Supatent skills for Claude Code...');
502
+ await installClaude({ cwd, force, version });
503
+ console.log('');
504
+ console.log('Installing Supatent skills for Codex CLI...');
505
+ await installCodex({ force, version, codexHomeOverride: args.codexHome });
506
+ return;
507
+ }
508
+
509
+ if (target === 'codex') {
510
+ await installCodex({ force, version, codexHomeOverride: args.codexHome });
511
+ return;
512
+ }
513
+
514
+ await installClaude({ cwd, force, version });
306
515
  }
307
516
 
308
517
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "@supatent/skills",
3
- "version": "0.4.0",
4
- "description": "Claude Code content authoring skills for Supatent CMS",
3
+ "version": "0.5.0",
4
+ "description": "Claude Code and Codex CLI content authoring skills for Supatent CMS",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "supatent-skills": "bin/install.mjs"
8
8
  },
9
9
  "files": [
10
10
  "bin",
11
- "skills"
11
+ "skills",
12
+ "skills-codex"
12
13
  ],
13
14
  "devDependencies": {
14
15
  "eslint": "^9.16.0",
@@ -21,6 +22,7 @@
21
22
  "supatent",
22
23
  "cms",
23
24
  "claude-code",
25
+ "codex-cli",
24
26
  "skills",
25
27
  "content-authoring"
26
28
  ],