emberflow-skills 1.11.0 → 1.13.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.
Files changed (3) hide show
  1. package/README.md +31 -9
  2. package/bin/install.js +210 -45
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,12 +8,21 @@ Publish beautiful docs from your AI coding tools to [Emberflow](https://www.embe
8
8
  npx emberflow-skills
9
9
  ```
10
10
 
11
- The installer auto-detects your project type (Claude Code or Cursor) and copies all skills to the right directory. You'll be publishing docs in under 10 seconds.
11
+ The installer auto-detects which AI coding tools you use and installs skills in the correct format for each.
12
+
13
+ | Tool | Install location | How to invoke |
14
+ |------|-----------------|---------------|
15
+ | **Claude Code** | `.claude/skills/` | `/ember-publish [topic]` |
16
+ | **Codex** | `.agents/skills/` | `$ember-publish [topic]` |
17
+ | **Cursor** | `.cursor/rules/*.mdc` | `@ember-publish-explainer` or ask naturally |
18
+ | **Windsurf** | `.windsurf/rules/*.md` | Ask naturally: "publish this to Emberflow" |
19
+
20
+ If multiple tools are detected, skills are installed for all of them.
12
21
 
13
22
  ### Options
14
23
 
15
24
  ```bash
16
- # Install to current project (default)
25
+ # Install to current project (default — auto-detects tools)
17
26
  npx emberflow-skills
18
27
 
19
28
  # Install globally for Claude Code (available in all projects)
@@ -22,9 +31,13 @@ npx emberflow-skills --global
22
31
 
23
32
  ### What the installer does
24
33
 
25
- 1. Detects if you're in a Claude Code project (`.claude/`) or Cursor project (`.cursor/`)
26
- 2. Copies all Emberflow skills into your project's skills directory
27
- 3. Done use any `/ember-publish` command in your next conversation
34
+ 1. Detects tool configs in your project (`.claude/`, `.agents/`, `.cursor/`, `.windsurf/`)
35
+ 2. Installs skills in each tool's native format:
36
+ - Claude Code & Codex: `SKILL.md` with frontmatter (native skill system)
37
+ - Cursor: `.mdc` rules with auto-attach descriptions
38
+ - Windsurf: `.md` rules
39
+ 3. Copies reference templates for the explainer skill
40
+ 4. Done — use the skills in your next conversation
28
41
 
29
42
  ## Skills
30
43
 
@@ -62,7 +75,7 @@ Publish CSV files as an interactive dataset viewer with virtual scroll (handles
62
75
 
63
76
  ### `/ember-publish-explainer`
64
77
 
65
- Generate interactive visual explainers — the AI chooses the best visualization type (flowchart, chart, timeline, grid, etc.) for the topic. Slide-based with animated transitions.
78
+ Generate interactive visual explainers — the AI chooses the best visualization type (funnel, heatmap, radar chart, waterfall, timeline, architecture diagram, etc.) for the topic. Slide-based with animated transitions.
66
79
 
67
80
  ```
68
81
  /ember-publish-explainer how our CI/CD pipeline works
@@ -95,12 +108,21 @@ If you prefer not to use npx:
95
108
 
96
109
  ```bash
97
110
  git clone https://github.com/pmccurley87/emberflow-skills.git
111
+
112
+ # Claude Code
98
113
  cp -r emberflow-skills/skills/* .claude/skills/
114
+
115
+ # Codex — same SKILL.md format
116
+ cp -r emberflow-skills/skills/* .agents/skills/
117
+
118
+ # Cursor — copy as .mdc rules (or use npx for auto-conversion)
119
+ # Windsurf — copy to .windsurf/rules/
99
120
  ```
100
121
 
101
122
  ## Works with
102
123
 
103
- - **Claude Code** — skills run natively
104
- - **Cursor** — skills auto-detected from `.cursor/skills/`
105
- - **Codex CLI** — supports SKILL.md format
124
+ - **Claude Code** — native skill system, invoke with `/ember-publish`
125
+ - **Codex** — native skill system, invoke with `$ember-publish`
126
+ - **Cursor** — auto-attached rules, invoke with `@ember-publish-explainer` or ask naturally
127
+ - **Windsurf** — rules loaded automatically, ask naturally
106
128
  - **Any MCP-compatible tool** — Emberflow also provides an MCP server
package/bin/install.js CHANGED
@@ -18,13 +18,39 @@ const green = (s) => `\x1b[32m${s}\x1b[0m`;
18
18
  const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
19
19
  const orange = (s) => `\x1b[38;5;208m${s}\x1b[0m`;
20
20
 
21
- const targets = [
22
- { dir: '.claude/skills', label: 'Claude Code (project)' },
23
- { dir: '.cursor/skills', label: 'Cursor (project)' },
24
- ];
25
-
26
- const globalTargets = [
27
- { dir: path.join(os.homedir(), '.claude', 'skills'), label: 'Claude Code (global)' },
21
+ // ── Tool definitions ──
22
+
23
+ const TOOLS = [
24
+ {
25
+ type: 'claude',
26
+ detect: ['.claude'],
27
+ projectDir: '.claude/skills',
28
+ globalDir: path.join(os.homedir(), '.claude', 'skills'),
29
+ label: 'Claude Code',
30
+ usage: '/ember-publish',
31
+ },
32
+ {
33
+ type: 'cursor',
34
+ detect: ['.cursor', '.cursorrules'],
35
+ projectDir: '.cursor/rules',
36
+ label: 'Cursor',
37
+ usage: '"publish this to Emberflow"',
38
+ },
39
+ {
40
+ type: 'windsurf',
41
+ detect: ['.windsurf', '.windsurfrules'],
42
+ projectDir: '.windsurf/rules',
43
+ label: 'Windsurf',
44
+ usage: '"publish this to Emberflow"',
45
+ },
46
+ {
47
+ type: 'codex',
48
+ detect: ['.agents', 'AGENTS.md'],
49
+ projectDir: '.agents/skills',
50
+ globalDir: path.join(os.homedir(), '.agents', 'skills'),
51
+ label: 'Codex',
52
+ usage: '$ember-publish',
53
+ },
28
54
  ];
29
55
 
30
56
  const args = process.argv.slice(2);
@@ -77,7 +103,22 @@ function sleep(ms) {
77
103
  return new Promise((r) => setTimeout(r, ms));
78
104
  }
79
105
 
80
- // ── Skill installer ──
106
+ // ── SKILL.md parsing ──
107
+
108
+ function parseFrontmatter(content) {
109
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
110
+ if (!match) return { meta: {}, body: content };
111
+ const meta = {};
112
+ for (const line of match[1].split('\n')) {
113
+ const idx = line.indexOf(':');
114
+ if (idx > 0) {
115
+ meta[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
116
+ }
117
+ }
118
+ return { meta, body: match[2] };
119
+ }
120
+
121
+ // ── File helpers ──
81
122
 
82
123
  function copyDirRecursive(src, dest) {
83
124
  fs.mkdirSync(dest, { recursive: true });
@@ -92,25 +133,138 @@ function copyDirRecursive(src, dest) {
92
133
  }
93
134
  }
94
135
 
95
- function install(destDir, label) {
136
+ function rewriteTemplatePaths(body, templatesRelPath) {
137
+ // Rewrite "Read templates/" to use the full relative path from project root
138
+ return body.replace(
139
+ /Read templates\//g,
140
+ `Read ${templatesRelPath}/`
141
+ );
142
+ }
143
+
144
+ // ── Installers per tool type ──
145
+
146
+ function installClaude(name, destDir) {
147
+ const srcDir = path.join(SKILLS_DIR, name);
148
+ const skillDir = path.join(destDir, name);
149
+ fs.mkdirSync(skillDir, { recursive: true });
150
+ fs.copyFileSync(path.join(srcDir, 'SKILL.md'), path.join(skillDir, 'SKILL.md'));
151
+
152
+ const templatesDir = path.join(srcDir, 'templates');
153
+ if (fs.existsSync(templatesDir)) {
154
+ copyDirRecursive(templatesDir, path.join(skillDir, 'templates'));
155
+ }
156
+ }
157
+
158
+ function installCursor(name, destDir, cwd) {
159
+ const srcDir = path.join(SKILLS_DIR, name);
160
+ const skillMd = fs.readFileSync(path.join(srcDir, 'SKILL.md'), 'utf8');
161
+ const { meta, body } = parseFrontmatter(skillMd);
162
+
163
+ // Copy templates to .cursor/rules/<name>/templates/
164
+ const templatesDir = path.join(srcDir, 'templates');
165
+ const hasTemplates = fs.existsSync(templatesDir);
166
+ if (hasTemplates) {
167
+ copyDirRecursive(templatesDir, path.join(destDir, name, 'templates'));
168
+ }
169
+
170
+ // Rewrite template paths relative to project root
171
+ let content = body;
172
+ if (hasTemplates) {
173
+ const relPath = path.relative(cwd, path.join(destDir, name, 'templates'));
174
+ content = rewriteTemplatePaths(content, relPath);
175
+ }
176
+
177
+ // Write .mdc file with Cursor frontmatter
178
+ const description = meta.description || name;
179
+ const hint = meta['argument-hint'] ? ` — ${meta['argument-hint']}` : '';
180
+ const mdc = `---
181
+ description: ${description}${hint}
182
+ globs:
183
+ alwaysApply: false
184
+ ---
185
+
186
+ ${content}`;
187
+
188
+ fs.mkdirSync(destDir, { recursive: true });
189
+ fs.writeFileSync(path.join(destDir, `${name}.mdc`), mdc);
190
+ }
191
+
192
+ function installWindsurf(name, destDir, cwd) {
193
+ const srcDir = path.join(SKILLS_DIR, name);
194
+ const skillMd = fs.readFileSync(path.join(srcDir, 'SKILL.md'), 'utf8');
195
+ const { body } = parseFrontmatter(skillMd);
196
+
197
+ // Copy templates
198
+ const templatesDir = path.join(srcDir, 'templates');
199
+ const hasTemplates = fs.existsSync(templatesDir);
200
+ if (hasTemplates) {
201
+ copyDirRecursive(templatesDir, path.join(destDir, name, 'templates'));
202
+ }
203
+
204
+ // Rewrite template paths
205
+ let content = body;
206
+ if (hasTemplates) {
207
+ const relPath = path.relative(cwd, path.join(destDir, name, 'templates'));
208
+ content = rewriteTemplatePaths(content, relPath);
209
+ }
210
+
211
+ fs.mkdirSync(destDir, { recursive: true });
212
+ fs.writeFileSync(path.join(destDir, `${name}.md`), content);
213
+ }
214
+
215
+ function installCodex(name, destDir, cwd) {
216
+ // Codex uses the same SKILL.md format as Claude Code, discovered from .agents/skills/<name>/
217
+ const srcDir = path.join(SKILLS_DIR, name);
218
+ const skillDir = path.join(destDir, name);
219
+ fs.mkdirSync(skillDir, { recursive: true });
220
+ fs.copyFileSync(path.join(srcDir, 'SKILL.md'), path.join(skillDir, 'SKILL.md'));
221
+
222
+ // Copy templates
223
+ const templatesDir = path.join(srcDir, 'templates');
224
+ if (fs.existsSync(templatesDir)) {
225
+ copyDirRecursive(templatesDir, path.join(skillDir, 'templates'));
226
+ }
227
+ }
228
+
229
+ // ── Main install function ──
230
+
231
+ function install(destDir, tool, cwd) {
96
232
  for (const name of SKILL_NAMES) {
97
- const srcDir = path.join(SKILLS_DIR, name);
98
- const skillDir = path.join(destDir, name);
99
- const destFile = path.join(skillDir, 'SKILL.md');
100
- fs.mkdirSync(skillDir, { recursive: true });
101
- fs.copyFileSync(path.join(srcDir, 'SKILL.md'), destFile);
102
-
103
- // Copy templates directory if it exists
104
- const templatesDir = path.join(srcDir, 'templates');
105
- if (fs.existsSync(templatesDir)) {
106
- copyDirRecursive(templatesDir, path.join(skillDir, 'templates'));
233
+ switch (tool.type) {
234
+ case 'claude':
235
+ installClaude(name, destDir);
236
+ break;
237
+ case 'cursor':
238
+ installCursor(name, destDir, cwd);
239
+ break;
240
+ case 'windsurf':
241
+ installWindsurf(name, destDir, cwd);
242
+ break;
243
+ case 'codex':
244
+ installCodex(name, destDir, cwd);
245
+ break;
107
246
  }
108
-
109
- console.log(` ${green('✓')} Installed ${name} to ${path.relative(process.cwd(), skillDir) || skillDir} ${dim(`(${label})`)}`);
247
+ const relDest = path.relative(cwd, destDir) || destDir;
248
+ console.log(` ${green('✓')} ${name} ${dim(relDest)} ${dim(`(${tool.label})`)}`);
110
249
  }
111
250
  return true;
112
251
  }
113
252
 
253
+ // ── Detection ──
254
+
255
+ function detectTools(cwd) {
256
+ const detected = [];
257
+ for (const tool of TOOLS) {
258
+ for (const marker of tool.detect) {
259
+ if (fs.existsSync(path.join(cwd, marker))) {
260
+ detected.push(tool);
261
+ break;
262
+ }
263
+ }
264
+ }
265
+ return detected;
266
+ }
267
+
114
268
  // ── Auth flow ──
115
269
 
116
270
  function hasValidToken() {
@@ -128,7 +282,6 @@ async function authenticate() {
128
282
  console.log(` ${dim('Your published docs will be attributed to your account.')}`);
129
283
  console.log();
130
284
 
131
- // Request device code
132
285
  let resp;
133
286
  try {
134
287
  resp = await request('POST', `${EMBERFLOW_URL}/api/device-code`);
@@ -151,7 +304,6 @@ async function authenticate() {
151
304
  console.log(` Your code: ${bold(code)}`);
152
305
  console.log();
153
306
 
154
- // Try to open the URL automatically
155
307
  try {
156
308
  const { exec } = require('child_process');
157
309
  if (process.platform === 'win32') {
@@ -165,8 +317,7 @@ async function authenticate() {
165
317
 
166
318
  process.stdout.write(` ${dim('Waiting for approval...')}`);
167
319
 
168
- // Poll for approval
169
- const maxAttempts = 60; // 3 minutes at 3s intervals
320
+ const maxAttempts = 60;
170
321
  for (let i = 0; i < maxAttempts; i++) {
171
322
  await sleep(3000);
172
323
 
@@ -174,7 +325,6 @@ async function authenticate() {
174
325
  const status = await request('GET', `${EMBERFLOW_URL}/api/device-code/${code}`);
175
326
 
176
327
  if (status.data.status === 'approved' && status.data.session_token) {
177
- // Strip cookie name prefix if present (e.g. "__Secure-better-auth.session_token=VALUE" -> "VALUE")
178
328
  const raw = status.data.session_token.replace(/^(?:__Secure-)?better-auth\.session_token=/, '');
179
329
  fs.mkdirSync(path.dirname(TOKEN_PATH), { recursive: true });
180
330
  fs.writeFileSync(TOKEN_PATH, JSON.stringify({ token: raw }, null, 2));
@@ -194,7 +344,6 @@ async function authenticate() {
194
344
  // Network error, keep polling
195
345
  }
196
346
 
197
- // Spinner
198
347
  const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
199
348
  process.stdout.clearLine(0);
200
349
  process.stdout.cursorTo(0);
@@ -217,38 +366,54 @@ async function main() {
217
366
  console.log();
218
367
 
219
368
  if (!authOnly) {
369
+ const cwd = process.cwd();
220
370
  let installed = 0;
221
371
 
222
372
  if (isGlobal) {
223
- for (const t of globalTargets) {
224
- install(t.dir, t.label);
225
- installed++;
226
- }
227
- } else {
228
- const cwd = process.cwd();
229
- const detected = [];
230
-
231
- for (const t of targets) {
232
- const parent = path.dirname(path.join(cwd, t.dir));
233
- if (fs.existsSync(path.join(cwd, t.dir)) || fs.existsSync(parent)) {
234
- detected.push(t);
373
+ // Global install — Claude Code + Codex (both support global skills)
374
+ for (const tool of TOOLS) {
375
+ if (tool.globalDir) {
376
+ install(tool.globalDir, tool, cwd);
377
+ installed++;
235
378
  }
236
379
  }
380
+ } else {
381
+ const detected = detectTools(cwd);
237
382
 
238
383
  if (detected.length === 0) {
239
- detected.push(targets[0]);
384
+ // Default to Claude Code
385
+ detected.push(TOOLS[0]);
386
+ console.log(` ${dim('No tool config detected — defaulting to Claude Code')}`);
387
+ console.log();
388
+ } else {
389
+ console.log(` ${dim(`Detected: ${detected.map(t => t.label).join(', ')}`)}`);
390
+ console.log();
240
391
  }
241
392
 
242
- for (const t of detected) {
243
- install(path.join(cwd, t.dir), t.label);
393
+ for (const tool of detected) {
394
+ const destDir = path.join(cwd, tool.projectDir);
395
+ install(destDir, tool, cwd);
244
396
  installed++;
397
+ console.log();
245
398
  }
246
399
  }
247
400
 
248
401
  if (installed > 0) {
249
- console.log();
250
- console.log(` Use: ${cyan('/ember-publish')} ${dim('[topic]')} — auto-picks format (doc, JSON, Space, or explainer)`);
251
- console.log(` ${cyan('/ember-publish-doc')} ${dim('[topic]')} ${cyan('/ember-publish-json')} ${dim('[data]')} ${cyan('/ember-publish-space')} ${dim('[directory]')} ${cyan('/ember-publish-explainer')} ${dim('[topic]')}`);
402
+ const tools = isGlobal ? TOOLS.filter(t => t.globalDir) : (detectTools(cwd).length > 0 ? detectTools(cwd) : [TOOLS[0]]);
403
+
404
+ for (const tool of tools) {
405
+ if (tool.type === 'claude') {
406
+ console.log(` ${bold('Claude Code:')} ${cyan('/ember-publish')} ${dim('[topic]')} — auto-picks format`);
407
+ console.log(` ${cyan('/ember-publish-doc')} ${cyan('/ember-publish-json')} ${cyan('/ember-publish-explainer')} ${cyan('/ember-publish-space')}`);
408
+ } else if (tool.type === 'codex') {
409
+ console.log(` ${bold('Codex:')} ${cyan('$ember-publish')} ${dim('[topic]')} — invoke skills with $ prefix`);
410
+ console.log(` ${cyan('$ember-publish-doc')} ${cyan('$ember-publish-json')} ${cyan('$ember-publish-explainer')} ${cyan('$ember-publish-space')}`);
411
+ } else if (tool.type === 'cursor') {
412
+ console.log(` ${bold('Cursor:')} Type ${cyan('@ember-publish-explainer')} or ask ${cyan('"publish this to Emberflow"')}`);
413
+ } else if (tool.type === 'windsurf') {
414
+ console.log(` ${bold('Windsurf:')} Ask ${cyan('"publish this to Emberflow"')} or ${cyan('"create an Emberflow explainer"')}`);
415
+ }
416
+ }
252
417
  }
253
418
  }
254
419
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emberflow-skills",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "description": "Install Emberflow skills for AI coding tools",
5
5
  "bin": {
6
6
  "emberflow-skills": "./bin/install.js"