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 +4 -1
- package/bin/imsflow.mjs +80 -4
- package/package.json +1 -1
- package/scan.mjs +30 -12
- package/template.html +13 -0
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
|
|
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
|
-
|
|
12
|
-
|
|
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
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('
|
|
545
|
-
console.error('
|
|
546
|
-
console.error(' ' +
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
697
|
-
refresh(
|
|
698
|
-
console.log(`[roots:${action}] ${raw} · 现在 ${
|
|
699
|
-
sendJson(200, { ok: true, 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: '
|
|
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
|
-
|
|
721
|
-
|
|
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');
|