@vima_tech/flywheel 1.1.0 → 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(`
@@ -203,68 +325,98 @@ Flywheel CLI — 自成长 AI Agent 飞轮框架
203
325
  用法: flywheel <command> [args]
204
326
 
205
327
  可用命令:
206
- new <name> 创建新项目
207
- start 启动 Claude Code 或 OpenCode
208
- version 显示版本号
209
- update 更新 flywheel 到最新版本
210
- install <skill> 安装 skill 包
211
- list 列出已安装的 skill
212
- update [skill] 更新 skill 包
213
- distill [project] 手动触发蒸馏
214
- help 显示帮助
328
+ new|create|init 创建新项目
329
+ start|run|launch 启动 Claude Code 或 OpenCode
330
+ -v|--version 显示版本号
331
+ install|add|i 安装 skill
332
+ list|ls 列出已安装的 skill
333
+ update [skill] 更新 skill 包
334
+ distill|dist 手动触发蒸馏
335
+ upgrade [path] 升级 flywheel 工具,并更新项目框架
336
+ -h|--help 显示帮助
337
+
338
+ 选项:
339
+ -x, --proxy <url> 使用代理
215
340
 
216
341
  示例:
217
342
  flywheel new my-project
218
- cd my-project && flywheel start
219
- flywheel install req-mining
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
  }
223
351
 
224
352
  switch (command) {
353
+ case '-v':
225
354
  case '--version':
355
+ case '-V':
226
356
  case 'version':
227
- console.log('@vima_tech/flywheel v1.1.0');
357
+ console.log('@vima_tech/flywheel v1.2.0');
358
+ break;
359
+ case 'new':
360
+ case 'create':
361
+ case 'init':
362
+ cmdNew(restArgs).catch((e) => { console.error(e); process.exit(1); });
363
+ break;
364
+ case 'start':
365
+ case 'run':
366
+ case 'launch':
367
+ cmdStart();
228
368
  break;
369
+ case '-h':
370
+ case '--help':
371
+ case '-?':
229
372
  case 'help':
373
+ case 'h':
230
374
  console.log(`
231
375
  Flywheel CLI — 自成长 AI Agent 飞轮框架
232
376
 
233
377
  用法: flywheel <command> [args]
234
378
 
235
379
  可用命令:
236
- new <name> 创建新项目
237
- start 启动 Claude Code 或 OpenCode
238
- version 显示版本号
239
- update 更新 flywheel 到最新版本
240
- install <skill> 安装 skill 包
241
- list 列出已安装的 skill
242
- update [skill] 更新 skill 包
243
- distill [project] 手动触发蒸馏
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> 使用代理
244
392
 
245
393
  示例:
246
- flywheel new my-project --agent claude --skill req-mining
247
- cd my-project && flywheel start
248
- flywheel install req-mining
394
+ flywheel new my-project
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
249
400
  `);
250
401
  break;
251
402
  case 'install':
252
- runScript(resolveScript('flywheel-install'), ['add', ...args]).catch((e) => process.exit(1));
403
+ case 'add':
404
+ case 'i':
405
+ runScript(resolveScript('flywheel-install'), ['add', ...restArgs]).catch((e) => process.exit(1));
253
406
  break;
254
407
  case 'list':
408
+ case 'ls':
255
409
  runScript(resolveScript('flywheel-install'), ['list']).catch((e) => process.exit(1));
256
410
  break;
257
411
  case 'update':
258
- runScript(resolveScript('flywheel-install'), ['update', ...args]).catch((e) => process.exit(1));
412
+ runScript(resolveScript('flywheel-install'), ['update', ...restArgs]).catch((e) => process.exit(1));
259
413
  break;
260
414
  case 'distill':
261
- runScript(resolveScript('auto-distill'), args).catch((e) => process.exit(1));
415
+ case 'dist':
416
+ runScript(resolveScript('auto-distill'), restArgs).catch((e) => process.exit(1));
262
417
  break;
263
- case 'update':
264
- spawn('npm', ['install', '-g', '@vima_tech/flywheel'], {
265
- cwd: process.cwd(),
266
- stdio: 'inherit',
267
- }).on('exit', (code) => process.exit(code));
418
+ case 'upgrade':
419
+ cmdUpgrade(restArgs).catch((e) => { console.error(e); process.exit(1); });
268
420
  break;
269
421
  default:
270
422
  console.error('未知命令: ' + command);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vima_tech/flywheel",
3
- "version": "1.1.0",
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