emberflow-skills 1.0.0 → 1.1.1

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 (2) hide show
  1. package/bin/install.js +212 -31
  2. package/package.json +1 -1
package/bin/install.js CHANGED
@@ -2,9 +2,21 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const https = require('https');
6
+ const http = require('http');
7
+ const readline = require('readline');
8
+ const os = require('os');
5
9
 
6
10
  const SKILL_NAME = 'ember-publish';
7
11
  const SKILL_SRC = path.join(__dirname, '..', 'skills', SKILL_NAME, 'SKILL.md');
12
+ const EMBERFLOW_URL = 'https://supportive-forgiveness-production.up.railway.app';
13
+ const TOKEN_PATH = path.join(os.homedir(), '.emberflow', 'token.json');
14
+
15
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
16
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
17
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
18
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
19
+ const orange = (s) => `\x1b[38;5;208m${s}\x1b[0m`;
8
20
 
9
21
  const targets = [
10
22
  { dir: '.claude/skills', label: 'Claude Code (project)' },
@@ -12,59 +24,228 @@ const targets = [
12
24
  ];
13
25
 
14
26
  const globalTargets = [
15
- { dir: path.join(require('os').homedir(), '.claude', 'skills'), label: 'Claude Code (global)' },
27
+ { dir: path.join(os.homedir(), '.claude', 'skills'), label: 'Claude Code (global)' },
16
28
  ];
17
29
 
18
30
  const args = process.argv.slice(2);
19
31
  const isGlobal = args.includes('--global') || args.includes('-g');
20
32
 
33
+ // ── HTTP helpers ──
34
+
35
+ function request(method, urlStr, body) {
36
+ return new Promise((resolve, reject) => {
37
+ const url = new URL(urlStr);
38
+ const mod = url.protocol === 'https:' ? https : http;
39
+ const opts = {
40
+ hostname: url.hostname,
41
+ port: url.port,
42
+ path: url.pathname + url.search,
43
+ method,
44
+ headers: {},
45
+ };
46
+ let data = null;
47
+ if (body) {
48
+ data = JSON.stringify(body);
49
+ opts.headers['Content-Type'] = 'application/json';
50
+ opts.headers['Content-Length'] = Buffer.byteLength(data);
51
+ }
52
+ const req = mod.request(opts, (res) => {
53
+ let chunks = '';
54
+ res.on('data', (c) => chunks += c);
55
+ res.on('end', () => {
56
+ try { resolve({ status: res.statusCode, data: JSON.parse(chunks) }); }
57
+ catch { resolve({ status: res.statusCode, data: chunks }); }
58
+ });
59
+ });
60
+ req.on('error', reject);
61
+ if (data) req.write(data);
62
+ req.end();
63
+ });
64
+ }
65
+
66
+ function ask(question) {
67
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
68
+ return new Promise((resolve) => {
69
+ rl.question(question, (answer) => {
70
+ rl.close();
71
+ resolve(answer.trim().toLowerCase());
72
+ });
73
+ });
74
+ }
75
+
76
+ function sleep(ms) {
77
+ return new Promise((r) => setTimeout(r, ms));
78
+ }
79
+
80
+ // ── Skill installer ──
81
+
21
82
  function install(destDir, label) {
22
83
  const skillDir = path.join(destDir, SKILL_NAME);
23
84
  const destFile = path.join(skillDir, 'SKILL.md');
24
-
25
85
  fs.mkdirSync(skillDir, { recursive: true });
26
86
  fs.copyFileSync(SKILL_SRC, destFile);
27
- console.log(` \x1b[32m✓\x1b[0m Installed to ${path.relative(process.cwd(), skillDir) || skillDir} \x1b[2m(${label})\x1b[0m`);
87
+ console.log(` ${green('✓')} Installed to ${path.relative(process.cwd(), skillDir) || skillDir} ${dim(`(${label})`)}`);
28
88
  return true;
29
89
  }
30
90
 
31
- console.log();
32
- console.log(' \x1b[1mEmberflow Skills Installer\x1b[0m');
33
- console.log();
34
-
35
- let installed = 0;
91
+ // ── Auth flow ──
36
92
 
37
- if (isGlobal) {
38
- for (const t of globalTargets) {
39
- install(t.dir, t.label);
40
- installed++;
93
+ function hasValidToken() {
94
+ try {
95
+ const token = JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf8'));
96
+ return !!token.token;
97
+ } catch {
98
+ return false;
41
99
  }
42
- } else {
43
- // Auto-detect which tool directories exist or can be created
44
- const cwd = process.cwd();
45
- const detected = [];
46
-
47
- for (const t of targets) {
48
- const parent = path.dirname(path.join(cwd, t.dir));
49
- // Install if parent config dir exists (e.g. .claude/ or .cursor/) or if neither exists (default to .claude)
50
- if (fs.existsSync(path.join(cwd, t.dir)) || fs.existsSync(parent)) {
51
- detected.push(t);
52
- }
100
+ }
101
+
102
+ async function authenticate() {
103
+ console.log();
104
+ console.log(` ${orange('🔥')} ${bold('Sign in to Emberflow')}`);
105
+ console.log(` ${dim('Your published docs will be attributed to your account.')}`);
106
+ console.log();
107
+
108
+ // Request device code
109
+ let resp;
110
+ try {
111
+ resp = await request('POST', `${EMBERFLOW_URL}/api/device-code`);
112
+ } catch {
113
+ console.log(` ${dim('Could not reach Emberflow. You can sign in later.')}`);
114
+ return false;
53
115
  }
54
116
 
55
- // Default to Claude Code if nothing detected
56
- if (detected.length === 0) {
57
- detected.push(targets[0]);
117
+ if (!resp.data || !resp.data.code) {
118
+ console.log(` ${dim('Could not start device auth. You can sign in later.')}`);
119
+ return false;
58
120
  }
59
121
 
60
- for (const t of detected) {
61
- install(path.join(cwd, t.dir), t.label);
62
- installed++;
122
+ const { code, verification_url } = resp.data;
123
+
124
+ console.log(` Open this URL in your browser:`);
125
+ console.log();
126
+ console.log(` ${cyan(verification_url)}`);
127
+ console.log();
128
+ console.log(` Your code: ${bold(code)}`);
129
+ console.log();
130
+
131
+ // Try to open the URL automatically
132
+ try {
133
+ const { exec } = require('child_process');
134
+ if (process.platform === 'win32') {
135
+ exec(`start "" "${verification_url}"`);
136
+ } else if (process.platform === 'darwin') {
137
+ exec(`open "${verification_url}"`);
138
+ } else {
139
+ exec(`xdg-open "${verification_url}"`);
140
+ }
141
+ } catch {}
142
+
143
+ process.stdout.write(` ${dim('Waiting for approval...')}`);
144
+
145
+ // Poll for approval
146
+ const maxAttempts = 60; // 3 minutes at 3s intervals
147
+ for (let i = 0; i < maxAttempts; i++) {
148
+ await sleep(3000);
149
+
150
+ try {
151
+ const status = await request('GET', `${EMBERFLOW_URL}/api/device-code/${code}`);
152
+
153
+ if (status.data.status === 'approved' && status.data.session_token) {
154
+ fs.mkdirSync(path.dirname(TOKEN_PATH), { recursive: true });
155
+ fs.writeFileSync(TOKEN_PATH, JSON.stringify({ token: status.data.session_token }, null, 2));
156
+ process.stdout.clearLine(0);
157
+ process.stdout.cursorTo(0);
158
+ console.log(` ${green('✓')} Signed in! Token saved to ${dim('~/.emberflow/token.json')}`);
159
+ return true;
160
+ }
161
+
162
+ if (status.data.status === 'expired') {
163
+ process.stdout.clearLine(0);
164
+ process.stdout.cursorTo(0);
165
+ console.log(` ${dim('Code expired. You can sign in later by running:')} npx emberflow-skills --auth`);
166
+ return false;
167
+ }
168
+ } catch {
169
+ // Network error, keep polling
170
+ }
171
+
172
+ // Spinner
173
+ const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
174
+ process.stdout.clearLine(0);
175
+ process.stdout.cursorTo(0);
176
+ process.stdout.write(` ${orange(frames[i % frames.length])} ${dim('Waiting for approval...')}`);
63
177
  }
178
+
179
+ process.stdout.clearLine(0);
180
+ process.stdout.cursorTo(0);
181
+ console.log(` ${dim('Timed out. You can sign in later by running:')} npx emberflow-skills --auth`);
182
+ return false;
64
183
  }
65
184
 
66
- if (installed > 0) {
185
+ // ── Main ──
186
+
187
+ async function main() {
188
+ const authOnly = args.includes('--auth');
189
+
67
190
  console.log();
68
- console.log(` Use: \x1b[36m/ember-publish\x1b[0m \x1b[2m[topic]\x1b[0m`);
191
+ console.log(` ${orange('🔥')} ${bold('Emberflow Skills')}`);
69
192
  console.log();
193
+
194
+ if (!authOnly) {
195
+ let installed = 0;
196
+
197
+ if (isGlobal) {
198
+ for (const t of globalTargets) {
199
+ install(t.dir, t.label);
200
+ installed++;
201
+ }
202
+ } else {
203
+ const cwd = process.cwd();
204
+ const detected = [];
205
+
206
+ for (const t of targets) {
207
+ const parent = path.dirname(path.join(cwd, t.dir));
208
+ if (fs.existsSync(path.join(cwd, t.dir)) || fs.existsSync(parent)) {
209
+ detected.push(t);
210
+ }
211
+ }
212
+
213
+ if (detected.length === 0) {
214
+ detected.push(targets[0]);
215
+ }
216
+
217
+ for (const t of detected) {
218
+ install(path.join(cwd, t.dir), t.label);
219
+ installed++;
220
+ }
221
+ }
222
+
223
+ if (installed > 0) {
224
+ console.log();
225
+ console.log(` Use: ${cyan('/ember-publish')} ${dim('[topic]')}`);
226
+ }
227
+ }
228
+
229
+ // Auth
230
+ if (hasValidToken() && !authOnly) {
231
+ console.log();
232
+ console.log(` ${green('✓')} Already signed in ${dim('(~/.emberflow/token.json)')}`);
233
+ console.log();
234
+ } else {
235
+ const answer = authOnly ? 'y' : await ask(`\n Sign in to link docs to your account? ${dim('[Y/n]')} `);
236
+
237
+ if (answer === '' || answer === 'y' || answer === 'yes') {
238
+ await authenticate();
239
+ } else {
240
+ console.log();
241
+ console.log(` ${dim('Skipped. Docs will be published anonymously.')}`);
242
+ console.log(` ${dim('Sign in later with:')} npx emberflow-skills --auth`);
243
+ }
244
+ console.log();
245
+ }
70
246
  }
247
+
248
+ main().catch((err) => {
249
+ console.error(err);
250
+ process.exit(1);
251
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emberflow-skills",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Install Emberflow skills for AI coding tools",
5
5
  "bin": {
6
6
  "emberflow-skills": "./bin/install.js"