@seeed-studio/solution-cli 0.3.1 → 0.4.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/README.md +157 -104
- package/bin/cli.js +410 -58
- package/bin/runner.js +1 -1
- package/catalog/client-config.json +2 -2
- package/catalog/knowledge-core.json +58452 -488
- package/docs/cli-guide.md +173 -200
- package/lib/agent.js +74 -15
- package/lib/catalog-dto.js +14 -1
- package/lib/detector.js +3 -3
- package/lib/doctor.js +19 -1
- package/lib/downloader.js +24 -0
- package/lib/gotchas.js +126 -0
- package/lib/installer.js +113 -19
- package/lib/integration.js +265 -0
- package/lib/knowledge.js +838 -100
- package/lib/ledger.js +1 -1
- package/package.json +3 -6
- package/skills/{sensecraft → solution-cli}/SKILL.md +125 -30
- package/skills/{sensecraft → solution-cli}/references/consult-and-select.md +61 -13
- package/skills/solution-cli/references/deploy.md +112 -0
- package/skills/solution-cli/references/device-management.md +298 -0
- package/skills/{sensecraft → solution-cli}/references/diagnose.md +15 -0
- package/skills/solution-cli/references/edge-gotchas.md +205 -0
- package/skills/solution-cli/references/generic-rk3588.md +106 -0
- package/skills/{sensecraft → solution-cli}/references/hardware-bom.md +9 -3
- package/skills/solution-cli/references/sales-feasibility.md +65 -0
- package/skills/{sensecraft → solution-cli}/references/secondary-development.md +10 -7
- package/skills/sensecraft/references/deploy.md +0 -300
- package/skills/sensecraft/references/device-management.md +0 -126
- /package/skills/{sensecraft → solution-cli}/references/app-only.md +0 -0
- /package/skills/{sensecraft → solution-cli}/references/error-catalog.md +0 -0
- /package/skills/{sensecraft → solution-cli}/references/offline.md +0 -0
- /package/skills/{sensecraft → solution-cli}/references/verify.md +0 -0
package/lib/detector.js
CHANGED
|
@@ -226,7 +226,7 @@ export function detectDesktopApp() {
|
|
|
226
226
|
appPath: appBase,
|
|
227
227
|
binPath: path.join(appBase, 'Contents', 'MacOS', 'sensecraft-engine'),
|
|
228
228
|
solutionsPath: path.join(appBase, 'Contents', 'Resources', '_up_', 'sensecraft-solutions', 'solutions'),
|
|
229
|
-
skillsPath: path.join(appBase, 'Contents', 'Resources', '_up_', 'sensecraft-solutions', 'skills', '
|
|
229
|
+
skillsPath: path.join(appBase, 'Contents', 'Resources', '_up_', 'sensecraft-solutions', 'skills', 'solution-cli')
|
|
230
230
|
});
|
|
231
231
|
const cacheBin = path.join(
|
|
232
232
|
os.homedir(),
|
|
@@ -258,7 +258,7 @@ export function detectDesktopApp() {
|
|
|
258
258
|
appPath,
|
|
259
259
|
binPath: path.join(appPath, 'resources', 'bin', 'sensecraft-engine.exe'),
|
|
260
260
|
solutionsPath: path.join(appPath, 'resources', 'sensecraft-solutions', 'solutions'),
|
|
261
|
-
skillsPath: path.join(appPath, 'resources', 'sensecraft-solutions', 'skills', '
|
|
261
|
+
skillsPath: path.join(appPath, 'resources', 'sensecraft-solutions', 'skills', 'solution-cli')
|
|
262
262
|
});
|
|
263
263
|
}
|
|
264
264
|
}
|
|
@@ -271,7 +271,7 @@ export function detectDesktopApp() {
|
|
|
271
271
|
appPath,
|
|
272
272
|
binPath: path.join(appPath, 'sensecraft-engine'),
|
|
273
273
|
solutionsPath: path.join(appPath, 'sensecraft-solutions', 'solutions'),
|
|
274
|
-
skillsPath: path.join(appPath, 'skills', '
|
|
274
|
+
skillsPath: path.join(appPath, 'skills', 'solution-cli')
|
|
275
275
|
});
|
|
276
276
|
}
|
|
277
277
|
}
|
package/lib/doctor.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import { getPlatformInfo, getEngineBinDir, getEngineBinaryPath, getConfiguredEnginePath, getContentDir } from './platform.js';
|
|
6
|
-
import { verifyEngineBinary } from './detector.js';
|
|
6
|
+
import { verifyEngineBinary, detectDesktopApp } from './detector.js';
|
|
7
7
|
import { loadLedger, computeFileHash } from './ledger.js';
|
|
8
8
|
|
|
9
9
|
export function runDoctor() {
|
|
@@ -166,6 +166,24 @@ export function runDoctor() {
|
|
|
166
166
|
});
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
// Check 8: Desktop App (GUI)
|
|
170
|
+
const app = detectDesktopApp();
|
|
171
|
+
if (app.detected) {
|
|
172
|
+
checks.push({
|
|
173
|
+
id: 'desktop_app',
|
|
174
|
+
status: 'ok',
|
|
175
|
+
title: 'SenseCraft Solution Desktop App (GUI)',
|
|
176
|
+
detail: `Detected at ${app.appPath || app.binPath} (v${app.version || 'installed'})`
|
|
177
|
+
});
|
|
178
|
+
} else {
|
|
179
|
+
checks.push({
|
|
180
|
+
id: 'desktop_app',
|
|
181
|
+
status: 'ok',
|
|
182
|
+
title: 'SenseCraft Solution Desktop App (GUI)',
|
|
183
|
+
detail: 'Not detected (optional). For visual dashboard & GUI workflows, download at https://sensecraft.seeed.cc/download'
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
169
187
|
const overall = checks.some(c => c.status === 'fail')
|
|
170
188
|
? 'fail'
|
|
171
189
|
: checks.some(c => c.status === 'warn')
|
package/lib/downloader.js
CHANGED
|
@@ -281,3 +281,27 @@ export function pullSolutionContent(binPath, onProgress = () => {}) {
|
|
|
281
281
|
return { success: false, warning: msg };
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
|
+
|
|
285
|
+
export function updateSolutionContent(binPath, onProgress = () => {}) {
|
|
286
|
+
const contentDir = path.join(getScHome(), 'content', 'solutions');
|
|
287
|
+
const isEmptyStore = !fs.existsSync(contentDir) || fs.readdirSync(contentDir).filter(f => !f.startsWith('.')).length === 0;
|
|
288
|
+
|
|
289
|
+
if (isEmptyStore) {
|
|
290
|
+
onProgress('Initial solution content store is empty; performing full initial pull...');
|
|
291
|
+
return pullSolutionContent(binPath, onProgress);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
onProgress('Updating official solution definitions from CDN (sensecraft-engine content update)...');
|
|
295
|
+
try {
|
|
296
|
+
execFileSync(binPath, ['content', 'update'], {
|
|
297
|
+
encoding: 'utf8',
|
|
298
|
+
timeout: 120000,
|
|
299
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
300
|
+
});
|
|
301
|
+
onProgress('Solution content successfully updated to latest.');
|
|
302
|
+
return { success: true };
|
|
303
|
+
} catch (err) {
|
|
304
|
+
onProgress('Notice: incremental update fell back to full pull.');
|
|
305
|
+
return pullSolutionContent(binPath, onProgress);
|
|
306
|
+
}
|
|
307
|
+
}
|
package/lib/gotchas.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file gotchas.js
|
|
3
|
+
* Edge hardware & chip troubleshooting knowledge base for Solution CLI.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const GOTCHAS_DATABASE = [
|
|
7
|
+
{
|
|
8
|
+
id: 'rk3588_npu_device_missing',
|
|
9
|
+
chip: 'rk3588',
|
|
10
|
+
title: 'RK3588/RK3576 NPU 设备节点缺失或无权访问 (/dev/rknn)',
|
|
11
|
+
keywords: ['rk3588', 'rk3576', 'rknn', '/dev/rknn', 'npu', 'permission denied', 'no such file'],
|
|
12
|
+
symptoms: '容器内运行推理时报错 "cannot open /dev/rknn" 或 "Permission denied"',
|
|
13
|
+
root_cause: '宿主机未加载 rknpu 内核模块,或者 Docker 容器启动时未正确挂载 /dev/rknn 设备节点及配置 udev 权限。',
|
|
14
|
+
solution: [
|
|
15
|
+
'1. 检查宿主机内核节点:ls -l /dev/rknn (若不存在,执行 sudo modprobe rknpu)',
|
|
16
|
+
'2. 赋予设备访问权限:sudo chmod 666 /dev/rknn',
|
|
17
|
+
'3. Docker 启动或 Compose 配置中必须显式透传设备:',
|
|
18
|
+
' devices:\n - /dev/rknn:/dev/rknn\n - /dev/dma_heap:/dev/dma_heap',
|
|
19
|
+
'4. 检查驱动日志:dmesg | grep -i rknpu'
|
|
20
|
+
],
|
|
21
|
+
forbidden: '不要在容器内尝试执行 apt install 重新安装原厂内核 BSP,底层驱动必须依赖宿主机内核。'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'rk3588_docker_nftables_conflict',
|
|
25
|
+
chip: 'rk3588',
|
|
26
|
+
title: 'Rockchip Ubuntu Docker 网络互通死锁 (iptables-legacy 冲突)',
|
|
27
|
+
keywords: ['rk3588', 'rk3576', 'docker', 'iptables', 'nftables', 'network', 'bridge'],
|
|
28
|
+
symptoms: 'Docker 服务启动失败,或容器内无法通过桥接网关访问外部网络与外设流。',
|
|
29
|
+
root_cause: 'Rockchip 定制 Linux 6.1 内核对 nftables 的支持不完备,而较新版 Docker 默认尝试使用 iptables-nft。',
|
|
30
|
+
solution: [
|
|
31
|
+
'1. 切换系统默认 iptables 为 legacy 模式:',
|
|
32
|
+
' sudo update-alternatives --set iptables /usr/sbin/iptables-legacy',
|
|
33
|
+
' sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy',
|
|
34
|
+
'2. 重启 Docker 守护进程:sudo systemctl restart docker'
|
|
35
|
+
],
|
|
36
|
+
forbidden: '切勿卸载 iptables 软件包或随意清空 iptables -F,这会导致 Docker 默认 NAT 转发链彻底损坏。'
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'recamera_vpss_overflow',
|
|
40
|
+
chip: 'recamera',
|
|
41
|
+
title: 'reCamera (SG2002 / RV1126B) 图像管道溢出 (VPSS FIFO overflow)',
|
|
42
|
+
keywords: ['recamera', 'vpss', 'fifo', 'overflow', 'sg2002', 'rv1126b', 'isp', 'vi'],
|
|
43
|
+
symptoms: '视频流卡死、黑屏,dmesg 频繁刷新 "VPSS FIFO overflow" 或 "VI drop frame"',
|
|
44
|
+
root_cause: '推理进程处理帧率低于摄像头输入帧率,导致硬件 ISP/VPSS 环形缓冲区被填满并阻塞硬件通道。',
|
|
45
|
+
solution: [
|
|
46
|
+
'1. 在 rkipc.ini 或流媒体配置中适当降低码流分辨率(从 1440P 降至 1080P 或 720P)',
|
|
47
|
+
'2. 限制推理帧率(如设置 skip_frames: 2,每秒处理 10~15 帧)',
|
|
48
|
+
'3. 检查模型加载是否启用了硬件 NPU 加速(.cvimodel / .rknn),避免 CPU 软解码'
|
|
49
|
+
],
|
|
50
|
+
forbidden: '不要在视频流已卡死时直接硬重启机器,应先优雅杀掉流媒体服务以释放 VI/VPSS 通道。'
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'recamera_process_kill_safety',
|
|
54
|
+
chip: 'recamera',
|
|
55
|
+
title: 'reCamera 服务热重启安全规约:严禁 while true 且必须 sleep 3',
|
|
56
|
+
keywords: ['recamera', 'restart', 'fork bomb', 'sleep 3', 'pgrep', 'kill', 'watchdog'],
|
|
57
|
+
symptoms: '修改二进制或配置后设备网络全断,SSH/HTTP 无法连接,只能拔电硬重启。',
|
|
58
|
+
root_cause: '1. 硬件 VPSS/VI 资源被旧进程锁定未完全释放即启动新进程;2. 监控脚本使用死循环在二进制闪退时触发 Fork 风暴耗尽内核 PID。',
|
|
59
|
+
solution: [
|
|
60
|
+
'1. 优雅停止进程:for p in $(pgrep -f rkipc); do kill -9 $p; done',
|
|
61
|
+
'2. 关键安全延迟:必须等待 3 秒释放硬件通道:sleep 3',
|
|
62
|
+
'3. 安全拉起新服务:/userdata/rkipc.patch -c /userdata/rkipc.ini &'
|
|
63
|
+
],
|
|
64
|
+
forbidden: '严禁在脚本中使用 while true; do /path/app; sleep 1; done 守护!一旦二进制参数错误快速退出,会导致系统瘫痪。'
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'jetson_nvidia_runtime_missing',
|
|
68
|
+
chip: 'jetson',
|
|
69
|
+
title: 'Jetson Orin (J4012/J2012) 容器缺失 NVIDIA Container Runtime',
|
|
70
|
+
keywords: ['jetson', 'orin', 'j4012', 'j4011', 'j2012', 'cuda', 'tensorrt', 'nvidia runtime'],
|
|
71
|
+
symptoms: 'Docker 容器启动报 "CUDA driver version is insufficient" 或无法找到 /dev/nvhost-* 设备',
|
|
72
|
+
root_cause: 'Docker 守护进程未配置默认 nvidia runtime,或运行 docker compose 时未指定 runtime: nvidia。',
|
|
73
|
+
solution: [
|
|
74
|
+
'1. 检查 /etc/docker/daemon.json 是否包含 "default-runtime": "nvidia"',
|
|
75
|
+
'2. 运行单个容器时添加参数:docker run --runtime nvidia ...',
|
|
76
|
+
'3. Compose 配置中加入:\n runtime: nvidia\n environment:\n - NVIDIA_VISIBLE_DEVICES=all\n - NVIDIA_DRIVER_CAPABILITIES=all'
|
|
77
|
+
],
|
|
78
|
+
forbidden: '不要在 Jetson 容器内盲目安装标准 x86 的 NVIDIA 官方显卡驱动,Jetson 采用系统级统一内存 L4T BSP。'
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'hailo_driver_version_mismatch',
|
|
82
|
+
chip: 'hailo',
|
|
83
|
+
title: 'reComputer R2000 / RPi Hailo-8 驱动与运行库版本不匹配',
|
|
84
|
+
keywords: ['hailo', 'hailo8', 'libhailort', 'hailortcli', 'version mismatch', 'r2000'],
|
|
85
|
+
symptoms: '容器或应用启动报 "libhailort.so.4.21.0 not found" 或 "Driver version mismatch"',
|
|
86
|
+
root_cause: 'HailoRT 的内核驱动 (PCIe Driver)、用户态动态库 (libhailort) 和 GStreamer 插件版本必须严格完全一致(如均为 4.21.0)。',
|
|
87
|
+
solution: [
|
|
88
|
+
'1. 检查当前内核驱动状态:hailortcli fw-control identify',
|
|
89
|
+
'2. 检查动态库版本:ls -l /usr/lib/libhailort.so*',
|
|
90
|
+
'3. 确保宿主机与容器内镜像使用的 HailoRT 版本一致(官方套件均已固定为 4.21.0)'
|
|
91
|
+
],
|
|
92
|
+
forbidden: '不要在已有应用正在占用 /dev/hailo0 时启动第二个推理容器,Hailo-8 默认排他独占访问。'
|
|
93
|
+
}
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Searches the gotchas troubleshooting database.
|
|
98
|
+
*
|
|
99
|
+
* @param {Object} options
|
|
100
|
+
* @param {string} [options.query] - Search term
|
|
101
|
+
* @param {string} [options.chip] - Target chip/platform (rk3588, recamera, jetson, hailo)
|
|
102
|
+
* @returns {Array<Object>} Matched gotchas records
|
|
103
|
+
*/
|
|
104
|
+
export function searchGotchas({ query = '', chip = '' } = {}) {
|
|
105
|
+
const q = String(query || '').trim().toLowerCase();
|
|
106
|
+
const c = String(chip || '').trim().toLowerCase();
|
|
107
|
+
|
|
108
|
+
return GOTCHAS_DATABASE.filter((item) => {
|
|
109
|
+
if (c && !item.chip.toLowerCase().includes(c)) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (!q) return true;
|
|
113
|
+
const tokens = q.split(/\s+/).filter(Boolean);
|
|
114
|
+
const haystack = [
|
|
115
|
+
item.id,
|
|
116
|
+
item.chip,
|
|
117
|
+
item.title,
|
|
118
|
+
item.symptoms,
|
|
119
|
+
item.root_cause,
|
|
120
|
+
...(item.keywords || []),
|
|
121
|
+
...(Array.isArray(item.solution) ? item.solution : [item.solution]),
|
|
122
|
+
].join(' ').toLowerCase();
|
|
123
|
+
|
|
124
|
+
return tokens.every((t) => haystack.includes(t));
|
|
125
|
+
});
|
|
126
|
+
}
|
package/lib/installer.js
CHANGED
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
getConfiguredEnginePath
|
|
10
10
|
} from './platform.js';
|
|
11
11
|
import { detectExistingEngine, detectDesktopApp, verifyEngineBinary, compareSemver } from './detector.js';
|
|
12
|
-
import { downloadEngine, fetchManifest, validateReleaseManifest, pullSolutionContent } from './downloader.js';
|
|
13
|
-
import { scanLocalAgents, promptAgentSelection } from './agent.js';
|
|
12
|
+
import { downloadEngine, fetchManifest, validateReleaseManifest, pullSolutionContent, updateSolutionContent } from './downloader.js';
|
|
13
|
+
import { getKnownAgents, scanLocalAgents, promptAgentSelection } from './agent.js';
|
|
14
14
|
import {
|
|
15
15
|
recordInstallation,
|
|
16
16
|
removeInstallation,
|
|
@@ -22,21 +22,47 @@ import {
|
|
|
22
22
|
|
|
23
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
24
24
|
const __dirname = path.dirname(__filename);
|
|
25
|
-
const
|
|
25
|
+
const SKILL_DIR_NAME = 'solution-cli';
|
|
26
|
+
const LEGACY_SKILL_DIR_NAME = 'sensecraft';
|
|
27
|
+
const BUNDLED_SKILLS_SRC = path.resolve(__dirname, '..', 'skills', SKILL_DIR_NAME);
|
|
28
|
+
|
|
29
|
+
function assertCanonicalSkillState(ledger, agents) {
|
|
30
|
+
const allAgents = agents;
|
|
31
|
+
const errors = [];
|
|
32
|
+
|
|
33
|
+
for (const agent of allAgents) {
|
|
34
|
+
const skillsDir = agent.skillsDir || agent.getSkillsDir();
|
|
35
|
+
const expectedTarget = path.resolve(path.join(skillsDir, SKILL_DIR_NAME));
|
|
36
|
+
const legacySkill = path.join(skillsDir, LEGACY_SKILL_DIR_NAME, 'SKILL.md');
|
|
37
|
+
if (fs.lstatSync(legacySkill, { throwIfNoEntry: false })) {
|
|
38
|
+
errors.push(`${agent.id} still has legacy skill at ${legacySkill}; run uninstall --agent ${agent.id} before installing solution-cli. If the file is untracked or remains after uninstall because it was modified, back it up and retire the legacy skill directory before retrying; user files are not removed automatically.`);
|
|
39
|
+
}
|
|
40
|
+
const record = ledger.installations?.[agent.id];
|
|
41
|
+
if (record && path.resolve(record.target_dir) !== expectedTarget) {
|
|
42
|
+
errors.push(`${agent.id} is managed at legacy target ${record.target_dir}; run uninstall --agent ${agent.id} before installing solution-cli`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (errors.length > 0) {
|
|
47
|
+
throw new Error(`Legacy SenseCraft skill state blocks canonical solution-cli installation:\n- ${errors.join('\n- ')}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
26
50
|
|
|
27
51
|
export async function runInstall(options = {}) {
|
|
28
52
|
const {
|
|
29
53
|
agent = null,
|
|
54
|
+
skillsDir = null,
|
|
30
55
|
yes = false,
|
|
31
56
|
dryRun = false,
|
|
32
57
|
skipContent = false,
|
|
33
58
|
force = false,
|
|
34
59
|
enginePath = null,
|
|
60
|
+
isUpdate = false,
|
|
35
61
|
logger = console.log
|
|
36
62
|
} = options;
|
|
37
63
|
|
|
38
64
|
logger('\x1b[1m\x1b[34m====================================================\x1b[0m');
|
|
39
|
-
logger(
|
|
65
|
+
logger(`\x1b[1m\x1b[34m Solution CLI ${isUpdate ? 'Updater' : 'Installer'} \x1b[0m`);
|
|
40
66
|
logger('\x1b[1m\x1b[34m====================================================\x1b[0m\n');
|
|
41
67
|
|
|
42
68
|
const platform = getPlatformInfo();
|
|
@@ -46,13 +72,16 @@ export async function runInstall(options = {}) {
|
|
|
46
72
|
|
|
47
73
|
// --- Step 0: Pre-validate agent targets before ANY download/side-effects (Codex Medium 11) ---
|
|
48
74
|
const scanned = scanLocalAgents();
|
|
49
|
-
const selectedAgents = await promptAgentSelection(scanned, agent, yes);
|
|
75
|
+
const selectedAgents = await promptAgentSelection(scanned, agent, yes, skillsDir);
|
|
50
76
|
|
|
51
77
|
if (selectedAgents.length === 0) {
|
|
52
|
-
logger('\nNo agents selected.
|
|
78
|
+
logger('\nNo agents selected. Operation halted with no filesystem modifications.');
|
|
53
79
|
return { success: true, engineBin: null, installedAgents: [] };
|
|
54
80
|
}
|
|
55
81
|
|
|
82
|
+
// Validate ownership and legacy targets before engine selection or pointer/download writes.
|
|
83
|
+
assertCanonicalSkillState(loadLedger(), selectedAgents);
|
|
84
|
+
|
|
56
85
|
// --- Step 1: Detect & Verify Engine Candidate ---
|
|
57
86
|
logger('🔍 Phase 1: Inspecting host environment for existing Engine/App...');
|
|
58
87
|
let engineBin = null;
|
|
@@ -120,8 +149,13 @@ export async function runInstall(options = {}) {
|
|
|
120
149
|
|
|
121
150
|
// --- Step 2: Content Cache ---
|
|
122
151
|
if (!skipContent && !dryRun) {
|
|
123
|
-
|
|
124
|
-
|
|
152
|
+
if (isUpdate) {
|
|
153
|
+
logger('\n🔄 Phase 2: Updating IoT solutions catalog to latest...');
|
|
154
|
+
updateSolutionContent(engineBin, msg => logger(` ${msg}`));
|
|
155
|
+
} else {
|
|
156
|
+
logger('\n🔄 Phase 2: Ensuring IoT solutions catalog is populated...');
|
|
157
|
+
pullSolutionContent(engineBin, msg => logger(` ${msg}`));
|
|
158
|
+
}
|
|
125
159
|
} else if (skipContent) {
|
|
126
160
|
logger('\n🔄 Phase 2: Skipped solution pull (--skip-content).');
|
|
127
161
|
}
|
|
@@ -135,9 +169,10 @@ export async function runInstall(options = {}) {
|
|
|
135
169
|
|
|
136
170
|
runWithLock(() => {
|
|
137
171
|
const ledger = dryRun ? { installations: {} } : loadLedger();
|
|
172
|
+
assertCanonicalSkillState(ledger, selectedAgents);
|
|
138
173
|
|
|
139
174
|
for (const a of selectedAgents) {
|
|
140
|
-
const destDir = path.resolve(path.join(a.skillsDir,
|
|
175
|
+
const destDir = path.resolve(path.join(a.skillsDir, SKILL_DIR_NAME));
|
|
141
176
|
logger(`\nConfiguring ${a.name} at: ${destDir}...`);
|
|
142
177
|
|
|
143
178
|
if (dryRun) {
|
|
@@ -161,7 +196,7 @@ export async function runInstall(options = {}) {
|
|
|
161
196
|
const preservedStat = fs.lstatSync(preservedSkillPath, { throwIfNoEntry: false });
|
|
162
197
|
if (preservedStat && !preservedStat.isSymbolicLink()) {
|
|
163
198
|
const preservedSkill = fs.readFileSync(preservedSkillPath, 'utf8');
|
|
164
|
-
const oldPathMatch = preservedSkill.match(/\*\*
|
|
199
|
+
const oldPathMatch = preservedSkill.match(/\*\*Solution CLI Runtime\*\*:[\s\S]*?full absolute path: `"([^"]+)"/);
|
|
165
200
|
const normalizePath = value => path.normalize(value.replaceAll('\\', '/'));
|
|
166
201
|
if (oldPathMatch && normalizePath(oldPathMatch[1]) !== normalizePath(engineBin)) {
|
|
167
202
|
logger(` ⚠️ Preserved user-modified SKILL.md still contains its previous Engine path; user text was not rewritten.`);
|
|
@@ -178,9 +213,9 @@ export async function runInstall(options = {}) {
|
|
|
178
213
|
if (skillStat && !skillStat.isSymbolicLink()) {
|
|
179
214
|
let content = fs.readFileSync(skillMdPath, 'utf8');
|
|
180
215
|
const escapedBinPath = engineBin.replace(/\\/g, '/');
|
|
181
|
-
const instructionBlock = `\n> **
|
|
216
|
+
const instructionBlock = `\n> **Solution CLI Runtime**: Engine operations such as \`device\`, \`deploy\`, and \`logs\` must use the full absolute path: \`"${escapedBinPath}" <subcommand>\`. Use \`solution-cli\` for catalog and product queries; if it is not on $PATH, invoke \`npx -y --package=@seeed-studio/solution-cli@latest solution-cli <args>\`.\n\n`;
|
|
182
217
|
|
|
183
|
-
if (!content.includes('
|
|
218
|
+
if (!content.includes('**Solution CLI Runtime**:')) {
|
|
184
219
|
content = content.replace(/(---[\s\S]*?---\n)/, `$1${instructionBlock}`);
|
|
185
220
|
fs.writeFileSync(skillMdPath, content, 'utf8');
|
|
186
221
|
}
|
|
@@ -189,7 +224,7 @@ export async function runInstall(options = {}) {
|
|
|
189
224
|
}
|
|
190
225
|
|
|
191
226
|
// Write installation marker safely without following external symlinks (Codex P2)
|
|
192
|
-
const markerPath = path.join(destDir, '.installed-by-
|
|
227
|
+
const markerPath = path.join(destDir, '.installed-by-solution-cli');
|
|
193
228
|
if (isSafePathInside(markerPath, destDir)) {
|
|
194
229
|
const markerStat = fs.lstatSync(markerPath, { throwIfNoEntry: false });
|
|
195
230
|
if (markerStat && markerStat.isSymbolicLink()) {
|
|
@@ -210,14 +245,17 @@ export async function runInstall(options = {}) {
|
|
|
210
245
|
});
|
|
211
246
|
|
|
212
247
|
logger('\n\x1b[1m\x1b[32m====================================================\x1b[0m');
|
|
213
|
-
logger('\x1b[1m\x1b[32m 🎉
|
|
248
|
+
logger('\x1b[1m\x1b[32m 🎉 Solution CLI Setup Completed Successfully! \x1b[0m');
|
|
214
249
|
logger('\x1b[1m\x1b[32m====================================================\x1b[0m');
|
|
215
250
|
logger(`\n🔧 Engine Binary: ${engineBin}`);
|
|
216
251
|
logger(`🤖 Configured Agents:`);
|
|
217
252
|
installedAgents.forEach(a => logger(` - ${a.name}: ${a.path}`));
|
|
218
|
-
logger('\n👉
|
|
219
|
-
logger('
|
|
220
|
-
logger(' "
|
|
253
|
+
logger('\n👉 [Instructions for AI Agent / 助手后续操作]:');
|
|
254
|
+
logger(' Solution CLI skill is now installed and activated.');
|
|
255
|
+
logger(' - Search IoT solutions: solution-cli hub search "<query>" --json');
|
|
256
|
+
logger(' - Check deployment specs: sensecraft-engine deploy-info <solution-id> --lang zh');
|
|
257
|
+
logger(' - Deploy (Local/Remote): sensecraft-engine deploy --params ./.sensecraft-work/body.json --yes');
|
|
258
|
+
logger(' Please inform the user that the skill is ready for solution selection and deployment.');
|
|
221
259
|
logger('----------------------------------------------------\n');
|
|
222
260
|
|
|
223
261
|
return {
|
|
@@ -234,6 +272,7 @@ export function runStatus(options = {}) {
|
|
|
234
272
|
const engineCheck = verifyEngineBinary(binPath);
|
|
235
273
|
const ledger = loadLedger();
|
|
236
274
|
const scannedAgents = scanLocalAgents();
|
|
275
|
+
const app = detectDesktopApp();
|
|
237
276
|
|
|
238
277
|
const statusObj = {
|
|
239
278
|
engine: {
|
|
@@ -243,6 +282,12 @@ export function runStatus(options = {}) {
|
|
|
243
282
|
valid: engineCheck.valid,
|
|
244
283
|
deployersCount: engineCheck.deployers?.length || 0
|
|
245
284
|
},
|
|
285
|
+
desktop_app: {
|
|
286
|
+
detected: Boolean(app.detected),
|
|
287
|
+
compatible: Boolean(app.compatible),
|
|
288
|
+
appPath: app.appPath || null,
|
|
289
|
+
version: app.version || null
|
|
290
|
+
},
|
|
246
291
|
content: {
|
|
247
292
|
dir: getContentDir(),
|
|
248
293
|
manifestExists: fs.existsSync(path.join(getContentDir(), 'manifest.json'))
|
|
@@ -262,10 +307,16 @@ export function runStatus(options = {}) {
|
|
|
262
307
|
return statusObj;
|
|
263
308
|
}
|
|
264
309
|
|
|
265
|
-
logger('\n📊
|
|
310
|
+
logger('\n📊 Solution CLI System Status:\n');
|
|
266
311
|
logger(` Engine Binary: ${binPath}`);
|
|
267
312
|
logger(` Engine Version: ${engineCheck.version || 'Not installed'} (${engineCheck.valid ? 'Valid' : 'Invalid'})`);
|
|
268
313
|
logger(` Deployers Available: ${engineCheck.deployers?.length || 0}`);
|
|
314
|
+
if (app.detected) {
|
|
315
|
+
logger(` Desktop App (GUI): Detected at ${app.appPath || app.binPath} (v${app.version || 'installed'})`);
|
|
316
|
+
} else {
|
|
317
|
+
logger(' Desktop App (GUI): \x1b[90mNot detected\x1b[0m');
|
|
318
|
+
logger(' 💡 可视化看板与桌面客户端下载: https://sensecraft.seeed.cc/download');
|
|
319
|
+
}
|
|
269
320
|
logger(` Content Directory: ${getContentDir()} (${statusObj.content.manifestExists ? 'Ready' : 'Not cached'})`);
|
|
270
321
|
logger('\n Configured Agent Skills:');
|
|
271
322
|
|
|
@@ -286,7 +337,7 @@ export function runUninstall(options = {}) {
|
|
|
286
337
|
const installedIds = Object.keys(ledger.installations || {});
|
|
287
338
|
|
|
288
339
|
if (installedIds.length === 0) {
|
|
289
|
-
logger('No managed
|
|
340
|
+
logger('No managed Solution CLI skills are currently recorded in the ledger.');
|
|
290
341
|
return;
|
|
291
342
|
}
|
|
292
343
|
|
|
@@ -380,3 +431,46 @@ function copyFolderSafely(srcDir, destDir, baseDestDir, priorFiles, force, logge
|
|
|
380
431
|
|
|
381
432
|
return { filesWritten, updatedFiles, preservedFiles };
|
|
382
433
|
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Runs update on existing installed agents and synchronizes solution content and engine.
|
|
437
|
+
*
|
|
438
|
+
* @param {Object} options
|
|
439
|
+
* @returns {Promise<Object>}
|
|
440
|
+
*/
|
|
441
|
+
export async function runUpdate(options = {}) {
|
|
442
|
+
const logger = options.logger || console.log;
|
|
443
|
+
|
|
444
|
+
let targetAgent = options.agent;
|
|
445
|
+
if (!targetAgent && !options.skillsDir) {
|
|
446
|
+
const scanned = scanLocalAgents();
|
|
447
|
+
const installed = scanned.filter(a => a.installed);
|
|
448
|
+
if (installed.length > 0) {
|
|
449
|
+
targetAgent = installed.map(a => a.id).join(',');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return runInstall({
|
|
454
|
+
...options,
|
|
455
|
+
agent: targetAgent,
|
|
456
|
+
yes: options.yes !== undefined ? options.yes : true,
|
|
457
|
+
isUpdate: true,
|
|
458
|
+
logger,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Returns formatted scan of local AI agents.
|
|
464
|
+
*
|
|
465
|
+
* @returns {Array<Object>}
|
|
466
|
+
*/
|
|
467
|
+
export function listAgents() {
|
|
468
|
+
const scanned = scanLocalAgents();
|
|
469
|
+
return scanned.map(a => ({
|
|
470
|
+
id: a.id,
|
|
471
|
+
name: a.name,
|
|
472
|
+
detected: a.detected,
|
|
473
|
+
installed: a.installed,
|
|
474
|
+
skillsDir: a.skillsDir,
|
|
475
|
+
}));
|
|
476
|
+
}
|