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.
- package/bin/install.js +212 -31
- 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(
|
|
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(`
|
|
87
|
+
console.log(` ${green('✓')} Installed to ${path.relative(process.cwd(), skillDir) || skillDir} ${dim(`(${label})`)}`);
|
|
28
88
|
return true;
|
|
29
89
|
}
|
|
30
90
|
|
|
31
|
-
|
|
32
|
-
console.log(' \x1b[1mEmberflow Skills Installer\x1b[0m');
|
|
33
|
-
console.log();
|
|
34
|
-
|
|
35
|
-
let installed = 0;
|
|
91
|
+
// ── Auth flow ──
|
|
36
92
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
185
|
+
// ── Main ──
|
|
186
|
+
|
|
187
|
+
async function main() {
|
|
188
|
+
const authOnly = args.includes('--auth');
|
|
189
|
+
|
|
67
190
|
console.log();
|
|
68
|
-
console.log(`
|
|
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
|
+
});
|