@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.
Files changed (33) hide show
  1. package/README.md +157 -104
  2. package/bin/cli.js +410 -58
  3. package/bin/runner.js +1 -1
  4. package/catalog/client-config.json +2 -2
  5. package/catalog/knowledge-core.json +58452 -488
  6. package/docs/cli-guide.md +173 -200
  7. package/lib/agent.js +74 -15
  8. package/lib/catalog-dto.js +14 -1
  9. package/lib/detector.js +3 -3
  10. package/lib/doctor.js +19 -1
  11. package/lib/downloader.js +24 -0
  12. package/lib/gotchas.js +126 -0
  13. package/lib/installer.js +113 -19
  14. package/lib/integration.js +265 -0
  15. package/lib/knowledge.js +838 -100
  16. package/lib/ledger.js +1 -1
  17. package/package.json +3 -6
  18. package/skills/{sensecraft → solution-cli}/SKILL.md +125 -30
  19. package/skills/{sensecraft → solution-cli}/references/consult-and-select.md +61 -13
  20. package/skills/solution-cli/references/deploy.md +112 -0
  21. package/skills/solution-cli/references/device-management.md +298 -0
  22. package/skills/{sensecraft → solution-cli}/references/diagnose.md +15 -0
  23. package/skills/solution-cli/references/edge-gotchas.md +205 -0
  24. package/skills/solution-cli/references/generic-rk3588.md +106 -0
  25. package/skills/{sensecraft → solution-cli}/references/hardware-bom.md +9 -3
  26. package/skills/solution-cli/references/sales-feasibility.md +65 -0
  27. package/skills/{sensecraft → solution-cli}/references/secondary-development.md +10 -7
  28. package/skills/sensecraft/references/deploy.md +0 -300
  29. package/skills/sensecraft/references/device-management.md +0 -126
  30. /package/skills/{sensecraft → solution-cli}/references/app-only.md +0 -0
  31. /package/skills/{sensecraft → solution-cli}/references/error-catalog.md +0 -0
  32. /package/skills/{sensecraft → solution-cli}/references/offline.md +0 -0
  33. /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', 'sensecraft')
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', 'sensecraft')
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', 'sensecraft')
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 BUNDLED_SKILLS_SRC = path.resolve(__dirname, '..', 'skills', 'sensecraft');
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('\x1b[1m\x1b[34m SenseCraft Engine & AI Agent Skill Installer \x1b[0m');
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. Installation halted with no filesystem modifications.');
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
- logger('\n🔄 Phase 2: Ensuring IoT solutions catalog is populated...');
124
- pullSolutionContent(engineBin, msg => logger(` ${msg}`));
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, 'sensecraft'));
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(/\*\*SenseCraft Runtime\*\*:[\s\S]*?full absolute path: `"([^"]+)"/);
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> **SenseCraft Runtime**: All steps run via \`${escapedBinPath}\`. If shell $PATH does not contain \`sensecraft-engine\`, always invoke using the full absolute path: \`"${escapedBinPath}" <subcommand>\`.\n\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('SenseCraft Runtime:')) {
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-sensecraft');
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 🎉 SenseCraft Setup Completed Successfully! \x1b[0m');
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👉 You can now open Work Buddy or your chosen Agent and ask:');
219
- logger(' "帮我把边缘安全方案 (edge_security) 部署到 Jetson (192.168.1.100)"');
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📊 SenseCraft System Status:\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 SenseCraft skills are currently recorded in the ledger.');
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
+ }