@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 +77 -2
- package/bin/cli.js +327 -0
- package/bin/runner.js +39 -0
- package/catalog/knowledge-core.json +7970 -0
- package/lib/agent.js +160 -0
- package/lib/catalog-dto.js +69 -0
- package/lib/compatibility.js +312 -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/knowledge.js +327 -0
- package/lib/ledger.js +302 -0
- package/lib/platform.js +113 -0
- package/package.json +52 -4
- package/skills/sensecraft/SKILL.md +183 -0
- package/skills/sensecraft/references/app-only.md +273 -0
- package/skills/sensecraft/references/consult-and-select.md +56 -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/hardware-bom.md +83 -0
- package/skills/sensecraft/references/offline.md +78 -0
- package/skills/sensecraft/references/verify.md +136 -0
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
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Catalog DTO definition and validator conforming to v2 schema.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const SCHEMA_VERSION = '2.0';
|
|
6
|
+
export const SCHEMA_URL = 'https://spec.seeedstudio.com/schemas/v2/catalog-dto.json';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a standardized Catalog DTO response.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} options
|
|
12
|
+
* @param {'typesense' | 'bundled_manifest'} options.source
|
|
13
|
+
* @param {'FRESH' | 'STALE' | 'UNKNOWN'} [options.freshness='FRESH']
|
|
14
|
+
* @param {string} [options.datasetVersion='2026-10-13.1']
|
|
15
|
+
* @param {Array} options.records
|
|
16
|
+
* @param {Object} [options.fieldCompleteness]
|
|
17
|
+
* @param {Array<string>} [options.warnings=[]]
|
|
18
|
+
* @returns {Object} Validated Catalog DTO
|
|
19
|
+
*/
|
|
20
|
+
export function createCatalogDto({
|
|
21
|
+
source = 'bundled_manifest',
|
|
22
|
+
freshness = 'FRESH',
|
|
23
|
+
datasetVersion = '2026-10-13.1',
|
|
24
|
+
records = [],
|
|
25
|
+
fieldCompleteness = { spec: true, compatibility: true, offer: source === 'typesense' },
|
|
26
|
+
warnings = [],
|
|
27
|
+
}) {
|
|
28
|
+
return {
|
|
29
|
+
$schema: SCHEMA_URL,
|
|
30
|
+
schema_version: SCHEMA_VERSION,
|
|
31
|
+
dataset_version: datasetVersion,
|
|
32
|
+
generated_at: new Date().toISOString(),
|
|
33
|
+
source,
|
|
34
|
+
freshness,
|
|
35
|
+
field_completeness: fieldCompleteness,
|
|
36
|
+
record_count: Array.isArray(records) ? records.length : 0,
|
|
37
|
+
records: Array.isArray(records) ? records : [],
|
|
38
|
+
warnings: Array.isArray(warnings) ? warnings : [],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validates whether an object complies with the v2 Catalog DTO structure.
|
|
44
|
+
*
|
|
45
|
+
* @param {any} dto
|
|
46
|
+
* @returns {{ valid: boolean, errors: Array<string> }}
|
|
47
|
+
*/
|
|
48
|
+
export function validateCatalogDto(dto) {
|
|
49
|
+
const errors = [];
|
|
50
|
+
if (!dto || typeof dto !== 'object') {
|
|
51
|
+
return { valid: false, errors: ['DTO must be an object'] };
|
|
52
|
+
}
|
|
53
|
+
if (dto.schema_version !== SCHEMA_VERSION) {
|
|
54
|
+
errors.push(`Invalid schema_version: expected ${SCHEMA_VERSION}, got ${dto.schema_version}`);
|
|
55
|
+
}
|
|
56
|
+
if (!['typesense', 'bundled_manifest'].includes(dto.source)) {
|
|
57
|
+
errors.push(`Invalid source: ${dto.source}`);
|
|
58
|
+
}
|
|
59
|
+
if (!['FRESH', 'STALE', 'UNKNOWN'].includes(dto.freshness)) {
|
|
60
|
+
errors.push(`Invalid freshness: ${dto.freshness}`);
|
|
61
|
+
}
|
|
62
|
+
if (!Array.isArray(dto.records)) {
|
|
63
|
+
errors.push('records must be an array');
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
valid: errors.length === 0,
|
|
67
|
+
errors,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic Machine Compatibility Evaluator for IoT Hardware and Accessories.
|
|
3
|
+
*
|
|
4
|
+
* Implements machine-evaluable compatibility assertions across 6 physical dimensions:
|
|
5
|
+
* 1. power_supply: Voltage, tolerance, min current/watts, plug dimensions, polarity, USB-C PD profiles
|
|
6
|
+
* 2. rf_antenna: RF connector gender (N-Type, SMA, RP-SMA), impedance, frequency bands
|
|
7
|
+
* 3. m2_storage: Key-M vs Key-E, form factor (2242 vs 2280), PCIe/NVMe vs SATA
|
|
8
|
+
* 4. poe: 802.3af (15.4W) vs 802.3at (30W) vs 802.3bt
|
|
9
|
+
* 5. usb: Port generation, bandwidth, and host power output
|
|
10
|
+
* 6. os_firmware: Kernel / JetPack / firmware version constraints
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const COMPATIBILITY_STATUS = {
|
|
14
|
+
VERIFIED: 'VERIFIED',
|
|
15
|
+
REQUIRES_ADAPTER: 'REQUIRES_ADAPTER',
|
|
16
|
+
INCOMPATIBLE: 'INCOMPATIBLE',
|
|
17
|
+
UNKNOWN: 'UNKNOWN',
|
|
18
|
+
CONFLICT: 'CONFLICT',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Evaluates power supply compatibility between a host device and an adapter/cable.
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} hostSpec - Host physical power requirements
|
|
25
|
+
* @param {Object} accessorySpec - Power accessory specifications
|
|
26
|
+
* @returns {{ status: string, dimension: string, reason: string, details: Object }}
|
|
27
|
+
*/
|
|
28
|
+
export function evaluatePowerCompatibility(hostSpec, accessorySpec) {
|
|
29
|
+
if (!hostSpec || !accessorySpec) {
|
|
30
|
+
return {
|
|
31
|
+
status: COMPATIBILITY_STATUS.UNKNOWN,
|
|
32
|
+
dimension: 'power_supply',
|
|
33
|
+
reason: 'Missing power specifications for host or accessory',
|
|
34
|
+
details: {},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 1. USB-C Power Delivery checks
|
|
39
|
+
if (hostSpec.connector === 'usb_c' || accessorySpec.connector === 'usb_c') {
|
|
40
|
+
if (hostSpec.connector !== accessorySpec.connector) {
|
|
41
|
+
return {
|
|
42
|
+
status: COMPATIBILITY_STATUS.INCOMPATIBLE,
|
|
43
|
+
dimension: 'power_supply',
|
|
44
|
+
reason: `Connector mismatch: host requires ${hostSpec.connector}, accessory provides ${accessorySpec.connector}`,
|
|
45
|
+
details: { hostConnector: hostSpec.connector, accessoryConnector: accessorySpec.connector },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const hostMinWatts = hostSpec.min_watts || ((hostSpec.voltage_v || 5.0) * (hostSpec.min_current_a || 2.0));
|
|
49
|
+
const accWatts = accessorySpec.watts || ((accessorySpec.voltage_v || 5.0) * (accessorySpec.max_current_a || 3.0));
|
|
50
|
+
if (accWatts < hostMinWatts) {
|
|
51
|
+
return {
|
|
52
|
+
status: COMPATIBILITY_STATUS.INCOMPATIBLE,
|
|
53
|
+
dimension: 'power_supply',
|
|
54
|
+
reason: `Insufficient wattage: host requires at least ${hostMinWatts}W, accessory provides ${accWatts}W`,
|
|
55
|
+
details: { requiredWatts: hostMinWatts, providedWatts: accWatts },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
status: COMPATIBILITY_STATUS.VERIFIED,
|
|
60
|
+
dimension: 'power_supply',
|
|
61
|
+
reason: `Verified USB-C power delivery: ${accWatts}W meets or exceeds required ${hostMinWatts}W`,
|
|
62
|
+
details: { requiredWatts: hostMinWatts, providedWatts: accWatts },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 2. DC Barrel Jack checks (Voltage, Current, Plug Dimensions, Polarity)
|
|
67
|
+
const hostV = hostSpec.voltage_v;
|
|
68
|
+
const accV = accessorySpec.voltage_v;
|
|
69
|
+
if (hostV && accV) {
|
|
70
|
+
const tolerance = hostSpec.tolerance_percent || 5.0;
|
|
71
|
+
const diffPercent = Math.abs(hostV - accV) / hostV * 100;
|
|
72
|
+
if (diffPercent > tolerance) {
|
|
73
|
+
return {
|
|
74
|
+
status: COMPATIBILITY_STATUS.INCOMPATIBLE,
|
|
75
|
+
dimension: 'power_supply',
|
|
76
|
+
reason: `Voltage mismatch: host requires ${hostV}V (±${tolerance}%), accessory supplies ${accV}V`,
|
|
77
|
+
details: { requiredVoltage: hostV, suppliedVoltage: accV },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (hostSpec.min_current_a && accessorySpec.max_current_a) {
|
|
83
|
+
if (accessorySpec.max_current_a < hostSpec.min_current_a) {
|
|
84
|
+
return {
|
|
85
|
+
status: COMPATIBILITY_STATUS.INCOMPATIBLE,
|
|
86
|
+
dimension: 'power_supply',
|
|
87
|
+
reason: `Current capacity too low: host requires at least ${hostSpec.min_current_a}A, adapter rated for ${accessorySpec.max_current_a}A`,
|
|
88
|
+
details: { requiredCurrent: hostSpec.min_current_a, providedCurrent: accessorySpec.max_current_a },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Plug dimension check (e.g. 5.5x2.5mm vs 5.5x2.1mm)
|
|
94
|
+
if (hostSpec.connector && accessorySpec.connector) {
|
|
95
|
+
if (hostSpec.connector !== accessorySpec.connector) {
|
|
96
|
+
return {
|
|
97
|
+
status: COMPATIBILITY_STATUS.REQUIRES_ADAPTER,
|
|
98
|
+
dimension: 'power_supply',
|
|
99
|
+
reason: `DC Barrel plug mismatch: host requires ${hostSpec.connector}, accessory has ${accessorySpec.connector}. Requires a barrel converter adapter.`,
|
|
100
|
+
details: { hostConnector: hostSpec.connector, accessoryConnector: accessorySpec.connector },
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Polarity check (center_positive vs center_negative)
|
|
106
|
+
if (hostSpec.polarity && accessorySpec.polarity) {
|
|
107
|
+
if (hostSpec.polarity !== accessorySpec.polarity) {
|
|
108
|
+
return {
|
|
109
|
+
status: COMPATIBILITY_STATUS.INCOMPATIBLE,
|
|
110
|
+
dimension: 'power_supply',
|
|
111
|
+
reason: `Polarity inversion danger: host requires ${hostSpec.polarity}, accessory is ${accessorySpec.polarity}`,
|
|
112
|
+
details: { hostPolarity: hostSpec.polarity, accessoryPolarity: accessorySpec.polarity },
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
status: COMPATIBILITY_STATUS.VERIFIED,
|
|
119
|
+
dimension: 'power_supply',
|
|
120
|
+
reason: `Power supply verified: ${accV || hostV}V @ ${accessorySpec.max_current_a || hostSpec.min_current_a}A matches requirements`,
|
|
121
|
+
details: { voltage: accV || hostV, current: accessorySpec.max_current_a || hostSpec.min_current_a },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Evaluates RF Antenna compatibility between a host device and an antenna.
|
|
127
|
+
*
|
|
128
|
+
* @param {Object} hostSpec
|
|
129
|
+
* @param {Object} antennaSpec
|
|
130
|
+
* @returns {{ status: string, dimension: string, reason: string, details: Object }}
|
|
131
|
+
*/
|
|
132
|
+
export function evaluateRfCompatibility(hostSpec, antennaSpec) {
|
|
133
|
+
if (!hostSpec?.rf_antenna || !antennaSpec) {
|
|
134
|
+
return {
|
|
135
|
+
status: COMPATIBILITY_STATUS.UNKNOWN,
|
|
136
|
+
dimension: 'rf_antenna',
|
|
137
|
+
reason: 'Missing RF or antenna specifications',
|
|
138
|
+
details: {},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const hostRf = hostSpec.rf_antenna;
|
|
143
|
+
const hostConn = (hostRf.connector || '').toLowerCase();
|
|
144
|
+
const antConn = (antennaSpec.connector || '').toLowerCase();
|
|
145
|
+
|
|
146
|
+
// 1. Connector compatibility
|
|
147
|
+
if (hostConn.includes('n-type female')) {
|
|
148
|
+
if (antConn.includes('n-type male') || antConn.includes('n-male')) {
|
|
149
|
+
// Direct mate
|
|
150
|
+
} else if (antConn.includes('sma')) {
|
|
151
|
+
return {
|
|
152
|
+
status: COMPATIBILITY_STATUS.REQUIRES_ADAPTER,
|
|
153
|
+
dimension: 'rf_antenna',
|
|
154
|
+
reason: 'Host has N-Type Female connector, antenna has SMA connector. Requires N-Male to SMA pigtail cable / adapter.',
|
|
155
|
+
details: { hostConnector: hostRf.connector, antennaConnector: antennaSpec.connector },
|
|
156
|
+
};
|
|
157
|
+
} else if (!antConn) {
|
|
158
|
+
return {
|
|
159
|
+
status: COMPATIBILITY_STATUS.UNKNOWN,
|
|
160
|
+
dimension: 'rf_antenna',
|
|
161
|
+
reason: 'Antenna connector type not specified',
|
|
162
|
+
details: { hostConnector: hostRf.connector },
|
|
163
|
+
};
|
|
164
|
+
} else {
|
|
165
|
+
return {
|
|
166
|
+
status: COMPATIBILITY_STATUS.INCOMPATIBLE,
|
|
167
|
+
dimension: 'rf_antenna',
|
|
168
|
+
reason: `Incompatible antenna connector: host has ${hostRf.connector}, antenna has ${antennaSpec.connector}`,
|
|
169
|
+
details: { hostConnector: hostRf.connector, antennaConnector: antennaSpec.connector },
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 2. Frequency band check (e.g. EU868, US915, 2.4GHz)
|
|
175
|
+
if (hostRf.frequency_bands && antennaSpec.frequency_band) {
|
|
176
|
+
const antBand = antennaSpec.frequency_band.toUpperCase();
|
|
177
|
+
const match = hostRf.frequency_bands.some((b) => b.toUpperCase().includes(antBand) || antBand.includes(b.toUpperCase()));
|
|
178
|
+
if (!match) {
|
|
179
|
+
return {
|
|
180
|
+
status: COMPATIBILITY_STATUS.INCOMPATIBLE,
|
|
181
|
+
dimension: 'rf_antenna',
|
|
182
|
+
reason: `Frequency band mismatch: host operating bands [${hostRf.frequency_bands.join(', ')}], antenna tuned for ${antennaSpec.frequency_band}`,
|
|
183
|
+
details: { supportedBands: hostRf.frequency_bands, antennaBand: antennaSpec.frequency_band },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
status: COMPATIBILITY_STATUS.VERIFIED,
|
|
190
|
+
dimension: 'rf_antenna',
|
|
191
|
+
reason: `Antenna verified: connector ${antennaSpec.connector || hostConn} and frequency band match host`,
|
|
192
|
+
details: { connector: antennaSpec.connector || hostConn },
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Evaluates M.2 expansion slot compatibility (Key-M vs Key-E, form factor 2280 vs 2242).
|
|
198
|
+
*
|
|
199
|
+
* @param {Object} hostSpec
|
|
200
|
+
* @param {Object} moduleSpec
|
|
201
|
+
* @returns {{ status: string, dimension: string, reason: string, details: Object }}
|
|
202
|
+
*/
|
|
203
|
+
export function evaluateM2Compatibility(hostSpec, moduleSpec) {
|
|
204
|
+
if (!hostSpec?.m2_slots || !moduleSpec) {
|
|
205
|
+
return {
|
|
206
|
+
status: COMPATIBILITY_STATUS.UNKNOWN,
|
|
207
|
+
dimension: 'm2_storage',
|
|
208
|
+
reason: 'No M.2 slot or module specification available',
|
|
209
|
+
details: {},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const modKey = (moduleSpec.key || '').toUpperCase(); // 'M' or 'E'
|
|
214
|
+
const modSize = moduleSpec.size || '2280';
|
|
215
|
+
|
|
216
|
+
const matchingSlot = hostSpec.m2_slots.find((slot) => {
|
|
217
|
+
return slot.key.toUpperCase() === modKey;
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
if (!matchingSlot) {
|
|
221
|
+
return {
|
|
222
|
+
status: COMPATIBILITY_STATUS.INCOMPATIBLE,
|
|
223
|
+
dimension: 'm2_storage',
|
|
224
|
+
reason: `M.2 Key mismatch: module requires Key-${modKey}, but host only provides [${hostSpec.m2_slots.map((s) => 'Key-' + s.key).join(', ')}]`,
|
|
225
|
+
details: { requiredKey: modKey, availableSlots: hostSpec.m2_slots },
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (matchingSlot.size && matchingSlot.size !== modSize) {
|
|
230
|
+
return {
|
|
231
|
+
status: COMPATIBILITY_STATUS.REQUIRES_ADAPTER,
|
|
232
|
+
dimension: 'm2_storage',
|
|
233
|
+
reason: `Form factor mismatch: module is ${modSize}, slot designed for ${matchingSlot.size}. May require mounting bracket / standoff.`,
|
|
234
|
+
details: { moduleSize: modSize, slotSize: matchingSlot.size },
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
status: COMPATIBILITY_STATUS.VERIFIED,
|
|
240
|
+
dimension: 'm2_storage',
|
|
241
|
+
reason: `M.2 slot verified: Key-${modKey} (${modSize}) matches slot ${matchingSlot.key}`,
|
|
242
|
+
details: { key: modKey, size: modSize, interface: matchingSlot.interface },
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Master compatibility assertion runner evaluating all relevant physical dimensions.
|
|
248
|
+
*
|
|
249
|
+
* @param {Object} hostPhysicalSpec - Host physical specifications
|
|
250
|
+
* @param {Object} accessoryItem - Accessory object with specs and kind
|
|
251
|
+
* @returns {{ isCompatible: boolean, status: string, dimensions: Array<Object> }}
|
|
252
|
+
*/
|
|
253
|
+
export function assertCompatibility(hostPhysicalSpec, accessoryItem) {
|
|
254
|
+
if (!hostPhysicalSpec || !accessoryItem) {
|
|
255
|
+
return {
|
|
256
|
+
isCompatible: false,
|
|
257
|
+
status: COMPATIBILITY_STATUS.UNKNOWN,
|
|
258
|
+
dimensions: [{ dimension: 'general', status: COMPATIBILITY_STATUS.UNKNOWN, reason: 'Insufficient specification data' }],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const evaluations = [];
|
|
263
|
+
|
|
264
|
+
// Check Power
|
|
265
|
+
if (accessoryItem.power_spec || accessoryItem.type?.includes('power') || accessoryItem.id?.includes('power')) {
|
|
266
|
+
evaluations.push(evaluatePowerCompatibility(hostPhysicalSpec.power_input, accessoryItem.power_spec || {
|
|
267
|
+
connector: hostPhysicalSpec.power_input?.connector,
|
|
268
|
+
voltage_v: hostPhysicalSpec.power_input?.voltage_v,
|
|
269
|
+
max_current_a: hostPhysicalSpec.power_input?.min_current_a,
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check RF / Antenna
|
|
274
|
+
if (accessoryItem.rf_spec || accessoryItem.type?.includes('antenna') || accessoryItem.id?.includes('antenna')) {
|
|
275
|
+
evaluations.push(evaluateRfCompatibility(hostPhysicalSpec, accessoryItem.rf_spec || {
|
|
276
|
+
connector: hostPhysicalSpec.rf_antenna?.required_antenna_connector || 'SMA-J (Male)',
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check M.2 Storage / Wireless
|
|
281
|
+
if (accessoryItem.m2_spec || accessoryItem.type?.includes('ssd') || accessoryItem.type?.includes('m2')) {
|
|
282
|
+
evaluations.push(evaluateM2Compatibility(hostPhysicalSpec, accessoryItem.m2_spec || {
|
|
283
|
+
key: 'M',
|
|
284
|
+
size: '2280',
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (evaluations.length === 0) {
|
|
289
|
+
return {
|
|
290
|
+
isCompatible: true,
|
|
291
|
+
status: COMPATIBILITY_STATUS.VERIFIED,
|
|
292
|
+
dimensions: [{ dimension: 'generic', status: COMPATIBILITY_STATUS.VERIFIED, reason: 'General peripheral without physical slot constraint' }],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const hasIncompatible = evaluations.some((e) => e.status === COMPATIBILITY_STATUS.INCOMPATIBLE);
|
|
297
|
+
const hasConflict = evaluations.some((e) => e.status === COMPATIBILITY_STATUS.CONFLICT);
|
|
298
|
+
const hasAdapter = evaluations.some((e) => e.status === COMPATIBILITY_STATUS.REQUIRES_ADAPTER);
|
|
299
|
+
const hasUnknown = evaluations.some((e) => e.status === COMPATIBILITY_STATUS.UNKNOWN);
|
|
300
|
+
|
|
301
|
+
let overallStatus = COMPATIBILITY_STATUS.VERIFIED;
|
|
302
|
+
if (hasIncompatible) overallStatus = COMPATIBILITY_STATUS.INCOMPATIBLE;
|
|
303
|
+
else if (hasConflict) overallStatus = COMPATIBILITY_STATUS.CONFLICT;
|
|
304
|
+
else if (hasAdapter) overallStatus = COMPATIBILITY_STATUS.REQUIRES_ADAPTER;
|
|
305
|
+
else if (hasUnknown) overallStatus = COMPATIBILITY_STATUS.UNKNOWN;
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
isCompatible: overallStatus === COMPATIBILITY_STATUS.VERIFIED || overallStatus === COMPATIBILITY_STATUS.REQUIRES_ADAPTER,
|
|
309
|
+
status: overallStatus,
|
|
310
|
+
dimensions: evaluations,
|
|
311
|
+
};
|
|
312
|
+
}
|