@vima_tech/flywheel 1.1.1 → 1.2.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/bin/flywheel.js CHANGED
@@ -2,9 +2,8 @@
2
2
  const { spawn, execSync } = require('child_process');
3
3
  const path = require('path');
4
4
  const fs = require('fs');
5
- const readline = require('readline');
5
+ const os = require('os');
6
6
 
7
- const REPO_DIR = process.env.FLYWHEEL_DIR || path.dirname(path.dirname(__dirname));
8
7
  const NPM_PKG = '@vima_tech/flywheel';
9
8
 
10
9
  function resolveScript(name) {
@@ -15,12 +14,15 @@ function resolveKernelFile(name) {
15
14
  return path.join(__dirname, '../' + name);
16
15
  }
17
16
 
18
- function runScript(scriptPath, args) {
17
+ function runScript(scriptPath, args, extraEnv = {}) {
18
+ const env = { ...process.env, FLYWHEEL_DIR: process.cwd(), ...extraEnv };
19
+ if (proxy) env.FLYWHEEL_PROXY = proxy;
20
+
19
21
  return new Promise((resolve, reject) => {
20
22
  const child = spawn('bash', [scriptPath, ...args], {
21
23
  cwd: process.cwd(),
22
24
  stdio: 'inherit',
23
- env: { ...process.env, FLYWHEEL_DIR: process.cwd() },
25
+ env,
24
26
  });
25
27
  child.on('exit', (code) => {
26
28
  if (code === 0) resolve(0);
@@ -32,7 +34,9 @@ function runScript(scriptPath, args) {
32
34
  function downloadFile(url, destPath) {
33
35
  return new Promise((resolve, reject) => {
34
36
  const file = fs.createWriteStream(destPath);
35
- spawn('curl', ['-fsSL', url, '-o', destPath], { stdio: 'ignore' })
37
+ const args = ['-fsSL', url, '-o', destPath];
38
+ if (proxy) args.splice(1, 0, '--proxy', proxy);
39
+ spawn('curl', args, { stdio: 'ignore' })
36
40
  .on('exit', (code) => {
37
41
  if (code === 0) resolve(0);
38
42
  else reject(new Error('curl failed'));
@@ -41,6 +45,8 @@ function downloadFile(url, destPath) {
41
45
  }
42
46
 
43
47
  async function initKernel(dir, skill) {
48
+ const baseUrl = 'https://raw.githubusercontent.com/renmengkai/flywheel/main';
49
+
44
50
  const kernelFiles = [
45
51
  'CLAUDE.md', '.gitignore',
46
52
  '.claude/commands/flywheel.md', '.claude/commands/skill.md',
@@ -52,17 +58,6 @@ async function initKernel(dir, skill) {
52
58
  'skills/_template/feedback-questions.sh',
53
59
  ];
54
60
 
55
- const optionalFiles = [
56
- 'skills/_template/artifacts.md',
57
- ];
58
-
59
- const baseUrl = 'https://raw.githubusercontent.com/renmengkai/flywheel/main';
60
- const skillFiles = skill ? [
61
- 'skills/' + skill + '/skill.yaml',
62
- 'skills/' + skill + '/domain.md',
63
- 'skills/' + skill + '/feedback-questions.sh',
64
- ] : [];
65
-
66
61
  for (const f of kernelFiles) {
67
62
  const dest = path.join(dir, f);
68
63
  fs.mkdirSync(path.dirname(dest), { recursive: true });
@@ -74,13 +69,12 @@ async function initKernel(dir, skill) {
74
69
  }
75
70
  }
76
71
 
77
- for (const f of optionalFiles) {
78
- const dest = path.join(dir, f);
79
- const srcPath = resolveKernelFile(f);
80
- if (fs.existsSync(srcPath)) fs.copyFileSync(srcPath, dest);
81
- }
82
-
83
72
  if (skill) {
73
+ const skillFiles = [
74
+ 'skills/' + skill + '/skill.yaml',
75
+ 'skills/' + skill + '/domain.md',
76
+ 'skills/' + skill + '/feedback-questions.sh',
77
+ ];
84
78
  for (const f of skillFiles) {
85
79
  const dest = path.join(dir, f);
86
80
  fs.mkdirSync(path.dirname(dest), { recursive: true });
@@ -106,6 +100,12 @@ async function initKernel(dir, skill) {
106
100
  fs.writeFileSync(path.join(dir, '.gitignore'), `# flywheel-managed\nprojects/*/\nepisodic-logs/\n.distill-needed/*\n!.distill-needed/.gitkeep\nmemory/.gitkeep\n*.log\n`);
107
101
  }
108
102
 
103
+ fs.writeFileSync(path.join(dir, '.flywheel'), JSON.stringify({
104
+ version: '1.0.0',
105
+ created: new Date().toISOString(),
106
+ skill: skill || 'none'
107
+ }));
108
+
109
109
  try {
110
110
  execSync('chmod +x ' + path.join(dir, 'scripts/*.sh'), { stdio: 'ignore', shell: true });
111
111
  } catch (e) {}
@@ -118,8 +118,7 @@ async function cmdNew(args) {
118
118
  }
119
119
 
120
120
  const projectName = args[0];
121
- const dirName = projectName;
122
- const targetDir = path.join(process.cwd(), dirName);
121
+ const targetDir = path.join(process.cwd(), projectName);
123
122
 
124
123
  if (fs.existsSync(targetDir)) {
125
124
  console.error('❌ 目录已存在: ' + targetDir);
@@ -159,14 +158,14 @@ async function cmdNew(args) {
159
158
  console.log(' ✅ 项目创建完成!');
160
159
  console.log('');
161
160
  console.log(' 进入目录并启动 ' + agent + ':');
162
- console.log(' cd ' + dirName + ' && flywheel start');
161
+ console.log(' cd ' + projectName + ' && flywheel start');
163
162
  console.log('');
164
- console.log(' 或手动启动:cd ' + dirName + ' && claude');
163
+ console.log(' 或手动启动:cd ' + projectName + ' && claude');
165
164
  console.log(' 然后输入 /sfw 开始飞轮工作');
166
165
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
167
166
  }
168
167
 
169
- async function cmdStart() {
168
+ function cmdStart() {
170
169
  const agent = detectAgent();
171
170
  if (!agent) {
172
171
  console.error('❌ 未检测到 Claude Code 或 OpenCode');
@@ -194,7 +193,130 @@ function detectAgent() {
194
193
  return null;
195
194
  }
196
195
 
197
- const [, , command, ...args] = process.argv;
196
+ function findFlywheelProjects(startPath, maxDepth = 3) {
197
+ const projects = [];
198
+ const homeDir = os.homedir();
199
+ const maxDepthParam = maxDepth || 3;
200
+
201
+ function scan(dir, depth) {
202
+ if (depth > maxDepthParam) return;
203
+ try {
204
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
205
+ for (const entry of entries) {
206
+ if (!entry.isDirectory()) continue;
207
+ if (entry.name === '.' || entry.name === '..') continue;
208
+ if (entry.name === 'node_modules') continue;
209
+
210
+ const fullPath = path.join(dir, entry.name);
211
+ const flywheelMarker = path.join(fullPath, '.flywheel');
212
+
213
+ if (fs.existsSync(flywheelMarker)) {
214
+ projects.push(fullPath);
215
+ } else if (fullPath.startsWith(homeDir)) {
216
+ scan(fullPath, depth + 1);
217
+ }
218
+ }
219
+ } catch (e) {}
220
+ }
221
+
222
+ scan(startPath, 0);
223
+ return projects;
224
+ }
225
+
226
+ async function updateProjectFramework(projectPath) {
227
+ const baseUrl = 'https://raw.githubusercontent.com/renmengkai/flywheel/main';
228
+
229
+ const kernelFiles = [
230
+ 'CLAUDE.md',
231
+ '.claude/commands/flywheel.md', '.claude/commands/skill.md',
232
+ 'scripts/auto-distill.sh', 'scripts/feedback-hook.sh',
233
+ 'scripts/bridge-to-coder.sh', 'scripts/flywheel-install.sh',
234
+ 'skills/_kernel/distillation.md',
235
+ 'skills/_template/skill.yaml', 'skills/_template/domain.md',
236
+ 'skills/_template/feedback-questions.sh',
237
+ ];
238
+
239
+ console.log(' 更新框架文件...');
240
+ for (const f of kernelFiles) {
241
+ const dest = path.join(projectPath, f);
242
+ try {
243
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
244
+ await downloadFile(baseUrl + '/' + f, dest);
245
+ console.log(' ✓ ' + f);
246
+ } catch (e) {
247
+ console.log(' ✗ ' + f + ' (失败)');
248
+ }
249
+ }
250
+ }
251
+
252
+ async function cmdUpgrade(args) {
253
+ console.log('');
254
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
255
+ console.log(' Flywheel 升级');
256
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
257
+
258
+ console.log('');
259
+ console.log('📦 升级 flywheel CLI...');
260
+ const npmArgs = ['install', '-g', NPM_PKG];
261
+ if (proxy) npmArgs.push('--proxy', proxy);
262
+ const npmSpawn = spawn('npm', npmArgs, { stdio: 'inherit' });
263
+ await new Promise((resolve) => npmSpawn.on('exit', resolve));
264
+
265
+ if (args.length > 0) {
266
+ const targetPath = path.resolve(args[0]);
267
+ if (fs.existsSync(path.join(targetPath, '.flywheel'))) {
268
+ console.log('');
269
+ console.log('📦 更新项目框架: ' + targetPath);
270
+ await updateProjectFramework(targetPath);
271
+ } else {
272
+ console.error('❌ 不是有效的 flywheel 项目: ' + targetPath);
273
+ }
274
+ } else {
275
+ console.log('');
276
+ console.log('🔍 扫描 flywheel 项目...');
277
+ const projects = findFlywheelProjects(os.homedir());
278
+
279
+ if (projects.length === 0) {
280
+ console.log(' 未找到 flywheel 项目');
281
+ } else {
282
+ console.log(' 找到 ' + projects.length + ' 个项目:');
283
+ projects.forEach((p, i) => console.log(' ' + (i + 1) + '. ' + p));
284
+
285
+ console.log('');
286
+ console.log(' 是否更新所有项目框架?(y/N)');
287
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
288
+ const answer = await new Promise((resolve) => rl.question('', resolve));
289
+ rl.close();
290
+
291
+ if (answer.toLowerCase() === 'y') {
292
+ for (const proj of projects) {
293
+ console.log('');
294
+ console.log('📦 更新: ' + proj);
295
+ await updateProjectFramework(proj);
296
+ }
297
+ }
298
+ }
299
+ }
300
+
301
+ console.log('');
302
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
303
+ console.log(' ✅ 升级完成');
304
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
305
+ }
306
+
307
+ let proxy = '';
308
+ const rawArgs = process.argv.slice(2);
309
+ const filteredArgs = [];
310
+ for (let i = 0; i < rawArgs.length; i++) {
311
+ if (rawArgs[i] === '-x' || rawArgs[i] === '--proxy') {
312
+ proxy = rawArgs[i + 1];
313
+ i++;
314
+ } else {
315
+ filteredArgs.push(rawArgs[i]);
316
+ }
317
+ }
318
+
319
+ const [command, ...restArgs] = filteredArgs;
198
320
 
199
321
  if (!command) {
200
322
  console.log(`
@@ -208,15 +330,21 @@ Flywheel CLI — 自成长 AI Agent 飞轮框架
208
330
  -v|--version 显示版本号
209
331
  install|add|i 安装 skill 包
210
332
  list|ls 列出已安装的 skill
211
- update|upgrade 更新 flywheel 或 skill
333
+ update [skill] 更新 skill
212
334
  distill|dist 手动触发蒸馏
213
- -h|--help|h 显示帮助
335
+ upgrade [path] 升级 flywheel 工具,并更新项目框架
336
+ -h|--help 显示帮助
337
+
338
+ 选项:
339
+ -x, --proxy <url> 使用代理
214
340
 
215
341
  示例:
216
342
  flywheel new my-project
217
- cd my-project && flywheel start
218
- flywheel install req-mining
219
- flywheel update flywheel # 更新 flywheel 本身
343
+ flywheel update # 更新所有 skill
344
+ flywheel update req-mining # 更新指定 skill
345
+ flywheel upgrade # 升级工具 + 交互更新项目框架
346
+ flywheel upgrade ./my-project # 更新指定项目框架
347
+ flywheel update -x http://127.0.0.1:7890
220
348
  `);
221
349
  process.exit(0);
222
350
  }
@@ -226,12 +354,12 @@ switch (command) {
226
354
  case '--version':
227
355
  case '-V':
228
356
  case 'version':
229
- console.log('@vima_tech/flywheel v1.1.0');
357
+ console.log('@vima_tech/flywheel v1.2.0');
230
358
  break;
231
359
  case 'new':
232
360
  case 'create':
233
361
  case 'init':
234
- cmdNew(args).catch((e) => { console.error(e); process.exit(1); });
362
+ cmdNew(restArgs).catch((e) => { console.error(e); process.exit(1); });
235
363
  break;
236
364
  case 'start':
237
365
  case 'run':
@@ -249,45 +377,46 @@ Flywheel CLI — 自成长 AI Agent 飞轮框架
249
377
  用法: flywheel <command> [args]
250
378
 
251
379
  可用命令:
252
- new <name> 创建新项目 (new|create|init)
253
- start 启动 Claude Code 或 OpenCode (start|run|launch)
254
- version 显示版本号 (-v|--version)
255
- update 更新 flywheel 到最新版本
256
- install <skill> 安装 skill 包 (install|add|i)
257
- list 列出已安装的 skill (list|ls)
258
- update skill 更新 skill 包 (update|upgrade)
259
- distill [project] 手动触发蒸馏 (distill|dist)
260
- help 显示帮助 (-h|--help)
380
+ new|create|init 创建新项目
381
+ start|run|launch 启动 Claude Code 或 OpenCode
382
+ -v|--version 显示版本号
383
+ install|add|i 安装 skill
384
+ list|ls 列出已安装的 skill
385
+ update [skill] 更新 skill 包
386
+ distill|dist 手动触发蒸馏
387
+ upgrade [path] 升级 flywheel 工具,并更新项目框架
388
+ -h|--help 显示帮助
389
+
390
+ 选项:
391
+ -x, --proxy <url> 使用代理
261
392
 
262
393
  示例:
263
394
  flywheel new my-project
264
- cd my-project && flywheel start
265
- flywheel install req-mining
395
+ flywheel update # 更新所有 skill
396
+ flywheel update req-mining # 更新指定 skill
397
+ flywheel upgrade # 升级工具 + 交互更新项目框架
398
+ flywheel upgrade ./my-project # 更新指定项目框架
399
+ flywheel update -x http://127.0.0.1:7890
266
400
  `);
267
401
  break;
268
402
  case 'install':
269
403
  case 'add':
270
404
  case 'i':
271
- runScript(resolveScript('flywheel-install'), ['add', ...args]).catch((e) => process.exit(1));
405
+ runScript(resolveScript('flywheel-install'), ['add', ...restArgs]).catch((e) => process.exit(1));
272
406
  break;
273
407
  case 'list':
274
408
  case 'ls':
275
409
  runScript(resolveScript('flywheel-install'), ['list']).catch((e) => process.exit(1));
276
410
  break;
277
411
  case 'update':
278
- case 'upgrade':
279
- if (args[0] === 'flywheel' || args[0] === 'self') {
280
- spawn('npm', ['install', '-g', '@vima_tech/flywheel'], {
281
- cwd: process.cwd(),
282
- stdio: 'inherit',
283
- }).on('exit', (code) => process.exit(code));
284
- } else {
285
- runScript(resolveScript('flywheel-install'), ['update', ...args]).catch((e) => process.exit(1));
286
- }
412
+ runScript(resolveScript('flywheel-install'), ['update', ...restArgs]).catch((e) => process.exit(1));
287
413
  break;
288
414
  case 'distill':
289
415
  case 'dist':
290
- runScript(resolveScript('auto-distill'), args).catch((e) => process.exit(1));
416
+ runScript(resolveScript('auto-distill'), restArgs).catch((e) => process.exit(1));
417
+ break;
418
+ case 'upgrade':
419
+ cmdUpgrade(restArgs).catch((e) => { console.error(e); process.exit(1); });
291
420
  break;
292
421
  default:
293
422
  console.error('未知命令: ' + command);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vima_tech/flywheel",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "自成长 AI Agent 飞轮框架 — 让任何需要积累迭代的 AI 工作流都能自我进化",
5
5
  "main": "bin/flywheel.js",
6
6
  "bin": {
@@ -17,6 +17,14 @@ BRANCH="main"
17
17
  BASE_URL="https://raw.githubusercontent.com/$REPO/$BRANCH"
18
18
  CMD="${1:-list}"
19
19
  TARGET="${2:-}"
20
+ PROXY="${FLYWHEEL_PROXY:-}"
21
+
22
+ [ -n "$PROXY" ] && CURL_PROXY="--proxy $PROXY" || CURL_PROXY=""
23
+
24
+ # ── curl helper ─────────────────────────────────────────────────
25
+ do_curl() {
26
+ curl -fsSL $CURL_PROXY "$@"
27
+ }
20
28
 
21
29
  # ── list ───────────────────────────────────────────────────────
22
30
  cmd_list() {
@@ -67,15 +75,13 @@ cmd_add() {
67
75
 
68
76
  echo "安装 skill:$skill"
69
77
 
70
- # 检查 skill 是否存在
71
- if ! curl -fsSL "$BASE_URL/skills/$skill/skill.yaml" -o /dev/null 2>/dev/null; then
78
+ if ! do_curl "$BASE_URL/skills/$skill/skill.yaml" -o /dev/null 2>/dev/null; then
72
79
  echo "❌ skill '$skill' 不存在"
73
80
  echo " 运行 list 查看可用 skill"
74
81
  exit 1
75
82
  fi
76
83
 
77
- # 使用主安装器的 install_skill 逻辑
78
- curl -fsSL "$BASE_URL/install.sh" | bash -s -- "$skill"
84
+ curl -fsSL $CURL_PROXY "$BASE_URL/install.sh" | bash -s -- "$skill"
79
85
  }
80
86
 
81
87
  # ── update ─────────────────────────────────────────────────────
@@ -87,29 +93,43 @@ cmd_update() {
87
93
  else
88
94
  for yaml in "$REPO_DIR/skills"/*/skill.yaml; do
89
95
  [ -f "$yaml" ] || continue
90
- skills+=("$(basename "$(dirname "$yaml")")")
96
+ local name=$(basename "$(dirname "$yaml")")
97
+ [ "$name" = "_kernel" ] && continue
98
+ [ "$name" = "_template" ] && continue
99
+ skills+=("$name")
91
100
  done
92
101
  fi
93
102
 
94
103
  [ ${#skills[@]} -eq 0 ] && { echo "没有已安装的 skill"; exit 0; }
95
104
 
96
105
  echo "━━━ 更新 Skill 包 ━━━"
106
+
107
+ local update_cmds=()
97
108
  for skill in "${skills[@]}"; do
98
- echo ""
99
- echo "更新 $skill..."
100
109
  local yaml_url="$BASE_URL/skills/$skill/skill.yaml"
101
- local remote_ver
102
- remote_ver=$(curl -fsSL "$yaml_url" 2>/dev/null | grep '^version:' | awk '{print $2}' || echo "?")
103
110
  local local_ver
104
111
  local_ver=$(grep '^version:' "$REPO_DIR/skills/$skill/skill.yaml" 2>/dev/null | awk '{print $2}' || echo "0")
105
112
 
113
+ local remote_ver
114
+ remote_ver=$(do_curl "$yaml_url" 2>/dev/null | grep '^version:' | awk '{print $2}' || echo "?")
115
+
106
116
  if [ "$remote_ver" = "$local_ver" ]; then
107
117
  echo " ✓ $skill 已是最新版 v$local_ver"
108
118
  else
109
- echo " $local_ver → $remote_ver,更新中..."
110
- cmd_add "$skill"
119
+ echo " $skill v$local_ver → v$remote_ver"
120
+ update_cmds+=("$skill")
111
121
  fi
112
122
  done
123
+
124
+ if [ ${#update_cmds[@]} -gt 0 ]; then
125
+ echo ""
126
+ echo "开始更新 ${#update_cmds[@]} 个 skill..."
127
+ for skill in "${update_cmds[@]}"; do
128
+ echo ""
129
+ echo "更新 $skill..."
130
+ cmd_add "$skill"
131
+ done
132
+ fi
113
133
  }
114
134
 
115
135
  case "$CMD" in