@seeed-studio/solution-cli 0.0.0-stage → 0.2.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 CHANGED
@@ -1,3 +1,78 @@
1
- # Temporary Holding Version
1
+ # @seeed-studio/solution-cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Zero-friction installer and CLI runner for **SenseCraft Engine** and AI Agent Skills (Work Buddy, Claude Code, Codex, CodeBuddy).
4
+
5
+ ## Quick Start (One-line Execution)
6
+
7
+ Install the engine and configure your AI Agent skills in one command:
8
+
9
+ ```bash
10
+ # Interactive mode (scans local agents and prompts for target selection)
11
+ npx @seeed-studio/solution-cli install
12
+
13
+ # Non-interactive mode for a specific agent (e.g. Work Buddy)
14
+ npx @seeed-studio/solution-cli install --agent workbuddy
15
+
16
+ # Install to multiple agents non-interactively
17
+ npx @seeed-studio/solution-cli install --agent workbuddy,claude -y
18
+ ```
19
+
20
+ Or install globally:
21
+
22
+ ```bash
23
+ npm install -g @seeed-studio/solution-cli
24
+ solution-cli install
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Features
30
+
31
+ 1. **Smart Environment Inspection & Reuse**:
32
+ - Detects if you already have the SenseCraft Desktop App installed.
33
+ - Reuses the App's built-in engine binary and bundled solution assets without re-downloading (~30 MB saved, <0.5s execution).
34
+ - If no App exists, automatically downloads the standalone engine binary from Seeed CDN and verifies cryptographic SHA256 integrity.
35
+
36
+ 2. **Selective Agent Integration**:
37
+ - Supports **Work Buddy / Workbody**, **Claude Code**, **Codex**, and **CodeBuddy**.
38
+ - Interactive prompt lets you choose which agent(s) to adapt without touching unselected directories.
39
+
40
+ 3. **Deterministic Absolute Path Execution**:
41
+ - Injects explicit, verified binary paths directly into `SKILL.md`, ensuring that desktop Agent GUI processes can invoke `sensecraft-engine` even if shell `$PATH` is not inherited.
42
+
43
+ 4. **Installation Ledger & Clean Lifecycle**:
44
+ - Tracks all installed files in `~/.sensecraft/skills-ledger.json`.
45
+ - Prevents overwriting user modifications.
46
+ - Supports clean uninstallation via `solution-cli uninstall`.
47
+
48
+ ---
49
+
50
+ ## Command Reference
51
+
52
+ | Command | Description |
53
+ |---|---|
54
+ | `solution-cli install` | Inspect environment, verify/download engine, and configure Agent skills |
55
+ | `solution-cli status` | Display current engine binary, solution cache, and installed skills |
56
+ | `solution-cli doctor` | Diagnose environment (OS, Docker, serial permissions, PATH, ledger) |
57
+ | `solution-cli uninstall` | Cleanly remove SenseCraft skills from specified agent(s) |
58
+ | `sensecraft-engine <cmd>` | Direct proxy to the underlying Rust engine CLI |
59
+
60
+ ---
61
+
62
+ ## AI Agent Usage Example
63
+
64
+ Once installed, open Work Buddy, Claude Code, or Codex and say:
65
+
66
+ > "帮我把边缘安全方案 (edge_security) 部署到我的 Jetson 设备 (IP: 192.168.1.120,用户名 harvest)"
67
+
68
+ The Agent will automatically:
69
+ 1. Match the `sensecraft` Skill;
70
+ 2. Query solution parameters via `sensecraft-engine deploy-info edge_security --lang zh`;
71
+ 3. Generate or fill in required parameters;
72
+ 4. Execute deployment and stream real-time progress via `sensecraft-engine deploy ...`.
73
+
74
+ ---
75
+
76
+ ## License
77
+
78
+ Apache-2.0 © Seeed Studio
package/bin/cli.js ADDED
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { runInstall, runStatus, runUninstall } from '../lib/installer.js';
6
+ import { runDoctor } from '../lib/doctor.js';
7
+ import { searchHub, queryProductFamily, queryAccessories, createDeploymentLockfile } from '../lib/knowledge.js';
8
+ import { assertCompatibility } from '../lib/compatibility.js';
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf8'));
12
+
13
+ const args = process.argv.slice(2);
14
+ const command = args[0] || 'install';
15
+
16
+ function printHelp() {
17
+ console.log(`
18
+ SenseCraft Solution CLI & Skill Toolset (${pkg.name})
19
+
20
+ Usage:
21
+ npx @seeed-studio/solution-cli [command] [options]
22
+ solution-cli [command] [options]
23
+ sensecraft-solution-cli [command] [options]
24
+
25
+ Commands:
26
+ install Inspect environment, verify/download engine, and configure AI Agent skills (default)
27
+ status Display current engine, solution content, and agent skills status
28
+ doctor Diagnose environment (platform, permissions, docker, PATH, ledger)
29
+ uninstall Cleanly remove SenseCraft skills from specified agent(s)
30
+ hub Search scenarios and reference designs from Seeed Solution Hub
31
+ Usage: solution-cli hub search <keyword> [--limit 5] [--locale zh|en] [--json]
32
+ product Query product families, SKUs, and verified accessories
33
+ Usage: solution-cli product query <family_id> [--locale zh|en] [--json]
34
+ solution-cli product accessories <family_id> [--sku <sku>] [--json]
35
+ solution-cli product verify-compatibility --host <family_id> [--json]
36
+ lockfile Create an immutable DeploymentLockfile to isolate deployment execution
37
+ Usage: solution-cli lockfile create --solution <id> [--version <v>] [--hardware <fams>] [--json]
38
+ help Show this help message
39
+
40
+ Options:
41
+ --agent <name|all> Target agent(s): workbuddy, claude, codex, codebuddy, or all
42
+ -y, --yes Accept default detected selections non-interactively
43
+ --dry-run Simulate operations without making changes to filesystem
44
+ --skip-content Skip pulling IoT solution packages from CDN
45
+ --force Force re-download or re-link even if already installed
46
+ --json Output machine-readable JSON (supported by all query/diagnostic commands)
47
+ --locale <zh|en> Language for hub and product results (default: zh)
48
+ --limit <num> Maximum results for search (default: 5)
49
+ --engine-path <path> Use custom engine executable path
50
+
51
+ Examples:
52
+ npx @seeed-studio/solution-cli install
53
+ solution-cli hub search "跌倒"
54
+ solution-cli product accessories recomputer_j40
55
+ solution-cli product query sensecap_watcher --json
56
+ solution-cli status
57
+ solution-cli doctor
58
+ `);
59
+ }
60
+
61
+ function parseFlags(rawArgs) {
62
+ const flags = {
63
+ agent: null,
64
+ yes: false,
65
+ dryRun: false,
66
+ skipContent: false,
67
+ force: false,
68
+ json: false,
69
+ enginePath: null,
70
+ limit: 5,
71
+ locale: 'zh',
72
+ sku: null,
73
+ host: null,
74
+ accessory: null,
75
+ solution: null,
76
+ version: '1.0.0',
77
+ hardware: null,
78
+ };
79
+
80
+ for (let i = 0; i < rawArgs.length; i++) {
81
+ const a = rawArgs[i];
82
+ if (a === '--agent' && rawArgs[i + 1]) {
83
+ flags.agent = rawArgs[++i];
84
+ } else if (a.startsWith('--agent=')) {
85
+ flags.agent = a.split('=')[1];
86
+ } else if (a === '-y' || a === '--yes') {
87
+ flags.yes = true;
88
+ } else if (a === '--dry-run') {
89
+ flags.dryRun = true;
90
+ } else if (a === '--skip-content') {
91
+ flags.skipContent = true;
92
+ } else if (a === '--force') {
93
+ flags.force = true;
94
+ } else if (a === '--json') {
95
+ flags.json = true;
96
+ } else if (a === '--engine-path' && rawArgs[i + 1]) {
97
+ flags.enginePath = rawArgs[++i];
98
+ } else if (a === '--limit' && rawArgs[i + 1]) {
99
+ flags.limit = parseInt(rawArgs[++i], 10) || 5;
100
+ } else if (a === '--locale' && rawArgs[i + 1]) {
101
+ flags.locale = rawArgs[++i];
102
+ } else if (a === '--sku' && rawArgs[i + 1]) {
103
+ flags.sku = rawArgs[++i];
104
+ } else if (a === '--host' && rawArgs[i + 1]) {
105
+ flags.host = rawArgs[++i];
106
+ } else if (a === '--accessory' && rawArgs[i + 1]) {
107
+ flags.accessory = rawArgs[++i];
108
+ } else if (a === '--solution' && rawArgs[i + 1]) {
109
+ flags.solution = rawArgs[++i];
110
+ } else if (a === '--version' && rawArgs[i + 1]) {
111
+ flags.version = rawArgs[++i];
112
+ } else if (a === '--hardware' && rawArgs[i + 1]) {
113
+ flags.hardware = rawArgs[++i];
114
+ }
115
+ }
116
+
117
+ return flags;
118
+ }
119
+
120
+ async function main() {
121
+ const flags = parseFlags(args.slice(1));
122
+
123
+ if (command === 'help' || command === '--help' || command === '-h') {
124
+ printHelp();
125
+ process.exit(0);
126
+ }
127
+
128
+ if (command === '--version' || command === '-v') {
129
+ console.log(pkg.version);
130
+ process.exit(0);
131
+ }
132
+
133
+ try {
134
+ switch (command) {
135
+ case 'install':
136
+ await runInstall(flags);
137
+ break;
138
+
139
+ case 'status':
140
+ runStatus(flags);
141
+ break;
142
+
143
+ case 'doctor': {
144
+ const report = runDoctor();
145
+ if (flags.json) {
146
+ console.log(JSON.stringify(report, null, 2));
147
+ } else {
148
+ console.log('\n🩺 SenseCraft Doctor Environment Report:\n');
149
+ report.checks.forEach(c => {
150
+ const symbol = c.status === 'ok' ? '\x1b[32m✔\x1b[0m' : c.status === 'warn' ? '\x1b[33m⚠️\x1b[0m' : '\x1b[31m✖\x1b[0m';
151
+ console.log(` ${symbol} ${c.title}`);
152
+ console.log(` ${c.detail}`);
153
+ });
154
+ console.log(`\nOverall Health: ${report.overall.toUpperCase()}\n`);
155
+ }
156
+ break;
157
+ }
158
+
159
+ case 'uninstall':
160
+ runUninstall(flags);
161
+ break;
162
+
163
+ case 'hub': {
164
+ const sub = args[1] || 'search';
165
+ if (sub !== 'search') {
166
+ console.error(`Unknown hub action "${sub}". Supported: "solution-cli hub search <keyword>"`);
167
+ process.exit(1);
168
+ }
169
+ const query = args.slice(2).filter((a) => !a.startsWith('-')).join(' ');
170
+ const dto = await searchHub({ query, locale: flags.locale, limit: flags.limit });
171
+ if (flags.json) {
172
+ console.log(JSON.stringify(dto, null, 2));
173
+ } else {
174
+ console.log(`\n🔍 Solutions Hub Search Results for "${query}" (${dto.source}):\n`);
175
+ if (dto.records.length === 0) {
176
+ console.log(' No matching scenarios or reference designs found.');
177
+ } else {
178
+ dto.records.forEach((r, idx) => {
179
+ const tag = r.type === 'scenario' ? '\x1b[36m[Scenario]\x1b[0m' : '\x1b[35m[Reference Design]\x1b[0m';
180
+ console.log(` ${idx + 1}. ${tag} \x1b[1m${r.title}\x1b[0m (${r.id})`);
181
+ if (r.summary) console.log(` ${r.summary}`);
182
+ if (r.reference_designs?.length) console.log(` 对应参考设计: ${r.reference_designs.join(', ')}`);
183
+ if (r.family_ids?.length) console.log(` 推荐硬件族: ${r.family_ids.join(', ')}`);
184
+ if (r.presets?.length) console.log(` 预设模式: ${r.presets.join(', ')}`);
185
+ if (r.wiki_url) console.log(` 开发 Wiki: ${r.wiki_url}`);
186
+ console.log('');
187
+ });
188
+ }
189
+ if (dto.warnings.length) {
190
+ dto.warnings.forEach((w) => console.log(`\x1b[33mNote:\x1b[0m ${w}`));
191
+ }
192
+ }
193
+ break;
194
+ }
195
+
196
+ case 'product': {
197
+ const sub = args[1] || 'query';
198
+ const familyId = args.slice(2).find((a) => !a.startsWith('-')) || flags.host;
199
+
200
+ if (sub === 'query') {
201
+ if (!familyId) {
202
+ console.error('Please specify a product family ID (e.g. "solution-cli product query recomputer_j40")');
203
+ process.exit(1);
204
+ }
205
+ const dto = queryProductFamily(familyId, { locale: flags.locale });
206
+ if (flags.json) {
207
+ console.log(JSON.stringify(dto, null, 2));
208
+ } else {
209
+ const fam = dto.records[0];
210
+ if (!fam) {
211
+ console.log(`Product family "${familyId}" not found.`);
212
+ } else {
213
+ console.log(`\n📦 Product Family: \x1b[1m${fam.title}\x1b[0m (${fam.family_id})`);
214
+ console.log(` Default SKU: ${fam.default_sku || 'N/A'}`);
215
+ console.log(` Regions: ${(fam.regions || []).join(', ') || 'Global'}`);
216
+ console.log(` SKUs Available: ${fam.skus_count}`);
217
+ if (fam.physical_spec?.power_input) {
218
+ const p = fam.physical_spec.power_input;
219
+ console.log(` Power Spec: ${p.voltage_v || ''}V ${p.min_current_a || ''}A (${p.connector}) ${p.polarity || ''}`);
220
+ }
221
+ console.log(` Required Accessories: ${fam.required_accessories_count}`);
222
+ console.log(` Optional Accessories: ${fam.optional_accessories_count}`);
223
+ }
224
+ }
225
+ } else if (sub === 'accessories') {
226
+ if (!familyId) {
227
+ console.error('Please specify a product family ID (e.g. "solution-cli product accessories recomputer_j40")');
228
+ process.exit(1);
229
+ }
230
+ const dto = queryAccessories(familyId, { sku: flags.sku, locale: flags.locale });
231
+ if (flags.json) {
232
+ console.log(JSON.stringify(dto, null, 2));
233
+ } else {
234
+ const record = dto.records[0];
235
+ if (!record) {
236
+ console.log(`Product family "${familyId}" not found.`);
237
+ } else {
238
+ console.log(`\n📦 Verified Accessories for \x1b[1m${record.host_title}\x1b[0m (${record.family_id}):`);
239
+ console.log(` Host Default SKU: ${record.host_sku}`);
240
+ if (record.physical_spec?.power_input) {
241
+ const p = record.physical_spec.power_input;
242
+ console.log(` Host Power Spec: ${p.voltage_v || ''}V ${p.min_current_a || ''}A (${p.connector || ''})`);
243
+ }
244
+
245
+ console.log('\n \x1b[33m[REQUIRED (必需配件)]\x1b[0m');
246
+ if (record.accessories.required.length === 0) {
247
+ console.log(' (无强制单独采购的配件)');
248
+ } else {
249
+ record.accessories.required.forEach((acc) => {
250
+ const stat = acc.compatibility.is_compatible ? '\x1b[32m✔\x1b[0m' : '\x1b[31m✖\x1b[0m';
251
+ console.log(` ${stat} \x1b[1m${acc.name}\x1b[0m (SKU: ${acc.sku || 'N/A'}, Qty: ${acc.qty})`);
252
+ if (acc.reason) console.log(` 原因: ${acc.reason}`);
253
+ if (acc.url) console.log(` 购买链接: ${acc.url}`);
254
+ });
255
+ }
256
+
257
+ console.log('\n \x1b[36m[OPTIONAL (推荐选配)]\x1b[0m');
258
+ if (record.accessories.optional.length === 0) {
259
+ console.log(' (无选配项)');
260
+ } else {
261
+ record.accessories.optional.slice(0, 10).forEach((acc) => {
262
+ const stat = acc.compatibility.is_compatible ? '\x1b[32m✔\x1b[0m' : '\x1b[31m✖\x1b[0m';
263
+ console.log(` ${stat} ${acc.name} (SKU: ${acc.sku || 'N/A'})`);
264
+ if (acc.url) console.log(` 购买链接: ${acc.url}`);
265
+ });
266
+ }
267
+ console.log('');
268
+ }
269
+ }
270
+ } else if (sub === 'verify-compatibility') {
271
+ const comp = assertCompatibility(null, {
272
+ id: flags.accessory || 'custom_accessory',
273
+ name: flags.accessory || 'Custom Accessory',
274
+ });
275
+ if (flags.json) {
276
+ console.log(JSON.stringify(comp, null, 2));
277
+ } else {
278
+ const sym = comp.isCompatible ? '\x1b[32m✔\x1b[0m' : '\x1b[31m✖\x1b[0m';
279
+ console.log(`\n${sym} Physical Compatibility Status: ${comp.status}\n`);
280
+ comp.dimensions.forEach((d) => {
281
+ console.log(` - [${d.dimension}] ${d.status}: ${d.reason}`);
282
+ });
283
+ console.log('');
284
+ }
285
+ } else {
286
+ console.error(`Unknown product action "${sub}". Supported: query, accessories, verify-compatibility`);
287
+ process.exit(1);
288
+ }
289
+ break;
290
+ }
291
+
292
+ case 'lockfile': {
293
+ const sub = args[1] || 'create';
294
+ if (sub !== 'create') {
295
+ console.error(`Unknown lockfile action "${sub}". Supported: "solution-cli lockfile create"`);
296
+ process.exit(1);
297
+ }
298
+ const solutionId = flags.solution || 'unspecified_solution';
299
+ const hardware = (flags.hardware || '').split(',').map((h) => h.trim()).filter(Boolean);
300
+ const lockfile = createDeploymentLockfile({
301
+ solutionId,
302
+ solutionVersion: flags.version,
303
+ selectedHardware: hardware,
304
+ });
305
+ if (flags.json) {
306
+ console.log(JSON.stringify(lockfile, null, 2));
307
+ } else {
308
+ console.log('\n🔒 Generated DeploymentLockfile:');
309
+ console.log(` Solution: ${lockfile.solution_id}@${lockfile.solution_version}`);
310
+ console.log(` Dataset: ${lockfile.dataset_version}`);
311
+ console.log(` Network Isolation: Commercial queries DENIED during apply`);
312
+ console.log(` Selected Hardware: ${lockfile.selected_hardware.join(', ') || 'Local Host'}\n`);
313
+ }
314
+ break;
315
+ }
316
+
317
+ default:
318
+ console.error(`Unknown command: "${command}". Run "solution-cli help" for usage.`);
319
+ process.exit(1);
320
+ }
321
+ } catch (err) {
322
+ console.error(`\n\x1b[31mError:\x1b[0m ${err.message}\n`);
323
+ process.exit(1);
324
+ }
325
+ }
326
+
327
+ main();
package/bin/runner.js ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import { spawn } from 'node:child_process';
5
+ import { getConfiguredEnginePath, isExecutable } from '../lib/platform.js';
6
+
7
+ const binPath = getConfiguredEnginePath();
8
+
9
+ if (!fs.existsSync(binPath) || !isExecutable(binPath)) {
10
+ console.error(`\x1b[31mError:\x1b[0m SenseCraft Engine binary not found at "${binPath}".`);
11
+ console.error('Please run "npx @sensecraft/cli install" first to provision the engine.\n');
12
+ process.exit(1);
13
+ }
14
+
15
+ const child = spawn(binPath, process.argv.slice(2), {
16
+ stdio: 'inherit'
17
+ });
18
+
19
+ const forwardSignal = sig => {
20
+ if (!child.killed) {
21
+ try { child.kill(sig); } catch {}
22
+ }
23
+ };
24
+ process.on('SIGINT', () => forwardSignal('SIGINT'));
25
+ process.on('SIGTERM', () => forwardSignal('SIGTERM'));
26
+
27
+ child.on('error', err => {
28
+ console.error(`Failed to launch engine binary: ${err.message}`);
29
+ process.exit(1);
30
+ });
31
+
32
+ child.on('exit', (code, signal) => {
33
+ if (signal) {
34
+ // POSIX standard: 128 + signal number (Codex Medium 14)
35
+ const sigNum = os.constants.signals[signal] || 1;
36
+ process.exit(128 + sigNum);
37
+ }
38
+ process.exit(code ?? 0);
39
+ });