ims-flow-dashboard 1.2.0 → 1.2.2

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/README.md CHANGED
@@ -53,11 +53,14 @@ imsflow --open
53
53
 
54
54
  | 命令 | 说明 |
55
55
  |---|---|
56
- | `imsflow` | 生成 dashboard.html |
56
+ | `imsflow` | 生成 dashboard.html(零配置时自动转服务模式) |
57
57
  | `imsflow --open` | 生成并打开浏览器 |
58
58
  | `imsflow serve` | 启动本地服务(默认端口 17771) |
59
59
  | `imsflow serve --port=17772` | 指定端口 |
60
60
  | `imsflow <过滤词>` | 按名称/路径模糊匹配工作区根 |
61
+ | `imsflow --version` / `-v` | 查看版本(本地 + npm 最新比对) |
62
+ | `imsflow --update` | 更新到 npm 最新版(源码模式给出 git pull 指引) |
63
+ | `imsflow --uninstall` | 卸载命令(npm 模式 `npm uninstall -g`;源码模式清理 shell profile;`~/.imsflow` 配置保留) |
61
64
  | `imsflow --help` | 帮助 |
62
65
 
63
66
  ## 配置
package/bin/imsflow.mjs CHANGED
@@ -2,21 +2,48 @@
2
2
  import { spawn } from 'node:child_process';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import * as path from 'node:path';
5
+ import * as fs from 'node:fs';
5
6
 
6
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- const scanScript = path.join(__dirname, '..', 'scan.mjs');
8
+ const pkgRoot = path.join(__dirname, '..');
9
+ const scanScript = path.join(pkgRoot, 'scan.mjs');
8
10
  const nodeExe = process.execPath;
9
11
  const args = process.argv.slice(2);
10
12
 
