@seeed-studio/solution-cli 0.0.0-stage → 0.1.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 +77 -2
- package/bin/cli.js +136 -0
- package/bin/runner.js +39 -0
- package/lib/agent.js +160 -0
- package/lib/detector.js +321 -0
- package/lib/doctor.js +179 -0
- package/lib/downloader.js +219 -0
- package/lib/installer.js +375 -0
- package/lib/ledger.js +302 -0
- package/lib/platform.js +113 -0
- package/package.json +51 -4
- package/skills/sensecraft/SKILL.md +176 -0
- package/skills/sensecraft/references/app-only.md +273 -0
- package/skills/sensecraft/references/deploy.md +300 -0
- package/skills/sensecraft/references/diagnose.md +133 -0
- package/skills/sensecraft/references/error-catalog.md +33 -0
- package/skills/sensecraft/references/offline.md +78 -0
- package/skills/sensecraft/references/verify.md +136 -0
package/lib/detector.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { getScHome, getEngineBinaryPath, isExecutable } from './platform.js';
|
|
6
|
+
|
|
7
|
+
const MIN_COMPATIBLE_VERSION = '0.0.3';
|
|
8
|
+
|
|
9
|
+
// Strict SemVer 2.0 parser (Codex Medium 4: Reject garbage, disallow leading zeros)
|
|
10
|
+
const SEMVER_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?$/;
|
|
11
|
+
|
|
12
|
+
export function parseSemver(vStr) {
|
|
13
|
+
if (!vStr) return null;
|
|
14
|
+
const clean = vStr.trim().replace(/^v/, '');
|
|
15
|
+
const match = clean.match(SEMVER_REGEX);
|
|
16
|
+
if (!match) return null;
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
major: parseInt(match[1], 10),
|
|
20
|
+
minor: parseInt(match[2], 10),
|
|
21
|
+
patch: parseInt(match[3], 10),
|
|
22
|
+
prerelease: match[4] || null,
|
|
23
|
+
raw: match[0]
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function compareSemver(v1Str, v2Str) {
|
|
28
|
+
const v1 = parseSemver(v1Str);
|
|
29
|
+
const v2 = parseSemver(v2Str);
|
|
30
|
+
if (!v1 || !v2) return 0;
|
|
31
|
+
|
|
32
|
+
if (v1.major !== v2.major) return v1.major > v2.major ? 1 : -1;
|
|
33
|
+
if (v1.minor !== v2.minor) return v1.minor > v2.minor ? 1 : -1;
|
|
34
|
+
if (v1.patch !== v2.patch) return v1.patch > v2.patch ? 1 : -1;
|
|
35
|
+
|
|
36
|
+
// SemVer 2.0: 1.0.0-alpha < 1.0.0 (normal version has higher precedence than prerelease)
|
|
37
|
+
if (v1.prerelease && !v2.prerelease) return -1;
|
|
38
|
+
if (!v1.prerelease && v2.prerelease) return 1;
|
|
39
|
+
if (!v1.prerelease && !v2.prerelease) return 0;
|
|
40
|
+
|
|
41
|
+
// Compare prerelease identifiers per SemVer 2.0 §11
|
|
42
|
+
const id1 = v1.prerelease.split('.');
|
|
43
|
+
const id2 = v2.prerelease.split('.');
|
|
44
|
+
const len = Math.max(id1.length, id2.length);
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < len; i++) {
|
|
47
|
+
if (id1[i] === undefined) return -1;
|
|
48
|
+
if (id2[i] === undefined) return 1;
|
|
49
|
+
if (id1[i] === id2[i]) continue;
|
|
50
|
+
|
|
51
|
+
const num1 = /^\d+$/.test(id1[i]) ? parseInt(id1[i], 10) : null;
|
|
52
|
+
const num2 = /^\d+$/.test(id2[i]) ? parseInt(id2[i], 10) : null;
|
|
53
|
+
|
|
54
|
+
if (num1 !== null && num2 !== null) {
|
|
55
|
+
return num1 > num2 ? 1 : -1;
|
|
56
|
+
}
|
|
57
|
+
if (num1 !== null && num2 === null) return -1; // numeric has lower precedence than non-numeric
|
|
58
|
+
if (num1 === null && num2 !== null) return 1;
|
|
59
|
+
|
|
60
|
+
return id1[i].localeCompare(id2[i]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function verifyEngineBinary(binPath) {
|
|
67
|
+
const absPath = path.resolve(binPath);
|
|
68
|
+
if (!fs.existsSync(absPath) || !isExecutable(absPath)) {
|
|
69
|
+
return { valid: false, reason: 'Binary not found or not executable' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const rawOut = execFileSync(absPath, ['--version'], {
|
|
74
|
+
encoding: 'utf8',
|
|
75
|
+
timeout: 3000,
|
|
76
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
77
|
+
}).trim();
|
|
78
|
+
|
|
79
|
+
// Extract exact version token (e.g. from "sensecraft-engine 0.0.3")
|
|
80
|
+
const tokens = rawOut.split(/\s+/);
|
|
81
|
+
let versionCandidate = null;
|
|
82
|
+
for (const t of tokens) {
|
|
83
|
+
if (parseSemver(t)) {
|
|
84
|
+
versionCandidate = t;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!versionCandidate) {
|
|
90
|
+
return { valid: false, reason: `Unrecognized engine version format in "${rawOut}"` };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const sem = parseSemver(versionCandidate);
|
|
94
|
+
if (!sem) {
|
|
95
|
+
return { valid: false, reason: `Invalid SemVer in engine version "${versionCandidate}"` };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (compareSemver(sem.raw, MIN_COMPATIBLE_VERSION) < 0) {
|
|
99
|
+
return {
|
|
100
|
+
valid: false,
|
|
101
|
+
version: sem.raw,
|
|
102
|
+
reason: `Version ${sem.raw} is below required minimum ${MIN_COMPATIBLE_VERSION}`
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Verify deployer capability via meta --json (Codex Medium 4: strict structure and string elements)
|
|
107
|
+
let deployers = [];
|
|
108
|
+
try {
|
|
109
|
+
const metaOut = execFileSync(absPath, ['meta', '--json'], {
|
|
110
|
+
encoding: 'utf8',
|
|
111
|
+
timeout: 4000,
|
|
112
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
113
|
+
});
|
|
114
|
+
const meta = JSON.parse(metaOut);
|
|
115
|
+
if (
|
|
116
|
+
!meta ||
|
|
117
|
+
!Array.isArray(meta.deployers) ||
|
|
118
|
+
meta.deployers.length === 0 ||
|
|
119
|
+
!meta.deployers.every(d => typeof d === 'string' && d.length > 0 && /^[a-z0-9_]+$/.test(d))
|
|
120
|
+
) {
|
|
121
|
+
return {
|
|
122
|
+
valid: false,
|
|
123
|
+
version: sem.raw,
|
|
124
|
+
reason: 'Engine meta --json returned invalid or empty deployers specification'
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
deployers = meta.deployers;
|
|
128
|
+
} catch (err) {
|
|
129
|
+
return {
|
|
130
|
+
valid: false,
|
|
131
|
+
version: sem.raw,
|
|
132
|
+
reason: `Engine capability validation failed (meta --json): ${err.message}`
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
valid: true,
|
|
138
|
+
version: sem.raw,
|
|
139
|
+
deployers,
|
|
140
|
+
binPath: absPath
|
|
141
|
+
};
|
|
142
|
+
} catch (err) {
|
|
143
|
+
return { valid: false, reason: `Execution failed: ${err.message}` };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function detectExistingEngine() {
|
|
148
|
+
const defaultBin = getEngineBinaryPath();
|
|
149
|
+
const candidates = [];
|
|
150
|
+
|
|
151
|
+
if (fs.existsSync(defaultBin)) {
|
|
152
|
+
const check = verifyEngineBinary(defaultBin);
|
|
153
|
+
if (check.valid) {
|
|
154
|
+
candidates.push({
|
|
155
|
+
source: 'default',
|
|
156
|
+
path: defaultBin,
|
|
157
|
+
version: check.version,
|
|
158
|
+
deployers: check.deployers
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Scan PATH candidates
|
|
164
|
+
const envPath = process.env.PATH || '';
|
|
165
|
+
const delimiter = os.platform() === 'win32' ? ';' : ':';
|
|
166
|
+
const binName = os.platform() === 'win32' ? 'sensecraft-engine.exe' : 'sensecraft-engine';
|
|
167
|
+
|
|
168
|
+
for (const dir of envPath.split(delimiter)) {
|
|
169
|
+
if (!dir.trim()) continue;
|
|
170
|
+
const fullPath = path.join(dir.trim(), binName);
|
|
171
|
+
if (fs.existsSync(fullPath) && isExecutable(fullPath)) {
|
|
172
|
+
// Exclude npm package's runner shim to prevent recursive loop (Codex P1)
|
|
173
|
+
try {
|
|
174
|
+
const real = fs.realpathSync(fullPath);
|
|
175
|
+
if (real.endsWith('bin/runner.js') || real.endsWith('bin\\runner.js')) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const head = fs.readFileSync(fullPath, 'utf8', { flag: 'r' }).slice(0, 500);
|
|
179
|
+
if (head.includes('runEngineProxy') || (head.includes('#!/usr/bin/env node') && head.includes('runner.js'))) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
} catch {}
|
|
183
|
+
|
|
184
|
+
const check = verifyEngineBinary(fullPath);
|
|
185
|
+
if (check.valid) {
|
|
186
|
+
candidates.push({
|
|
187
|
+
source: 'path',
|
|
188
|
+
path: fullPath,
|
|
189
|
+
version: check.version,
|
|
190
|
+
deployers: check.deployers
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (candidates.length === 0) return null;
|
|
197
|
+
candidates.sort((a, b) => compareSemver(b.version, a.version));
|
|
198
|
+
return candidates[0];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function detectDesktopApp() {
|
|
202
|
+
const p = os.platform();
|
|
203
|
+
const candidates = [];
|
|
204
|
+
|
|
205
|
+
// Check 1: Tauri app pointer at ~/.sensecraft/engine.json
|
|
206
|
+
const pointerFile = path.join(getScHome(), 'engine.json');
|
|
207
|
+
if (fs.existsSync(pointerFile)) {
|
|
208
|
+
try {
|
|
209
|
+
const data = JSON.parse(fs.readFileSync(pointerFile, 'utf8'));
|
|
210
|
+
if (data && data.bin && fs.existsSync(data.bin)) {
|
|
211
|
+
candidates.push({
|
|
212
|
+
source: 'pointer',
|
|
213
|
+
appVersion: data.app_version || 'unknown',
|
|
214
|
+
binPath: path.resolve(data.bin)
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
} catch {}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check 2: Standard OS installation locations
|
|
221
|
+
if (p === 'darwin') {
|
|
222
|
+
const appBase = '/Applications/SenseCraft Solution.app';
|
|
223
|
+
if (fs.existsSync(appBase)) {
|
|
224
|
+
candidates.push({
|
|
225
|
+
source: 'os_app',
|
|
226
|
+
appPath: appBase,
|
|
227
|
+
binPath: path.join(appBase, 'Contents', 'MacOS', 'sensecraft-engine'),
|
|
228
|
+
solutionsPath: path.join(appBase, 'Contents', 'Resources', '_up_', 'sensecraft-solutions', 'solutions'),
|
|
229
|
+
skillsPath: path.join(appBase, 'Contents', 'Resources', '_up_', 'sensecraft-solutions', 'skills', 'sensecraft')
|
|
230
|
+
});
|
|
231
|
+
const cacheBin = path.join(
|
|
232
|
+
os.homedir(),
|
|
233
|
+
'Library',
|
|
234
|
+
'Caches',
|
|
235
|
+
'com.seeedstudio.sensecraft-solution',
|
|
236
|
+
'sidecar',
|
|
237
|
+
'sensecraft-engine'
|
|
238
|
+
);
|
|
239
|
+
if (fs.existsSync(cacheBin)) {
|
|
240
|
+
candidates.push({
|
|
241
|
+
source: 'app_cache',
|
|
242
|
+
appPath: appBase,
|
|
243
|
+
binPath: cacheBin
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} else if (p === 'win32') {
|
|
248
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
249
|
+
const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
|
|
250
|
+
const appPaths = [
|
|
251
|
+
path.join(localAppData, 'Programs', 'SenseCraft Solution'),
|
|
252
|
+
path.join(programFiles, 'SenseCraft Solution')
|
|
253
|
+
];
|
|
254
|
+
for (const appPath of appPaths) {
|
|
255
|
+
if (fs.existsSync(appPath)) {
|
|
256
|
+
candidates.push({
|
|
257
|
+
source: 'os_app',
|
|
258
|
+
appPath,
|
|
259
|
+
binPath: path.join(appPath, 'resources', 'bin', 'sensecraft-engine.exe'),
|
|
260
|
+
solutionsPath: path.join(appPath, 'resources', 'sensecraft-solutions', 'solutions'),
|
|
261
|
+
skillsPath: path.join(appPath, 'resources', 'sensecraft-solutions', 'skills', 'sensecraft')
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} else if (p === 'linux') {
|
|
266
|
+
const linuxPaths = ['/opt/SenseCraft Solution', '/opt/sensecraft-solution', '/usr/lib/sensecraft-solution'];
|
|
267
|
+
for (const appPath of linuxPaths) {
|
|
268
|
+
if (fs.existsSync(appPath)) {
|
|
269
|
+
candidates.push({
|
|
270
|
+
source: 'os_app',
|
|
271
|
+
appPath,
|
|
272
|
+
binPath: path.join(appPath, 'sensecraft-engine'),
|
|
273
|
+
solutionsPath: path.join(appPath, 'sensecraft-solutions', 'solutions'),
|
|
274
|
+
skillsPath: path.join(appPath, 'skills', 'sensecraft')
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const validCandidates = [];
|
|
281
|
+
const rejectedReasons = [];
|
|
282
|
+
|
|
283
|
+
for (const c of candidates) {
|
|
284
|
+
if (c.binPath && fs.existsSync(c.binPath)) {
|
|
285
|
+
const verified = verifyEngineBinary(c.binPath);
|
|
286
|
+
if (verified.valid) {
|
|
287
|
+
validCandidates.push({
|
|
288
|
+
...c,
|
|
289
|
+
version: verified.version,
|
|
290
|
+
deployers: verified.deployers
|
|
291
|
+
});
|
|
292
|
+
} else {
|
|
293
|
+
rejectedReasons.push(`${c.binPath}: ${verified.reason}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (validCandidates.length > 0) {
|
|
299
|
+
validCandidates.sort((a, b) => compareSemver(b.version, a.version));
|
|
300
|
+
const chosen = validCandidates[0];
|
|
301
|
+
return {
|
|
302
|
+
detected: true,
|
|
303
|
+
compatible: true,
|
|
304
|
+
binPath: chosen.binPath,
|
|
305
|
+
version: chosen.version,
|
|
306
|
+
appPath: chosen.appPath,
|
|
307
|
+
solutionsPath: chosen.solutionsPath && fs.existsSync(chosen.solutionsPath) ? chosen.solutionsPath : null,
|
|
308
|
+
skillsPath: chosen.skillsPath && fs.existsSync(chosen.skillsPath) ? chosen.skillsPath : null
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (rejectedReasons.length > 0) {
|
|
313
|
+
return {
|
|
314
|
+
detected: true,
|
|
315
|
+
compatible: false,
|
|
316
|
+
reason: rejectedReasons.join('; ')
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return { detected: false };
|
|
321
|
+
}
|
package/lib/doctor.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { getPlatformInfo, getEngineBinDir, getEngineBinaryPath, getConfiguredEnginePath, getContentDir } from './platform.js';
|
|
6
|
+
import { verifyEngineBinary } from './detector.js';
|
|
7
|
+
import { loadLedger, computeFileHash } from './ledger.js';
|
|
8
|
+
|
|
9
|
+
export function runDoctor() {
|
|
10
|
+
const platform = getPlatformInfo();
|
|
11
|
+
const checks = [];
|
|
12
|
+
|
|
13
|
+
// Check 1: Platform & Architecture
|
|
14
|
+
if (platform.supported) {
|
|
15
|
+
checks.push({
|
|
16
|
+
id: 'platform',
|
|
17
|
+
status: 'ok',
|
|
18
|
+
title: 'Host Platform & Architecture',
|
|
19
|
+
detail: `${platform.platform} (${platform.arch}) -> target ${platform.target}`
|
|
20
|
+
});
|
|
21
|
+
} else {
|
|
22
|
+
checks.push({
|
|
23
|
+
id: 'platform',
|
|
24
|
+
status: 'fail',
|
|
25
|
+
title: 'Host Platform & Architecture',
|
|
26
|
+
detail: platform.note
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check 2: Engine Binary
|
|
31
|
+
const binPath = getConfiguredEnginePath();
|
|
32
|
+
const engineCheck = verifyEngineBinary(binPath);
|
|
33
|
+
if (engineCheck.valid) {
|
|
34
|
+
checks.push({
|
|
35
|
+
id: 'engine_binary',
|
|
36
|
+
status: 'ok',
|
|
37
|
+
title: 'SenseCraft Engine Binary',
|
|
38
|
+
detail: `v${engineCheck.version} at ${binPath} (${engineCheck.deployers?.length || 0} deployers verified)`
|
|
39
|
+
});
|
|
40
|
+
} else {
|
|
41
|
+
checks.push({
|
|
42
|
+
id: 'engine_binary',
|
|
43
|
+
status: 'warn',
|
|
44
|
+
title: 'SenseCraft Engine Binary',
|
|
45
|
+
detail: `${engineCheck.reason || 'Not installed'}. Run "sensecraft install" to provision.`
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check 3: PATH Configuration
|
|
50
|
+
const binDir = getEngineBinDir();
|
|
51
|
+
const envPath = process.env.PATH || '';
|
|
52
|
+
const delimiter = platform.isWindows ? ';' : ':';
|
|
53
|
+
const isInPath = envPath.split(delimiter).some(p => path.resolve(p) === path.resolve(binDir));
|
|
54
|
+
|
|
55
|
+
if (isInPath) {
|
|
56
|
+
checks.push({
|
|
57
|
+
id: 'path',
|
|
58
|
+
status: 'ok',
|
|
59
|
+
title: 'Shell PATH Configuration',
|
|
60
|
+
detail: `Directory ${binDir} is present in $PATH`
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
const isWin = platform.isWindows;
|
|
64
|
+
const fixCmd = isWin
|
|
65
|
+
? `[Environment]::SetEnvironmentVariable("Path", $env:Path + ";${binDir}", "User")`
|
|
66
|
+
: `export PATH="${binDir}:$PATH" (add to ~/.bashrc or ~/.zshrc)`;
|
|
67
|
+
checks.push({
|
|
68
|
+
id: 'path',
|
|
69
|
+
status: 'warn',
|
|
70
|
+
title: 'Shell PATH Configuration',
|
|
71
|
+
detail: `${binDir} is not in $PATH. Note: Skills use absolute paths, but for manual CLI use run: ${fixCmd}`
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check 4: Solution Content Cache
|
|
76
|
+
const contentDir = getContentDir();
|
|
77
|
+
const manifestFile = path.join(contentDir, 'manifest.json');
|
|
78
|
+
const solutionsDir = path.join(contentDir, 'solutions');
|
|
79
|
+
|
|
80
|
+
if (fs.existsSync(manifestFile) && fs.existsSync(solutionsDir)) {
|
|
81
|
+
try {
|
|
82
|
+
const solutionEntries = fs.readdirSync(solutionsDir).filter(f => !f.startsWith('.'));
|
|
83
|
+
checks.push({
|
|
84
|
+
id: 'content',
|
|
85
|
+
status: 'ok',
|
|
86
|
+
title: 'IoT Solutions Catalog',
|
|
87
|
+
detail: `${solutionEntries.length} solutions cached in ${solutionsDir}`
|
|
88
|
+
});
|
|
89
|
+
} catch {
|
|
90
|
+
checks.push({
|
|
91
|
+
id: 'content',
|
|
92
|
+
status: 'warn',
|
|
93
|
+
title: 'IoT Solutions Catalog',
|
|
94
|
+
detail: 'Catalog exists but could not read solutions directory.'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
checks.push({
|
|
99
|
+
id: 'content',
|
|
100
|
+
status: 'warn',
|
|
101
|
+
title: 'IoT Solutions Catalog',
|
|
102
|
+
detail: 'No local solution catalog cached. Run "sensecraft-engine content pull".'
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check 5: Docker daemon
|
|
107
|
+
try {
|
|
108
|
+
execFileSync('docker', ['info'], { stdio: 'ignore', timeout: 3000 });
|
|
109
|
+
checks.push({
|
|
110
|
+
id: 'docker',
|
|
111
|
+
status: 'ok',
|
|
112
|
+
title: 'Docker Daemon',
|
|
113
|
+
detail: 'Docker CLI and daemon accessible for container deployments'
|
|
114
|
+
});
|
|
115
|
+
} catch {
|
|
116
|
+
checks.push({
|
|
117
|
+
id: 'docker',
|
|
118
|
+
status: 'warn',
|
|
119
|
+
title: 'Docker Daemon',
|
|
120
|
+
detail: 'Docker not running or current user lacks access. (Only needed for local Docker deployments)'
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check 6: Linux serial permissions
|
|
125
|
+
if (platform.platform === 'linux') {
|
|
126
|
+
try {
|
|
127
|
+
const groups = execFileSync('groups', { encoding: 'utf8', timeout: 2000 }).trim();
|
|
128
|
+
const hasDialout = groups.includes('dialout') || groups.includes('uucp');
|
|
129
|
+
if (hasDialout) {
|
|
130
|
+
checks.push({
|
|
131
|
+
id: 'serial_permission',
|
|
132
|
+
status: 'ok',
|
|
133
|
+
title: 'Serial Port Permissions',
|
|
134
|
+
detail: 'User belongs to dialout/uucp group'
|
|
135
|
+
});
|
|
136
|
+
} else {
|
|
137
|
+
checks.push({
|
|
138
|
+
id: 'serial_permission',
|
|
139
|
+
status: 'warn',
|
|
140
|
+
title: 'Serial Port Permissions',
|
|
141
|
+
detail: 'User not in "dialout" group. USB serial flashing may require: sudo usermod -aG dialout $USER'
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
} catch {}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check 7: Skill Ledger
|
|
148
|
+
const ledger = loadLedger();
|
|
149
|
+
const installEntries = Object.entries(ledger.installations || {});
|
|
150
|
+
if (installEntries.length > 0) {
|
|
151
|
+
let allIntact = true;
|
|
152
|
+
for (const [agentId, rec] of installEntries) {
|
|
153
|
+
for (const [rel, recordedHash] of Object.entries(rec.files || {})) {
|
|
154
|
+
const full = path.join(rec.target_dir, rel);
|
|
155
|
+
if (!fs.existsSync(full) || computeFileHash(full) !== recordedHash) {
|
|
156
|
+
allIntact = false;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
checks.push({
|
|
162
|
+
id: 'skills_ledger',
|
|
163
|
+
status: allIntact ? 'ok' : 'warn',
|
|
164
|
+
title: 'Agent Skills Tracking',
|
|
165
|
+
detail: `${installEntries.length} agent(s) tracked. Status: ${allIntact ? 'all files intact' : 'files modified or missing'}`
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const overall = checks.some(c => c.status === 'fail')
|
|
170
|
+
? 'fail'
|
|
171
|
+
: checks.some(c => c.status === 'warn')
|
|
172
|
+
? 'warn'
|
|
173
|
+
: 'ok';
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
overall,
|
|
177
|
+
checks
|
|
178
|
+
};
|
|
179
|
+
}
|