@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/README.md
CHANGED
|
@@ -1,3 +1,78 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @seeed-studio/solution-cli
|
|
2
2
|
|
|
3
|
-
|
|
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,136 @@
|
|
|
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
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf8'));
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const command = args[0] || 'install';
|
|
13
|
+
|
|
14
|
+
function printHelp() {
|
|
15
|
+
console.log(`
|
|
16
|
+
SenseCraft Solution CLI & Skill Toolset (${pkg.name})
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
npx @seeed-studio/solution-cli [command] [options]
|
|
20
|
+
solution-cli [command] [options]
|
|
21
|
+
sensecraft-solution-cli [command] [options]
|
|
22
|
+
|
|
23
|
+
Commands:
|
|
24
|
+
install Inspect environment, verify/download engine, and configure AI Agent skills (default)
|
|
25
|
+
status Display current engine, solution content, and agent skills status
|
|
26
|
+
doctor Diagnose environment (platform, permissions, docker, PATH, ledger)
|
|
27
|
+
uninstall Cleanly remove SenseCraft skills from specified agent(s)
|
|
28
|
+
help Show this help message
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
--agent <name|all> Target agent(s): workbuddy, claude, codex, codebuddy, or all
|
|
32
|
+
-y, --yes Accept default detected selections non-interactively
|
|
33
|
+
--dry-run Simulate operations without making changes to filesystem
|
|
34
|
+
--skip-content Skip pulling IoT solution packages from CDN
|
|
35
|
+
--force Force re-download or re-link even if already installed
|
|
36
|
+
--json Output machine-readable JSON (supported by status and doctor)
|
|
37
|
+
--engine-path <path> Use custom engine executable path
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
npx @seeed-studio/solution-cli install
|
|
41
|
+
npx @seeed-studio/solution-cli install --agent workbuddy
|
|
42
|
+
npx @seeed-studio/solution-cli install --agent workbuddy,claude -y
|
|
43
|
+
solution-cli status
|
|
44
|
+
solution-cli doctor
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseFlags(rawArgs) {
|
|
49
|
+
const flags = {
|
|
50
|
+
agent: null,
|
|
51
|
+
yes: false,
|
|
52
|
+
dryRun: false,
|
|
53
|
+
skipContent: false,
|
|
54
|
+
force: false,
|
|
55
|
+
json: false,
|
|
56
|
+
enginePath: null
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
60
|
+
const a = rawArgs[i];
|
|
61
|
+
if (a === '--agent' && rawArgs[i + 1]) {
|
|
62
|
+
flags.agent = rawArgs[++i];
|
|
63
|
+
} else if (a.startsWith('--agent=')) {
|
|
64
|
+
flags.agent = a.split('=')[1];
|
|
65
|
+
} else if (a === '-y' || a === '--yes') {
|
|
66
|
+
flags.yes = true;
|
|
67
|
+
} else if (a === '--dry-run') {
|
|
68
|
+
flags.dryRun = true;
|
|
69
|
+
} else if (a === '--skip-content') {
|
|
70
|
+
flags.skipContent = true;
|
|
71
|
+
} else if (a === '--force') {
|
|
72
|
+
flags.force = true;
|
|
73
|
+
} else if (a === '--json') {
|
|
74
|
+
flags.json = true;
|
|
75
|
+
} else if (a === '--engine-path' && rawArgs[i + 1]) {
|
|
76
|
+
flags.enginePath = rawArgs[++i];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return flags;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function main() {
|
|
84
|
+
const flags = parseFlags(args.slice(1));
|
|
85
|
+
|
|
86
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
87
|
+
printHelp();
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (command === '--version' || command === '-v') {
|
|
92
|
+
console.log(pkg.version);
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
switch (command) {
|
|
98
|
+
case 'install':
|
|
99
|
+
await runInstall(flags);
|
|
100
|
+
break;
|
|
101
|
+
|
|
102
|
+
case 'status':
|
|
103
|
+
runStatus(flags);
|
|
104
|
+
break;
|
|
105
|
+
|
|
106
|
+
case 'doctor': {
|
|
107
|
+
const report = runDoctor();
|
|
108
|
+
if (flags.json) {
|
|
109
|
+
console.log(JSON.stringify(report, null, 2));
|
|
110
|
+
} else {
|
|
111
|
+
console.log('\n🩺 SenseCraft Doctor Environment Report:\n');
|
|
112
|
+
report.checks.forEach(c => {
|
|
113
|
+
const symbol = c.status === 'ok' ? '\x1b[32m✔\x1b[0m' : c.status === 'warn' ? '\x1b[33m⚠️\x1b[0m' : '\x1b[31m✖\x1b[0m';
|
|
114
|
+
console.log(` ${symbol} ${c.title}`);
|
|
115
|
+
console.log(` ${c.detail}`);
|
|
116
|
+
});
|
|
117
|
+
console.log(`\nOverall Health: ${report.overall.toUpperCase()}\n`);
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
case 'uninstall':
|
|
123
|
+
runUninstall(flags);
|
|
124
|
+
break;
|
|
125
|
+
|
|
126
|
+
default:
|
|
127
|
+
console.error(`Unknown command: "${command}". Run "solution-cli help" for usage.`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error(`\n\x1b[31mError:\x1b[0m ${err.message}\n`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
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
|
+
});
|
package/lib/agent.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import readline from 'node:readline';
|
|
5
|
+
|
|
6
|
+
export function getKnownAgents() {
|
|
7
|
+
const home = os.homedir();
|
|
8
|
+
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
id: 'workbuddy',
|
|
12
|
+
name: 'Work Buddy / Workbody',
|
|
13
|
+
homeDirs: [path.join(home, '.workbuddy'), path.join(home, '.workbody')],
|
|
14
|
+
getSkillsDir: () => {
|
|
15
|
+
// Check where an existing installation might already be
|
|
16
|
+
const p1 = path.join(home, '.workbuddy', 'skills');
|
|
17
|
+
const p2 = path.join(home, '.workbody', 'skills');
|
|
18
|
+
if (fs.existsSync(path.join(p1, 'sensecraft', 'SKILL.md'))) return p1;
|
|
19
|
+
if (fs.existsSync(path.join(p2, 'sensecraft', 'SKILL.md'))) return p2;
|
|
20
|
+
if (fs.existsSync(path.join(home, '.workbuddy'))) return p1;
|
|
21
|
+
if (fs.existsSync(path.join(home, '.workbody'))) return p2;
|
|
22
|
+
return p1;
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'claude',
|
|
27
|
+
name: 'Claude Code',
|
|
28
|
+
homeDirs: [path.join(home, '.claude')],
|
|
29
|
+
getSkillsDir: () => path.join(home, '.claude', 'skills')
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'codex',
|
|
33
|
+
name: 'Codex',
|
|
34
|
+
homeDirs: [path.join(home, '.agents'), path.join(home, '.codex')],
|
|
35
|
+
getSkillsDir: () => {
|
|
36
|
+
// Honor existing installation first to avoid path drift (Codex Medium 13)
|
|
37
|
+
const pAgents = path.join(home, '.agents', 'skills');
|
|
38
|
+
const pCodex = path.join(home, '.codex', 'skills');
|
|
39
|
+
if (fs.existsSync(path.join(pAgents, 'sensecraft', 'SKILL.md'))) return pAgents;
|
|
40
|
+
if (fs.existsSync(path.join(pCodex, 'sensecraft', 'SKILL.md'))) return pCodex;
|
|
41
|
+
// Default to .agents/skills if .agents exists, otherwise .codex/skills if .codex exists
|
|
42
|
+
if (fs.existsSync(path.join(home, '.agents'))) return pAgents;
|
|
43
|
+
if (fs.existsSync(path.join(home, '.codex'))) return pCodex;
|
|
44
|
+
return pAgents;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'codebuddy',
|
|
49
|
+
name: 'CodeBuddy',
|
|
50
|
+
homeDirs: [path.join(home, '.codebuddy')],
|
|
51
|
+
getSkillsDir: () => {
|
|
52
|
+
if (process.env.CODEBUDDY_SKILL_DIR) {
|
|
53
|
+
return path.resolve(process.env.CODEBUDDY_SKILL_DIR);
|
|
54
|
+
}
|
|
55
|
+
return path.join(home, '.codebuddy', 'skills');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function scanLocalAgents() {
|
|
62
|
+
const agents = getKnownAgents();
|
|
63
|
+
return agents.map(a => {
|
|
64
|
+
const skillsDir = a.getSkillsDir();
|
|
65
|
+
const detected = a.homeDirs.some(dir => fs.existsSync(dir)) || fs.existsSync(skillsDir);
|
|
66
|
+
const installed = fs.existsSync(path.join(skillsDir, 'sensecraft', 'SKILL.md'));
|
|
67
|
+
return {
|
|
68
|
+
...a,
|
|
69
|
+
detected,
|
|
70
|
+
installed,
|
|
71
|
+
skillsDir
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function promptAgentSelection(scannedAgents, cliArg, autoYes = false) {
|
|
77
|
+
// If explicitly specified in CLI flags
|
|
78
|
+
if (cliArg) {
|
|
79
|
+
if (cliArg.toLowerCase() === 'all') {
|
|
80
|
+
return scannedAgents;
|
|
81
|
+
}
|
|
82
|
+
const requested = cliArg.split(',').map(s => s.trim().toLowerCase());
|
|
83
|
+
const matched = scannedAgents.filter(a => requested.includes(a.id));
|
|
84
|
+
if (matched.length === 0) {
|
|
85
|
+
throw new Error(`No matching agents for: "${cliArg}". Available: ${scannedAgents.map(a => a.id).join(', ')}`);
|
|
86
|
+
}
|
|
87
|
+
return matched;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Non-interactive check (CI / piped / daemon)
|
|
91
|
+
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
92
|
+
|
|
93
|
+
if (!isInteractive) {
|
|
94
|
+
if (autoYes) {
|
|
95
|
+
const detectedOnly = scannedAgents.filter(a => a.detected);
|
|
96
|
+
return detectedOnly.length > 0 ? detectedOnly : scannedAgents.slice(0, 1);
|
|
97
|
+
}
|
|
98
|
+
throw new Error('Non-interactive environment detected. Please specify --agent <workbuddy|claude|codex|codebuddy|all> or pass -y.');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (autoYes) {
|
|
102
|
+
const detectedOnly = scannedAgents.filter(a => a.detected);
|
|
103
|
+
return detectedOnly.length > 0 ? detectedOnly : scannedAgents;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Interactive CLI Selection using Readline
|
|
107
|
+
console.log('\n🤖 Available AI Agent targets:');
|
|
108
|
+
scannedAgents.forEach((a, idx) => {
|
|
109
|
+
const tag = a.detected ? '\x1b[32m[Detected]\x1b[0m' : '\x1b[90m[Not Detected]\x1b[0m';
|
|
110
|
+
const status = a.installed ? ' \x1b[36m(Already installed)\x1b[0m' : '';
|
|
111
|
+
console.log(` ${idx + 1}. ${a.name} ${tag}${status}`);
|
|
112
|
+
console.log(` Path: ${a.skillsDir}`);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const defaultChoices = scannedAgents
|
|
116
|
+
.map((a, idx) => (a.detected ? idx + 1 : null))
|
|
117
|
+
.filter(Boolean);
|
|
118
|
+
|
|
119
|
+
const defaultPromptStr = defaultChoices.length > 0 ? defaultChoices.join(',') : 'all';
|
|
120
|
+
|
|
121
|
+
const rl = readline.createInterface({
|
|
122
|
+
input: process.stdin,
|
|
123
|
+
output: process.stdout
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return new Promise((resolve) => {
|
|
127
|
+
rl.question(`\nSelect agents to install skill into (e.g. 1,2 or "all") [default: ${defaultPromptStr}]: `, answer => {
|
|
128
|
+
rl.close();
|
|
129
|
+
const input = (answer || '').trim();
|
|
130
|
+
|
|
131
|
+
if (!input) {
|
|
132
|
+
if (defaultChoices.length > 0) {
|
|
133
|
+
resolve(scannedAgents.filter((_, idx) => defaultChoices.includes(idx + 1)));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
resolve(scannedAgents);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (input.toLowerCase() === 'all') {
|
|
141
|
+
resolve(scannedAgents);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const indices = input
|
|
146
|
+
.split(/[,\s]+/)
|
|
147
|
+
.map(x => parseInt(x, 10))
|
|
148
|
+
.filter(n => !isNaN(n) && n >= 1 && n <= scannedAgents.length);
|
|
149
|
+
|
|
150
|
+
if (indices.length === 0) {
|
|
151
|
+
console.log('No valid selection. Nothing to install.');
|
|
152
|
+
resolve([]);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const selected = scannedAgents.filter((_, idx) => indices.includes(idx + 1));
|
|
157
|
+
resolve(selected);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|