11
- if (args.includes('--help') || args.includes('-h')) {
12
- console.log(`imsflow IMS Flow 控制台
13
+ // 本地版本(package.json 随包分发, 永远和代码同步)
14
+ const VERSION = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8')).version;
15
+
16
+ // npm 全局安装 = 包体在 node_modules 里; 否则是源码 clone + install.mjs 的 profile 模式
17
+ const isNpmInstall = __dirname.includes(`node_modules${path.sep}`);
18
+
19
+ // Windows 下 npm 是 npm.cmd, 必须走 shell 解析
20
+ const npm = (npmArgs, opts = {}) => {
21
+ const child = spawn('npm', npmArgs, { stdio: 'inherit', shell: true, ...opts });
22
+ child.on('exit', (code) => process.exit(code ?? 0));
23
+ };
24
+
25
+ // 查 npm 远程版本(离线/未发布时静默, 不影响本地版本显示)
26
+ function remoteVersion() {
27
+ return new Promise((resolve) => {
28
+ const child = spawn('npm', ['view', 'ims-flow-dashboard', 'version'], { shell: true });
29
+ let out = '';
30
+ child.stdout.on('data', (c) => { out += c; });
31
+ child.on('error', () => resolve(null));
32
+ child.on('exit', () => resolve(out.trim() || null));
33
+ setTimeout(() => { try { child.kill(); } catch {} resolve(null); }, 8000); // 网络差时别卡住
34
+ });
35
+ }
36
+
37
+ const HELP = `imsflow — IMS Flow 控制台 (v${VERSION})
13
38
 
14
39
  用法:
15
40
  imsflow [serve] [过滤词] [--open] [--port=N]
41
+ imsflow --version | --update | --uninstall
16
42
  imsflow --help
17
43
 
18
44
  模式:
19
45
  快照模式 (默认) 生成单文件 dashboard.html,--open 自动打开浏览器
46
+ 零配置时自动转服务模式(页面「工作区」面板里添加扫描根)
20
47
  服务模式 (serve) 本地服务,页面内「重新扫描」按钮生效
21
48
  imsflow serve [--port=17771]
22
49
 
@@ -25,13 +52,62 @@ if (args.includes('--help') || args.includes('-h')) {
25
52
  <过滤词> 按名称/路径模糊匹配工作区根
26
53
  --open 生成后自动打开浏览器
27
54
  --port=<N> serve 模式端口(默认 17771)
55
+ --version, -v 显示版本(本地 + npm 最新)
56
+ --update 更新到 npm 最新版(${isNpmInstall ? 'npm 安装模式' : '当前为源码模式, 见提示'})
57
+ --uninstall 卸载 imsflow 命令并清理
28
58
  --help, -h 显示本帮助
29
59
 
30
60
  配置:
31
61
  projects.json 注册要扫描的工作区根路径(参考 projects.example.json)
32
- `);
62
+ 也可在页面顶栏「工作区」面板里添加, 自动写入配置
63
+ `;
64
+
65
+ // ---------- 子命令分发 ----------
66
+ if (args.includes('--help') || args.includes('-h')) {
67
+ console.log(HELP);
68
+ process.exit(0);
69
+ }
70
+
71
+ if (args.includes('--version') || args.includes('-v') || args.includes('version')) {
72
+ console.log(`imsflow v${VERSION} (${isNpmInstall ? 'npm 全局安装' : '源码模式'})`);
73
+ const remote = await remoteVersion();
74
+ if (remote === null) {
75
+ console.log('npm registry 不可达, 无法比对最新版本');
76
+ } else if (remote === VERSION) {
77
+ console.log(`已是最新版本 (${remote})`);
78
+ } else {
79
+ console.log(`npm 最新版本: ${remote} → 可执行 imsflow --update 更新`);
80
+ }
81
+ process.exit(0);
82
+ }
83
+
84
+ if (args.includes('--update') || args.includes('update')) {
85
+ console.log(`当前版本: v${VERSION} (${isNpmInstall ? 'npm 全局安装' : '源码模式'})`);
86
+ if (!isNpmInstall) {
87
+ console.log('\n源码 clone 模式不走 npm, 更新方式:');
88
+ console.log(' git pull');
89
+ console.log(' node install.mjs # 刷新 profile 里的命令指向');
90
+ process.exit(0);
91
+ }
92
+ console.log('\n正在更新 ims-flow-dashboard@latest ...\n');
93
+ npm(['install', '-g', 'ims-flow-dashboard@latest']);
94
+ // npm 退出码透传后不会走到这
95
+ }
96
+
97
+ if (args.includes('--uninstall') || args.includes('uninstall')) {
98
+ if (!isNpmInstall) {
99
+ // 源码模式: install.mjs 自己会清 profile 标记块
100
+ console.log('源码模式: 清理各 shell profile 中的 imsflow 命令...\n');
101
+ const child = spawn(nodeExe, [path.join(pkgRoot, 'install.mjs'), '--uninstall'], { stdio: 'inherit' });
102
+ child.on('exit', (code) => process.exit(code ?? 0));
103
+ } else {
104
+ console.log('npm 全局安装模式: 执行 npm uninstall -g ims-flow-dashboard ...\n');
105
+ console.log('(配置 ~/.imsflow/projects.json 会保留, 想彻底清理可手动删除该目录)\n');
106
+ npm(['uninstall', '-g', 'ims-flow-dashboard']);
107
+ }
33
108
  process.exit(0);
34
109
  }
35
110
 
111
+ // ---------- 默认: 透传给 scan.mjs ----------
36
112
  const child = spawn(nodeExe, [scanScript, ...args], { stdio: 'inherit' });
37
113
  child.on('exit', (code) => process.exit(code ?? 0));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ims-flow-dashboard",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "IMS Flow 控制台 — 可视化 OpenSpec 变更生命周期、产物完整性与任务进度",
5
5
  "type": "module",
6
6
  "files": [
package/scan.mjs CHANGED
@@ -541,14 +541,19 @@ function loadRoots() {
541
541
  console.error('[warn] projects.json 读取失败:', e.message);
542
542
  }
543
543
  if (roots.length === 0) {
544
- console.error('未配置扫描根目录。请在以下任一位置创建 projects.json (参考 projects.example.json):');
545
- console.error(' ' + path.join(TOOL_DIR, 'projects.json') + ' (源码 clone 模式)');
546
- console.error(' ' + path.join(USER_CONFIG_DIR, 'projects.json') + ' (npm 全局安装模式)');
544
+ console.error('未配置扫描根目录。两种方式配置:');
545
+ console.error(' 1. 直接运行 imsflow(会自动进入服务模式),在页面顶栏「工作区」面板里添加(推荐,自动写入配置)');
546
+ console.error(' 2. 手动创建 ' + PROJECTS_FILE + '(参考 projects.example.json');
547
547
  process.exit(1);
548
548
  }
549
549
  return roots;
550
550
  }
551
551
 
552
+ // 零配置空数据:serve 允许「先打开页面、再在面板里加根」,空态是合法状态而非错误
553
+ function emptyData() {
554
+ return { generatedAt: new Date().toISOString(), roots: [], workspaces: [] };
555
+ }
556
+
552
557
  function filterRoots(roots, filterWord) {
553
558
  if (!filterWord) return roots;
554
559
  const w = filterWord.toLowerCase();
@@ -641,10 +646,16 @@ function buildHtml(data) {
641
646
  const args = process.argv.slice(2);
642
647
  const wantOpen = args.includes('--open');
643
648
  const positional = args.filter(a => !a.startsWith('--'));
644
- const serveMode = positional.includes('serve');
649
+ let serveMode = positional.includes('serve');
645
650
  // 非 flag 位置参数(除 serve 外)= 根路径过滤词(按名称或路径模糊匹配)
646
651
  const filterWord = positional.filter(a => a !== 'serve').pop() || null;
647
652
 
653
+ // 零配置: 快照页面只读、无法在页面上添加根 → 自动转服务模式(页面「工作区」面板可添加, 添加即持久化)
654
+ if (!serveMode && safeLoadRoots().length === 0) {
655
+ console.log('[info] 未配置扫描根目录 → 自动进入服务模式,打开页面后在顶栏「工作区」面板中添加扫描根');
656
+ serveMode = true;
657
+ }
658
+
648
659
  if (serveMode) {
649
660
  // ---- 服务模式: imsflow serve [--port=N] ----
650
661
  // 页面内「重新扫描」按钮调 /api/rescan 实时重扫, 解决快照模式的刷新问题
@@ -687,16 +698,16 @@ if (serveMode) {
687
698
  saveRoots(roots);
688
699
  } else if (action === 'remove') {
689
700
  if (!roots.some(r => samePath(r, raw))) { sendJson(400, { ok: false, error: '该路径不在扫描列表中' }); return; }
690
- if (roots.length <= 1) { sendJson(400, { ok: false, error: '至少保留一个扫描根,移除后列表为空会导致服务无内容可扫' }); return; }
701
+ // 允许删到 0:空态是合法状态,页面上还能继续添加(面板始终可用)
691
702
  saveRoots(roots.filter(r => !samePath(r, raw)));
692
703
  } else {
693
704
  sendJson(400, { ok: false, error: 'action 必须是 add 或 remove' });
694
705
  return;
695
706
  }
696
- const data = scanAll(null);
697
- refresh(data);
698
- console.log(`[roots:${action}] ${raw} · 现在 ${data.workspaces.length} 个工作区`);
699
- sendJson(200, { ok: true, roots: data.roots });
707
+ const configured = safeLoadRoots();
708
+ refresh(configured.length > 0 ? scanAll(null, configured) : emptyData());
709
+ console.log(`[roots:${action}] ${raw} · 现在 ${lastData.workspaces.length} 个工作区`);
710
+ sendJson(200, { ok: true, roots: configured });
700
711
  } else if (url === '/api/rescan') {
701
712
  // GET = 全量重扫(页面路径筛选器负责聚焦); POST {roots:[...]} = 只扫勾选的根, 不落盘
702
713
  let subset = null;
@@ -706,7 +717,7 @@ if (serveMode) {
706
717
  subset = [...new Set((Array.isArray(body.roots) ? body.roots : []).filter(r => configured.some(c => samePath(c, String(r)))))];
707
718
  if (subset.length === 0) { sendJson(400, { ok: false, error: '未选择任何已配置的扫描根' }); return; }
708
719
  } else if (safeLoadRoots().length === 0) {
709
- sendJson(500, { ok: false, error: 'projects.json 未配置任何扫描根' });
720
+ sendJson(500, { ok: false, error: '未配置扫描根,请在顶栏「工作区」面板添加' });
710
721
  return;
711
722
  }
712
723
  const t0 = Date.now();
@@ -717,8 +728,15 @@ if (serveMode) {
717
728
  res.end(JSON.stringify(data));
718
729
  } else if (url === '/' || url === '/index.html') {
719
730
  if (!cachedHtml) {
720
- refresh(scanAll(null));
721
- console.log(`初始扫描完成, 页面 ${(cachedHtml.length / 1024).toFixed(0)} KB`);
731
+ const configured = safeLoadRoots();
732
+ if (configured.length > 0) {
733
+ refresh(scanAll(null, configured));
734
+ console.log(`初始扫描完成, 页面 ${(cachedHtml.length / 1024).toFixed(0)} KB`);
735
+ } else {
736
+ // 零配置: 渲染空态页面, 让用户在「工作区」面板里添加根(不能在这里 exit —— 会杀掉整个 serve)
737
+ refresh(emptyData());
738
+ console.log('未配置扫描根, 页面显示空态(配置文件: ' + PROJECTS_FILE + ')');
739
+ }
722
740
  } else if (templateMtime() !== cachedTpl && lastData) {
723
741
  // 模板文件改过了 → 用上次扫描数据重建(不必重扫),刷新页面即可看到新 UI
724
742
  refresh(lastData);
package/template.html CHANGED
@@ -1031,6 +1031,13 @@ const EMPTY_HTML = '<div class="placeholder">'
1031
1031
  + '<div class="ph-2">生命周期 · 产物清单 · 任务状态 · 产物全文</div>'
1032
1032
  + '</div>';
1033
1033
 
1034
+ /* 零配置引导空态 —— serve 模式且没有任何扫描根时显示(首次安装的典型状态) */
1035
+ const NO_ROOT_HTML = '<div class="placeholder">'
1036
+ + '<svg class="ph-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 7a2 2 0 0 1 2-2h4l2 2.5h7a2 2 0 0 1 2 2V17a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2Z"/><path d="M12 11v5M9.5 13.5h5"/></svg>'
1037
+ + '<div class="ph-1">未配置扫描根目录</div>'
1038
+ + '<div class="ph-2">在右侧弹出的「工作区」面板中粘贴目录路径并添加,配置会自动保存</div>'
1039
+ + '</div>';
1040
+
1034
1041
  /* ---------- 轻量 markdown 渲染 ---------- */
1035
1042
  function renderMd(src) {
1036
1043
  if (!src) return '<div class="empty-panel">(空文件)</div>';
@@ -1524,6 +1531,12 @@ function resetView() {
1524
1531
  };
1525
1532
  updateStats();
1526
1533
  renderSidebar();
1534
+ // 零配置(首次安装典型状态): serve 模式 + 无任何扫描根 → 引导空态 + 自动弹出「工作区」面板
1535
+ if (/^https?:$/.test(location.protocol) && (DATA.workspaces || []).length === 0 && (DATA.roots || []).length === 0) {
1536
+ document.getElementById('main').innerHTML = NO_ROOT_HTML;
1537
+ const b = document.getElementById('wsBtn');
1538
+ if (b) setTimeout(() => { if (!b.disabled) b.click(); }, 150); // 等面板事件绑定完成后再弹
1539
+ }
1527
1540
  // 恢复重扫前选中的变更(serve 模式整页重载后从 sessionStorage 找回)
1528
1541
  try {
1529
1542
  const savedSel = sessionStorage.getItem('imsflow.sel');